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