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