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