JAL-4380 Improved annotation tooltips especially secondary structure tooltips
[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 java.awt.AlphaComposite;
24 import java.awt.Color;
25 import java.awt.Dimension;
26 import java.awt.FontMetrics;
27 import java.awt.Graphics;
28 import java.awt.Graphics2D;
29 import java.awt.Image;
30 import java.awt.Rectangle;
31 import java.awt.RenderingHints;
32 import java.awt.event.ActionEvent;
33 import java.awt.event.ActionListener;
34 import java.awt.event.AdjustmentEvent;
35 import java.awt.event.AdjustmentListener;
36 import java.awt.event.MouseEvent;
37 import java.awt.event.MouseListener;
38 import java.awt.event.MouseMotionListener;
39 import java.awt.event.MouseWheelEvent;
40 import java.awt.event.MouseWheelListener;
41 import java.awt.image.BufferedImage;
42 import java.beans.PropertyChangeEvent;
43 import java.util.ArrayList;
44 import java.util.BitSet;
45 import java.util.Collections;
46 import java.util.List;
47
48 import javax.swing.JMenuItem;
49 import javax.swing.JPanel;
50 import javax.swing.JPopupMenu;
51 import javax.swing.Scrollable;
52 import javax.swing.ToolTipManager;
53
54 import jalview.api.AlignViewportI;
55 import jalview.datamodel.AlignmentAnnotation;
56 import jalview.datamodel.AlignmentI;
57 import jalview.datamodel.Annotation;
58 import jalview.datamodel.ColumnSelection;
59 import jalview.datamodel.ContactListI;
60 import jalview.datamodel.ContactMatrixI;
61 import jalview.datamodel.ContactRange;
62 import jalview.datamodel.GraphLine;
63 import jalview.datamodel.HiddenColumns;
64 import jalview.datamodel.SequenceI;
65 import jalview.gui.JalviewColourChooser.ColourChooserListener;
66 import jalview.renderer.AnnotationRenderer;
67 import jalview.renderer.AwtRenderPanelI;
68 import jalview.renderer.ContactGeometry;
69 import jalview.schemes.ResidueProperties;
70 import jalview.util.Comparison;
71 import jalview.util.Format;
72 import jalview.util.MessageManager;
73 import jalview.util.Platform;
74 import jalview.viewmodel.ViewportListenerI;
75 import jalview.viewmodel.ViewportRanges;
76 import jalview.ws.datamodel.MappableContactMatrixI;
77 import jalview.ws.datamodel.alphafold.PAEContactMatrix;
78
79 /**
80  * AnnotationPanel displays visible portion of annotation rows below unwrapped
81  * alignment
82  * 
83  * @author $author$
84  * @version $Revision$
85  */
86 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
87         MouseListener, MouseWheelListener, MouseMotionListener,
88         ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
89 {
90   enum DragMode
91   {
92     Select, Resize, Undefined, MatrixSelect
93   };
94
95   String HELIX = MessageManager.getString("label.helix");
96
97   String SHEET = MessageManager.getString("label.sheet");
98
99   /**
100    * For RNA secondary structure "stems" aka helices
101    */
102   String STEM = MessageManager.getString("label.rna_helix");
103
104   String LABEL = MessageManager.getString("label.label");
105
106   String REMOVE = MessageManager.getString("label.remove_annotation");
107
108   String COLOUR = MessageManager.getString("action.colour");
109
110   public final Color HELIX_COLOUR = Color.red.darker();
111
112   public final Color SHEET_COLOUR = Color.green.darker().darker();
113
114   public final Color STEM_COLOUR = Color.blue.darker();
115
116   /** DOCUMENT ME!! */
117   public AlignViewport av;
118
119   AlignmentPanel ap;
120
121   public int activeRow = -1;
122
123   public BufferedImage image;
124
125   public volatile BufferedImage fadedImage;
126
127   // private Graphics2D gg;
128
129   public FontMetrics fm;
130
131   public int imgWidth = 0;
132
133   boolean fastPaint = false;
134
135   // Used For mouse Dragging and resizing graphs
136   int graphStretch = -1;
137
138   int mouseDragLastX = -1;
139
140   int mouseDragLastY = -1;
141
142   int firstDragX = -1;
143
144   int firstDragY = -1;
145
146   DragMode dragMode = DragMode.Undefined;
147
148   boolean mouseDragging = false;
149
150   // for editing cursor
151   int cursorX = 0;
152
153   int cursorY = 0;
154
155   public final AnnotationRenderer renderer;
156
157   private MouseWheelListener[] _mwl;
158
159   private boolean notJustOne;
160
161   /**
162    * Creates a new AnnotationPanel object.
163    * 
164    * @param ap
165    *          DOCUMENT ME!
166    */
167   public AnnotationPanel(AlignmentPanel ap)
168   {
169     ToolTipManager.sharedInstance().registerComponent(this);
170     ToolTipManager.sharedInstance().setInitialDelay(0);
171     ToolTipManager.sharedInstance().setDismissDelay(10000);
172     this.ap = ap;
173     av = ap.av;
174     this.setLayout(null);
175     addMouseListener(this);
176     addMouseMotionListener(this);
177     ap.annotationScroller.getVerticalScrollBar()
178             .addAdjustmentListener(this);
179     // save any wheel listeners on the scroller, so we can propagate scroll
180     // events to them.
181     _mwl = ap.annotationScroller.getMouseWheelListeners();
182     // and then set our own listener to consume all mousewheel events
183     ap.annotationScroller.addMouseWheelListener(this);
184     renderer = new AnnotationRenderer();
185
186     av.getRanges().addPropertyChangeListener(this);
187   }
188
189   public AnnotationPanel(AlignViewport av)
190   {
191     this.av = av;
192     renderer = new AnnotationRenderer();
193   }
194
195   /**
196    * Responds to a mouse wheel movement by scrolling the annotations up or down.
197    * Annotation labels are scrolled via method adjustmentValueChanged when the
198    * vertical scrollbar is adjusted.
199    * <p>
200    * If shift is pressed, then scrolling is left or right instead, and is
201    * delegated to AlignmentPanel, so that both sequences and annotations are
202    * scrolled together.
203    * <p>
204    * This object is a MouseWheelListener to AnnotationLabels, so mouse wheel
205    * events over the labels are delegated to this method.
206    * <p>
207    * Note that this method may also be fired by scrolling with a gesture on a
208    * trackpad.
209    */
210   @Override
211   public void mouseWheelMoved(MouseWheelEvent e)
212   {
213     if (e.isShiftDown())
214     {
215       ap.getSeqPanel().mouseWheelMoved(e);
216     }
217     else
218     {
219       // TODO: find the correct way to let the event bubble up to
220       // ap.annotationScroller
221       for (MouseWheelListener mwl : _mwl)
222       {
223         if (mwl != null)
224         {
225           mwl.mouseWheelMoved(e);
226         }
227         if (e.isConsumed())
228         {
229           break;
230         }
231       }
232     }
233   }
234
235   @Override
236   public Dimension getPreferredScrollableViewportSize()
237   {
238     Dimension ps = getPreferredSize();
239     return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
240   }
241
242   @Override
243   public int getScrollableBlockIncrement(Rectangle visibleRect,
244           int orientation, int direction)
245   {
246     return 30;
247   }
248
249   @Override
250   public boolean getScrollableTracksViewportHeight()
251   {
252     return false;
253   }
254
255   @Override
256   public boolean getScrollableTracksViewportWidth()
257   {
258     return true;
259   }
260
261   @Override
262   public int getScrollableUnitIncrement(Rectangle visibleRect,
263           int orientation, int direction)
264   {
265     return 30;
266   }
267
268   /*
269    * (non-Javadoc)
270    * 
271    * @see
272    * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
273    * .AdjustmentEvent)
274    */
275   @Override
276   public void adjustmentValueChanged(AdjustmentEvent evt)
277   {
278     // update annotation label display
279     ap.getAlabels().setScrollOffset(-evt.getValue());
280   }
281
282   /**
283    * Calculates the height of the annotation displayed in the annotation panel.
284    * Callers should normally call the ap.adjustAnnotationHeight method to ensure
285    * all annotation associated components are updated correctly.
286    * 
287    */
288   public int adjustPanelHeight()
289   {
290     int height = av.calcPanelHeight();
291     this.setPreferredSize(new Dimension(1, height));
292     if (ap != null)
293     {
294       // revalidate only when the alignment panel is fully constructed
295       ap.validate();
296     }
297
298     return height;
299   }
300
301   /**
302    * DOCUMENT ME!
303    * 
304    * @param evt
305    *          DOCUMENT ME!
306    */
307   @Override
308   public void actionPerformed(ActionEvent evt)
309   {
310     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
311     if (aa == null)
312     {
313       return;
314     }
315     Annotation[] anot = aa[activeRow].annotations;
316
317     if (anot.length < av.getColumnSelection().getMax())
318     {
319       Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
320               + 2];
321       System.arraycopy(anot, 0, temp, 0, anot.length);
322       anot = temp;
323       aa[activeRow].annotations = anot;
324     }
325
326     String action = evt.getActionCommand();
327     if (action.equals(REMOVE))
328     {
329       for (int index : av.getColumnSelection().getSelected())
330       {
331         if (av.getAlignment().getHiddenColumns().isVisible(index))
332         {
333           anot[index] = null;
334         }
335       }
336     }
337     else if (action.equals(LABEL))
338     {
339       String exMesg = collectAnnotVals(anot, LABEL);
340       String label = JvOptionPane.showInputDialog(
341               MessageManager.getString("label.enter_label"), exMesg);
342
343       if (label == null)
344       {
345         return;
346       }
347
348       if ((label.length() > 0) && !aa[activeRow].hasText)
349       {
350         aa[activeRow].hasText = true;
351       }
352
353       for (int index : av.getColumnSelection().getSelected())
354       {
355         if (!av.getAlignment().getHiddenColumns().isVisible(index))
356         {
357           continue;
358         }
359
360         if (anot[index] == null)
361         {
362           anot[index] = new Annotation(label, "", ' ', 0);
363         }
364         else
365         {
366           anot[index].displayCharacter = label;
367         }
368       }
369     }
370     else if (action.equals(COLOUR))
371     {
372       final Annotation[] fAnot = anot;
373       String title = MessageManager
374               .getString("label.select_foreground_colour");
375       ColourChooserListener listener = new ColourChooserListener()
376       {
377         @Override
378         public void colourSelected(Color c)
379         {
380           HiddenColumns hiddenColumns = av.getAlignment()
381                   .getHiddenColumns();
382           for (int index : av.getColumnSelection().getSelected())
383           {
384             if (hiddenColumns.isVisible(index))
385             {
386               if (fAnot[index] == null)
387               {
388                 fAnot[index] = new Annotation("", "", ' ', 0);
389               }
390               fAnot[index].colour = c;
391             }
392           }
393         };
394       };
395       JalviewColourChooser.showColourChooser(this, title, Color.black,
396               listener);
397     }
398     else
399     // HELIX, SHEET or STEM
400     {
401       char type = 0;
402       String symbol = "\u03B1"; // alpha
403
404       if (action.equals(HELIX))
405       {
406         type = 'H';
407       }
408       else if (action.equals(SHEET))
409       {
410         type = 'E';
411         symbol = "\u03B2"; // beta
412       }
413
414       // Added by LML to color stems
415       else if (action.equals(STEM))
416       {
417         type = 'S';
418         int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
419         symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
420       }
421
422       if (!aa[activeRow].hasIcons)
423       {
424         aa[activeRow].hasIcons = true;
425       }
426
427       String label = JvOptionPane.showInputDialog(MessageManager
428               .getString("label.enter_label_for_the_structure"), symbol);
429
430       if (label == null)
431       {
432         return;
433       }
434
435       if ((label.length() > 0) && !aa[activeRow].hasText)
436       {
437         aa[activeRow].hasText = true;
438         if (action.equals(STEM))
439         {
440           aa[activeRow].showAllColLabels = true;
441         }
442       }
443       for (int index : av.getColumnSelection().getSelected())
444       {
445         if (!av.getAlignment().getHiddenColumns().isVisible(index))
446         {
447           continue;
448         }
449
450         if (anot[index] == null)
451         {
452           anot[index] = new Annotation(label, "", type, 0);
453         }
454
455         anot[index].secondaryStructure = type != 'S' ? type
456                 : label.length() == 0 ? ' ' : label.charAt(0);
457         anot[index].displayCharacter = label;
458
459       }
460     }
461
462     av.getAlignment().validateAnnotation(aa[activeRow]);
463     ap.alignmentChanged();
464     ap.alignFrame.setMenusForViewport();
465     adjustPanelHeight();
466     repaint();
467
468     return;
469   }
470
471   /**
472    * Returns any existing annotation concatenated as a string. For each
473    * annotation, takes the description, if any, else the secondary structure
474    * character (if type is HELIX, SHEET or STEM), else the display character (if
475    * type is LABEL).
476    * 
477    * @param anots
478    * @param type
479    * @return
480    */
481   private String collectAnnotVals(Annotation[] anots, String type)
482   {
483     // TODO is this method wanted? why? 'last' is not used
484
485     StringBuilder collatedInput = new StringBuilder(64);
486     String last = "";
487     ColumnSelection viscols = av.getColumnSelection();
488     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
489
490     /*
491      * the selection list (read-only view) is in selection order, not
492      * column order; make a copy so we can sort it
493      */
494     List<Integer> selected = new ArrayList<>(viscols.getSelected());
495     Collections.sort(selected);
496     for (int index : selected)
497     {
498       // always check for current display state - just in case
499       if (!hidden.isVisible(index))
500       {
501         continue;
502       }
503       String tlabel = null;
504       if (anots[index] != null)
505       { // LML added stem code
506         if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
507                 || type.equals(LABEL))
508         {
509           tlabel = anots[index].description;
510           if (tlabel == null || tlabel.length() < 1)
511           {
512             if (type.equals(HELIX) || type.equals(SHEET)
513                     || type.equals(STEM))
514             {
515               tlabel = "" + anots[index].secondaryStructure;
516             }
517             else
518             {
519               tlabel = "" + anots[index].displayCharacter;
520             }
521           }
522         }
523         if (tlabel != null && !tlabel.equals(last))
524         {
525           if (last.length() > 0)
526           {
527             collatedInput.append(" ");
528           }
529           collatedInput.append(tlabel);
530         }
531       }
532     }
533     return collatedInput.toString();
534   }
535
536   /**
537    * Action on right mouse pressed on Mac is to show a pop-up menu for the
538    * annotation. Action on left mouse pressed is to find which annotation is
539    * pressed and mark the start of a column selection or graph resize operation.
540    * 
541    * @param evt
542    */
543   @Override
544   public void mousePressed(MouseEvent evt)
545   {
546
547     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
548     if (aa == null)
549     {
550       return;
551     }
552     mouseDragLastX = evt.getX();
553     mouseDragLastY = evt.getY();
554
555     /*
556      * add visible annotation heights until we reach the y
557      * position, to find which annotation it is in
558      */
559     int height = 0;
560     activeRow = -1;
561     int yOffset = 0;
562     // todo could reuse getRowIndexAndOffset ?
563     final int y = evt.getY();
564
565     for (int i = 0; i < aa.length; i++)
566     {
567       if (aa[i].visible)
568       {
569         height += aa[i].height;
570       }
571
572       if (y < height)
573       {
574         if (aa[i].editable)
575         {
576           activeRow = i;
577         }
578         else if (aa[i].graph != 0)
579         {
580           /*
581            * we have clicked on a resizable graph annotation
582            */
583           graphStretch = i;
584           yOffset = height - y;
585         }
586         break;
587       }
588     }
589
590     /*
591      * isPopupTrigger fires in mousePressed on Mac,
592      * not until mouseRelease on Windows
593      */
594     if (evt.isPopupTrigger() && activeRow != -1)
595     {
596       showPopupMenu(y, evt.getX());
597       return;
598     }
599
600     if (graphStretch != -1)
601     {
602
603       if (aa[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP)
604       {
605         // data in row has position on y as well as x axis
606         if (evt.isAltDown() || evt.isAltGraphDown())
607         {
608           dragMode = DragMode.MatrixSelect;
609           firstDragX = mouseDragLastX;
610           firstDragY = mouseDragLastY;
611         }
612       }
613     }
614     else
615     {
616       // no row (or row that can be adjusted) was pressed. Simulate a ruler
617       // click
618       ap.getScalePanel().mousePressed(evt);
619     }
620   }
621
622   /**
623    * checks whether the annotation row under the mouse click evt's handles the
624    * event
625    * 
626    * @param evt
627    * @return false if evt was not handled
628    */
629   boolean matrix_clicked(MouseEvent evt)
630   {
631     int[] rowIndex = getRowIndexAndOffset(evt.getY(),
632             av.getAlignment().getAlignmentAnnotation());
633     if (rowIndex == null)
634     {
635       jalview.bin.Console
636               .error("IMPLEMENTATION ERROR: matrix click out of range.");
637       return false;
638     }
639     int yOffset = rowIndex[1];
640     AlignmentAnnotation[] allAnnotation = av.getAlignment()
641             .getAlignmentAnnotation();
642     if (allAnnotation == null || rowIndex[0] < 0
643             || rowIndex[0] >= allAnnotation.length)
644     {
645       return false;
646     }
647     AlignmentAnnotation clicked = av.getAlignment()
648             .getAlignmentAnnotation()[rowIndex[0]];
649     if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
650     {
651       return false;
652     }
653
654     // TODO - use existing threshold to select related sections of matrix
655     GraphLine thr = clicked.getThreshold();
656
657     int currentX = getColumnForXPos(evt.getX());
658     ContactListI forCurrentX = av.getContactList(clicked, currentX);
659     if (forCurrentX != null)
660     {
661       ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
662               clicked.graphHeight);
663       ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset);
664       if (cXci != null)
665       {
666         /**
667          * start and end range corresponding to the row range under the mouse at
668          * column currentX
669          */
670         int fr, to;
671         fr = Math.min(cXci.cStart, cXci.cEnd);
672         to = Math.max(cXci.cStart, cXci.cEnd);
673
674         // double click selects the whole group
675         if (evt.getClickCount() == 2)
676         {
677           ContactMatrixI matrix = av.getContactMatrix(clicked);
678
679           if (matrix != null)
680           {
681             // simplest approach is to select all group containing column
682             if (matrix.hasGroups())
683             {
684               SequenceI rseq = clicked.sequenceRef;
685               BitSet grp = new BitSet();
686               grp.or(matrix.getGroupsFor(forCurrentX.getPosition()));
687               // TODO: cXci needs to be mapped to real groups
688               for (int c = fr; c <= to; c++)
689               {
690                 BitSet additionalGrp = matrix.getGroupsFor(c);
691                 grp.or(additionalGrp);
692               }
693
694               HiddenColumns hc = av.getAlignment().getHiddenColumns();
695               ColumnSelection cs = av.getColumnSelection();
696
697               for (int p = grp.nextSetBit(0); p >= 0; p = grp
698                       .nextSetBit(p + 1))
699               {
700                 if (matrix instanceof MappableContactMatrixI)
701                 {
702                   // find the end of this run of set bits
703                   int nextp = grp.nextClearBit(p) - 1;
704                   int[] pos = ((MappableContactMatrixI) matrix)
705                           .getMappedPositionsFor(rseq, p, nextp);
706                   p = nextp;
707
708                   if (pos != null)
709                   {
710                     for (int pos_p = pos[0]; pos_p <= pos[1]; pos_p++)
711                     {
712                       int col = rseq.findIndex(pos_p) - 1;
713                       if (col >= 0 && (!av.hasHiddenColumns()
714                               || hc.isVisible(col)))
715                       {
716                         cs.addElement(col);
717                       }
718                     }
719                   }
720                 }
721                 else
722                 {
723                   int offp = (rseq != null)
724                           ? rseq.findIndex(rseq.getStart() - 1 + p)
725                           : p;
726
727                   if (!av.hasHiddenColumns() || hc.isVisible(offp))
728                   {
729                     cs.addElement(offp);
730                   }
731                 }
732               }
733             }
734             // possible alternative for interactive selection - threshold
735             // gives 'ceiling' for forming a cluster
736             // when a row+column is selected, farthest common ancestor less
737             // than thr is used to compute cluster
738
739           }
740         }
741         else
742         {
743           // select corresponding range in segment under mouse
744           {
745             int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
746             if (rng != null)
747             {
748               av.getColumnSelection().addRangeOfElements(rng, true);
749             }
750             av.getColumnSelection().addElement(currentX);
751           }
752           // PAE SPECIFIC
753           // and also select everything lower than the max range adjacent
754           // (kind of works)
755           if (evt.isControlDown()
756                   && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
757           {
758             int c = fr;
759             ContactRange cr = forCurrentX.getRangeFor(fr, to);
760             double cval;
761             // TODO: could use GraphLine instead of arbitrary picking
762             // TODO: could report mean/median/variance for partitions
763             // (contiguous selected vs unselected regions and inter-contig
764             // regions)
765             // controls feathering - what other elements in row/column
766             // should we select
767             double thresh = cr.getMean()
768                     + (cr.getMax() - cr.getMean()) * .15;
769             while (c >= 0)
770             {
771               cval = forCurrentX.getContactAt(c);
772               if (// cr.getMin() <= cval &&
773               cval <= thresh)
774               {
775                 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
776                 if (cols != null)
777                 {
778                   av.getColumnSelection().addRangeOfElements(cols, true);
779                 }
780                 else
781                 {
782                   break;
783                 }
784               }
785               c--;
786             }
787             c = to;
788             while (c < forCurrentX.getContactHeight())
789             {
790               cval = forCurrentX.getContactAt(c);
791               if (// cr.getMin() <= cval &&
792               cval <= thresh)
793               {
794                 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
795                 if (cols != null)
796                 {
797                   av.getColumnSelection().addRangeOfElements(cols, true);
798                 }
799               }
800               else
801               {
802                 break;
803               }
804               c++;
805
806             }
807           }
808
809         }
810       }
811     }
812     ap.paintAlignment(false, false);
813     PaintRefresher.Refresh(ap, av.getSequenceSetId());
814     av.sendSelection();
815     return true;
816   }
817
818   /**
819    * Construct and display a context menu at the right-click position
820    * 
821    * @param y
822    * @param x
823    */
824   void showPopupMenu(final int y, int x)
825   {
826     if (av.getColumnSelection() == null
827             || av.getColumnSelection().isEmpty())
828     {
829       return;
830     }
831
832     JPopupMenu pop = new JPopupMenu(
833             MessageManager.getString("label.structure_type"));
834     JMenuItem item;
835     /*
836      * Just display the needed structure options
837      */
838     if (av.getAlignment().isNucleotide())
839     {
840       item = new JMenuItem(STEM);
841       item.addActionListener(this);
842       pop.add(item);
843     }
844     else
845     {
846       item = new JMenuItem(HELIX);
847       item.addActionListener(this);
848       pop.add(item);
849       item = new JMenuItem(SHEET);
850       item.addActionListener(this);
851       pop.add(item);
852     }
853     item = new JMenuItem(LABEL);
854     item.addActionListener(this);
855     pop.add(item);
856     item = new JMenuItem(COLOUR);
857     item.addActionListener(this);
858     pop.add(item);
859     item = new JMenuItem(REMOVE);
860     item.addActionListener(this);
861     pop.add(item);
862     pop.show(this, x, y);
863   }
864
865   /**
866    * Action on mouse up is to clear mouse drag data and call mouseReleased on
867    * ScalePanel, to deal with defining the selection group (if any) defined by
868    * the mouse drag
869    * 
870    * @param evt
871    */
872   @Override
873   public void mouseReleased(MouseEvent evt)
874   {
875     if (dragMode == DragMode.MatrixSelect)
876     {
877       matrixSelectRange(evt);
878     }
879     graphStretch = -1;
880     mouseDragLastX = -1;
881     mouseDragLastY = -1;
882     firstDragX = -1;
883     firstDragY = -1;
884     mouseDragging = false;
885     if (dragMode == DragMode.Resize)
886     {
887       ap.adjustAnnotationHeight();
888     }
889     dragMode = DragMode.Undefined;
890     if (!matrix_clicked(evt))
891     {
892       ap.getScalePanel().mouseReleased(evt);
893     }
894
895     /*
896      * isPopupTrigger is set in mouseReleased on Windows
897      * (in mousePressed on Mac)
898      */
899     if (evt.isPopupTrigger() && activeRow != -1)
900     {
901       showPopupMenu(evt.getY(), evt.getX());
902     }
903
904   }
905
906   /**
907    * DOCUMENT ME!
908    * 
909    * @param evt
910    *          DOCUMENT ME!
911    */
912   @Override
913   public void mouseEntered(MouseEvent evt)
914   {
915     this.mouseDragging = false;
916     ap.getScalePanel().mouseEntered(evt);
917   }
918
919   /**
920    * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
921    * with column selection on a mouse drag
922    * 
923    * @param evt
924    */
925   @Override
926   public void mouseExited(MouseEvent evt)
927   {
928     ap.getScalePanel().mouseExited(evt);
929   }
930
931   /**
932    * Action on starting or continuing a mouse drag. There are two possible
933    * actions:
934    * <ul>
935    * <li>drag up or down on a graphed annotation increases or decreases the
936    * height of the graph</li>
937    * <li>dragging left or right selects the columns dragged across</li>
938    * </ul>
939    * A drag on a graph annotation is treated as column selection if it starts
940    * with more horizontal than vertical movement, and as resize if it starts
941    * with more vertical than horizontal movement. Once started, the drag does
942    * not change mode.
943    * 
944    * @param evt
945    */
946   @Override
947   public void mouseDragged(MouseEvent evt)
948   {
949     /*
950      * if dragMode is Undefined:
951      * - set to Select if dx > dy
952      * - set to Resize if dy > dx
953      * - do nothing if dx == dy
954      */
955     final int x = evt.getX();
956     final int y = evt.getY();
957     if (dragMode == DragMode.Undefined)
958     {
959       int dx = Math.abs(x - mouseDragLastX);
960       int dy = Math.abs(y - mouseDragLastY);
961       if (graphStretch == -1 || dx > dy)
962       {
963         /*
964          * mostly horizontal drag, or not a graph annotation
965          */
966         dragMode = DragMode.Select;
967       }
968       else if (dy > dx)
969       {
970         /*
971          * mostly vertical drag
972          */
973         dragMode = DragMode.Resize;
974         notJustOne = evt.isShiftDown();
975
976         /*
977          * but could also be a matrix drag
978          */
979         if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
980                 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
981         {
982           /*
983            * dragging in a matrix
984            */
985           dragMode = DragMode.MatrixSelect;
986           firstDragX = mouseDragLastX;
987           firstDragY = mouseDragLastY;
988         }
989       }
990     }
991
992     if (dragMode == DragMode.Undefined)
993
994     {
995       /*
996        * drag is diagonal - defer deciding whether to
997        * treat as up/down or left/right
998        */
999       return;
1000     }
1001
1002     try
1003     {
1004       if (dragMode == DragMode.Resize)
1005       {
1006         /*
1007          * resize graph annotation if mouse was dragged up or down
1008          */
1009         int deltaY = mouseDragLastY - evt.getY();
1010         if (deltaY != 0)
1011         {
1012           AlignmentAnnotation graphAnnotation = av.getAlignment()
1013                   .getAlignmentAnnotation()[graphStretch];
1014           int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
1015           if (notJustOne)
1016           {
1017             for (AlignmentAnnotation similar : av.getAlignment()
1018                     .findAnnotations(null, graphAnnotation.getCalcId(),
1019                             graphAnnotation.label))
1020             {
1021               similar.graphHeight = newHeight;
1022             }
1023
1024           }
1025           else
1026           {
1027             graphAnnotation.graphHeight = newHeight;
1028           }
1029           adjustPanelHeight();
1030           ap.paintAlignment(false, false);
1031         }
1032       }
1033       else if (dragMode == DragMode.MatrixSelect)
1034       {
1035         /*
1036          * TODO draw a rubber band for range
1037          */
1038         mouseDragLastX = x;
1039         mouseDragLastY = y;
1040         ap.paintAlignment(false, false);
1041       }
1042       else
1043       {
1044         /*
1045          * for mouse drag left or right, delegate to 
1046          * ScalePanel to adjust the column selection
1047          */
1048         ap.getScalePanel().mouseDragged(evt);
1049       }
1050     } finally
1051     {
1052       mouseDragLastX = x;
1053       mouseDragLastY = y;
1054     }
1055   }
1056
1057   public void matrixSelectRange(MouseEvent evt)
1058   {
1059     /*
1060      * get geometry of drag
1061      */
1062     int fromY = Math.min(firstDragY, evt.getY());
1063     int toY = Math.max(firstDragY, evt.getY());
1064     int fromX = Math.min(firstDragX, evt.getX());
1065     int toX = Math.max(firstDragX, evt.getX());
1066
1067     int deltaY = toY - fromY;
1068     int deltaX = toX - fromX;
1069
1070     int[] rowIndex = getRowIndexAndOffset(fromY,
1071             av.getAlignment().getAlignmentAnnotation());
1072     int[] toRowIndex = getRowIndexAndOffset(toY,
1073             av.getAlignment().getAlignmentAnnotation());
1074
1075     if (rowIndex == null || toRowIndex == null)
1076     {
1077       jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1078
1079     }
1080     if (rowIndex[0] != toRowIndex[0])
1081     {
1082       jalview.bin.Console
1083               .trace("Drag went to another row. needs to be clipped");
1084     }
1085
1086     // rectangular selection on matrix style annotation
1087     AlignmentAnnotation cma = av.getAlignment()
1088             .getAlignmentAnnotation()[rowIndex[0]];
1089
1090     int lastX = getColumnForXPos(fromX);
1091     int currentX = getColumnForXPos(toX);
1092     int fromXc = Math.min(lastX, currentX);
1093     int toXc = Math.max(lastX, currentX);
1094     ContactListI forFromX = av.getContactList(cma, fromXc);
1095     ContactListI forToX = av.getContactList(cma, toXc);
1096
1097     if (forFromX != null && forToX != null)
1098     {
1099       // FIXME will need two ContactGeometry objects when handling contact
1100       // matrices with differing numbers of rows at each
1101       // column
1102       ContactGeometry xcgeom = new ContactGeometry(forFromX,
1103               cma.graphHeight);
1104       ContactGeometry.contactInterval lastXci = xcgeom.mapFor(rowIndex[1]);
1105       ContactGeometry.contactInterval cXci = xcgeom
1106               .mapFor(rowIndex[1] + deltaY);
1107
1108       // mark rectangular region formed by drag
1109       jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
1110               + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
1111               + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1112       int fr, to;
1113       fr = Math.min(lastXci.cStart, cXci.cStart);
1114       to = Math.max(lastXci.cEnd, cXci.cEnd);
1115       int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
1116       if (mappedPos != null)
1117       {
1118         jalview.bin.Console.trace("Marking " + fr + " to " + to
1119                 + " mapping to sequence positions " + mappedPos[0] + " to "
1120                 + mappedPos[1]);
1121         for (int pair = 0; pair < mappedPos.length; pair += 2)
1122         {
1123           for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1124           // {
1125           // if (cma.sequenceRef != null)
1126           // {
1127           // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1128           // av.getColumnSelection().addElement(col);
1129           // }
1130           // else
1131           {
1132             av.getColumnSelection().addElement(c - 1);
1133           }
1134         }
1135       }
1136       fr = Math.min(lastX, currentX);
1137       to = Math.max(lastX, currentX);
1138
1139       jalview.bin.Console.trace("Marking " + fr + " to " + to);
1140       for (int c = fr; c <= to; c++)
1141       {
1142         av.getColumnSelection().addElement(c);
1143       }
1144     }
1145
1146   }
1147
1148   /**
1149    * Constructs the tooltip, and constructs and displays a status message, for
1150    * the current mouse position
1151    * 
1152    * @param evt
1153    */
1154   @Override
1155   public void mouseMoved(MouseEvent evt)
1156   {
1157     int yPos = evt.getY();
1158     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1159     int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1160     int row = rowAndOffset[0];
1161
1162     if (row == -1)
1163     {
1164       this.setToolTipText(null);
1165       return;
1166     }
1167
1168     int column = getColumnForXPos(evt.getX());
1169
1170     AlignmentAnnotation ann = aa[row];
1171     if (row > -1 && ann.annotations != null
1172             && column < ann.annotations.length)
1173     {
1174       String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1175               ap);
1176       setToolTipText(toolTip == null ? null
1177               : JvSwingUtils.wrapTooltip(true, toolTip));
1178       String msg = getStatusMessage(av.getAlignment(), column, ann,
1179               rowAndOffset[1], av);
1180       ap.alignFrame.setStatus(msg);
1181     }
1182     else
1183     {
1184       this.setToolTipText(null);
1185       ap.alignFrame.setStatus(" ");
1186     }
1187   }
1188
1189   private int getColumnForXPos(int x)
1190   {
1191     int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1192     column = Math.min(column, av.getRanges().getEndRes());
1193
1194     if (av.hasHiddenColumns())
1195     {
1196       column = av.getAlignment().getHiddenColumns()
1197               .visibleToAbsoluteColumn(column);
1198     }
1199     return column;
1200   }
1201
1202   /**
1203    * Answers the index in the annotations array of the visible annotation at the
1204    * given y position. This is done by adding the heights of visible annotations
1205    * until the y position has been exceeded. Answers -1 if no annotations are
1206    * visible, or the y position is below all annotations.
1207    * 
1208    * @param yPos
1209    * @param aa
1210    * @return
1211    */
1212   static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1213   {
1214     if (aa == null)
1215     {
1216       return -1;
1217     }
1218     return getRowIndexAndOffset(yPos, aa)[0];
1219   }
1220
1221   static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1222   {
1223     int[] res = new int[2];
1224     res[0] = -1;
1225     res[1] = 0;
1226     if (aa == null)
1227     {
1228       return res;
1229     }
1230     int row = -1;
1231     int height = 0, lheight = 0;
1232     for (int i = 0; i < aa.length; i++)
1233     {
1234       if (aa[i].visible)
1235       {
1236         lheight = height;
1237         height += aa[i].height;
1238       }
1239
1240       if (height > yPos)
1241       {
1242         row = i;
1243         res[0] = row;
1244         res[1] = yPos - lheight;
1245         break;
1246       }
1247     }
1248     return res;
1249   }
1250
1251   /**
1252    * Answers a tooltip for the annotation at the current mouse position, not
1253    * wrapped in &lt;html&gt; tags (apply if wanted). Answers null if there is no
1254    * tooltip to show.
1255    * 
1256    * @param ann
1257    * @param column
1258    * @param anns
1259    * @param rowAndOffset
1260    */
1261   static String buildToolTip(AlignmentAnnotation ann, int column,
1262           AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1263           AlignmentPanel ap)
1264   {
1265     String tooltip = null;
1266     if (ann.graphGroup > -1)
1267     {
1268       StringBuilder tip = new StringBuilder(32);
1269       boolean first = true;
1270       for (int i = 0; i < anns.length; i++)
1271       {
1272         if (anns[i].graphGroup == ann.graphGroup
1273                 && anns[i].annotations[column] != null)
1274         {
1275           if (!first)
1276           {
1277             tip.append("<br>");
1278             first = false;
1279           }
1280           tip.append(anns[i].label);
1281           String description = anns[i].annotations[column].description;
1282           if (description != null && description.length() > 0)
1283           {
1284             tip.append(" ").append(description);
1285           }
1286         }
1287       }
1288       tooltip = first ? null : tip.toString();
1289     }
1290     else if (column < ann.annotations.length
1291             && ann.annotations[column] != null)
1292     {
1293       tooltip = getAnnotationBriefSummary(ann.annotations[column]);
1294     }
1295     // TODO abstract tooltip generator so different implementations can be built
1296     if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1297     {
1298       if (rowAndOffset >= ann.graphHeight)
1299       {
1300         return null;
1301       }
1302       ContactListI clist = av.getContactList(ann, column);
1303       if (clist != null)
1304       {
1305         ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1306         ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1307         ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1308         StringBuilder tooltipb = new StringBuilder();
1309         tooltipb.append("Contact from ").append(clist.getPosition())
1310                 .append(", [").append(ci.cStart).append(" - ")
1311                 .append(ci.cEnd).append("]").append("<br/>Mean:");
1312         Format.appendPercentage(tooltipb, (float) cr.getMean(), 2);
1313         tooltip = tooltipb.toString();
1314         int col = ann.sequenceRef.findPosition(column);
1315         int[][] highlightPos;
1316         int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1317         if (mappedPos != null)
1318         {
1319           highlightPos = new int[1 + mappedPos.length][2];
1320           highlightPos[0] = new int[] { col, col };
1321           for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1322           {
1323             highlightPos[h][0] = ann.sequenceRef
1324                     .findPosition(mappedPos[p] - 1);
1325             highlightPos[h][1] = ann.sequenceRef
1326                     .findPosition(mappedPos[p + 1] - 1);
1327           }
1328         }
1329         else
1330         {
1331           highlightPos = new int[][] { new int[] { col, col } };
1332         }
1333         ap.getStructureSelectionManager()
1334                 .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1335       }
1336     }
1337     return tooltip == null || tooltip.length() == 0 ? null : tooltip;
1338   }
1339
1340   private static String getAnnotationBriefSummary(Annotation a)
1341   {
1342     StringBuilder ttSB = new StringBuilder();
1343     if (a.secondaryStructure != 0 && a.secondaryStructure != ' ')
1344     {
1345       ttSB.append(a.secondaryStructure);
1346     }
1347     else if (a.description != null && a.description.trim().length() > 0)
1348     {
1349       ttSB.append(a.description);
1350     }
1351     else if (a.displayCharacter != null
1352             && a.displayCharacter.trim().length() > 0)
1353     {
1354       ttSB.append(a.displayCharacter);
1355     }
1356     else if (!Float.isNaN(a.value))
1357     {
1358       if (a.value == Math.floor(a.value)) // likely integer value
1359       {
1360         ttSB.append(String.format("%.0f", a.value));
1361       }
1362       else // display as is
1363       {
1364         ttSB.append(String.valueOf(a.value));
1365       }
1366     }
1367     return ttSB.toString();
1368   }
1369
1370   /**
1371    * Constructs and returns the status bar message
1372    * 
1373    * @param al
1374    * @param column
1375    * @param ann
1376    * @param rowAndOffset
1377    */
1378   static String getStatusMessage(AlignmentI al, int column,
1379           AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1380   {
1381     /*
1382      * show alignment column and annotation description if any
1383      */
1384     StringBuilder text = new StringBuilder(32);
1385     text.append(MessageManager.getString("label.column")).append(" ")
1386             .append(column + 1);
1387
1388     if (column < ann.annotations.length && ann.annotations[column] != null)
1389     {
1390       String description = getAnnotationBriefSummary(
1391               ann.annotations[column]);
1392       if (description != null && description.trim().length() > 0)
1393       {
1394         text.append("  ").append(description);
1395       }
1396     }
1397
1398     /*
1399      * if the annotation is sequence-specific, show the sequence number
1400      * in the alignment, and (if not a gap) the residue and position
1401      */
1402     SequenceI seqref = ann.sequenceRef;
1403     if (seqref != null)
1404     {
1405       int seqIndex = al.findIndex(seqref);
1406       if (seqIndex != -1)
1407       {
1408         text.append(", ").append(MessageManager.getString("label.sequence"))
1409                 .append(" ").append(seqIndex + 1);
1410         char residue = seqref.getCharAt(column);
1411         if (!Comparison.isGap(residue))
1412         {
1413           text.append(" ");
1414           String name;
1415           if (al.isNucleotide())
1416           {
1417             name = ResidueProperties.nucleotideName
1418                     .get(String.valueOf(residue));
1419             text.append(" Nucleotide: ")
1420                     .append(name != null ? name : residue);
1421           }
1422           else
1423           {
1424             name = 'X' == residue ? "X"
1425                     : ('*' == residue ? "STOP"
1426                             : ResidueProperties.aa2Triplet
1427                                     .get(String.valueOf(residue)));
1428             text.append(" Residue: ").append(name != null ? name : residue);
1429           }
1430           int residuePos = seqref.findPosition(column);
1431           text.append(" (").append(residuePos).append(")");
1432         }
1433       }
1434     }
1435
1436     return text.toString();
1437   }
1438
1439   /**
1440    * DOCUMENT ME!
1441    * 
1442    * @param evt
1443    *          DOCUMENT ME!
1444    */
1445   @Override
1446   public void mouseClicked(MouseEvent evt)
1447   {
1448     // if (activeRow != -1)
1449     // {
1450     // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1451     // AlignmentAnnotation anot = aa[activeRow];
1452     // }
1453   }
1454
1455   // TODO mouseClicked-content and drawCursor are quite experimental!
1456   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1457           int y1)
1458   {
1459     int pady = av.getCharHeight() / 5;
1460     int charOffset = 0;
1461     graphics.setColor(Color.black);
1462     graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1463
1464     if (av.validCharWidth)
1465     {
1466       graphics.setColor(Color.white);
1467
1468       char s = seq.getCharAt(res);
1469
1470       charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1471       graphics.drawString(String.valueOf(s), charOffset + x1,
1472               (y1 + av.getCharHeight()) - pady);
1473     }
1474
1475   }
1476
1477   private volatile boolean imageFresh = false;
1478
1479   private Rectangle visibleRect = new Rectangle(),
1480           clipBounds = new Rectangle();
1481
1482   /**
1483    * DOCUMENT ME!
1484    * 
1485    * @param g
1486    *          DOCUMENT ME!
1487    */
1488   @Override
1489   public void paintComponent(Graphics g)
1490   {
1491
1492     // BH: note that this method is generally recommended to
1493     // call super.paintComponent(g). Otherwise, the children of this
1494     // component will not be rendered. That is not needed here
1495     // because AnnotationPanel does not have any children. It is
1496     // just a JPanel contained in a JViewPort.
1497
1498     computeVisibleRect(visibleRect);
1499
1500     g.setColor(Color.white);
1501     g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1502
1503     if (image != null)
1504     {
1505       // BH 2018 optimizing generation of new Rectangle().
1506       if (fastPaint
1507               || (visibleRect.width != (clipBounds = g
1508                       .getClipBounds(clipBounds)).width)
1509               || (visibleRect.height != clipBounds.height))
1510       {
1511
1512         g.drawImage(image, 0, 0, this);
1513         fastPaint = false;
1514         return;
1515       }
1516     }
1517     updateFadedImageWidth();
1518     if (imgWidth < 1)
1519     {
1520       return;
1521     }
1522     Graphics2D gg;
1523     if (image == null || imgWidth != image.getWidth(this)
1524             || image.getHeight(this) != getHeight())
1525     {
1526       boolean tried = false;
1527       image = null;
1528       while (image == null && !tried)
1529       {
1530         try
1531         {
1532           image = new BufferedImage(imgWidth,
1533                   ap.getAnnotationPanel().getHeight(),
1534                   BufferedImage.TYPE_INT_RGB);
1535           tried = true;
1536         } catch (IllegalArgumentException exc)
1537         {
1538           jalview.bin.Console.errPrintln(
1539                   "Serious issue with viewport geometry imgWidth requested was "
1540                           + imgWidth);
1541           return;
1542         } catch (OutOfMemoryError oom)
1543         {
1544           try
1545           {
1546             System.gc();
1547           } catch (Exception x)
1548           {
1549           }
1550           ;
1551           new OOMWarning(
1552                   "Couldn't allocate memory to redraw screen. Please restart Jalview",
1553                   oom);
1554           return;
1555         }
1556
1557       }
1558       gg = (Graphics2D) image.getGraphics();
1559
1560       if (av.antiAlias)
1561       {
1562         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1563                 RenderingHints.VALUE_ANTIALIAS_ON);
1564       }
1565
1566       gg.setFont(av.getFont());
1567       fm = gg.getFontMetrics();
1568       gg.setColor(Color.white);
1569       gg.fillRect(0, 0, imgWidth, image.getHeight());
1570       imageFresh = true;
1571     }
1572     else
1573     {
1574       gg = (Graphics2D) image.getGraphics();
1575
1576     }
1577
1578     drawComponent(gg, av.getRanges().getStartRes(),
1579             av.getRanges().getEndRes() + 1);
1580     gg.dispose();
1581     imageFresh = false;
1582     g.drawImage(image, 0, 0, this);
1583   }
1584
1585   public void updateFadedImageWidth()
1586   {
1587     imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1588             + 1) * av.getCharWidth();
1589
1590   }
1591
1592   /**
1593    * set true to enable redraw timing debug output on stderr
1594    */
1595   private final boolean debugRedraw = false;
1596
1597   /**
1598    * non-Thread safe repaint
1599    * 
1600    * @param horizontal
1601    *          repaint with horizontal shift in alignment
1602    */
1603   public void fastPaint(int horizontal)
1604   {
1605     if ((horizontal == 0) || image == null
1606             || av.getAlignment().getAlignmentAnnotation() == null
1607             || av.getAlignment().getAlignmentAnnotation().length < 1
1608             || av.isCalcInProgress())
1609     {
1610       repaint();
1611       return;
1612     }
1613
1614     int sr = av.getRanges().getStartRes();
1615     int er = av.getRanges().getEndRes() + 1;
1616     int transX = 0;
1617
1618     Graphics2D gg = (Graphics2D) image.getGraphics();
1619
1620     if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1621     {
1622       // scroll is less than imgWidth away so can re-use buffered graphics
1623       gg.copyArea(0, 0, imgWidth, getHeight(),
1624               -horizontal * av.getCharWidth(), 0);
1625
1626       if (horizontal > 0) // scrollbar pulled right, image to the left
1627       {
1628         transX = (er - sr - horizontal) * av.getCharWidth();
1629         sr = er - horizontal;
1630       }
1631       else if (horizontal < 0)
1632       {
1633         er = sr - horizontal;
1634       }
1635     }
1636     gg.translate(transX, 0);
1637
1638     drawComponent(gg, sr, er);
1639
1640     gg.translate(-transX, 0);
1641
1642     gg.dispose();
1643
1644     fastPaint = true;
1645
1646     // Call repaint on alignment panel so that repaints from other alignment
1647     // panel components can be aggregated. Otherwise performance of the overview
1648     // window and others may be adversely affected.
1649     av.getAlignPanel().repaint();
1650   }
1651
1652   private volatile boolean lastImageGood = false;
1653
1654   /**
1655    * DOCUMENT ME!
1656    * 
1657    * @param g
1658    *          DOCUMENT ME!
1659    * @param startRes
1660    *          DOCUMENT ME!
1661    * @param endRes
1662    *          DOCUMENT ME!
1663    */
1664   public void drawComponent(Graphics g, int startRes, int endRes)
1665   {
1666     BufferedImage oldFaded = fadedImage;
1667     if (av.isCalcInProgress())
1668     {
1669       if (image == null)
1670       {
1671         lastImageGood = false;
1672         return;
1673       }
1674       // We'll keep a record of the old image,
1675       // and draw a faded image until the calculation
1676       // has completed
1677       if (lastImageGood
1678               && (fadedImage == null || fadedImage.getWidth() != imgWidth
1679                       || fadedImage.getHeight() != image.getHeight()))
1680       {
1681         // jalview.bin.Console.errPrintln("redraw faded image
1682         // ("+(fadedImage==null ?
1683         // "null image" : "") + " lastGood="+lastImageGood+")");
1684         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1685                 BufferedImage.TYPE_INT_RGB);
1686
1687         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1688
1689         fadedG.setColor(Color.white);
1690         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1691
1692         fadedG.setComposite(
1693                 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1694         fadedG.drawImage(image, 0, 0, this);
1695
1696       }
1697       // make sure we don't overwrite the last good faded image until all
1698       // calculations have finished
1699       lastImageGood = false;
1700
1701     }
1702     else
1703     {
1704       if (fadedImage != null)
1705       {
1706         oldFaded = fadedImage;
1707       }
1708       fadedImage = null;
1709     }
1710
1711     g.setColor(Color.white);
1712     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1713
1714     g.setFont(av.getFont());
1715     if (fm == null)
1716     {
1717       fm = g.getFontMetrics();
1718     }
1719
1720     if ((av.getAlignment().getAlignmentAnnotation() == null)
1721             || (av.getAlignment().getAlignmentAnnotation().length < 1))
1722     {
1723       g.setColor(Color.white);
1724       g.fillRect(0, 0, getWidth(), getHeight());
1725       g.setColor(Color.black);
1726       if (av.validCharWidth)
1727       {
1728         g.drawString(MessageManager
1729                 .getString("label.alignment_has_no_annotations"), 20, 15);
1730       }
1731
1732       return;
1733     }
1734     lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1735             endRes);
1736     if (!lastImageGood && fadedImage == null)
1737     {
1738       fadedImage = oldFaded;
1739     }
1740     if (dragMode == DragMode.MatrixSelect)
1741     {
1742       g.setColor(Color.yellow);
1743       g.drawRect(Math.min(firstDragX, mouseDragLastX),
1744               Math.min(firstDragY, mouseDragLastY),
1745               Math.max(firstDragX, mouseDragLastX)
1746                       - Math.min(firstDragX, mouseDragLastX),
1747               Math.max(firstDragY, mouseDragLastY)
1748                       - Math.min(firstDragY, mouseDragLastY));
1749
1750     }
1751   }
1752
1753   @Override
1754   public FontMetrics getFontMetrics()
1755   {
1756     return fm;
1757   }
1758
1759   @Override
1760   public Image getFadedImage()
1761   {
1762     return fadedImage;
1763   }
1764
1765   @Override
1766   public int getFadedImageWidth()
1767   {
1768     updateFadedImageWidth();
1769     return imgWidth;
1770   }
1771
1772   private int[] bounds = new int[2];
1773
1774   @Override
1775   public int[] getVisibleVRange()
1776   {
1777     if (ap != null && ap.getAlabels() != null)
1778     {
1779       int sOffset = -ap.getAlabels().getScrollOffset();
1780       int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1781       bounds[0] = sOffset;
1782       bounds[1] = visHeight;
1783       return bounds;
1784     }
1785     else
1786     {
1787       return null;
1788     }
1789   }
1790
1791   /**
1792    * Try to ensure any references held are nulled
1793    */
1794   public void dispose()
1795   {
1796     av = null;
1797     ap = null;
1798     image = null;
1799     fadedImage = null;
1800     // gg = null;
1801     _mwl = null;
1802
1803     /*
1804      * I created the renderer so I will dispose of it
1805      */
1806     if (renderer != null)
1807     {
1808       renderer.dispose();
1809     }
1810   }
1811
1812   @Override
1813   public void propertyChange(PropertyChangeEvent evt)
1814   {
1815     // Respond to viewport range changes (e.g. alignment panel was scrolled)
1816     // Both scrolling and resizing change viewport ranges: scrolling changes
1817     // both start and end points, but resize only changes end values.
1818     // Here we only want to fastpaint on a scroll, with resize using a normal
1819     // paint, so scroll events are identified as changes to the horizontal or
1820     // vertical start value.
1821     if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1822     {
1823       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1824     }
1825     else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1826     {
1827       fastPaint(((int[]) evt.getNewValue())[0]
1828               - ((int[]) evt.getOldValue())[0]);
1829     }
1830     else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1831     {
1832       repaint();
1833     }
1834   }
1835
1836   /**
1837    * computes the visible height of the annotation panel
1838    * 
1839    * @param adjustPanelHeight
1840    *          - when false, just adjust existing height according to other
1841    *          windows
1842    * @param annotationHeight
1843    * @return height to use for the ScrollerPreferredVisibleSize
1844    */
1845   public int adjustForAlignFrame(boolean adjustPanelHeight,
1846           int annotationHeight)
1847   {
1848     /*
1849      * Estimate available height in the AlignFrame for alignment +
1850      * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1851      * hscroll, status bar, insets. 
1852      */
1853     int stuff = (ap.getViewName() != null ? 30 : 0)
1854             + (Platform.isAMacAndNotJS() ? 120 : 140);
1855     int availableHeight = ap.alignFrame.getHeight() - stuff;
1856     int rowHeight = av.getCharHeight();
1857
1858     if (adjustPanelHeight)
1859     {
1860       int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1861
1862       /*
1863        * If not enough vertical space, maximize annotation height while keeping
1864        * at least two rows of alignment visible
1865        */
1866       if (annotationHeight + alignmentHeight > availableHeight)
1867       {
1868         annotationHeight = Math.min(annotationHeight,
1869                 availableHeight - 2 * rowHeight);
1870       }
1871     }
1872     else
1873     {
1874       // maintain same window layout whilst updating sliders
1875       annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1876               availableHeight - 2 * rowHeight);
1877     }
1878     return annotationHeight;
1879   }
1880 }