JAL-3071 IdPanel.ScrollThread for JalviewJS, Javadoc
[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(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       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     ap.getScalePanel().mouseEntered(evt);
658   }
659
660   /**
661    * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
662    * with column selection on a mouse drag
663    * 
664    * @param evt
665    */
666   @Override
667   public void mouseExited(MouseEvent evt)
668   {
669     ap.getScalePanel().mouseExited(evt);
670   }
671
672   /**
673    * DOCUMENT ME!
674    * 
675    * @param evt
676    *          DOCUMENT ME!
677    */
678   @Override
679   public void mouseDragged(MouseEvent evt)
680   {
681     /*
682      * todo: if dragMode is Undefined:
683      * - set to Select if dx > dy
684      * - set to Resize if dy > dx
685      * - do nothing if dx == dy
686      */
687     final int x = evt.getX();
688     final int y = evt.getY();
689     if (dragMode == DragMode.Undefined)
690     {
691       int dx = Math.abs(x - mouseDragLastX);
692       int dy = Math.abs(y - mouseDragLastY);
693       if (graphStretch == -1 || dx > dy)
694       {
695         /*
696          * mostly horizontal drag, or not a graph annotation
697          */
698         dragMode = DragMode.Select;
699       }
700       else if (dy > dx)
701       {
702         /*
703          * mostly vertical drag
704          */
705         dragMode = DragMode.Resize;
706       }
707     }
708
709     if (dragMode == DragMode.Undefined)
710     {
711       /*
712        * drag is diagonal - defer deciding whether to
713        * treat as up/down or left/right
714        */
715       return;
716     }
717
718     try
719     {
720       if (dragMode == DragMode.Resize)
721       {
722         /*
723          * resize graph annotation if mouse was dragged up or down
724          */
725         int deltaY = mouseDragLastY - evt.getY();
726         if (deltaY != 0)
727         {
728           AlignmentAnnotation graphAnnotation = av.getAlignment()
729                   .getAlignmentAnnotation()[graphStretch];
730           int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
731           graphAnnotation.graphHeight = newHeight;
732           adjustPanelHeight();
733           ap.paintAlignment(false, false);
734         }
735       }
736       else
737       {
738         /*
739          * for mouse drag left or right, delegate to 
740          * ScalePanel to adjust the column selection
741          */
742         ap.getScalePanel().mouseDragged(evt);
743       }
744     } finally
745     {
746       mouseDragLastX = x;
747       mouseDragLastY = y;
748     }
749   }
750
751   /**
752    * Constructs the tooltip, and constructs and displays a status message, for
753    * the current mouse position
754    * 
755    * @param evt
756    */
757   @Override
758   public void mouseMoved(MouseEvent evt)
759   {
760     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
761
762     if (aa == null)
763     {
764       this.setToolTipText(null);
765       return;
766     }
767
768     int row = -1;
769     int height = 0;
770
771     for (int i = 0; i < aa.length; i++)
772     {
773       if (aa[i].visible)
774       {
775         height += aa[i].height;
776       }
777
778       if (evt.getY() < height)
779       {
780         row = i;
781         break;
782       }
783     }
784
785     if (row == -1)
786     {
787       this.setToolTipText(null);
788       return;
789     }
790
791     int column = (evt.getX() / av.getCharWidth())
792             + av.getRanges().getStartRes();
793
794     if (av.hasHiddenColumns())
795     {
796       column = av.getAlignment().getHiddenColumns()
797               .visibleToAbsoluteColumn(column);
798     }
799
800     AlignmentAnnotation ann = aa[row];
801     if (row > -1 && ann.annotations != null
802             && column < ann.annotations.length)
803     {
804       buildToolTip(ann, column, aa);
805       setStatusMessage(column, ann);
806     }
807     else
808     {
809       this.setToolTipText(null);
810       ap.alignFrame.statusBar.setText(" ");
811     }
812   }
813
814   /**
815    * Builds a tooltip for the annotation at the current mouse position.
816    * 
817    * @param ann
818    * @param column
819    * @param anns
820    */
821   void buildToolTip(AlignmentAnnotation ann, int column,
822           AlignmentAnnotation[] anns)
823   {
824     if (ann.graphGroup > -1)
825     {
826       StringBuilder tip = new StringBuilder(32);
827       tip.append("<html>");
828       for (int i = 0; i < anns.length; i++)
829       {
830         if (anns[i].graphGroup == ann.graphGroup
831                 && anns[i].annotations[column] != null)
832         {
833           tip.append(anns[i].label);
834           String description = anns[i].annotations[column].description;
835           if (description != null && description.length() > 0)
836           {
837             tip.append(" ").append(description);
838           }
839           tip.append("<br>");
840         }
841       }
842       if (tip.length() != 6)
843       {
844         tip.setLength(tip.length() - 4);
845         this.setToolTipText(tip.toString() + "</html>");
846       }
847     }
848     else if (ann.annotations[column] != null)
849     {
850       String description = ann.annotations[column].description;
851       if (description != null && description.length() > 0)
852       {
853         this.setToolTipText(JvSwingUtils.wrapTooltip(true, description));
854       }
855       else
856       {
857         this.setToolTipText(null); // no tooltip if null or empty description
858       }
859     }
860     else
861     {
862       // clear the tooltip.
863       this.setToolTipText(null);
864     }
865   }
866
867   /**
868    * Constructs and displays the status bar message
869    * 
870    * @param column
871    * @param ann
872    */
873   void setStatusMessage(int column, AlignmentAnnotation ann)
874   {
875     /*
876      * show alignment column and annotation description if any
877      */
878     StringBuilder text = new StringBuilder(32);
879     text.append(MessageManager.getString("label.column")).append(" ")
880             .append(column + 1);
881
882     if (ann.annotations[column] != null)
883     {
884       String description = ann.annotations[column].description;
885       if (description != null && description.trim().length() > 0)
886       {
887         text.append("  ").append(description);
888       }
889     }
890
891     /*
892      * if the annotation is sequence-specific, show the sequence number
893      * in the alignment, and (if not a gap) the residue and position
894      */
895     SequenceI seqref = ann.sequenceRef;
896     if (seqref != null)
897     {
898       int seqIndex = av.getAlignment().findIndex(seqref);
899       if (seqIndex != -1)
900       {
901         text.append(", ").append(MessageManager.getString("label.sequence"))
902                 .append(" ").append(seqIndex + 1);
903         char residue = seqref.getCharAt(column);
904         if (!Comparison.isGap(residue))
905         {
906           text.append(" ");
907           String name;
908           if (av.getAlignment().isNucleotide())
909           {
910             name = ResidueProperties.nucleotideName
911                     .get(String.valueOf(residue));
912             text.append(" Nucleotide: ")
913                     .append(name != null ? name : residue);
914           }
915           else
916           {
917             name = 'X' == residue ? "X"
918                     : ('*' == residue ? "STOP"
919                             : ResidueProperties.aa2Triplet
920                                     .get(String.valueOf(residue)));
921             text.append(" Residue: ").append(name != null ? name : residue);
922           }
923           int residuePos = seqref.findPosition(column);
924           text.append(" (").append(residuePos).append(")");
925         }
926       }
927     }
928
929     ap.alignFrame.statusBar.setText(text.toString());
930   }
931
932   /**
933    * DOCUMENT ME!
934    * 
935    * @param evt
936    *          DOCUMENT ME!
937    */
938   @Override
939   public void mouseClicked(MouseEvent evt)
940   {
941     // if (activeRow != -1)
942     // {
943     // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
944     // AlignmentAnnotation anot = aa[activeRow];
945     // }
946   }
947
948   // TODO mouseClicked-content and drawCursor are quite experimental!
949   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
950           int y1)
951   {
952     int pady = av.getCharHeight() / 5;
953     int charOffset = 0;
954     graphics.setColor(Color.black);
955     graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
956
957     if (av.validCharWidth)
958     {
959       graphics.setColor(Color.white);
960
961       char s = seq.getCharAt(res);
962
963       charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
964       graphics.drawString(String.valueOf(s), charOffset + x1,
965               (y1 + av.getCharHeight()) - pady);
966     }
967
968   }
969
970   private volatile boolean imageFresh = false;
971
972   /**
973    * DOCUMENT ME!
974    * 
975    * @param g
976    *          DOCUMENT ME!
977    */
978   @Override
979   public void paintComponent(Graphics g)
980   {
981     super.paintComponent(g);
982
983     g.setColor(Color.white);
984     g.fillRect(0, 0, getWidth(), getHeight());
985
986     if (image != null)
987     {
988       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
989               || (getVisibleRect().height != g.getClipBounds().height))
990       {
991         g.drawImage(image, 0, 0, this);
992         fastPaint = false;
993         return;
994       }
995     }
996     imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
997             + 1) * av.getCharWidth();
998     if (imgWidth < 1)
999     {
1000       return;
1001     }
1002     if (image == null || imgWidth != image.getWidth(this)
1003             || image.getHeight(this) != getHeight())
1004     {
1005       try
1006       {
1007         image = new BufferedImage(imgWidth,
1008                 ap.getAnnotationPanel().getHeight(),
1009                 BufferedImage.TYPE_INT_RGB);
1010       } catch (OutOfMemoryError oom)
1011       {
1012         try
1013         {
1014           System.gc();
1015         } catch (Exception x)
1016         {
1017         }
1018         ;
1019         new OOMWarning(
1020                 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1021                 oom);
1022         return;
1023       }
1024       gg = (Graphics2D) image.getGraphics();
1025
1026       if (av.antiAlias)
1027       {
1028         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1029                 RenderingHints.VALUE_ANTIALIAS_ON);
1030       }
1031
1032       gg.setFont(av.getFont());
1033       fm = gg.getFontMetrics();
1034       gg.setColor(Color.white);
1035       gg.fillRect(0, 0, imgWidth, image.getHeight());
1036       imageFresh = true;
1037     }
1038     
1039     drawComponent(gg, av.getRanges().getStartRes(),
1040             av.getRanges().getEndRes() + 1);
1041     imageFresh = false;
1042     g.drawImage(image, 0, 0, this);
1043   }
1044
1045   /**
1046    * set true to enable redraw timing debug output on stderr
1047    */
1048   private final boolean debugRedraw = false;
1049
1050   /**
1051    * non-Thread safe repaint
1052    * 
1053    * @param horizontal
1054    *          repaint with horizontal shift in alignment
1055    */
1056   public void fastPaint(int horizontal)
1057   {
1058     if ((horizontal == 0) || gg == null
1059             || av.getAlignment().getAlignmentAnnotation() == null
1060             || av.getAlignment().getAlignmentAnnotation().length < 1
1061             || av.isCalcInProgress())
1062     {
1063       repaint();
1064       return;
1065     }
1066
1067     int sr = av.getRanges().getStartRes();
1068     int er = av.getRanges().getEndRes() + 1;
1069     int transX = 0;
1070
1071     gg.copyArea(0, 0, imgWidth, getHeight(),
1072             -horizontal * av.getCharWidth(), 0);
1073
1074     if (horizontal > 0) // scrollbar pulled right, image to the left
1075     {
1076       transX = (er - sr - horizontal) * av.getCharWidth();
1077       sr = er - horizontal;
1078     }
1079     else if (horizontal < 0)
1080     {
1081       er = sr - horizontal;
1082     }
1083
1084     gg.translate(transX, 0);
1085
1086     drawComponent(gg, sr, er);
1087
1088     gg.translate(-transX, 0);
1089
1090     fastPaint = true;
1091
1092     // Call repaint on alignment panel so that repaints from other alignment
1093     // panel components can be aggregated. Otherwise performance of the overview
1094     // window and others may be adversely affected.
1095     av.getAlignPanel().repaint();
1096   }
1097
1098   private volatile boolean lastImageGood = false;
1099
1100   /**
1101    * DOCUMENT ME!
1102    * 
1103    * @param g
1104    *          DOCUMENT ME!
1105    * @param startRes
1106    *          DOCUMENT ME!
1107    * @param endRes
1108    *          DOCUMENT ME!
1109    */
1110   public void drawComponent(Graphics g, int startRes, int endRes)
1111   {
1112     BufferedImage oldFaded = fadedImage;
1113     if (av.isCalcInProgress())
1114     {
1115       if (image == null)
1116       {
1117         lastImageGood = false;
1118         return;
1119       }
1120       // We'll keep a record of the old image,
1121       // and draw a faded image until the calculation
1122       // has completed
1123       if (lastImageGood
1124               && (fadedImage == null || fadedImage.getWidth() != imgWidth
1125                       || fadedImage.getHeight() != image.getHeight()))
1126       {
1127         // System.err.println("redraw faded image ("+(fadedImage==null ?
1128         // "null image" : "") + " lastGood="+lastImageGood+")");
1129         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1130                 BufferedImage.TYPE_INT_RGB);
1131
1132         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1133
1134         fadedG.setColor(Color.white);
1135         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1136
1137         fadedG.setComposite(
1138                 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1139         fadedG.drawImage(image, 0, 0, this);
1140
1141       }
1142       // make sure we don't overwrite the last good faded image until all
1143       // calculations have finished
1144       lastImageGood = false;
1145
1146     }
1147     else
1148     {
1149       if (fadedImage != null)
1150       {
1151         oldFaded = fadedImage;
1152       }
1153       fadedImage = null;
1154     }
1155
1156     g.setColor(Color.white);
1157     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1158
1159     g.setFont(av.getFont());
1160     if (fm == null)
1161     {
1162       fm = g.getFontMetrics();
1163     }
1164
1165     if ((av.getAlignment().getAlignmentAnnotation() == null)
1166             || (av.getAlignment().getAlignmentAnnotation().length < 1))
1167     {
1168       g.setColor(Color.white);
1169       g.fillRect(0, 0, getWidth(), getHeight());
1170       g.setColor(Color.black);
1171       if (av.validCharWidth)
1172       {
1173         g.drawString(MessageManager
1174                 .getString("label.alignment_has_no_annotations"), 20, 15);
1175       }
1176
1177       return;
1178     }
1179     lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1180             endRes);
1181     if (!lastImageGood && fadedImage == null)
1182     {
1183       fadedImage = oldFaded;
1184     }
1185   }
1186
1187   @Override
1188   public FontMetrics getFontMetrics()
1189   {
1190     return fm;
1191   }
1192
1193   @Override
1194   public Image getFadedImage()
1195   {
1196     return fadedImage;
1197   }
1198
1199   @Override
1200   public int getFadedImageWidth()
1201   {
1202     return imgWidth;
1203   }
1204
1205   private int[] bounds = new int[2];
1206
1207   @Override
1208   public int[] getVisibleVRange()
1209   {
1210     if (ap != null && ap.getAlabels() != null)
1211     {
1212       int sOffset = -ap.getAlabels().getScrollOffset();
1213       int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1214       bounds[0] = sOffset;
1215       bounds[1] = visHeight;
1216       return bounds;
1217     }
1218     else
1219     {
1220       return null;
1221     }
1222   }
1223
1224   /**
1225    * Try to ensure any references held are nulled
1226    */
1227   public void dispose()
1228   {
1229     av = null;
1230     ap = null;
1231     image = null;
1232     fadedImage = null;
1233     gg = null;
1234     _mwl = null;
1235
1236     /*
1237      * I created the renderer so I will dispose of it
1238      */
1239     if (renderer != null)
1240     {
1241       renderer.dispose();
1242     }
1243   }
1244
1245   @Override
1246   public void propertyChange(PropertyChangeEvent evt)
1247   {
1248     // Respond to viewport range changes (e.g. alignment panel was scrolled)
1249     // Both scrolling and resizing change viewport ranges: scrolling changes
1250     // both start and end points, but resize only changes end values.
1251     // Here we only want to fastpaint on a scroll, with resize using a normal
1252     // paint, so scroll events are identified as changes to the horizontal or
1253     // vertical start value.
1254     if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1255     {
1256       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1257     }
1258     else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1259     {
1260       fastPaint(((int[]) evt.getNewValue())[0]
1261               - ((int[]) evt.getOldValue())[0]);
1262     }
1263     else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1264     {
1265       repaint();
1266     }
1267   }
1268 }