JAL-1155 fix panel length calculation for grouped graphs and refactor to alignment...
[jalview.git] / src / jalview / gui / AnnotationPanel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
3  * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle
4  *
5  * This file is part of Jalview.
6  *
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
10  *
11  * Jalview is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
14  * PURPOSE.  See the GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.gui;
19
20 import java.awt.*;
21 import java.awt.event.*;
22 import java.awt.image.*;
23 import javax.swing.*;
24
25 import jalview.datamodel.*;
26 import jalview.renderer.AnnotationRenderer;
27 import jalview.renderer.AwtRenderPanelI;
28
29 /**
30  * DOCUMENT ME!
31  *
32  * @author $author$
33  * @version $Revision$
34  */
35 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
36         MouseListener, MouseMotionListener, ActionListener,
37         AdjustmentListener
38 {
39   final String HELIX = "Helix";
40
41   final String SHEET = "Sheet";
42
43   /**
44    * For RNA secondary structure "stems" aka helices
45    */
46   final String STEM = "RNA Helix";
47
48   final String LABEL = "Label";
49
50   final String REMOVE = "Remove Annotation";
51
52   final String COLOUR = "Colour";
53
54   public final Color HELIX_COLOUR = Color.red.darker();
55
56   public final Color SHEET_COLOUR = Color.green.darker().darker();
57
58   public final Color STEM_COLOUR = Color.blue.darker();
59
60   /** DOCUMENT ME!! */
61   public AlignViewport av;
62
63   AlignmentPanel ap;
64
65   public int activeRow = -1;
66
67   public BufferedImage image;
68
69   public volatile BufferedImage fadedImage;
70
71   Graphics2D gg;
72
73   public FontMetrics fm;
74
75   public int imgWidth = 0;
76
77   boolean fastPaint = false;
78
79   // Used For mouse Dragging and resizing graphs
80   int graphStretch = -1;
81
82   int graphStretchY = -1;
83
84   int min; // used by mouseDragged to see if user
85
86   int max; // used by mouseDragged to see if user
87
88   boolean mouseDragging = false;
89
90   boolean MAC = false;
91
92   // for editing cursor
93   int cursorX = 0;
94
95   int cursorY = 0;
96
97   public final AnnotationRenderer renderer;
98
99   /**
100    * Creates a new AnnotationPanel object.
101    *
102    * @param ap
103    *          DOCUMENT ME!
104    */
105   public AnnotationPanel(AlignmentPanel ap)
106   {
107
108     MAC = new jalview.util.Platform().isAMac();
109
110     ToolTipManager.sharedInstance().registerComponent(this);
111     ToolTipManager.sharedInstance().setInitialDelay(0);
112     ToolTipManager.sharedInstance().setDismissDelay(10000);
113     this.ap = ap;
114     av = ap.av;
115     this.setLayout(null);
116     addMouseListener(this);
117     addMouseMotionListener(this);
118     ap.annotationScroller.getVerticalScrollBar()
119             .addAdjustmentListener(this);
120     renderer = new AnnotationRenderer();
121   }
122
123   public AnnotationPanel(AlignViewport av)
124   {
125     this.av = av;
126     renderer = new AnnotationRenderer();
127   }
128
129   /**
130    * DOCUMENT ME!
131    *
132    * @param evt
133    *          DOCUMENT ME!
134    */
135   @Override
136   public void adjustmentValueChanged(AdjustmentEvent evt)
137   {
138     ap.alabels.setScrollOffset(-evt.getValue());
139   }
140
141   /**
142    * Calculates the height of the annotation displayed in the annotation panel.
143    * Callers should normally call the ap.adjustAnnotationHeight method to ensure
144    * all annotation associated components are updated correctly.
145    *
146    */
147   public int adjustPanelHeight()
148   {
149     int height = av.calcPanelHeight();
150     this.setPreferredSize(new Dimension(1, height));
151     if (ap != null)
152     {
153       // revalidate only when the alignment panel is fully constructed
154       ap.validate();
155     }
156
157     return height;
158   }
159
160   /**
161    * DOCUMENT ME!
162    *
163    * @param evt
164    *          DOCUMENT ME!
165    */
166   @Override
167   public void actionPerformed(ActionEvent evt)
168   {
169     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
170     if (aa == null)
171     {
172       return;
173     }
174     Annotation[] anot = aa[activeRow].annotations;
175
176     if (anot.length < av.getColumnSelection().getMax())
177     {
178       Annotation[] temp = new Annotation[av.getColumnSelection().getMax() + 2];
179       System.arraycopy(anot, 0, temp, 0, anot.length);
180       anot = temp;
181       aa[activeRow].annotations = anot;
182     }
183
184     if (evt.getActionCommand().equals(REMOVE))
185     {
186       for (int i = 0; i < av.getColumnSelection().size(); i++)
187       {
188         anot[av.getColumnSelection().columnAt(i)] = null;
189       }
190     }
191     else if (evt.getActionCommand().equals(LABEL))
192     {
193       String exMesg = collectAnnotVals(anot, av.getColumnSelection(), LABEL);
194       String label = JOptionPane.showInputDialog(this, "Enter label",
195               exMesg);
196
197       if (label == null)
198       {
199         return;
200       }
201
202       if ((label.length() > 0) && !aa[activeRow].hasText)
203       {
204         aa[activeRow].hasText = true;
205       }
206
207       for (int i = 0; i < av.getColumnSelection().size(); i++)
208       {
209         int index = av.getColumnSelection().columnAt(i);
210
211         if (!av.getColumnSelection().isVisible(index))
212           continue;
213
214         if (anot[index] == null)
215         {
216           anot[index] = new Annotation(label, "", ' ', 0); // TODO: verify that
217           // null exceptions
218           // aren't raised
219           // elsewhere.
220         }
221         else
222         {
223           anot[index].displayCharacter = label;
224         }
225       }
226     }
227     else if (evt.getActionCommand().equals(COLOUR))
228     {
229       Color col = JColorChooser.showDialog(this,
230               "Choose foreground colour", Color.black);
231
232       for (int i = 0; i < av.getColumnSelection().size(); i++)
233       {
234         int index = av.getColumnSelection().columnAt(i);
235
236         if (!av.getColumnSelection().isVisible(index))
237           continue;
238
239         if (anot[index] == null)
240         {
241           anot[index] = new Annotation("", "", ' ', 0);
242         }
243
244         anot[index].colour = col;
245       }
246     }
247     else
248     // HELIX OR SHEET
249     {
250       char type = 0;
251       String symbol = "\u03B1";
252
253       if (evt.getActionCommand().equals(HELIX))
254       {
255         type = 'H';
256       }
257       else if (evt.getActionCommand().equals(SHEET))
258       {
259         type = 'E';
260         symbol = "\u03B2";
261       }
262
263       // Added by LML to color stems
264       else if (evt.getActionCommand().equals(STEM))
265       {
266         type = 'S';
267         symbol = "\u03C3";
268       }
269
270       if (!aa[activeRow].hasIcons)
271       {
272         aa[activeRow].hasIcons = true;
273       }
274
275       String label = JOptionPane.showInputDialog(
276               "Enter a label for the structure?", symbol);
277
278       if (label == null)
279       {
280         return;
281       }
282
283       if ((label.length() > 0) && !aa[activeRow].hasText)
284       {
285         aa[activeRow].hasText = true;
286       }
287
288       for (int i = 0; i < av.getColumnSelection().size(); i++)
289       {
290         int index = av.getColumnSelection().columnAt(i);
291
292         if (!av.getColumnSelection().isVisible(index))
293           continue;
294
295         if (anot[index] == null)
296         {
297           anot[index] = new Annotation(label, "", type, 0);
298         }
299
300         anot[index].secondaryStructure = type;
301         anot[index].displayCharacter = label;
302       }
303     }
304     aa[activeRow].validateRangeAndDisplay();
305
306     adjustPanelHeight();
307     ap.alignmentChanged();
308     repaint();
309
310     return;
311   }
312
313   private String collectAnnotVals(Annotation[] anot,
314           ColumnSelection columnSelection, String label2)
315   {
316     String collatedInput = "";
317     String last = "";
318     ColumnSelection viscols=av.getColumnSelection();
319     // TODO: refactor and save av.getColumnSelection for efficiency
320     for (int i = 0; i < columnSelection.size(); i++)
321     {
322       int index = columnSelection.columnAt(i);
323       // always check for current display state - just in case
324       if (!viscols.isVisible(index))
325         continue;
326       String tlabel = null;
327       if (anot[index] != null)
328       { // LML added stem code
329         if (label2.equals(HELIX) || label2.equals(SHEET)
330                 || label2.equals(STEM) || label2.equals(LABEL))
331         {
332           tlabel = anot[index].description;
333           if (tlabel == null || tlabel.length() < 1)
334           {
335             if (label2.equals(HELIX) || label2.equals(SHEET)
336                     || label2.equals(STEM))
337             {
338               tlabel = "" + anot[index].secondaryStructure;
339             }
340             else
341             {
342               tlabel = "" + anot[index].displayCharacter;
343             }
344           }
345         }
346         if (tlabel != null && !tlabel.equals(last))
347         {
348           if (last.length() > 0)
349           {
350             collatedInput += " ";
351           }
352           collatedInput += tlabel;
353         }
354       }
355     }
356     return collatedInput;
357   }
358
359   /**
360    * DOCUMENT ME!
361    *
362    * @param evt
363    *          DOCUMENT ME!
364    */
365   @Override
366   public void mousePressed(MouseEvent evt)
367   {
368
369     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
370     if (aa == null)
371     {
372       return;
373     }
374
375     int height = 0;
376     activeRow = -1;
377
378     for (int i = 0; i < aa.length; i++)
379     {
380       if (aa[i].visible)
381       {
382         height += aa[i].height;
383       }
384
385       if (evt.getY() < height)
386       {
387         if (aa[i].editable)
388         {
389           activeRow = i;
390         }
391         else if (aa[i].graph > 0)
392         {
393           // Stretch Graph
394           graphStretch = i;
395           graphStretchY = evt.getY();
396         }
397
398         break;
399       }
400     }
401
402     if (SwingUtilities.isRightMouseButton(evt) && activeRow != -1)
403     {
404       if (av.getColumnSelection() == null)
405       {
406         return;
407       }
408
409       JPopupMenu pop = new JPopupMenu("Structure type");
410       JMenuItem item;
411       /*
412        * Just display the needed structure options
413        */
414       if (av.getAlignment().isNucleotide() == true)
415       {
416         item = new JMenuItem(STEM);
417         item.addActionListener(this);
418         pop.add(item);
419       }
420       else
421       {
422         item = new JMenuItem(HELIX);
423         item.addActionListener(this);
424         pop.add(item);
425         item = new JMenuItem(SHEET);
426         item.addActionListener(this);
427         pop.add(item);
428       }
429       item = new JMenuItem(LABEL);
430       item.addActionListener(this);
431       pop.add(item);
432       item = new JMenuItem(COLOUR);
433       item.addActionListener(this);
434       pop.add(item);
435       item = new JMenuItem(REMOVE);
436       item.addActionListener(this);
437       pop.add(item);
438       pop.show(this, evt.getX(), evt.getY());
439
440       return;
441     }
442
443     if (aa == null)
444     {
445       return;
446     }
447
448     ap.scalePanel.mousePressed(evt);
449
450   }
451
452   /**
453    * DOCUMENT ME!
454    *
455    * @param evt
456    *          DOCUMENT ME!
457    */
458   @Override
459   public void mouseReleased(MouseEvent evt)
460   {
461     graphStretch = -1;
462     graphStretchY = -1;
463     mouseDragging = false;
464     ap.scalePanel.mouseReleased(evt);
465   }
466
467   /**
468    * DOCUMENT ME!
469    *
470    * @param evt
471    *          DOCUMENT ME!
472    */
473   @Override
474   public void mouseEntered(MouseEvent evt)
475   {
476     ap.scalePanel.mouseEntered(evt);
477   }
478
479   /**
480    * DOCUMENT ME!
481    *
482    * @param evt
483    *          DOCUMENT ME!
484    */
485   @Override
486   public void mouseExited(MouseEvent evt)
487   {
488     ap.scalePanel.mouseExited(evt);
489   }
490
491   /**
492    * DOCUMENT ME!
493    *
494    * @param evt
495    *          DOCUMENT ME!
496    */
497   @Override
498   public void mouseDragged(MouseEvent evt)
499   {
500     if (graphStretch > -1)
501     {
502       av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
503               - evt.getY();
504       if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0)
505       {
506         av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0;
507       }
508       graphStretchY = evt.getY();
509       adjustPanelHeight();
510       ap.paintAlignment(true);
511     }
512     else
513     {
514       ap.scalePanel.mouseDragged(evt);
515     }
516   }
517
518   /**
519    * DOCUMENT ME!
520    *
521    * @param evt
522    *          DOCUMENT ME!
523    */
524   @Override
525   public void mouseMoved(MouseEvent evt)
526   {
527     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
528
529     if (aa == null)
530     {
531       this.setToolTipText(null);
532       return;
533     }
534
535     int row = -1;
536     int height = 0;
537
538     for (int i = 0; i < aa.length; i++)
539     {
540       if (aa[i].visible)
541       {
542         height += aa[i].height;
543       }
544
545       if (evt.getY() < height)
546       {
547         row = i;
548
549         break;
550       }
551     }
552
553     if (row == -1)
554     {
555       this.setToolTipText(null);
556       return;
557     }
558
559     int res = (evt.getX() / av.getCharWidth()) + av.getStartRes();
560
561     if (av.hasHiddenColumns())
562     {
563       res = av.getColumnSelection().adjustForHiddenColumns(res);
564     }
565
566     if (row > -1 && aa[row].annotations != null
567             && res < aa[row].annotations.length)
568     {
569       if (aa[row].graphGroup > -1)
570       {
571         StringBuffer tip = new StringBuffer("<html>");
572         for (int gg = 0; gg < aa.length; gg++)
573         {
574           if (aa[gg].graphGroup == aa[row].graphGroup
575                   && aa[gg].annotations[res] != null)
576           {
577             tip.append(aa[gg].label + " "
578                     + aa[gg].annotations[res].description + "<br>");
579           }
580         }
581         if (tip.length() != 6)
582         {
583           tip.setLength(tip.length() - 4);
584           this.setToolTipText(tip.toString() + "</html>");
585         }
586       }
587       else if (aa[row].annotations[res] != null
588               && aa[row].annotations[res].description != null
589               && aa[row].annotations[res].description.length() > 0)
590       {
591         this.setToolTipText(aa[row].annotations[res].description);
592       }
593       else
594       {
595         // clear the tooltip.
596         this.setToolTipText(null);
597       }
598
599       if (aa[row].annotations[res] != null)
600       {
601         StringBuffer text = new StringBuffer("Sequence position "
602                 + (res + 1));
603
604         if (aa[row].annotations[res].description != null)
605         {
606           text.append("  " + aa[row].annotations[res].description);
607         }
608
609         ap.alignFrame.statusBar.setText(text.toString());
610       }
611     }
612     else
613     {
614       this.setToolTipText(null);
615     }
616   }
617
618   /**
619    * DOCUMENT ME!
620    *
621    * @param evt
622    *          DOCUMENT ME!
623    */
624   @Override
625   public void mouseClicked(MouseEvent evt)
626   {
627     if (activeRow != -1)
628     {
629       AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
630       AlignmentAnnotation anot = aa[activeRow];
631
632       if (anot.description.equals("secondary structure"))
633       {
634         // System.out.println(anot.description+" "+anot.getRNAStruc());
635       }
636     }
637   }
638
639   // TODO mouseClicked-content and drawCursor are quite experimental!
640   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
641           int y1)
642   {
643     int pady = av.charHeight / 5;
644     int charOffset = 0;
645     graphics.setColor(Color.black);
646     graphics.fillRect(x1, y1, av.charWidth, av.charHeight);
647
648     if (av.validCharWidth)
649     {
650       graphics.setColor(Color.white);
651
652       char s = seq.getCharAt(res);
653
654       charOffset = (av.charWidth - fm.charWidth(s)) / 2;
655       graphics.drawString(String.valueOf(s), charOffset + x1,
656               (y1 + av.charHeight) - pady);
657     }
658
659   }
660   private volatile boolean imageFresh=false;
661   /**
662    * DOCUMENT ME!
663    *
664    * @param g
665    *          DOCUMENT ME!
666    */
667   @Override
668   public void paintComponent(Graphics g)
669   {
670     g.setColor(Color.white);
671     g.fillRect(0, 0, getWidth(), getHeight());
672
673     if (image != null)
674     {
675       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
676               || (getVisibleRect().height != g.getClipBounds().height))
677       {
678         g.drawImage(image, 0, 0, this);
679         fastPaint = false;
680         return;
681       }
682     }
683     imgWidth = (av.endRes - av.startRes + 1) * av.charWidth;
684     if (imgWidth < 1)
685       return;
686     if (image == null || imgWidth != image.getWidth()
687             || image.getHeight(this) != getHeight())
688     {
689       image = new BufferedImage(imgWidth, ap.annotationPanel.getHeight(),
690               BufferedImage.TYPE_INT_RGB);
691       gg = (Graphics2D) image.getGraphics();
692
693       if (av.antiAlias)
694       {
695         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
696                 RenderingHints.VALUE_ANTIALIAS_ON);
697       }
698
699       gg.setFont(av.getFont());
700       fm = gg.getFontMetrics();
701       gg.setColor(Color.white);
702       gg.fillRect(0, 0, imgWidth, image.getHeight());
703       imageFresh=true;
704     }
705
706     drawComponent(gg, av.startRes, av.endRes + 1);
707     imageFresh=false;
708     g.drawImage(image, 0, 0, this);
709   }
710
711   /**
712    * non-Thread safe repaint
713    *
714    * @param horizontal
715    *          repaint with horizontal shift in alignment
716    */
717   public void fastPaint(int horizontal)
718   {
719
720     if ((horizontal == 0) || gg == null
721             || av.getAlignment().getAlignmentAnnotation() == null
722             || av.getAlignment().getAlignmentAnnotation().length < 1
723             || av.isCalcInProgress())
724     {
725       repaint();
726       return;
727     }
728     gg.copyArea(0, 0, imgWidth, getHeight(), -horizontal * av.charWidth, 0);
729
730     int sr = av.startRes;
731     int er = av.endRes + 1;
732     int transX = 0;
733
734     if (horizontal > 0) // scrollbar pulled right, image to the left
735     {
736       transX = (er - sr - horizontal) * av.charWidth;
737       sr = er - horizontal;
738     }
739     else if (horizontal < 0)
740     {
741       er = sr - horizontal;
742     }
743
744     gg.translate(transX, 0);
745
746     drawComponent(gg, sr, er);
747
748     gg.translate(-transX, 0);
749
750     fastPaint = true;
751     repaint();
752
753   }
754   private volatile boolean lastImageGood=false;
755   /**
756    * DOCUMENT ME!
757    *
758    * @param g
759    *          DOCUMENT ME!
760    * @param startRes
761    *          DOCUMENT ME!
762    * @param endRes
763    *          DOCUMENT ME!
764    */
765   public void drawComponent(Graphics g, int startRes, int endRes)
766   {
767     BufferedImage oldFaded=fadedImage;
768     if (av.isCalcInProgress())
769     {
770       if (image == null)
771       {
772         lastImageGood=false;
773         return;
774       }
775       // We'll keep a record of the old image,
776       // and draw a faded image until the calculation
777       // has completed
778       if (lastImageGood && (fadedImage == null || fadedImage.getWidth() != imgWidth
779               || fadedImage.getHeight() != image.getHeight()))
780       {
781 //        System.err.println("redraw faded image ("+(fadedImage==null ? "null image" : "") + " lastGood="+lastImageGood+")");
782         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
783                 BufferedImage.TYPE_INT_RGB);
784
785         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
786
787         fadedG.setColor(Color.white);
788         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
789
790         fadedG.setComposite(AlphaComposite.getInstance(
791                 AlphaComposite.SRC_OVER, .3f));
792         fadedG.drawImage(image, 0, 0, this);
793
794       }
795       // make sure we don't overwrite the last good faded image until all calculations have finished
796       lastImageGood=false;
797
798     }
799     else
800     {
801       if (fadedImage!=null)
802       {
803         oldFaded=fadedImage;
804       }
805       fadedImage = null;
806     }
807
808     g.setColor(Color.white);
809     g.fillRect(0, 0, (endRes - startRes) * av.charWidth, getHeight());
810
811     g.setFont(av.getFont());
812     if (fm == null)
813     {
814       fm = g.getFontMetrics();
815     }
816
817     if ((av.getAlignment().getAlignmentAnnotation() == null)
818             || (av.getAlignment().getAlignmentAnnotation().length < 1))
819     {
820       g.setColor(Color.white);
821       g.fillRect(0, 0, getWidth(), getHeight());
822       g.setColor(Color.black);
823       if (av.validCharWidth)
824       {
825         g.drawString("Alignment has no annotations", 20, 15);
826       }
827
828       return;
829     }
830     lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes, endRes);
831     if (!lastImageGood && fadedImage==null)
832     {
833       fadedImage=oldFaded;
834     }
835   }
836
837   @Override
838   public FontMetrics getFontMetrics()
839   {
840     return fm;
841   }
842
843   @Override
844   public Image getFadedImage()
845   {
846     return fadedImage;
847   }
848
849   @Override
850   public int getFadedImageWidth()
851   {
852     return imgWidth;
853   }
854 }