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