(JAL-889) ensure annotation row is revalidated after an edit occurs
[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     aa[activeRow].validateRangeAndDisplay();
366
367     adjustPanelHeight();
368     repaint();
369
370     return;
371   }
372
373   private String collectAnnotVals(Annotation[] anot,
374           ColumnSelection columnSelection, String label2)
375   {
376     String collatedInput = "";
377     String last = "";
378     // TODO: refactor and save av.getColumnSelection for efficiency
379     for (int i = 0; i < columnSelection.size(); i++)
380     {
381       int index = columnSelection.columnAt(i);
382       // always check for current display state - just in case
383       if (!av.getColumnSelection().isVisible(index))
384         continue;
385       String tlabel = null;
386       if (anot[index] != null)
387       { // LML added stem code
388         if (label2.equals(HELIX) || label2.equals(SHEET)
389                 || label2.equals(STEM) || label2.equals(LABEL))
390         {
391           tlabel = anot[index].description;
392           if (tlabel == null || tlabel.length() < 1)
393           {
394             if (label2.equals(HELIX) || label2.equals(SHEET)
395                     || label2.equals(STEM))
396             {
397               tlabel = "" + anot[index].secondaryStructure;
398             }
399             else
400             {
401               tlabel = "" + anot[index].displayCharacter;
402             }
403           }
404         }
405         if (tlabel != null && !tlabel.equals(last))
406         {
407           if (last.length() > 0)
408           {
409             collatedInput += " ";
410           }
411           collatedInput += tlabel;
412         }
413       }
414     }
415     return collatedInput;
416   }
417
418   /**
419    * DOCUMENT ME!
420    * 
421    * @param evt
422    *          DOCUMENT ME!
423    */
424   public void mousePressed(MouseEvent evt)
425   {
426
427     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
428     if (aa == null)
429     {
430       return;
431     }
432
433     int height = 0;
434     activeRow = -1;
435
436     for (int i = 0; i < aa.length; i++)
437     {
438       if (aa[i].visible)
439       {
440         height += aa[i].height;
441       }
442
443       if (evt.getY() < height)
444       {
445         if (aa[i].editable)
446         {
447           activeRow = i;
448         }
449         else if (aa[i].graph > 0)
450         {
451           // Stretch Graph
452           graphStretch = i;
453           graphStretchY = evt.getY();
454         }
455
456         break;
457       }
458     }
459
460     if (SwingUtilities.isRightMouseButton(evt) && activeRow != -1)
461     {
462       if (av.getColumnSelection() == null)
463       {
464         return;
465       }
466
467       JPopupMenu pop = new JPopupMenu("Structure type");
468       JMenuItem item;
469       /*
470        * Just display the needed structure options
471        */
472       if (av.getAlignment().isNucleotide() == true)
473       {
474         item = new JMenuItem(STEM);
475         item.addActionListener(this);
476         pop.add(item);
477       }
478       else
479       {
480         item = new JMenuItem(HELIX);
481         item.addActionListener(this);
482         pop.add(item);
483         item = new JMenuItem(SHEET);
484         item.addActionListener(this);
485         pop.add(item);
486       }
487       item = new JMenuItem(LABEL);
488       item.addActionListener(this);
489       pop.add(item);
490       item = new JMenuItem(COLOUR);
491       item.addActionListener(this);
492       pop.add(item);
493       item = new JMenuItem(REMOVE);
494       item.addActionListener(this);
495       pop.add(item);
496       pop.show(this, evt.getX(), evt.getY());
497
498       return;
499     }
500
501     if (aa == null)
502     {
503       return;
504     }
505
506     ap.scalePanel.mousePressed(evt);
507
508   }
509
510   /**
511    * DOCUMENT ME!
512    * 
513    * @param evt
514    *          DOCUMENT ME!
515    */
516   public void mouseReleased(MouseEvent evt)
517   {
518     graphStretch = -1;
519     graphStretchY = -1;
520     mouseDragging = false;
521     ap.scalePanel.mouseReleased(evt);
522   }
523
524   /**
525    * DOCUMENT ME!
526    * 
527    * @param evt
528    *          DOCUMENT ME!
529    */
530   public void mouseEntered(MouseEvent evt)
531   {
532     ap.scalePanel.mouseEntered(evt);
533   }
534
535   /**
536    * DOCUMENT ME!
537    * 
538    * @param evt
539    *          DOCUMENT ME!
540    */
541   public void mouseExited(MouseEvent evt)
542   {
543     ap.scalePanel.mouseExited(evt);
544   }
545
546   /**
547    * DOCUMENT ME!
548    * 
549    * @param evt
550    *          DOCUMENT ME!
551    */
552   public void mouseDragged(MouseEvent evt)
553   {
554     if (graphStretch > -1)
555     {
556       av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
557               - evt.getY();
558       if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0)
559       {
560         av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0;
561       }
562       graphStretchY = evt.getY();
563       adjustPanelHeight();
564       ap.paintAlignment(true);
565     }
566     else
567     {
568       ap.scalePanel.mouseDragged(evt);
569     }
570   }
571
572   /**
573    * DOCUMENT ME!
574    * 
575    * @param evt
576    *          DOCUMENT ME!
577    */
578   public void mouseMoved(MouseEvent evt)
579   {
580     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
581
582     if (aa == null)
583     {
584       this.setToolTipText(null);
585       return;
586     }
587
588     int row = -1;
589     int height = 0;
590
591     for (int i = 0; i < aa.length; i++)
592     {
593       if (aa[i].visible)
594       {
595         height += aa[i].height;
596       }
597
598       if (evt.getY() < height)
599       {
600         row = i;
601
602         break;
603       }
604     }
605
606     if (row == -1)
607     {
608       this.setToolTipText(null);
609       return;
610     }
611
612     int res = (evt.getX() / av.getCharWidth()) + av.getStartRes();
613
614     if (av.hasHiddenColumns())
615     {
616       res = av.getColumnSelection().adjustForHiddenColumns(res);
617     }
618
619     if (row > -1 && aa[row].annotations != null
620             && res < (int) aa[row].annotations.length)
621     {
622       if (aa[row].graphGroup > -1)
623       {
624         StringBuffer tip = new StringBuffer("<html>");
625         for (int gg = 0; gg < aa.length; gg++)
626         {
627           if (aa[gg].graphGroup == aa[row].graphGroup
628                   && aa[gg].annotations[res] != null)
629           {
630             tip.append(aa[gg].label + " "
631                     + aa[gg].annotations[res].description + "<br>");
632           }
633         }
634         if (tip.length() != 6)
635         {
636           tip.setLength(tip.length() - 4);
637           this.setToolTipText(tip.toString() + "</html>");
638         }
639       }
640       else if (aa[row].annotations[res] != null
641               && aa[row].annotations[res].description != null
642               && aa[row].annotations[res].description.length() > 0)
643       {
644         this.setToolTipText(aa[row].annotations[res].description);
645       }
646       else
647       {
648         // clear the tooltip.
649         this.setToolTipText(null);
650       }
651
652       if (aa[row].annotations[res] != null)
653       {
654         StringBuffer text = new StringBuffer("Sequence position "
655                 + (res + 1));
656
657         if (aa[row].annotations[res].description != null)
658         {
659           text.append("  " + aa[row].annotations[res].description);
660         }
661
662         ap.alignFrame.statusBar.setText(text.toString());
663       }
664     }
665     else
666     {
667       this.setToolTipText(null);
668     }
669   }
670
671   /**
672    * DOCUMENT ME!
673    * 
674    * @param evt
675    *          DOCUMENT ME!
676    */
677   public void mouseClicked(MouseEvent evt)
678   {
679     if (activeRow != -1)
680     {
681       AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
682       AlignmentAnnotation anot = aa[activeRow];
683
684       if (anot.description.equals("secondary structure"))
685       {
686         // System.out.println(anot.description+" "+anot.getRNAStruc());
687       }
688     }
689   }
690
691   // TODO mouseClicked-content and drawCursor are quite experimental!
692   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
693           int y1)
694   {
695     int pady = av.charHeight / 5;
696     int charOffset = 0;
697     graphics.setColor(Color.black);
698     graphics.fillRect(x1, y1, av.charWidth, av.charHeight);
699
700     if (av.validCharWidth)
701     {
702       graphics.setColor(Color.white);
703
704       char s = seq.getCharAt(res);
705
706       charOffset = (av.charWidth - fm.charWidth(s)) / 2;
707       graphics.drawString(String.valueOf(s), charOffset + x1,
708               (y1 + av.charHeight) - pady);
709     }
710
711   }
712
713   /**
714    * DOCUMENT ME!
715    * 
716    * @param g
717    *          DOCUMENT ME!
718    */
719   public void paintComponent(Graphics g)
720   {
721     g.setColor(Color.white);
722     g.fillRect(0, 0, getWidth(), getHeight());
723
724     if (image != null)
725     {
726       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
727               || (getVisibleRect().height != g.getClipBounds().height))
728       {
729         g.drawImage(image, 0, 0, this);
730         fastPaint = false;
731         return;
732       }
733     }
734     imgWidth = (av.endRes - av.startRes + 1) * av.charWidth;
735     if (imgWidth < 1)
736       return;
737     if (image == null || imgWidth != image.getWidth()
738             || image.getHeight(this) != getHeight())
739     {
740       image = new BufferedImage(imgWidth, ap.annotationPanel.getHeight(),
741               BufferedImage.TYPE_INT_RGB);
742       gg = (Graphics2D) image.getGraphics();
743
744       if (av.antiAlias)
745       {
746         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
747                 RenderingHints.VALUE_ANTIALIAS_ON);
748       }
749
750       gg.setFont(av.getFont());
751       fm = gg.getFontMetrics();
752       gg.setColor(Color.white);
753       gg.fillRect(0, 0, imgWidth, image.getHeight());
754     }
755
756     drawComponent(gg, av.startRes, av.endRes + 1);
757     g.drawImage(image, 0, 0, this);
758   }
759
760   /**
761    * non-Thread safe repaint
762    * 
763    * @param horizontal
764    *          repaint with horizontal shift in alignment
765    */
766   public void fastPaint(int horizontal)
767   {
768
769     if ((horizontal == 0) || gg == null
770             || av.getAlignment().getAlignmentAnnotation() == null
771             || av.getAlignment().getAlignmentAnnotation().length < 1
772             || av.isCalcInProgress())
773     {
774       repaint();
775       return;
776     }
777     gg.copyArea(0, 0, imgWidth, getHeight(), -horizontal * av.charWidth, 0);
778
779     int sr = av.startRes;
780     int er = av.endRes + 1;
781     int transX = 0;
782
783     if (horizontal > 0) // scrollbar pulled right, image to the left
784     {
785       transX = (er - sr - horizontal) * av.charWidth;
786       sr = er - horizontal;
787     }
788     else if (horizontal < 0)
789     {
790       er = sr - horizontal;
791     }
792
793     gg.translate(transX, 0);
794
795     drawComponent(gg, sr, er);
796
797     gg.translate(-transX, 0);
798
799     fastPaint = true;
800     repaint();
801
802   }
803
804   /**
805    * DOCUMENT ME!
806    * 
807    * @param g
808    *          DOCUMENT ME!
809    * @param startRes
810    *          DOCUMENT ME!
811    * @param endRes
812    *          DOCUMENT ME!
813    */
814   public void drawComponent(Graphics g, int startRes, int endRes)
815   {
816     if (av.isCalcInProgress())
817     {
818       if (image == null)
819       {
820         return;
821       }
822       // We'll keep a record of the old image,
823       // and draw a faded image until the calculation
824       // has completed
825       if (fadedImage == null || fadedImage.getWidth() != imgWidth
826               || fadedImage.getHeight() != image.getHeight())
827       {
828         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
829                 BufferedImage.TYPE_INT_RGB);
830
831         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
832
833         fadedG.setColor(Color.white);
834         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
835
836         fadedG.setComposite(AlphaComposite.getInstance(
837                 AlphaComposite.SRC_OVER, .3f));
838         fadedG.drawImage(image, 0, 0, this);
839
840       }
841
842     }
843     else
844     {
845       fadedImage = null;
846     }
847
848     g.setColor(Color.white);
849     g.fillRect(0, 0, (endRes - startRes) * av.charWidth, getHeight());
850
851     g.setFont(av.getFont());
852     if (fm == null)
853     {
854       fm = g.getFontMetrics();
855     }
856
857     if ((av.getAlignment().getAlignmentAnnotation() == null)
858             || (av.getAlignment().getAlignmentAnnotation().length < 1))
859     {
860       g.setColor(Color.white);
861       g.fillRect(0, 0, getWidth(), getHeight());
862       g.setColor(Color.black);
863       if (av.validCharWidth)
864       {
865         g.drawString("Alignment has no annotations", 20, 15);
866       }
867
868       return;
869     }
870     renderer.drawComponent(this, av, g, activeRow, startRes, endRes);
871   }
872
873   @Override
874   public FontMetrics getFontMetrics()
875   {
876     return fm;
877   }
878
879   @Override
880   public Image getFadedImage()
881   {
882     return fadedImage;
883   }
884
885   @Override
886   public int getFadedImageWidth()
887   {
888     return imgWidth;
889   }
890 }