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