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