JAL-1118 - hack to try and get rid of flicker. consider moving greyed out image rende...
[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 = 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    * calculate the height for visible annotation, revalidating bounds where
162    * necessary ABSTRACT GUI METHOD
163    *
164    * @return total height of annotation
165    */
166   public int calcPanelHeight()
167   {
168     // setHeight of panels
169     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
170     int height = 0;
171
172     if (aa != null)
173     {
174       for (int i = 0; i < aa.length; i++)
175       {
176         if (aa[i] == null)
177         {
178           System.err.println("Null annotation row: ignoring.");
179           continue;
180         }
181         if (!aa[i].visible)
182         {
183           continue;
184         }
185
186         aa[i].height = 0;
187
188         if (aa[i].hasText)
189         {
190           aa[i].height += av.charHeight;
191         }
192
193         if (aa[i].hasIcons)
194         {
195           aa[i].height += 16;
196         }
197
198         if (aa[i].graph > 0)
199         {
200           aa[i].height += aa[i].graphHeight;
201         }
202
203         if (aa[i].height == 0)
204         {
205           aa[i].height = 20;
206         }
207
208         height += aa[i].height;
209       }
210     }
211     if (height == 0)
212     {
213       // set minimum
214       height = 20;
215     }
216     return height;
217   }
218
219   /**
220    * DOCUMENT ME!
221    *
222    * @param evt
223    *          DOCUMENT ME!
224    */
225   @Override
226   public void actionPerformed(ActionEvent evt)
227   {
228     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
229     if (aa == null)
230     {
231       return;
232     }
233     Annotation[] anot = aa[activeRow].annotations;
234
235     if (anot.length < av.getColumnSelection().getMax())
236     {
237       Annotation[] temp = new Annotation[av.getColumnSelection().getMax() + 2];
238       System.arraycopy(anot, 0, temp, 0, anot.length);
239       anot = temp;
240       aa[activeRow].annotations = anot;
241     }
242
243     if (evt.getActionCommand().equals(REMOVE))
244     {
245       for (int i = 0; i < av.getColumnSelection().size(); i++)
246       {
247         anot[av.getColumnSelection().columnAt(i)] = null;
248       }
249     }
250     else if (evt.getActionCommand().equals(LABEL))
251     {
252       String exMesg = collectAnnotVals(anot, av.getColumnSelection(), LABEL);
253       String label = JOptionPane.showInputDialog(this, "Enter label",
254               exMesg);
255
256       if (label == null)
257       {
258         return;
259       }
260
261       if ((label.length() > 0) && !aa[activeRow].hasText)
262       {
263         aa[activeRow].hasText = true;
264       }
265
266       for (int i = 0; i < av.getColumnSelection().size(); i++)
267       {
268         int index = av.getColumnSelection().columnAt(i);
269
270         if (!av.getColumnSelection().isVisible(index))
271           continue;
272
273         if (anot[index] == null)
274         {
275           anot[index] = new Annotation(label, "", ' ', 0); // TODO: verify that
276           // null exceptions
277           // aren't raised
278           // elsewhere.
279         }
280         else
281         {
282           anot[index].displayCharacter = label;
283         }
284       }
285     }
286     else if (evt.getActionCommand().equals(COLOUR))
287     {
288       Color col = JColorChooser.showDialog(this,
289               "Choose foreground colour", Color.black);
290
291       for (int i = 0; i < av.getColumnSelection().size(); i++)
292       {
293         int index = av.getColumnSelection().columnAt(i);
294
295         if (!av.getColumnSelection().isVisible(index))
296           continue;
297
298         if (anot[index] == null)
299         {
300           anot[index] = new Annotation("", "", ' ', 0);
301         }
302
303         anot[index].colour = col;
304       }
305     }
306     else
307     // HELIX OR SHEET
308     {
309       char type = 0;
310       String symbol = "\u03B1";
311
312       if (evt.getActionCommand().equals(HELIX))
313       {
314         type = 'H';
315       }
316       else if (evt.getActionCommand().equals(SHEET))
317       {
318         type = 'E';
319         symbol = "\u03B2";
320       }
321
322       // Added by LML to color stems
323       else if (evt.getActionCommand().equals(STEM))
324       {
325         type = 'S';
326         symbol = "\u03C3";
327       }
328
329       if (!aa[activeRow].hasIcons)
330       {
331         aa[activeRow].hasIcons = true;
332       }
333
334       String label = JOptionPane.showInputDialog(
335               "Enter a label for the structure?", symbol);
336
337       if (label == null)
338       {
339         return;
340       }
341
342       if ((label.length() > 0) && !aa[activeRow].hasText)
343       {
344         aa[activeRow].hasText = true;
345       }
346
347       for (int i = 0; i < av.getColumnSelection().size(); i++)
348       {
349         int index = av.getColumnSelection().columnAt(i);
350
351         if (!av.getColumnSelection().isVisible(index))
352           continue;
353
354         if (anot[index] == null)
355         {
356           anot[index] = new Annotation(label, "", type, 0);
357         }
358
359         anot[index].secondaryStructure = type;
360         anot[index].displayCharacter = label;
361       }
362     }
363     aa[activeRow].validateRangeAndDisplay();
364
365     adjustPanelHeight();
366     repaint();
367
368     return;
369   }
370
371   private String collectAnnotVals(Annotation[] anot,
372           ColumnSelection columnSelection, String label2)
373   {
374     String collatedInput = "";
375     String last = "";
376     ColumnSelection viscols=av.getColumnSelection();
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 (!viscols.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   @Override
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   @Override
517   public void mouseReleased(MouseEvent evt)
518   {
519     graphStretch = -1;
520     graphStretchY = -1;
521     mouseDragging = false;
522     ap.scalePanel.mouseReleased(evt);
523   }
524
525   /**
526    * DOCUMENT ME!
527    *
528    * @param evt
529    *          DOCUMENT ME!
530    */
531   @Override
532   public void mouseEntered(MouseEvent evt)
533   {
534     ap.scalePanel.mouseEntered(evt);
535   }
536
537   /**
538    * DOCUMENT ME!
539    *
540    * @param evt
541    *          DOCUMENT ME!
542    */
543   @Override
544   public void mouseExited(MouseEvent evt)
545   {
546     ap.scalePanel.mouseExited(evt);
547   }
548
549   /**
550    * DOCUMENT ME!
551    *
552    * @param evt
553    *          DOCUMENT ME!
554    */
555   @Override
556   public void mouseDragged(MouseEvent evt)
557   {
558     if (graphStretch > -1)
559     {
560       av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
561               - evt.getY();
562       if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0)
563       {
564         av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0;
565       }
566       graphStretchY = evt.getY();
567       adjustPanelHeight();
568       ap.paintAlignment(true);
569     }
570     else
571     {
572       ap.scalePanel.mouseDragged(evt);
573     }
574   }
575
576   /**
577    * DOCUMENT ME!
578    *
579    * @param evt
580    *          DOCUMENT ME!
581    */
582   @Override
583   public void mouseMoved(MouseEvent evt)
584   {
585     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
586
587     if (aa == null)
588     {
589       this.setToolTipText(null);
590       return;
591     }
592
593     int row = -1;
594     int height = 0;
595
596     for (int i = 0; i < aa.length; i++)
597     {
598       if (aa[i].visible)
599       {
600         height += aa[i].height;
601       }
602
603       if (evt.getY() < height)
604       {
605         row = i;
606
607         break;
608       }
609     }
610
611     if (row == -1)
612     {
613       this.setToolTipText(null);
614       return;
615     }
616
617     int res = (evt.getX() / av.getCharWidth()) + av.getStartRes();
618
619     if (av.hasHiddenColumns())
620     {
621       res = av.getColumnSelection().adjustForHiddenColumns(res);
622     }
623
624     if (row > -1 && aa[row].annotations != null
625             && res < aa[row].annotations.length)
626     {
627       if (aa[row].graphGroup > -1)
628       {
629         StringBuffer tip = new StringBuffer("<html>");
630         for (int gg = 0; gg < aa.length; gg++)
631         {
632           if (aa[gg].graphGroup == aa[row].graphGroup
633                   && aa[gg].annotations[res] != null)
634           {
635             tip.append(aa[gg].label + " "
636                     + aa[gg].annotations[res].description + "<br>");
637           }
638         }
639         if (tip.length() != 6)
640         {
641           tip.setLength(tip.length() - 4);
642           this.setToolTipText(tip.toString() + "</html>");
643         }
644       }
645       else if (aa[row].annotations[res] != null
646               && aa[row].annotations[res].description != null
647               && aa[row].annotations[res].description.length() > 0)
648       {
649         this.setToolTipText(aa[row].annotations[res].description);
650       }
651       else
652       {
653         // clear the tooltip.
654         this.setToolTipText(null);
655       }
656
657       if (aa[row].annotations[res] != null)
658       {
659         StringBuffer text = new StringBuffer("Sequence position "
660                 + (res + 1));
661
662         if (aa[row].annotations[res].description != null)
663         {
664           text.append("  " + aa[row].annotations[res].description);
665         }
666
667         ap.alignFrame.statusBar.setText(text.toString());
668       }
669     }
670     else
671     {
672       this.setToolTipText(null);
673     }
674   }
675
676   /**
677    * DOCUMENT ME!
678    *
679    * @param evt
680    *          DOCUMENT ME!
681    */
682   @Override
683   public void mouseClicked(MouseEvent evt)
684   {
685     if (activeRow != -1)
686     {
687       AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
688       AlignmentAnnotation anot = aa[activeRow];
689
690       if (anot.description.equals("secondary structure"))
691       {
692         // System.out.println(anot.description+" "+anot.getRNAStruc());
693       }
694     }
695   }
696
697   // TODO mouseClicked-content and drawCursor are quite experimental!
698   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
699           int y1)
700   {
701     int pady = av.charHeight / 5;
702     int charOffset = 0;
703     graphics.setColor(Color.black);
704     graphics.fillRect(x1, y1, av.charWidth, av.charHeight);
705
706     if (av.validCharWidth)
707     {
708       graphics.setColor(Color.white);
709
710       char s = seq.getCharAt(res);
711
712       charOffset = (av.charWidth - fm.charWidth(s)) / 2;
713       graphics.drawString(String.valueOf(s), charOffset + x1,
714               (y1 + av.charHeight) - pady);
715     }
716
717   }
718   private volatile boolean imageFresh=false;
719   /**
720    * DOCUMENT ME!
721    *
722    * @param g
723    *          DOCUMENT ME!
724    */
725   @Override
726   public void paintComponent(Graphics g)
727   {
728     g.setColor(Color.white);
729     g.fillRect(0, 0, getWidth(), getHeight());
730
731     if (image != null)
732     {
733       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
734               || (getVisibleRect().height != g.getClipBounds().height))
735       {
736         g.drawImage(image, 0, 0, this);
737         fastPaint = false;
738         return;
739       }
740     }
741     imgWidth = (av.endRes - av.startRes + 1) * av.charWidth;
742     if (imgWidth < 1)
743       return;
744     if (image == null || imgWidth != image.getWidth()
745             || image.getHeight(this) != getHeight())
746     {
747       image = new BufferedImage(imgWidth, ap.annotationPanel.getHeight(),
748               BufferedImage.TYPE_INT_RGB);
749       gg = (Graphics2D) image.getGraphics();
750
751       if (av.antiAlias)
752       {
753         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
754                 RenderingHints.VALUE_ANTIALIAS_ON);
755       }
756
757       gg.setFont(av.getFont());
758       fm = gg.getFontMetrics();
759       gg.setColor(Color.white);
760       gg.fillRect(0, 0, imgWidth, image.getHeight());
761       imageFresh=true;
762     }
763
764     drawComponent(gg, av.startRes, av.endRes + 1);
765     imageFresh=false;
766     g.drawImage(image, 0, 0, this);
767   }
768
769   /**
770    * non-Thread safe repaint
771    *
772    * @param horizontal
773    *          repaint with horizontal shift in alignment
774    */
775   public void fastPaint(int horizontal)
776   {
777
778     if ((horizontal == 0) || gg == null
779             || av.getAlignment().getAlignmentAnnotation() == null
780             || av.getAlignment().getAlignmentAnnotation().length < 1
781             || av.isCalcInProgress())
782     {
783       repaint();
784       return;
785     }
786     gg.copyArea(0, 0, imgWidth, getHeight(), -horizontal * av.charWidth, 0);
787
788     int sr = av.startRes;
789     int er = av.endRes + 1;
790     int transX = 0;
791
792     if (horizontal > 0) // scrollbar pulled right, image to the left
793     {
794       transX = (er - sr - horizontal) * av.charWidth;
795       sr = er - horizontal;
796     }
797     else if (horizontal < 0)
798     {
799       er = sr - horizontal;
800     }
801
802     gg.translate(transX, 0);
803
804     drawComponent(gg, sr, er);
805
806     gg.translate(-transX, 0);
807
808     fastPaint = true;
809     repaint();
810
811   }
812   private volatile boolean lastImageGood=false;
813   /**
814    * DOCUMENT ME!
815    *
816    * @param g
817    *          DOCUMENT ME!
818    * @param startRes
819    *          DOCUMENT ME!
820    * @param endRes
821    *          DOCUMENT ME!
822    */
823   public void drawComponent(Graphics g, int startRes, int endRes)
824   {
825     BufferedImage oldFaded=fadedImage;
826     if (av.isCalcInProgress())
827     {
828       if (image == null)
829       {
830         lastImageGood=false;
831         return;
832       }
833       // We'll keep a record of the old image,
834       // and draw a faded image until the calculation
835       // has completed
836       if (lastImageGood && (fadedImage == null || fadedImage.getWidth() != imgWidth
837               || fadedImage.getHeight() != image.getHeight()))
838       {
839 //        System.err.println("redraw faded image ("+(fadedImage==null ? "null image" : "") + " lastGood="+lastImageGood+")");
840         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
841                 BufferedImage.TYPE_INT_RGB);
842
843         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
844
845         fadedG.setColor(Color.white);
846         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
847
848         fadedG.setComposite(AlphaComposite.getInstance(
849                 AlphaComposite.SRC_OVER, .3f));
850         fadedG.drawImage(image, 0, 0, this);
851
852       }
853       // make sure we don't overwrite the last good faded image until all calculations have finished
854       lastImageGood=false;
855
856     }
857     else
858     {
859       if (fadedImage!=null)
860       {
861         oldFaded=fadedImage;
862       }
863       fadedImage = null;
864     }
865
866     g.setColor(Color.white);
867     g.fillRect(0, 0, (endRes - startRes) * av.charWidth, getHeight());
868
869     g.setFont(av.getFont());
870     if (fm == null)
871     {
872       fm = g.getFontMetrics();
873     }
874
875     if ((av.getAlignment().getAlignmentAnnotation() == null)
876             || (av.getAlignment().getAlignmentAnnotation().length < 1))
877     {
878       g.setColor(Color.white);
879       g.fillRect(0, 0, getWidth(), getHeight());
880       g.setColor(Color.black);
881       if (av.validCharWidth)
882       {
883         g.drawString("Alignment has no annotations", 20, 15);
884       }
885
886       return;
887     }
888     lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes, endRes);
889     if (!lastImageGood && fadedImage==null)
890     {
891       fadedImage=oldFaded;
892     }
893   }
894
895   @Override
896   public FontMetrics getFontMetrics()
897   {
898     return fm;
899   }
900
901   @Override
902   public Image getFadedImage()
903   {
904     return fadedImage;
905   }
906
907   @Override
908   public int getFadedImageWidth()
909   {
910     return imgWidth;
911   }
912 }