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