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