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