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