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