Merge branch 'bug/JAL-2934proportionalScrolling' into develop
[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           }
1279           first = false;
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 = ann.annotations[column].description;
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;
1338   }
1339
1340   /**
1341    * Constructs and returns the status bar message
1342    * 
1343    * @param al
1344    * @param column
1345    * @param ann
1346    * @param rowAndOffset
1347    */
1348   static String getStatusMessage(AlignmentI al, int column,
1349           AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1350   {
1351     /*
1352      * show alignment column and annotation description if any
1353      */
1354     StringBuilder text = new StringBuilder(32);
1355     text.append(MessageManager.getString("label.column")).append(" ")
1356             .append(column + 1);
1357
1358     if (column < ann.annotations.length && ann.annotations[column] != null)
1359     {
1360       String description = ann.annotations[column].description;
1361       if (description != null && description.trim().length() > 0)
1362       {
1363         text.append("  ").append(description);
1364       }
1365     }
1366
1367     /*
1368      * if the annotation is sequence-specific, show the sequence number
1369      * in the alignment, and (if not a gap) the residue and position
1370      */
1371     SequenceI seqref = ann.sequenceRef;
1372     if (seqref != null)
1373     {
1374       int seqIndex = al.findIndex(seqref);
1375       if (seqIndex != -1)
1376       {
1377         text.append(", ").append(MessageManager.getString("label.sequence"))
1378                 .append(" ").append(seqIndex + 1);
1379         char residue = seqref.getCharAt(column);
1380         if (!Comparison.isGap(residue))
1381         {
1382           text.append(" ");
1383           String name;
1384           if (al.isNucleotide())
1385           {
1386             name = ResidueProperties.nucleotideName
1387                     .get(String.valueOf(residue));
1388             text.append(" Nucleotide: ")
1389                     .append(name != null ? name : residue);
1390           }
1391           else
1392           {
1393             name = 'X' == residue ? "X"
1394                     : ('*' == residue ? "STOP"
1395                             : ResidueProperties.aa2Triplet
1396                                     .get(String.valueOf(residue)));
1397             text.append(" Residue: ").append(name != null ? name : residue);
1398           }
1399           int residuePos = seqref.findPosition(column);
1400           text.append(" (").append(residuePos).append(")");
1401         }
1402       }
1403     }
1404
1405     return text.toString();
1406   }
1407
1408   /**
1409    * DOCUMENT ME!
1410    * 
1411    * @param evt
1412    *          DOCUMENT ME!
1413    */
1414   @Override
1415   public void mouseClicked(MouseEvent evt)
1416   {
1417     // if (activeRow != -1)
1418     // {
1419     // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1420     // AlignmentAnnotation anot = aa[activeRow];
1421     // }
1422   }
1423
1424   // TODO mouseClicked-content and drawCursor are quite experimental!
1425   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1426           int y1)
1427   {
1428     int pady = av.getCharHeight() / 5;
1429     int charOffset = 0;
1430     graphics.setColor(Color.black);
1431     graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1432
1433     if (av.validCharWidth)
1434     {
1435       graphics.setColor(Color.white);
1436
1437       char s = seq.getCharAt(res);
1438
1439       charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1440       graphics.drawString(String.valueOf(s), charOffset + x1,
1441               (y1 + av.getCharHeight()) - pady);
1442     }
1443
1444   }
1445
1446   private volatile boolean imageFresh = false;
1447
1448   private Rectangle visibleRect = new Rectangle(),
1449           clipBounds = new Rectangle();
1450
1451   /**
1452    * DOCUMENT ME!
1453    * 
1454    * @param g
1455    *          DOCUMENT ME!
1456    */
1457   @Override
1458   public void paintComponent(Graphics g)
1459   {
1460
1461     // BH: note that this method is generally recommended to
1462     // call super.paintComponent(g). Otherwise, the children of this
1463     // component will not be rendered. That is not needed here
1464     // because AnnotationPanel does not have any children. It is
1465     // just a JPanel contained in a JViewPort.
1466
1467     computeVisibleRect(visibleRect);
1468
1469     g.setColor(Color.white);
1470     g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1471
1472     if (image != null)
1473     {
1474       // BH 2018 optimizing generation of new Rectangle().
1475       if (fastPaint
1476               || (visibleRect.width != (clipBounds = g
1477                       .getClipBounds(clipBounds)).width)
1478               || (visibleRect.height != clipBounds.height))
1479       {
1480
1481         g.drawImage(image, 0, 0, this);
1482         fastPaint = false;
1483         return;
1484       }
1485     }
1486     updateFadedImageWidth();
1487     if (imgWidth < 1)
1488     {
1489       return;
1490     }
1491     Graphics2D gg;
1492     if (image == null || imgWidth != image.getWidth(this)
1493             || image.getHeight(this) != getHeight())
1494     {
1495       boolean tried = false;
1496       image = null;
1497       while (image == null && !tried)
1498       {
1499         try
1500         {
1501           image = new BufferedImage(imgWidth,
1502                   ap.getAnnotationPanel().getHeight(),
1503                   BufferedImage.TYPE_INT_RGB);
1504           tried = true;
1505         } catch (IllegalArgumentException exc)
1506         {
1507           jalview.bin.Console.errPrintln(
1508                   "Serious issue with viewport geometry imgWidth requested was "
1509                           + imgWidth);
1510           return;
1511         } catch (OutOfMemoryError oom)
1512         {
1513           try
1514           {
1515             System.gc();
1516           } catch (Exception x)
1517           {
1518           }
1519           ;
1520           new OOMWarning(
1521                   "Couldn't allocate memory to redraw screen. Please restart Jalview",
1522                   oom);
1523           return;
1524         }
1525
1526       }
1527       gg = (Graphics2D) image.getGraphics();
1528
1529       if (av.antiAlias)
1530       {
1531         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1532                 RenderingHints.VALUE_ANTIALIAS_ON);
1533       }
1534
1535       gg.setFont(av.getFont());
1536       fm = gg.getFontMetrics();
1537       gg.setColor(Color.white);
1538       gg.fillRect(0, 0, imgWidth, image.getHeight());
1539       imageFresh = true;
1540     }
1541     else
1542     {
1543       gg = (Graphics2D) image.getGraphics();
1544
1545     }
1546
1547     drawComponent(gg, av.getRanges().getStartRes(),
1548             av.getRanges().getEndRes() + 1);
1549     gg.dispose();
1550     imageFresh = false;
1551     g.drawImage(image, 0, 0, this);
1552   }
1553
1554   public void updateFadedImageWidth()
1555   {
1556     imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1557             + 1) * av.getCharWidth();
1558
1559   }
1560
1561   /**
1562    * set true to enable redraw timing debug output on stderr
1563    */
1564   private final boolean debugRedraw = false;
1565
1566   /**
1567    * non-Thread safe repaint
1568    * 
1569    * @param horizontal
1570    *          repaint with horizontal shift in alignment
1571    */
1572   public void fastPaint(int horizontal)
1573   {
1574     if ((horizontal == 0) || image == null
1575             || av.getAlignment().getAlignmentAnnotation() == null
1576             || av.getAlignment().getAlignmentAnnotation().length < 1
1577             || av.isCalcInProgress())
1578     {
1579       repaint();
1580       return;
1581     }
1582
1583     int sr = av.getRanges().getStartRes();
1584     int er = av.getRanges().getEndRes() + 1;
1585     int transX = 0;
1586
1587     Graphics2D gg = (Graphics2D) image.getGraphics();
1588
1589     if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1590     {
1591       // scroll is less than imgWidth away so can re-use buffered graphics
1592       gg.copyArea(0, 0, imgWidth, getHeight(),
1593               -horizontal * av.getCharWidth(), 0);
1594
1595       if (horizontal > 0) // scrollbar pulled right, image to the left
1596       {
1597         transX = (er - sr - horizontal) * av.getCharWidth();
1598         sr = er - horizontal;
1599       }
1600       else if (horizontal < 0)
1601       {
1602         er = sr - horizontal;
1603       }
1604     }
1605     gg.translate(transX, 0);
1606
1607     drawComponent(gg, sr, er);
1608
1609     gg.translate(-transX, 0);
1610
1611     gg.dispose();
1612
1613     fastPaint = true;
1614
1615     // Call repaint on alignment panel so that repaints from other alignment
1616     // panel components can be aggregated. Otherwise performance of the overview
1617     // window and others may be adversely affected.
1618     av.getAlignPanel().repaint();
1619   }
1620
1621   private volatile boolean lastImageGood = false;
1622
1623   /**
1624    * DOCUMENT ME!
1625    * 
1626    * @param g
1627    *          DOCUMENT ME!
1628    * @param startRes
1629    *          DOCUMENT ME!
1630    * @param endRes
1631    *          DOCUMENT ME!
1632    */
1633   public void drawComponent(Graphics g, int startRes, int endRes)
1634   {
1635     BufferedImage oldFaded = fadedImage;
1636     if (av.isCalcInProgress())
1637     {
1638       if (image == null)
1639       {
1640         lastImageGood = false;
1641         return;
1642       }
1643       // We'll keep a record of the old image,
1644       // and draw a faded image until the calculation
1645       // has completed
1646       if (lastImageGood
1647               && (fadedImage == null || fadedImage.getWidth() != imgWidth
1648                       || fadedImage.getHeight() != image.getHeight()))
1649       {
1650         // jalview.bin.Console.errPrintln("redraw faded image
1651         // ("+(fadedImage==null ?
1652         // "null image" : "") + " lastGood="+lastImageGood+")");
1653         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1654                 BufferedImage.TYPE_INT_RGB);
1655
1656         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1657
1658         fadedG.setColor(Color.white);
1659         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1660
1661         fadedG.setComposite(
1662                 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1663         fadedG.drawImage(image, 0, 0, this);
1664
1665       }
1666       // make sure we don't overwrite the last good faded image until all
1667       // calculations have finished
1668       lastImageGood = false;
1669
1670     }
1671     else
1672     {
1673       if (fadedImage != null)
1674       {
1675         oldFaded = fadedImage;
1676       }
1677       fadedImage = null;
1678     }
1679
1680     g.setColor(Color.white);
1681     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1682
1683     g.setFont(av.getFont());
1684     if (fm == null)
1685     {
1686       fm = g.getFontMetrics();
1687     }
1688
1689     if ((av.getAlignment().getAlignmentAnnotation() == null)
1690             || (av.getAlignment().getAlignmentAnnotation().length < 1))
1691     {
1692       g.setColor(Color.white);
1693       g.fillRect(0, 0, getWidth(), getHeight());
1694       g.setColor(Color.black);
1695       if (av.validCharWidth)
1696       {
1697         g.drawString(MessageManager
1698                 .getString("label.alignment_has_no_annotations"), 20, 15);
1699       }
1700
1701       return;
1702     }
1703     lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1704             endRes);
1705     if (!lastImageGood && fadedImage == null)
1706     {
1707       fadedImage = oldFaded;
1708     }
1709     if (dragMode == DragMode.MatrixSelect)
1710     {
1711       g.setColor(Color.yellow);
1712       g.drawRect(Math.min(firstDragX, mouseDragLastX),
1713               Math.min(firstDragY, mouseDragLastY),
1714               Math.max(firstDragX, mouseDragLastX)
1715                       - Math.min(firstDragX, mouseDragLastX),
1716               Math.max(firstDragY, mouseDragLastY)
1717                       - Math.min(firstDragY, mouseDragLastY));
1718
1719     }
1720   }
1721
1722   @Override
1723   public FontMetrics getFontMetrics()
1724   {
1725     return fm;
1726   }
1727
1728   @Override
1729   public Image getFadedImage()
1730   {
1731     return fadedImage;
1732   }
1733
1734   @Override
1735   public int getFadedImageWidth()
1736   {
1737     updateFadedImageWidth();
1738     return imgWidth;
1739   }
1740
1741   private int[] bounds = new int[2];
1742
1743   @Override
1744   public int[] getVisibleVRange()
1745   {
1746     if (ap != null && ap.getAlabels() != null)
1747     {
1748       int sOffset = -ap.getAlabels().getScrollOffset();
1749       int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1750       bounds[0] = sOffset;
1751       bounds[1] = visHeight;
1752       return bounds;
1753     }
1754     else
1755     {
1756       return null;
1757     }
1758   }
1759
1760   /**
1761    * Try to ensure any references held are nulled
1762    */
1763   public void dispose()
1764   {
1765     av = null;
1766     ap = null;
1767     image = null;
1768     fadedImage = null;
1769     // gg = null;
1770     _mwl = null;
1771
1772     /*
1773      * I created the renderer so I will dispose of it
1774      */
1775     if (renderer != null)
1776     {
1777       renderer.dispose();
1778     }
1779   }
1780
1781   @Override
1782   public void propertyChange(PropertyChangeEvent evt)
1783   {
1784     // Respond to viewport range changes (e.g. alignment panel was scrolled)
1785     // Both scrolling and resizing change viewport ranges: scrolling changes
1786     // both start and end points, but resize only changes end values.
1787     // Here we only want to fastpaint on a scroll, with resize using a normal
1788     // paint, so scroll events are identified as changes to the horizontal or
1789     // vertical start value.
1790     if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1791     {
1792       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1793     }
1794     else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1795     {
1796       fastPaint(((int[]) evt.getNewValue())[0]
1797               - ((int[]) evt.getOldValue())[0]);
1798     }
1799     else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1800     {
1801       repaint();
1802     }
1803   }
1804
1805   /**
1806    * computes the visible height of the annotation panel
1807    * 
1808    * @param adjustPanelHeight
1809    *          - when false, just adjust existing height according to other
1810    *          windows
1811    * @param annotationHeight
1812    * @return height to use for the ScrollerPreferredVisibleSize
1813    */
1814   public int adjustForAlignFrame(boolean adjustPanelHeight,
1815           int annotationHeight)
1816   {
1817     /*
1818      * Estimate available height in the AlignFrame for alignment +
1819      * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1820      * hscroll, status bar, insets. 
1821      */
1822     int stuff = (ap.getViewName() != null ? 30 : 0)
1823             + (Platform.isAMacAndNotJS() ? 120 : 140);
1824     int availableHeight = ap.alignFrame.getHeight() - stuff;
1825     int rowHeight = av.getCharHeight();
1826
1827     if (adjustPanelHeight)
1828     {
1829       int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1830
1831       /*
1832        * If not enough vertical space, maximize annotation height while keeping
1833        * at least two rows of alignment visible
1834        */
1835       if (annotationHeight + alignmentHeight > availableHeight)
1836       {
1837         annotationHeight = Math.min(annotationHeight,
1838                 availableHeight - 2 * rowHeight);
1839       }
1840     }
1841     else
1842     {
1843       // maintain same window layout whilst updating sliders
1844       annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1845               availableHeight - 2 * rowHeight);
1846     }
1847     return annotationHeight;
1848   }
1849 }