JAL-2930 use MouseWheelEvent.getPreciseWheelRotation, not getWheelRotation
[jalview.git] / src / jalview / gui / AnnotationPanel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
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
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.datamodel.AlignmentAnnotation;
24 import jalview.datamodel.Annotation;
25 import jalview.datamodel.ColumnSelection;
26 import jalview.datamodel.HiddenColumns;
27 import jalview.datamodel.SequenceI;
28 import jalview.renderer.AnnotationRenderer;
29 import jalview.renderer.AwtRenderPanelI;
30 import jalview.schemes.ResidueProperties;
31 import jalview.util.Comparison;
32 import jalview.util.MessageManager;
33 import jalview.viewmodel.ViewportListenerI;
34 import jalview.viewmodel.ViewportRanges;
35
36 import java.awt.AlphaComposite;
37 import java.awt.Color;
38 import java.awt.Dimension;
39 import java.awt.FontMetrics;
40 import java.awt.Graphics;
41 import java.awt.Graphics2D;
42 import java.awt.Image;
43 import java.awt.Rectangle;
44 import java.awt.RenderingHints;
45 import java.awt.event.ActionEvent;
46 import java.awt.event.ActionListener;
47 import java.awt.event.AdjustmentEvent;
48 import java.awt.event.AdjustmentListener;
49 import java.awt.event.MouseEvent;
50 import java.awt.event.MouseListener;
51 import java.awt.event.MouseMotionListener;
52 import java.awt.event.MouseWheelEvent;
53 import java.awt.event.MouseWheelListener;
54 import java.awt.image.BufferedImage;
55 import java.beans.PropertyChangeEvent;
56 import java.util.ArrayList;
57 import java.util.Collections;
58 import java.util.List;
59
60 import javax.swing.JColorChooser;
61 import javax.swing.JMenuItem;
62 import javax.swing.JPanel;
63 import javax.swing.JPopupMenu;
64 import javax.swing.Scrollable;
65 import javax.swing.ToolTipManager;
66
67 /**
68  * AnnotationPanel displays visible portion of annotation rows below unwrapped
69  * alignment
70  * 
71  * @author $author$
72  * @version $Revision$
73  */
74 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
75         MouseListener, MouseWheelListener, MouseMotionListener,
76         ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
77 {
78   String HELIX = MessageManager.getString("label.helix");
79
80   String SHEET = MessageManager.getString("label.sheet");
81
82   /**
83    * For RNA secondary structure "stems" aka helices
84    */
85   String STEM = MessageManager.getString("label.rna_helix");
86
87   String LABEL = MessageManager.getString("label.label");
88
89   String REMOVE = MessageManager.getString("label.remove_annotation");
90
91   String COLOUR = MessageManager.getString("action.colour");
92
93   public final Color HELIX_COLOUR = Color.red.darker();
94
95   public final Color SHEET_COLOUR = Color.green.darker().darker();
96
97   public final Color STEM_COLOUR = Color.blue.darker();
98
99   /** DOCUMENT ME!! */
100   public AlignViewport av;
101
102   AlignmentPanel ap;
103
104   public int activeRow = -1;
105
106   public BufferedImage image;
107
108   public volatile BufferedImage fadedImage;
109
110   Graphics2D gg;
111
112   public FontMetrics fm;
113
114   public int imgWidth = 0;
115
116   boolean fastPaint = false;
117
118   // Used For mouse Dragging and resizing graphs
119   int graphStretch = -1;
120
121   int graphStretchY = -1;
122
123   int min; // used by mouseDragged to see if user
124
125   int max; // used by mouseDragged to see if user
126
127   boolean mouseDragging = false;
128
129   // for editing cursor
130   int cursorX = 0;
131
132   int cursorY = 0;
133
134   public final AnnotationRenderer renderer;
135
136   private MouseWheelListener[] _mwl;
137
138   /**
139    * Creates a new AnnotationPanel object.
140    * 
141    * @param ap
142    *          DOCUMENT ME!
143    */
144   public AnnotationPanel(AlignmentPanel ap)
145   {
146     ToolTipManager.sharedInstance().registerComponent(this);
147     ToolTipManager.sharedInstance().setInitialDelay(0);
148     ToolTipManager.sharedInstance().setDismissDelay(10000);
149     this.ap = ap;
150     av = ap.av;
151     this.setLayout(null);
152     addMouseListener(this);
153     addMouseMotionListener(this);
154     ap.annotationScroller.getVerticalScrollBar()
155             .addAdjustmentListener(this);
156     // save any wheel listeners on the scroller, so we can propagate scroll
157     // events to them.
158     _mwl = ap.annotationScroller.getMouseWheelListeners();
159     // and then set our own listener to consume all mousewheel events
160     ap.annotationScroller.addMouseWheelListener(this);
161     renderer = new AnnotationRenderer();
162
163     av.getRanges().addPropertyChangeListener(this);
164   }
165
166   public AnnotationPanel(AlignViewport av)
167   {
168     this.av = av;
169     renderer = new AnnotationRenderer();
170   }
171
172   @Override
173   public void mouseWheelMoved(MouseWheelEvent e)
174   {
175     if (e.isShiftDown())
176     {
177       e.consume();
178       double wheelRotation = e.getPreciseWheelRotation();
179       if (wheelRotation > 0)
180       {
181         av.getRanges().scrollRight(true);
182       }
183       else if (wheelRotation < 0)
184       {
185         av.getRanges().scrollRight(false);
186       }
187     }
188     else
189     {
190       // TODO: find the correct way to let the event bubble up to
191       // ap.annotationScroller
192       for (MouseWheelListener mwl : _mwl)
193       {
194         if (mwl != null)
195         {
196           mwl.mouseWheelMoved(e);
197         }
198         if (e.isConsumed())
199         {
200           break;
201         }
202       }
203     }
204   }
205
206   @Override
207   public Dimension getPreferredScrollableViewportSize()
208   {
209     return getPreferredSize();
210   }
211
212   @Override
213   public int getScrollableBlockIncrement(Rectangle visibleRect,
214           int orientation, int direction)
215   {
216     return 30;
217   }
218
219   @Override
220   public boolean getScrollableTracksViewportHeight()
221   {
222     return false;
223   }
224
225   @Override
226   public boolean getScrollableTracksViewportWidth()
227   {
228     return true;
229   }
230
231   @Override
232   public int getScrollableUnitIncrement(Rectangle visibleRect,
233           int orientation, int direction)
234   {
235     return 30;
236   }
237
238   /*
239    * (non-Javadoc)
240    * 
241    * @see
242    * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
243    * .AdjustmentEvent)
244    */
245   @Override
246   public void adjustmentValueChanged(AdjustmentEvent evt)
247   {
248     // update annotation label display
249     ap.getAlabels().setScrollOffset(-evt.getValue());
250   }
251
252   /**
253    * Calculates the height of the annotation displayed in the annotation panel.
254    * Callers should normally call the ap.adjustAnnotationHeight method to ensure
255    * all annotation associated components are updated correctly.
256    * 
257    */
258   public int adjustPanelHeight()
259   {
260     int height = av.calcPanelHeight();
261     this.setPreferredSize(new Dimension(1, height));
262     if (ap != null)
263     {
264       // revalidate only when the alignment panel is fully constructed
265       ap.validate();
266     }
267
268     return height;
269   }
270
271   /**
272    * DOCUMENT ME!
273    * 
274    * @param evt
275    *          DOCUMENT ME!
276    */
277   @Override
278   public void actionPerformed(ActionEvent evt)
279   {
280     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
281     if (aa == null)
282     {
283       return;
284     }
285     Annotation[] anot = aa[activeRow].annotations;
286
287     if (anot.length < av.getColumnSelection().getMax())
288     {
289       Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
290               + 2];
291       System.arraycopy(anot, 0, temp, 0, anot.length);
292       anot = temp;
293       aa[activeRow].annotations = anot;
294     }
295
296     String action = evt.getActionCommand();
297     if (action.equals(REMOVE))
298     {
299       for (int index : av.getColumnSelection().getSelected())
300       {
301         if (av.getAlignment().getHiddenColumns().isVisible(index))
302         {
303           anot[index] = null;
304         }
305       }
306     }
307     else if (action.equals(LABEL))
308     {
309       String exMesg = collectAnnotVals(anot, LABEL);
310       String label = JvOptionPane.showInputDialog(this,
311               MessageManager.getString("label.enter_label"), exMesg);
312
313       if (label == null)
314       {
315         return;
316       }
317
318       if ((label.length() > 0) && !aa[activeRow].hasText)
319       {
320         aa[activeRow].hasText = true;
321       }
322
323       for (int index : av.getColumnSelection().getSelected())
324       {
325         if (!av.getAlignment().getHiddenColumns().isVisible(index))
326         {
327           continue;
328         }
329
330         if (anot[index] == null)
331         {
332           anot[index] = new Annotation(label, "", ' ', 0);
333         }
334         else
335         {
336           anot[index].displayCharacter = label;
337         }
338       }
339     }
340     else if (action.equals(COLOUR))
341     {
342       Color col = JColorChooser.showDialog(this,
343               MessageManager.getString("label.select_foreground_colour"),
344               Color.black);
345
346       for (int index : av.getColumnSelection().getSelected())
347       {
348         if (!av.getAlignment().getHiddenColumns().isVisible(index))
349         {
350           continue;
351         }
352
353         if (anot[index] == null)
354         {
355           anot[index] = new Annotation("", "", ' ', 0);
356         }
357
358         anot[index].colour = col;
359       }
360     }
361     else
362     // HELIX, SHEET or STEM
363     {
364       char type = 0;
365       String symbol = "\u03B1"; // alpha
366
367       if (action.equals(HELIX))
368       {
369         type = 'H';
370       }
371       else if (action.equals(SHEET))
372       {
373         type = 'E';
374         symbol = "\u03B2"; // beta
375       }
376
377       // Added by LML to color stems
378       else if (action.equals(STEM))
379       {
380         type = 'S';
381         int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
382         symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
383       }
384
385       if (!aa[activeRow].hasIcons)
386       {
387         aa[activeRow].hasIcons = true;
388       }
389
390       String label = JvOptionPane.showInputDialog(MessageManager
391               .getString("label.enter_label_for_the_structure"), symbol);
392
393       if (label == null)
394       {
395         return;
396       }
397
398       if ((label.length() > 0) && !aa[activeRow].hasText)
399       {
400         aa[activeRow].hasText = true;
401         if (action.equals(STEM))
402         {
403           aa[activeRow].showAllColLabels = true;
404         }
405       }
406       for (int index : av.getColumnSelection().getSelected())
407       {
408         if (!av.getAlignment().getHiddenColumns().isVisible(index))
409         {
410           continue;
411         }
412
413         if (anot[index] == null)
414         {
415           anot[index] = new Annotation(label, "", type, 0);
416         }
417
418         anot[index].secondaryStructure = type != 'S' ? type
419                 : label.length() == 0 ? ' ' : label.charAt(0);
420         anot[index].displayCharacter = label;
421
422       }
423     }
424
425     av.getAlignment().validateAnnotation(aa[activeRow]);
426     ap.alignmentChanged();
427     ap.alignFrame.setMenusForViewport();
428     adjustPanelHeight();
429     repaint();
430
431     return;
432   }
433
434   /**
435    * Returns any existing annotation concatenated as a string. For each
436    * annotation, takes the description, if any, else the secondary structure
437    * character (if type is HELIX, SHEET or STEM), else the display character (if
438    * type is LABEL).
439    * 
440    * @param anots
441    * @param type
442    * @return
443    */
444   private String collectAnnotVals(Annotation[] anots, String type)
445   {
446     // TODO is this method wanted? why? 'last' is not used
447
448     StringBuilder collatedInput = new StringBuilder(64);
449     String last = "";
450     ColumnSelection viscols = av.getColumnSelection();
451     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
452
453     /*
454      * the selection list (read-only view) is in selection order, not
455      * column order; make a copy so we can sort it
456      */
457     List<Integer> selected = new ArrayList<>(viscols.getSelected());
458     Collections.sort(selected);
459     for (int index : selected)
460     {
461       // always check for current display state - just in case
462       if (!hidden.isVisible(index))
463       {
464         continue;
465       }
466       String tlabel = null;
467       if (anots[index] != null)
468       { // LML added stem code
469         if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
470                 || type.equals(LABEL))
471         {
472           tlabel = anots[index].description;
473           if (tlabel == null || tlabel.length() < 1)
474           {
475             if (type.equals(HELIX) || type.equals(SHEET)
476                     || type.equals(STEM))
477             {
478               tlabel = "" + anots[index].secondaryStructure;
479             }
480             else
481             {
482               tlabel = "" + anots[index].displayCharacter;
483             }
484           }
485         }
486         if (tlabel != null && !tlabel.equals(last))
487         {
488           if (last.length() > 0)
489           {
490             collatedInput.append(" ");
491           }
492           collatedInput.append(tlabel);
493         }
494       }
495     }
496     return collatedInput.toString();
497   }
498
499   /**
500    * DOCUMENT ME!
501    * 
502    * @param evt
503    *          DOCUMENT ME!
504    */
505   @Override
506   public void mousePressed(MouseEvent evt)
507   {
508
509     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
510     if (aa == null)
511     {
512       return;
513     }
514
515     int height = 0;
516     activeRow = -1;
517
518     final int y = evt.getY();
519     for (int i = 0; i < aa.length; i++)
520     {
521       if (aa[i].visible)
522       {
523         height += aa[i].height;
524       }
525
526       if (y < height)
527       {
528         if (aa[i].editable)
529         {
530           activeRow = i;
531         }
532         else if (aa[i].graph > 0)
533         {
534           // Stretch Graph
535           graphStretch = i;
536           graphStretchY = y;
537         }
538
539         break;
540       }
541     }
542
543     /*
544      * isPopupTrigger fires in mousePressed on Mac,
545      * not until mouseRelease on Windows
546      */
547     if (evt.isPopupTrigger() && activeRow != -1)
548     {
549       showPopupMenu(y, evt.getX());
550       return;
551     }
552
553     ap.getScalePanel().mousePressed(evt);
554   }
555
556   /**
557    * Construct and display a context menu at the right-click position
558    * 
559    * @param y
560    * @param x
561    */
562   void showPopupMenu(final int y, int x)
563   {
564     if (av.getColumnSelection() == null
565             || av.getColumnSelection().isEmpty())
566     {
567       return;
568     }
569
570     JPopupMenu pop = new JPopupMenu(
571             MessageManager.getString("label.structure_type"));
572     JMenuItem item;
573     /*
574      * Just display the needed structure options
575      */
576     if (av.getAlignment().isNucleotide())
577     {
578       item = new JMenuItem(STEM);
579       item.addActionListener(this);
580       pop.add(item);
581     }
582     else
583     {
584       item = new JMenuItem(HELIX);
585       item.addActionListener(this);
586       pop.add(item);
587       item = new JMenuItem(SHEET);
588       item.addActionListener(this);
589       pop.add(item);
590     }
591     item = new JMenuItem(LABEL);
592     item.addActionListener(this);
593     pop.add(item);
594     item = new JMenuItem(COLOUR);
595     item.addActionListener(this);
596     pop.add(item);
597     item = new JMenuItem(REMOVE);
598     item.addActionListener(this);
599     pop.add(item);
600     pop.show(this, x, y);
601   }
602
603   /**
604    * DOCUMENT ME!
605    * 
606    * @param evt
607    *          DOCUMENT ME!
608    */
609   @Override
610   public void mouseReleased(MouseEvent evt)
611   {
612     graphStretch = -1;
613     graphStretchY = -1;
614     mouseDragging = false;
615     ap.getScalePanel().mouseReleased(evt);
616
617     /*
618      * isPopupTrigger is set in mouseReleased on Windows
619      * (in mousePressed on Mac)
620      */
621     if (evt.isPopupTrigger() && activeRow != -1)
622     {
623       showPopupMenu(evt.getY(), evt.getX());
624     }
625
626   }
627
628   /**
629    * DOCUMENT ME!
630    * 
631    * @param evt
632    *          DOCUMENT ME!
633    */
634   @Override
635   public void mouseEntered(MouseEvent evt)
636   {
637     ap.getScalePanel().mouseEntered(evt);
638   }
639
640   /**
641    * DOCUMENT ME!
642    * 
643    * @param evt
644    *          DOCUMENT ME!
645    */
646   @Override
647   public void mouseExited(MouseEvent evt)
648   {
649     ap.getScalePanel().mouseExited(evt);
650   }
651
652   /**
653    * DOCUMENT ME!
654    * 
655    * @param evt
656    *          DOCUMENT ME!
657    */
658   @Override
659   public void mouseDragged(MouseEvent evt)
660   {
661     if (graphStretch > -1)
662     {
663       av.getAlignment()
664               .getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
665                       - evt.getY();
666       if (av.getAlignment()
667               .getAlignmentAnnotation()[graphStretch].graphHeight < 0)
668       {
669         av.getAlignment()
670                 .getAlignmentAnnotation()[graphStretch].graphHeight = 0;
671       }
672       graphStretchY = evt.getY();
673       adjustPanelHeight();
674       ap.paintAlignment(false, false);
675     }
676     else
677     {
678       ap.getScalePanel().mouseDragged(evt);
679     }
680   }
681
682   /**
683    * Constructs the tooltip, and constructs and displays a status message, for
684    * the current mouse position
685    * 
686    * @param evt
687    */
688   @Override
689   public void mouseMoved(MouseEvent evt)
690   {
691     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
692
693     if (aa == null)
694     {
695       this.setToolTipText(null);
696       return;
697     }
698
699     int row = -1;
700     int height = 0;
701
702     for (int i = 0; i < aa.length; i++)
703     {
704       if (aa[i].visible)
705       {
706         height += aa[i].height;
707       }
708
709       if (evt.getY() < height)
710       {
711         row = i;
712         break;
713       }
714     }
715
716     if (row == -1)
717     {
718       this.setToolTipText(null);
719       return;
720     }
721
722     int column = (evt.getX() / av.getCharWidth())
723             + av.getRanges().getStartRes();
724
725     if (av.hasHiddenColumns())
726     {
727       column = av.getAlignment().getHiddenColumns()
728               .visibleToAbsoluteColumn(column);
729     }
730
731     AlignmentAnnotation ann = aa[row];
732     if (row > -1 && ann.annotations != null
733             && column < ann.annotations.length)
734     {
735       buildToolTip(ann, column, aa);
736       setStatusMessage(column, ann);
737     }
738     else
739     {
740       this.setToolTipText(null);
741       ap.alignFrame.statusBar.setText(" ");
742     }
743   }
744
745   /**
746    * Builds a tooltip for the annotation at the current mouse position.
747    * 
748    * @param ann
749    * @param column
750    * @param anns
751    */
752   void buildToolTip(AlignmentAnnotation ann, int column,
753           AlignmentAnnotation[] anns)
754   {
755     if (ann.graphGroup > -1)
756     {
757       StringBuilder tip = new StringBuilder(32);
758       tip.append("<html>");
759       for (int i = 0; i < anns.length; i++)
760       {
761         if (anns[i].graphGroup == ann.graphGroup
762                 && anns[i].annotations[column] != null)
763         {
764           tip.append(anns[i].label);
765           String description = anns[i].annotations[column].description;
766           if (description != null && description.length() > 0)
767           {
768             tip.append(" ").append(description);
769           }
770           tip.append("<br>");
771         }
772       }
773       if (tip.length() != 6)
774       {
775         tip.setLength(tip.length() - 4);
776         this.setToolTipText(tip.toString() + "</html>");
777       }
778     }
779     else if (ann.annotations[column] != null)
780     {
781       String description = ann.annotations[column].description;
782       if (description != null && description.length() > 0)
783       {
784         this.setToolTipText(JvSwingUtils.wrapTooltip(true, description));
785       }
786       else
787       {
788         this.setToolTipText(null); // no tooltip if null or empty description
789       }
790     }
791     else
792     {
793       // clear the tooltip.
794       this.setToolTipText(null);
795     }
796   }
797
798   /**
799    * Constructs and displays the status bar message
800    * 
801    * @param column
802    * @param ann
803    */
804   void setStatusMessage(int column, AlignmentAnnotation ann)
805   {
806     /*
807      * show alignment column and annotation description if any
808      */
809     StringBuilder text = new StringBuilder(32);
810     text.append(MessageManager.getString("label.column")).append(" ")
811             .append(column + 1);
812
813     if (ann.annotations[column] != null)
814     {
815       String description = ann.annotations[column].description;
816       if (description != null && description.trim().length() > 0)
817       {
818         text.append("  ").append(description);
819       }
820     }
821
822     /*
823      * if the annotation is sequence-specific, show the sequence number
824      * in the alignment, and (if not a gap) the residue and position
825      */
826     SequenceI seqref = ann.sequenceRef;
827     if (seqref != null)
828     {
829       int seqIndex = av.getAlignment().findIndex(seqref);
830       if (seqIndex != -1)
831       {
832         text.append(", ").append(MessageManager.getString("label.sequence"))
833                 .append(" ").append(seqIndex + 1);
834         char residue = seqref.getCharAt(column);
835         if (!Comparison.isGap(residue))
836         {
837           text.append(" ");
838           String name;
839           if (av.getAlignment().isNucleotide())
840           {
841             name = ResidueProperties.nucleotideName
842                     .get(String.valueOf(residue));
843             text.append(" Nucleotide: ")
844                     .append(name != null ? name : residue);
845           }
846           else
847           {
848             name = 'X' == residue ? "X"
849                     : ('*' == residue ? "STOP"
850                             : ResidueProperties.aa2Triplet
851                                     .get(String.valueOf(residue)));
852             text.append(" Residue: ").append(name != null ? name : residue);
853           }
854           int residuePos = seqref.findPosition(column);
855           text.append(" (").append(residuePos).append(")");
856         }
857       }
858     }
859
860     ap.alignFrame.statusBar.setText(text.toString());
861   }
862
863   /**
864    * DOCUMENT ME!
865    * 
866    * @param evt
867    *          DOCUMENT ME!
868    */
869   @Override
870   public void mouseClicked(MouseEvent evt)
871   {
872     // if (activeRow != -1)
873     // {
874     // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
875     // AlignmentAnnotation anot = aa[activeRow];
876     // }
877   }
878
879   // TODO mouseClicked-content and drawCursor are quite experimental!
880   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
881           int y1)
882   {
883     int pady = av.getCharHeight() / 5;
884     int charOffset = 0;
885     graphics.setColor(Color.black);
886     graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
887
888     if (av.validCharWidth)
889     {
890       graphics.setColor(Color.white);
891
892       char s = seq.getCharAt(res);
893
894       charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
895       graphics.drawString(String.valueOf(s), charOffset + x1,
896               (y1 + av.getCharHeight()) - pady);
897     }
898
899   }
900
901   private volatile boolean imageFresh = false;
902
903   /**
904    * DOCUMENT ME!
905    * 
906    * @param g
907    *          DOCUMENT ME!
908    */
909   @Override
910   public void paintComponent(Graphics g)
911   {
912     super.paintComponent(g);
913
914     g.setColor(Color.white);
915     g.fillRect(0, 0, getWidth(), getHeight());
916
917     if (image != null)
918     {
919       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
920               || (getVisibleRect().height != g.getClipBounds().height))
921       {
922         g.drawImage(image, 0, 0, this);
923         fastPaint = false;
924         return;
925       }
926     }
927     imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
928             + 1) * av.getCharWidth();
929     if (imgWidth < 1)
930     {
931       return;
932     }
933     if (image == null || imgWidth != image.getWidth(this)
934             || image.getHeight(this) != getHeight())
935     {
936       try
937       {
938         image = new BufferedImage(imgWidth,
939                 ap.getAnnotationPanel().getHeight(),
940                 BufferedImage.TYPE_INT_RGB);
941       } catch (OutOfMemoryError oom)
942       {
943         try
944         {
945           System.gc();
946         } catch (Exception x)
947         {
948         }
949         ;
950         new OOMWarning(
951                 "Couldn't allocate memory to redraw screen. Please restart Jalview",
952                 oom);
953         return;
954       }
955       gg = (Graphics2D) image.getGraphics();
956
957       if (av.antiAlias)
958       {
959         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
960                 RenderingHints.VALUE_ANTIALIAS_ON);
961       }
962
963       gg.setFont(av.getFont());
964       fm = gg.getFontMetrics();
965       gg.setColor(Color.white);
966       gg.fillRect(0, 0, imgWidth, image.getHeight());
967       imageFresh = true;
968     }
969     
970     drawComponent(gg, av.getRanges().getStartRes(),
971             av.getRanges().getEndRes() + 1);
972     imageFresh = false;
973     g.drawImage(image, 0, 0, this);
974   }
975
976   /**
977    * set true to enable redraw timing debug output on stderr
978    */
979   private final boolean debugRedraw = false;
980
981   /**
982    * non-Thread safe repaint
983    * 
984    * @param horizontal
985    *          repaint with horizontal shift in alignment
986    */
987   public void fastPaint(int horizontal)
988   {
989     if ((horizontal == 0) || gg == null
990             || av.getAlignment().getAlignmentAnnotation() == null
991             || av.getAlignment().getAlignmentAnnotation().length < 1
992             || av.isCalcInProgress())
993     {
994       repaint();
995       return;
996     }
997
998     int sr = av.getRanges().getStartRes();
999     int er = av.getRanges().getEndRes() + 1;
1000     int transX = 0;
1001
1002     gg.copyArea(0, 0, imgWidth, getHeight(),
1003             -horizontal * av.getCharWidth(), 0);
1004
1005     if (horizontal > 0) // scrollbar pulled right, image to the left
1006     {
1007       transX = (er - sr - horizontal) * av.getCharWidth();
1008       sr = er - horizontal;
1009     }
1010     else if (horizontal < 0)
1011     {
1012       er = sr - horizontal;
1013     }
1014
1015     gg.translate(transX, 0);
1016
1017     drawComponent(gg, sr, er);
1018
1019     gg.translate(-transX, 0);
1020
1021     fastPaint = true;
1022
1023     // Call repaint on alignment panel so that repaints from other alignment
1024     // panel components can be aggregated. Otherwise performance of the overview
1025     // window and others may be adversely affected.
1026     av.getAlignPanel().repaint();
1027   }
1028
1029   private volatile boolean lastImageGood = false;
1030
1031   /**
1032    * DOCUMENT ME!
1033    * 
1034    * @param g
1035    *          DOCUMENT ME!
1036    * @param startRes
1037    *          DOCUMENT ME!
1038    * @param endRes
1039    *          DOCUMENT ME!
1040    */
1041   public void drawComponent(Graphics g, int startRes, int endRes)
1042   {
1043     BufferedImage oldFaded = fadedImage;
1044     if (av.isCalcInProgress())
1045     {
1046       if (image == null)
1047       {
1048         lastImageGood = false;
1049         return;
1050       }
1051       // We'll keep a record of the old image,
1052       // and draw a faded image until the calculation
1053       // has completed
1054       if (lastImageGood
1055               && (fadedImage == null || fadedImage.getWidth() != imgWidth
1056                       || fadedImage.getHeight() != image.getHeight()))
1057       {
1058         // System.err.println("redraw faded image ("+(fadedImage==null ?
1059         // "null image" : "") + " lastGood="+lastImageGood+")");
1060         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1061                 BufferedImage.TYPE_INT_RGB);
1062
1063         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1064
1065         fadedG.setColor(Color.white);
1066         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1067
1068         fadedG.setComposite(
1069                 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1070         fadedG.drawImage(image, 0, 0, this);
1071
1072       }
1073       // make sure we don't overwrite the last good faded image until all
1074       // calculations have finished
1075       lastImageGood = false;
1076
1077     }
1078     else
1079     {
1080       if (fadedImage != null)
1081       {
1082         oldFaded = fadedImage;
1083       }
1084       fadedImage = null;
1085     }
1086
1087     g.setColor(Color.white);
1088     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1089
1090     g.setFont(av.getFont());
1091     if (fm == null)
1092     {
1093       fm = g.getFontMetrics();
1094     }
1095
1096     if ((av.getAlignment().getAlignmentAnnotation() == null)
1097             || (av.getAlignment().getAlignmentAnnotation().length < 1))
1098     {
1099       g.setColor(Color.white);
1100       g.fillRect(0, 0, getWidth(), getHeight());
1101       g.setColor(Color.black);
1102       if (av.validCharWidth)
1103       {
1104         g.drawString(MessageManager
1105                 .getString("label.alignment_has_no_annotations"), 20, 15);
1106       }
1107
1108       return;
1109     }
1110     lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1111             endRes);
1112     if (!lastImageGood && fadedImage == null)
1113     {
1114       fadedImage = oldFaded;
1115     }
1116   }
1117
1118   @Override
1119   public FontMetrics getFontMetrics()
1120   {
1121     return fm;
1122   }
1123
1124   @Override
1125   public Image getFadedImage()
1126   {
1127     return fadedImage;
1128   }
1129
1130   @Override
1131   public int getFadedImageWidth()
1132   {
1133     return imgWidth;
1134   }
1135
1136   private int[] bounds = new int[2];
1137
1138   @Override
1139   public int[] getVisibleVRange()
1140   {
1141     if (ap != null && ap.getAlabels() != null)
1142     {
1143       int sOffset = -ap.getAlabels().getScrollOffset();
1144       int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1145       bounds[0] = sOffset;
1146       bounds[1] = visHeight;
1147       return bounds;
1148     }
1149     else
1150     {
1151       return null;
1152     }
1153   }
1154
1155   /**
1156    * Try to ensure any references held are nulled
1157    */
1158   public void dispose()
1159   {
1160     av = null;
1161     ap = null;
1162     image = null;
1163     fadedImage = null;
1164     gg = null;
1165     _mwl = null;
1166
1167     /*
1168      * I created the renderer so I will dispose of it
1169      */
1170     if (renderer != null)
1171     {
1172       renderer.dispose();
1173     }
1174   }
1175
1176   @Override
1177   public void propertyChange(PropertyChangeEvent evt)
1178   {
1179     // Respond to viewport range changes (e.g. alignment panel was scrolled)
1180     // Both scrolling and resizing change viewport ranges: scrolling changes
1181     // both start and end points, but resize only changes end values.
1182     // Here we only want to fastpaint on a scroll, with resize using a normal
1183     // paint, so scroll events are identified as changes to the horizontal or
1184     // vertical start value.
1185     if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1186     {
1187       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1188     }
1189     else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1190     {
1191       fastPaint(((int[]) evt.getNewValue())[0]
1192               - ((int[]) evt.getOldValue())[0]);
1193     }
1194     else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1195     {
1196       repaint();
1197     }
1198   }
1199 }