JAL-3073 select columns if dragging sideways in a graph annotation
[jalview.git] / src / jalview / gui / AnnotationPanel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.datamodel.AlignmentAnnotation;
24 import jalview.datamodel.Annotation;
25 import jalview.datamodel.ColumnSelection;
26 import jalview.datamodel.HiddenColumns;
27 import jalview.datamodel.SequenceI;
28 import jalview.renderer.AnnotationRenderer;
29 import jalview.renderer.AwtRenderPanelI;
30 import jalview.schemes.ResidueProperties;
31 import jalview.util.Comparison;
32 import jalview.util.MessageManager;
33 import jalview.viewmodel.ViewportListenerI;
34 import jalview.viewmodel.ViewportRanges;
35
36 import java.awt.AlphaComposite;
37 import java.awt.Color;
38 import java.awt.Dimension;
39 import java.awt.FontMetrics;
40 import java.awt.Graphics;
41 import java.awt.Graphics2D;
42 import java.awt.Image;
43 import java.awt.Rectangle;
44 import java.awt.RenderingHints;
45 import java.awt.event.ActionEvent;
46 import java.awt.event.ActionListener;
47 import java.awt.event.AdjustmentEvent;
48 import java.awt.event.AdjustmentListener;
49 import java.awt.event.MouseEvent;
50 import java.awt.event.MouseListener;
51 import java.awt.event.MouseMotionListener;
52 import java.awt.event.MouseWheelEvent;
53 import java.awt.event.MouseWheelListener;
54 import java.awt.image.BufferedImage;
55 import java.beans.PropertyChangeEvent;
56 import java.util.ArrayList;
57 import java.util.Collections;
58 import java.util.List;
59
60 import javax.swing.JColorChooser;
61 import javax.swing.JMenuItem;
62 import javax.swing.JPanel;
63 import javax.swing.JPopupMenu;
64 import javax.swing.Scrollable;
65 import javax.swing.ToolTipManager;
66
67 /**
68  * AnnotationPanel displays visible portion of annotation rows below unwrapped
69  * alignment
70  * 
71  * @author $author$
72  * @version $Revision$
73  */
74 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
75         MouseListener, MouseWheelListener, MouseMotionListener,
76         ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
77 {
78   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(this,
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       Color col = JColorChooser.showDialog(this,
348               MessageManager.getString("label.select_foreground_colour"),
349               Color.black);
350
351       for (int index : av.getColumnSelection().getSelected())
352       {
353         if (!av.getAlignment().getHiddenColumns().isVisible(index))
354         {
355           continue;
356         }
357
358         if (anot[index] == null)
359         {
360           anot[index] = new Annotation("", "", ' ', 0);
361         }
362
363         anot[index].colour = col;
364       }
365     }
366     else
367     // HELIX, SHEET or STEM
368     {
369       char type = 0;
370       String symbol = "\u03B1"; // alpha
371
372       if (action.equals(HELIX))
373       {
374         type = 'H';
375       }
376       else if (action.equals(SHEET))
377       {
378         type = 'E';
379         symbol = "\u03B2"; // beta
380       }
381
382       // Added by LML to color stems
383       else if (action.equals(STEM))
384       {
385         type = 'S';
386         int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
387         symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
388       }
389
390       if (!aa[activeRow].hasIcons)
391       {
392         aa[activeRow].hasIcons = true;
393       }
394
395       String label = JvOptionPane.showInputDialog(MessageManager
396               .getString("label.enter_label_for_the_structure"), symbol);
397
398       if (label == null)
399       {
400         return;
401       }
402
403       if ((label.length() > 0) && !aa[activeRow].hasText)
404       {
405         aa[activeRow].hasText = true;
406         if (action.equals(STEM))
407         {
408           aa[activeRow].showAllColLabels = true;
409         }
410       }
411       for (int index : av.getColumnSelection().getSelected())
412       {
413         if (!av.getAlignment().getHiddenColumns().isVisible(index))
414         {
415           continue;
416         }
417
418         if (anot[index] == null)
419         {
420           anot[index] = new Annotation(label, "", type, 0);
421         }
422
423         anot[index].secondaryStructure = type != 'S' ? type
424                 : label.length() == 0 ? ' ' : label.charAt(0);
425         anot[index].displayCharacter = label;
426
427       }
428     }
429
430     av.getAlignment().validateAnnotation(aa[activeRow]);
431     ap.alignmentChanged();
432     ap.alignFrame.setMenusForViewport();
433     adjustPanelHeight();
434     repaint();
435
436     return;
437   }
438
439   /**
440    * Returns any existing annotation concatenated as a string. For each
441    * annotation, takes the description, if any, else the secondary structure
442    * character (if type is HELIX, SHEET or STEM), else the display character (if
443    * type is LABEL).
444    * 
445    * @param anots
446    * @param type
447    * @return
448    */
449   private String collectAnnotVals(Annotation[] anots, String type)
450   {
451     // TODO is this method wanted? why? 'last' is not used
452
453     StringBuilder collatedInput = new StringBuilder(64);
454     String last = "";
455     ColumnSelection viscols = av.getColumnSelection();
456     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
457
458     /*
459      * the selection list (read-only view) is in selection order, not
460      * column order; make a copy so we can sort it
461      */
462     List<Integer> selected = new ArrayList<>(viscols.getSelected());
463     Collections.sort(selected);
464     for (int index : selected)
465     {
466       // always check for current display state - just in case
467       if (!hidden.isVisible(index))
468       {
469         continue;
470       }
471       String tlabel = null;
472       if (anots[index] != null)
473       { // LML added stem code
474         if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
475                 || type.equals(LABEL))
476         {
477           tlabel = anots[index].description;
478           if (tlabel == null || tlabel.length() < 1)
479           {
480             if (type.equals(HELIX) || type.equals(SHEET)
481                     || type.equals(STEM))
482             {
483               tlabel = "" + anots[index].secondaryStructure;
484             }
485             else
486             {
487               tlabel = "" + anots[index].displayCharacter;
488             }
489           }
490         }
491         if (tlabel != null && !tlabel.equals(last))
492         {
493           if (last.length() > 0)
494           {
495             collatedInput.append(" ");
496           }
497           collatedInput.append(tlabel);
498         }
499       }
500     }
501     return collatedInput.toString();
502   }
503
504   /**
505    * Action on right mouse pressed on Mac is to show a pop-up menu for the
506    * annotation. Action on left mouse pressed is to find which annotation is
507    * pressed and mark the start of a column selection or graph resize operation.
508    * 
509    * @param evt
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     mouseDragLastX = evt.getX();
521     mouseDragLastY = evt.getY();
522
523     /*
524      * add visible annotation heights until we reach the y
525      * position, to find which annotation it is in
526      */
527     int height = 0;
528     activeRow = -1;
529
530     final int y = evt.getY();
531     for (int i = 0; i < aa.length; i++)
532     {
533       if (aa[i].visible)
534       {
535         height += aa[i].height;
536       }
537
538       if (y < height)
539       {
540         if (aa[i].editable)
541         {
542           activeRow = i;
543         }
544         else if (aa[i].graph > 0)
545         {
546           /*
547            * we have clicked on a resizable graph annotation
548            */
549           graphStretch = i;
550         }
551         break;
552       }
553     }
554
555     /*
556      * isPopupTrigger fires in mousePressed on Mac,
557      * not until mouseRelease on Windows
558      */
559     if (evt.isPopupTrigger() && activeRow != -1)
560     {
561       showPopupMenu(y, evt.getX());
562       return;
563     }
564
565     ap.getScalePanel().mousePressed(evt);
566   }
567
568   /**
569    * Construct and display a context menu at the right-click position
570    * 
571    * @param y
572    * @param x
573    */
574   void showPopupMenu(final int y, int x)
575   {
576     if (av.getColumnSelection() == null
577             || av.getColumnSelection().isEmpty())
578     {
579       return;
580     }
581
582     JPopupMenu pop = new JPopupMenu(
583             MessageManager.getString("label.structure_type"));
584     JMenuItem item;
585     /*
586      * Just display the needed structure options
587      */
588     if (av.getAlignment().isNucleotide())
589     {
590       item = new JMenuItem(STEM);
591       item.addActionListener(this);
592       pop.add(item);
593     }
594     else
595     {
596       item = new JMenuItem(HELIX);
597       item.addActionListener(this);
598       pop.add(item);
599       item = new JMenuItem(SHEET);
600       item.addActionListener(this);
601       pop.add(item);
602     }
603     item = new JMenuItem(LABEL);
604     item.addActionListener(this);
605     pop.add(item);
606     item = new JMenuItem(COLOUR);
607     item.addActionListener(this);
608     pop.add(item);
609     item = new JMenuItem(REMOVE);
610     item.addActionListener(this);
611     pop.add(item);
612     pop.show(this, x, y);
613   }
614
615   /**
616    * Action on mouse up is to clear mouse drag data and call mouseReleased on
617    * ScalePanel, to deal with defining the selection group (if any) defined by
618    * the mouse drag
619    * 
620    * @param evt
621    */
622   @Override
623   public void mouseReleased(MouseEvent evt)
624   {
625     graphStretch = -1;
626     mouseDragLastX = -1;
627     mouseDragLastY = -1;
628     mouseDragging = false;
629     dragMode = DragMode.Undefined;
630     ap.getScalePanel().mouseReleased(evt);
631
632     /*
633      * isPopupTrigger is set in mouseReleased on Windows
634      * (in mousePressed on Mac)
635      */
636     if (evt.isPopupTrigger() && activeRow != -1)
637     {
638       showPopupMenu(evt.getY(), evt.getX());
639     }
640
641   }
642
643   /**
644    * DOCUMENT ME!
645    * 
646    * @param evt
647    *          DOCUMENT ME!
648    */
649   @Override
650   public void mouseEntered(MouseEvent evt)
651   {
652     ap.getScalePanel().mouseEntered(evt);
653   }
654
655   /**
656    * DOCUMENT ME!
657    * 
658    * @param evt
659    *          DOCUMENT ME!
660    */
661   @Override
662   public void mouseExited(MouseEvent evt)
663   {
664     ap.getScalePanel().mouseExited(evt);
665   }
666
667   /**
668    * DOCUMENT ME!
669    * 
670    * @param evt
671    *          DOCUMENT ME!
672    */
673   @Override
674   public void mouseDragged(MouseEvent evt)
675   {
676     /*
677      * todo: if dragMode is Undefined:
678      * - set to Select if dx > dy
679      * - set to Resize if dy > dx
680      * - do nothing if dx == dy
681      */
682     final int x = evt.getX();
683     final int y = evt.getY();
684     if (dragMode == DragMode.Undefined)
685     {
686       int dx = Math.abs(x - mouseDragLastX);
687       int dy = Math.abs(y - mouseDragLastY);
688       if (graphStretch == -1 || dx > dy)
689       {
690         /*
691          * mostly horizontal drag, or not a graph annotation
692          */
693         dragMode = DragMode.Select;
694       }
695       else if (dy > dx)
696       {
697         /*
698          * mostly vertical drag
699          */
700         dragMode = DragMode.Resize;
701       }
702     }
703
704     if (dragMode == DragMode.Undefined)
705     {
706       /*
707        * drag is diagonal - defer deciding whether to
708        * treat as up/down or left/right
709        */
710       return;
711     }
712
713     try
714     {
715       if (dragMode == DragMode.Resize)
716       {
717         /*
718          * resize graph annotation if mouse was dragged up or down
719          */
720         int deltaY = mouseDragLastY - evt.getY();
721         if (deltaY != 0)
722         {
723           AlignmentAnnotation graphAnnotation = av.getAlignment()
724                   .getAlignmentAnnotation()[graphStretch];
725           int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
726           graphAnnotation.graphHeight = newHeight;
727           adjustPanelHeight();
728           ap.paintAlignment(false, false);
729         }
730       }
731       else
732       {
733         /*
734          * for mouse drag left or right, delegate to 
735          * ScalePanel to adjust the column selection
736          */
737         ap.getScalePanel().mouseDragged(evt);
738       }
739     } finally
740     {
741       mouseDragLastX = x;
742       mouseDragLastY = y;
743     }
744   }
745
746   /**
747    * Constructs the tooltip, and constructs and displays a status message, for
748    * the current mouse position
749    * 
750    * @param evt
751    */
752   @Override
753   public void mouseMoved(MouseEvent evt)
754   {
755     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
756
757     if (aa == null)
758     {
759       this.setToolTipText(null);
760       return;
761     }
762
763     int row = -1;
764     int height = 0;
765
766     for (int i = 0; i < aa.length; i++)
767     {
768       if (aa[i].visible)
769       {
770         height += aa[i].height;
771       }
772
773       if (evt.getY() < height)
774       {
775         row = i;
776         break;
777       }
778     }
779
780     if (row == -1)
781     {
782       this.setToolTipText(null);
783       return;
784     }
785
786     int column = (evt.getX() / av.getCharWidth())
787             + av.getRanges().getStartRes();
788
789     if (av.hasHiddenColumns())
790     {
791       column = av.getAlignment().getHiddenColumns()
792               .visibleToAbsoluteColumn(column);
793     }
794
795     AlignmentAnnotation ann = aa[row];
796     if (row > -1 && ann.annotations != null
797             && column < ann.annotations.length)
798     {
799       buildToolTip(ann, column, aa);
800       setStatusMessage(column, ann);
801     }
802     else
803     {
804       this.setToolTipText(null);
805       ap.alignFrame.statusBar.setText(" ");
806     }
807   }
808
809   /**
810    * Builds a tooltip for the annotation at the current mouse position.
811    * 
812    * @param ann
813    * @param column
814    * @param anns
815    */
816   void buildToolTip(AlignmentAnnotation ann, int column,
817           AlignmentAnnotation[] anns)
818   {
819     if (ann.graphGroup > -1)
820     {
821       StringBuilder tip = new StringBuilder(32);
822       tip.append("<html>");
823       for (int i = 0; i < anns.length; i++)
824       {
825         if (anns[i].graphGroup == ann.graphGroup
826                 && anns[i].annotations[column] != null)
827         {
828           tip.append(anns[i].label);
829           String description = anns[i].annotations[column].description;
830           if (description != null && description.length() > 0)
831           {
832             tip.append(" ").append(description);
833           }
834           tip.append("<br>");
835         }
836       }
837       if (tip.length() != 6)
838       {
839         tip.setLength(tip.length() - 4);
840         this.setToolTipText(tip.toString() + "</html>");
841       }
842     }
843     else if (ann.annotations[column] != null)
844     {
845       String description = ann.annotations[column].description;
846       if (description != null && description.length() > 0)
847       {
848         this.setToolTipText(JvSwingUtils.wrapTooltip(true, description));
849       }
850       else
851       {
852         this.setToolTipText(null); // no tooltip if null or empty description
853       }
854     }
855     else
856     {
857       // clear the tooltip.
858       this.setToolTipText(null);
859     }
860   }
861
862   /**
863    * Constructs and displays the status bar message
864    * 
865    * @param column
866    * @param ann
867    */
868   void setStatusMessage(int column, AlignmentAnnotation ann)
869   {
870     /*
871      * show alignment column and annotation description if any
872      */
873     StringBuilder text = new StringBuilder(32);
874     text.append(MessageManager.getString("label.column")).append(" ")
875             .append(column + 1);
876
877     if (ann.annotations[column] != null)
878     {
879       String description = ann.annotations[column].description;
880       if (description != null && description.trim().length() > 0)
881       {
882         text.append("  ").append(description);
883       }
884     }
885
886     /*
887      * if the annotation is sequence-specific, show the sequence number
888      * in the alignment, and (if not a gap) the residue and position
889      */
890     SequenceI seqref = ann.sequenceRef;
891     if (seqref != null)
892     {
893       int seqIndex = av.getAlignment().findIndex(seqref);
894       if (seqIndex != -1)
895       {
896         text.append(", ").append(MessageManager.getString("label.sequence"))
897                 .append(" ").append(seqIndex + 1);
898         char residue = seqref.getCharAt(column);
899         if (!Comparison.isGap(residue))
900         {
901           text.append(" ");
902           String name;
903           if (av.getAlignment().isNucleotide())
904           {
905             name = ResidueProperties.nucleotideName
906                     .get(String.valueOf(residue));
907             text.append(" Nucleotide: ")
908                     .append(name != null ? name : residue);
909           }
910           else
911           {
912             name = 'X' == residue ? "X"
913                     : ('*' == residue ? "STOP"
914                             : ResidueProperties.aa2Triplet
915                                     .get(String.valueOf(residue)));
916             text.append(" Residue: ").append(name != null ? name : residue);
917           }
918           int residuePos = seqref.findPosition(column);
919           text.append(" (").append(residuePos).append(")");
920         }
921       }
922     }
923
924     ap.alignFrame.statusBar.setText(text.toString());
925   }
926
927   /**
928    * DOCUMENT ME!
929    * 
930    * @param evt
931    *          DOCUMENT ME!
932    */
933   @Override
934   public void mouseClicked(MouseEvent evt)
935   {
936     // if (activeRow != -1)
937     // {
938     // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
939     // AlignmentAnnotation anot = aa[activeRow];
940     // }
941   }
942
943   // TODO mouseClicked-content and drawCursor are quite experimental!
944   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
945           int y1)
946   {
947     int pady = av.getCharHeight() / 5;
948     int charOffset = 0;
949     graphics.setColor(Color.black);
950     graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
951
952     if (av.validCharWidth)
953     {
954       graphics.setColor(Color.white);
955
956       char s = seq.getCharAt(res);
957
958       charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
959       graphics.drawString(String.valueOf(s), charOffset + x1,
960               (y1 + av.getCharHeight()) - pady);
961     }
962
963   }
964
965   private volatile boolean imageFresh = false;
966
967   /**
968    * DOCUMENT ME!
969    * 
970    * @param g
971    *          DOCUMENT ME!
972    */
973   @Override
974   public void paintComponent(Graphics g)
975   {
976     super.paintComponent(g);
977
978     g.setColor(Color.white);
979     g.fillRect(0, 0, getWidth(), getHeight());
980
981     if (image != null)
982     {
983       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
984               || (getVisibleRect().height != g.getClipBounds().height))
985       {
986         g.drawImage(image, 0, 0, this);
987         fastPaint = false;
988         return;
989       }
990     }
991     imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
992             + 1) * av.getCharWidth();
993     if (imgWidth < 1)
994     {
995       return;
996     }
997     if (image == null || imgWidth != image.getWidth(this)
998             || image.getHeight(this) != getHeight())
999     {
1000       try
1001       {
1002         image = new BufferedImage(imgWidth,
1003                 ap.getAnnotationPanel().getHeight(),
1004                 BufferedImage.TYPE_INT_RGB);
1005       } catch (OutOfMemoryError oom)
1006       {
1007         try
1008         {
1009           System.gc();
1010         } catch (Exception x)
1011         {
1012         }
1013         ;
1014         new OOMWarning(
1015                 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1016                 oom);
1017         return;
1018       }
1019       gg = (Graphics2D) image.getGraphics();
1020
1021       if (av.antiAlias)
1022       {
1023         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1024                 RenderingHints.VALUE_ANTIALIAS_ON);
1025       }
1026
1027       gg.setFont(av.getFont());
1028       fm = gg.getFontMetrics();
1029       gg.setColor(Color.white);
1030       gg.fillRect(0, 0, imgWidth, image.getHeight());
1031       imageFresh = true;
1032     }
1033     
1034     drawComponent(gg, av.getRanges().getStartRes(),
1035             av.getRanges().getEndRes() + 1);
1036     imageFresh = false;
1037     g.drawImage(image, 0, 0, this);
1038   }
1039
1040   /**
1041    * set true to enable redraw timing debug output on stderr
1042    */
1043   private final boolean debugRedraw = false;
1044
1045   /**
1046    * non-Thread safe repaint
1047    * 
1048    * @param horizontal
1049    *          repaint with horizontal shift in alignment
1050    */
1051   public void fastPaint(int horizontal)
1052   {
1053     if ((horizontal == 0) || gg == null
1054             || av.getAlignment().getAlignmentAnnotation() == null
1055             || av.getAlignment().getAlignmentAnnotation().length < 1
1056             || av.isCalcInProgress())
1057     {
1058       repaint();
1059       return;
1060     }
1061
1062     int sr = av.getRanges().getStartRes();
1063     int er = av.getRanges().getEndRes() + 1;
1064     int transX = 0;
1065
1066     gg.copyArea(0, 0, imgWidth, getHeight(),
1067             -horizontal * av.getCharWidth(), 0);
1068
1069     if (horizontal > 0) // scrollbar pulled right, image to the left
1070     {
1071       transX = (er - sr - horizontal) * av.getCharWidth();
1072       sr = er - horizontal;
1073     }
1074     else if (horizontal < 0)
1075     {
1076       er = sr - horizontal;
1077     }
1078
1079     gg.translate(transX, 0);
1080
1081     drawComponent(gg, sr, er);
1082
1083     gg.translate(-transX, 0);
1084
1085     fastPaint = true;
1086
1087     // Call repaint on alignment panel so that repaints from other alignment
1088     // panel components can be aggregated. Otherwise performance of the overview
1089     // window and others may be adversely affected.
1090     av.getAlignPanel().repaint();
1091   }
1092
1093   private volatile boolean lastImageGood = false;
1094
1095   /**
1096    * DOCUMENT ME!
1097    * 
1098    * @param g
1099    *          DOCUMENT ME!
1100    * @param startRes
1101    *          DOCUMENT ME!
1102    * @param endRes
1103    *          DOCUMENT ME!
1104    */
1105   public void drawComponent(Graphics g, int startRes, int endRes)
1106   {
1107     BufferedImage oldFaded = fadedImage;
1108     if (av.isCalcInProgress())
1109     {
1110       if (image == null)
1111       {
1112         lastImageGood = false;
1113         return;
1114       }
1115       // We'll keep a record of the old image,
1116       // and draw a faded image until the calculation
1117       // has completed
1118       if (lastImageGood
1119               && (fadedImage == null || fadedImage.getWidth() != imgWidth
1120                       || fadedImage.getHeight() != image.getHeight()))
1121       {
1122         // System.err.println("redraw faded image ("+(fadedImage==null ?
1123         // "null image" : "") + " lastGood="+lastImageGood+")");
1124         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1125                 BufferedImage.TYPE_INT_RGB);
1126
1127         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1128
1129         fadedG.setColor(Color.white);
1130         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1131
1132         fadedG.setComposite(
1133                 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1134         fadedG.drawImage(image, 0, 0, this);
1135
1136       }
1137       // make sure we don't overwrite the last good faded image until all
1138       // calculations have finished
1139       lastImageGood = false;
1140
1141     }
1142     else
1143     {
1144       if (fadedImage != null)
1145       {
1146         oldFaded = fadedImage;
1147       }
1148       fadedImage = null;
1149     }
1150
1151     g.setColor(Color.white);
1152     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1153
1154     g.setFont(av.getFont());
1155     if (fm == null)
1156     {
1157       fm = g.getFontMetrics();
1158     }
1159
1160     if ((av.getAlignment().getAlignmentAnnotation() == null)
1161             || (av.getAlignment().getAlignmentAnnotation().length < 1))
1162     {
1163       g.setColor(Color.white);
1164       g.fillRect(0, 0, getWidth(), getHeight());
1165       g.setColor(Color.black);
1166       if (av.validCharWidth)
1167       {
1168         g.drawString(MessageManager
1169                 .getString("label.alignment_has_no_annotations"), 20, 15);
1170       }
1171
1172       return;
1173     }
1174     lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1175             endRes);
1176     if (!lastImageGood && fadedImage == null)
1177     {
1178       fadedImage = oldFaded;
1179     }
1180   }
1181
1182   @Override
1183   public FontMetrics getFontMetrics()
1184   {
1185     return fm;
1186   }
1187
1188   @Override
1189   public Image getFadedImage()
1190   {
1191     return fadedImage;
1192   }
1193
1194   @Override
1195   public int getFadedImageWidth()
1196   {
1197     return imgWidth;
1198   }
1199
1200   private int[] bounds = new int[2];
1201
1202   @Override
1203   public int[] getVisibleVRange()
1204   {
1205     if (ap != null && ap.getAlabels() != null)
1206     {
1207       int sOffset = -ap.getAlabels().getScrollOffset();
1208       int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1209       bounds[0] = sOffset;
1210       bounds[1] = visHeight;
1211       return bounds;
1212     }
1213     else
1214     {
1215       return null;
1216     }
1217   }
1218
1219   /**
1220    * Try to ensure any references held are nulled
1221    */
1222   public void dispose()
1223   {
1224     av = null;
1225     ap = null;
1226     image = null;
1227     fadedImage = null;
1228     gg = null;
1229     _mwl = null;
1230
1231     /*
1232      * I created the renderer so I will dispose of it
1233      */
1234     if (renderer != null)
1235     {
1236       renderer.dispose();
1237     }
1238   }
1239
1240   @Override
1241   public void propertyChange(PropertyChangeEvent evt)
1242   {
1243     // Respond to viewport range changes (e.g. alignment panel was scrolled)
1244     // Both scrolling and resizing change viewport ranges: scrolling changes
1245     // both start and end points, but resize only changes end values.
1246     // Here we only want to fastpaint on a scroll, with resize using a normal
1247     // paint, so scroll events are identified as changes to the horizontal or
1248     // vertical start value.
1249     if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1250     {
1251       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1252     }
1253     else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1254     {
1255       fastPaint(((int[]) evt.getNewValue())[0]
1256               - ((int[]) evt.getOldValue())[0]);
1257     }
1258     else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1259     {
1260       repaint();
1261     }
1262   }
1263 }