JAL-3858 JAL-4090 pae matrix docs. WIP
[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               yOffset);
653       /**
654        * start and end range corresponding to the row range under the mouse at
655        * column currentX
656        */
657       int fr, to;
658       fr = Math.min(cXci.cStart, cXci.cEnd);
659       to = Math.max(cXci.cStart, cXci.cEnd);
660
661       // double click selects the whole group
662       if (evt.getClickCount() == 2)
663       {
664         ContactMatrixI matrix = av.getContactMatrix(clicked);
665
666         if (matrix != null)
667         {
668           // simplest approach is to select all group containing column
669           if (matrix.hasGroups())
670           {
671             SequenceI rseq = clicked.sequenceRef;
672             BitSet grp = new BitSet();
673             grp.or(matrix.getGroupsFor(currentX));
674             // TODO: cXci needs to be mapped to real groups
675             for (int c = fr; c <= to; c++)
676             {
677               BitSet additionalGrp = matrix.getGroupsFor(c);
678               grp.or(additionalGrp);
679             }
680
681             HiddenColumns hc = av.getAlignment().getHiddenColumns();
682             ColumnSelection cs = av.getColumnSelection();
683             
684             for (int p=grp.nextSetBit(0); p >= 0; p = grp
685                     .nextSetBit(p + 1))
686             {
687               if (matrix instanceof MappableContactMatrixI)
688               {
689                 // find the end of this run of set bits
690                 int nextp = grp.nextClearBit(p)-1;
691                 int[] pos = ((MappableContactMatrixI)matrix).getMappedPositionsFor(rseq, p,nextp);
692                 p=nextp;
693                 
694                 if (pos!=null)
695                 {
696                   for (int pos_p = pos[0];pos_p<=pos[1];pos_p++)
697                   {
698                     int col = rseq.findIndex(pos_p)-1;
699                     if (col>=0 && (!av.hasHiddenColumns() || hc.isVisible(col)))
700                     {
701                       cs.addElement(col);
702                     }
703                   }
704                 }
705               } else {
706                 int offp = (rseq != null)
707                         ? rseq.findIndex(rseq.getStart() - 1 + p)
708                         : p;
709   
710                 if (!av.hasHiddenColumns() || hc.isVisible(offp))
711                 {
712                   cs.addElement(offp);
713                 }
714               }
715             }
716           }
717           // possible alternative for interactive selection - threshold
718           // gives 'ceiling' for forming a cluster
719           // when a row+column is selected, farthest common ancestor less
720           // than thr is used to compute cluster
721
722         }
723       }
724       else
725       {
726         // select corresponding range in segment under mouse
727         {
728           int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
729           if (rng != null)
730           {
731             av.getColumnSelection().addRangeOfElements(rng, true);
732           }
733           av.getColumnSelection().addElement(currentX);
734         }
735         // PAE SPECIFIC
736         // and also select everything lower than the max range adjacent
737         // (kind of works)
738         if (evt.isControlDown()
739                 && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
740         {
741           int c = fr;
742           ContactRange cr = forCurrentX.getRangeFor(fr, to);
743           double cval;
744           // TODO: could use GraphLine instead of arbitrary picking
745           // TODO: could report mean/median/variance for partitions
746           // (contiguous selected vs unselected regions and inter-contig
747           // regions)
748           // controls feathering - what other elements in row/column
749           // should we select
750           double thresh = cr.getMean() + (cr.getMax() - cr.getMean()) * .15;
751           while (c >= 0)
752           {
753             cval = forCurrentX.getContactAt(c);
754             if (// cr.getMin() <= cval &&
755             cval <= thresh)
756             {
757               int[] cols = forCurrentX.getMappedPositionsFor(c, c);
758               if (cols != null)
759               {
760                 av.getColumnSelection().addRangeOfElements(cols, true);
761               }
762               else
763               {
764                 break;
765               }
766             }
767             c--;
768           }
769           c = to;
770           while (c < forCurrentX.getContactHeight())
771           {
772             cval = forCurrentX.getContactAt(c);
773             if (// cr.getMin() <= cval &&
774             cval <= thresh)
775             {
776               int[] cols = forCurrentX.getMappedPositionsFor(c, c);
777               if (cols != null)
778               {
779                 av.getColumnSelection().addRangeOfElements(cols, true);
780               }
781             }
782             else
783             {
784               break;
785             }
786             c++;
787
788           }
789         }
790       }
791     }
792     ap.paintAlignment(false, false);
793     PaintRefresher.Refresh(ap, av.getSequenceSetId());
794     av.sendSelection();
795     return true;
796   }
797
798   /**
799    * Construct and display a context menu at the right-click position
800    * 
801    * @param y
802    * @param x
803    */
804   void showPopupMenu(final int y, int x)
805   {
806     if (av.getColumnSelection() == null
807             || av.getColumnSelection().isEmpty())
808     {
809       return;
810     }
811
812     JPopupMenu pop = new JPopupMenu(
813             MessageManager.getString("label.structure_type"));
814     JMenuItem item;
815     /*
816      * Just display the needed structure options
817      */
818     if (av.getAlignment().isNucleotide())
819     {
820       item = new JMenuItem(STEM);
821       item.addActionListener(this);
822       pop.add(item);
823     }
824     else
825     {
826       item = new JMenuItem(HELIX);
827       item.addActionListener(this);
828       pop.add(item);
829       item = new JMenuItem(SHEET);
830       item.addActionListener(this);
831       pop.add(item);
832     }
833     item = new JMenuItem(LABEL);
834     item.addActionListener(this);
835     pop.add(item);
836     item = new JMenuItem(COLOUR);
837     item.addActionListener(this);
838     pop.add(item);
839     item = new JMenuItem(REMOVE);
840     item.addActionListener(this);
841     pop.add(item);
842     pop.show(this, x, y);
843   }
844
845   /**
846    * Action on mouse up is to clear mouse drag data and call mouseReleased on
847    * ScalePanel, to deal with defining the selection group (if any) defined by
848    * the mouse drag
849    * 
850    * @param evt
851    */
852   @Override
853   public void mouseReleased(MouseEvent evt)
854   {
855     if (dragMode == DragMode.MatrixSelect)
856     {
857       matrixSelectRange(evt);
858     }
859     graphStretch = -1;
860     mouseDragLastX = -1;
861     mouseDragLastY = -1;
862     firstDragX = -1;
863     firstDragY = -1;
864     mouseDragging = false;
865     if (dragMode == DragMode.Resize)
866     {
867       ap.adjustAnnotationHeight();
868     }
869     dragMode = DragMode.Undefined;
870     if (!matrix_clicked(evt))
871     {
872       ap.getScalePanel().mouseReleased(evt);
873     }
874
875     /*
876      * isPopupTrigger is set in mouseReleased on Windows
877      * (in mousePressed on Mac)
878      */
879     if (evt.isPopupTrigger() && activeRow != -1)
880     {
881       showPopupMenu(evt.getY(), evt.getX());
882     }
883
884   }
885
886   /**
887    * DOCUMENT ME!
888    * 
889    * @param evt
890    *          DOCUMENT ME!
891    */
892   @Override
893   public void mouseEntered(MouseEvent evt)
894   {
895     this.mouseDragging = false;
896     ap.getScalePanel().mouseEntered(evt);
897   }
898
899   /**
900    * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
901    * with column selection on a mouse drag
902    * 
903    * @param evt
904    */
905   @Override
906   public void mouseExited(MouseEvent evt)
907   {
908     ap.getScalePanel().mouseExited(evt);
909   }
910
911   /**
912    * Action on starting or continuing a mouse drag. There are two possible
913    * actions:
914    * <ul>
915    * <li>drag up or down on a graphed annotation increases or decreases the
916    * height of the graph</li>
917    * <li>dragging left or right selects the columns dragged across</li>
918    * </ul>
919    * A drag on a graph annotation is treated as column selection if it starts
920    * with more horizontal than vertical movement, and as resize if it starts
921    * with more vertical than horizontal movement. Once started, the drag does
922    * not change mode.
923    * 
924    * @param evt
925    */
926   @Override
927   public void mouseDragged(MouseEvent evt)
928   {
929     /*
930      * if dragMode is Undefined:
931      * - set to Select if dx > dy
932      * - set to Resize if dy > dx
933      * - do nothing if dx == dy
934      */
935     final int x = evt.getX();
936     final int y = evt.getY();
937     if (dragMode == DragMode.Undefined)
938     {
939       int dx = Math.abs(x - mouseDragLastX);
940       int dy = Math.abs(y - mouseDragLastY);
941       if (graphStretch == -1 || dx > dy)
942       {
943         /*
944          * mostly horizontal drag, or not a graph annotation
945          */
946         dragMode = DragMode.Select;
947       }
948       else if (dy > dx)
949       {
950         /*
951          * mostly vertical drag
952          */
953         dragMode = DragMode.Resize;
954         notJustOne = evt.isShiftDown();
955
956         /*
957          * but could also be a matrix drag
958          */
959         if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
960                 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
961         {
962           /*
963            * dragging in a matrix
964            */
965           dragMode = DragMode.MatrixSelect;
966           firstDragX = mouseDragLastX;
967           firstDragY = mouseDragLastY;
968         }
969       }
970     }
971
972     if (dragMode == DragMode.Undefined)
973
974     {
975       /*
976        * drag is diagonal - defer deciding whether to
977        * treat as up/down or left/right
978        */
979       return;
980     }
981
982     try
983     {
984       if (dragMode == DragMode.Resize)
985       {
986         /*
987          * resize graph annotation if mouse was dragged up or down
988          */
989         int deltaY = mouseDragLastY - evt.getY();
990         if (deltaY != 0)
991         {
992           AlignmentAnnotation graphAnnotation = av.getAlignment()
993                   .getAlignmentAnnotation()[graphStretch];
994           int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
995           if (notJustOne)
996           {
997             for (AlignmentAnnotation similar : av.getAlignment()
998                     .findAnnotations(null, graphAnnotation.getCalcId(),
999                             graphAnnotation.label))
1000             {
1001               similar.graphHeight = newHeight;
1002             }
1003
1004           }
1005           else
1006           {
1007             graphAnnotation.graphHeight = newHeight;
1008           }
1009           adjustPanelHeight();
1010           ap.paintAlignment(false, false);
1011         }
1012       }
1013       else if (dragMode == DragMode.MatrixSelect)
1014       {
1015         /*
1016          * TODO draw a rubber band for range
1017          */
1018         mouseDragLastX = x;
1019         mouseDragLastY = y;
1020         ap.paintAlignment(false, false);
1021       }
1022       else
1023       {
1024         /*
1025          * for mouse drag left or right, delegate to 
1026          * ScalePanel to adjust the column selection
1027          */
1028         ap.getScalePanel().mouseDragged(evt);
1029       }
1030     } finally
1031     {
1032       mouseDragLastX = x;
1033       mouseDragLastY = y;
1034     }
1035   }
1036
1037   public void matrixSelectRange(MouseEvent evt)
1038   {
1039     /*
1040      * get geometry of drag
1041      */
1042     int fromY = Math.min(firstDragY, evt.getY());
1043     int toY = Math.max(firstDragY, evt.getY());
1044     int fromX = Math.min(firstDragX, evt.getX());
1045     int toX = Math.max(firstDragX, evt.getX());
1046
1047     int deltaY = toY - fromY;
1048     int deltaX = toX - fromX;
1049
1050     int[] rowIndex = getRowIndexAndOffset(fromY,
1051             av.getAlignment().getAlignmentAnnotation());
1052     int[] toRowIndex = getRowIndexAndOffset(toY,
1053             av.getAlignment().getAlignmentAnnotation());
1054
1055     if (rowIndex == null || toRowIndex == null)
1056     {
1057       jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1058
1059     }
1060     if (rowIndex[0] != toRowIndex[0])
1061     {
1062       jalview.bin.Console
1063               .trace("Drag went to another row. needs to be clipped");
1064     }
1065
1066     // rectangular selection on matrix style annotation
1067     AlignmentAnnotation cma = av.getAlignment()
1068             .getAlignmentAnnotation()[rowIndex[0]];
1069
1070     int lastX = getColumnForXPos(fromX);
1071     int currentX = getColumnForXPos(toX);
1072     int fromXc = Math.min(lastX, currentX);
1073     int toXc = Math.max(lastX, currentX);
1074     ContactListI forFromX = av.getContactList(cma, fromXc);
1075     ContactListI forToX = av.getContactList(cma, toXc);
1076
1077     if (forFromX != null && forToX != null)
1078     {
1079       ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
1080               cma.graphHeight);
1081       ContactGeometry.contactInterval lastXci = lastXcgeom
1082               .mapFor(rowIndex[1], rowIndex[1] + deltaY);
1083
1084       ContactGeometry cXcgeom = new ContactGeometry(forToX,
1085               cma.graphHeight);
1086       ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
1087               rowIndex[1] + deltaY);
1088
1089       // mark rectangular region formed by drag
1090       jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
1091               + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
1092               + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1093       int fr, to;
1094       fr = Math.min(lastXci.cStart, lastXci.cEnd);
1095       to = Math.max(lastXci.cStart, lastXci.cEnd);
1096       int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
1097       if (mappedPos != null)
1098       {
1099         jalview.bin.Console.trace("Marking " + fr + " to " + to
1100                 + " mapping to sequence positions " + mappedPos[0] + " to "
1101                 + mappedPos[1]);
1102         for (int pair = 0; pair < mappedPos.length; pair += 2)
1103         {
1104           for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1105           // {
1106           // if (cma.sequenceRef != null)
1107           // {
1108           // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1109           // av.getColumnSelection().addElement(col);
1110           // }
1111           // else
1112           {
1113             av.getColumnSelection().addElement(c);
1114           }
1115         }
1116       }
1117       // and again for most recent corner of drag
1118       fr = Math.min(cXci.cStart, cXci.cEnd);
1119       to = Math.max(cXci.cStart, cXci.cEnd);
1120       mappedPos = forFromX.getMappedPositionsFor(fr, to);
1121       if (mappedPos != null)
1122       {
1123         for (int pair = 0; pair < mappedPos.length; pair += 2)
1124         {
1125           jalview.bin.Console.trace("Marking " + fr + " to " + to
1126                   + " mapping to sequence positions " + mappedPos[pair]
1127                   + " to " + mappedPos[pair + 1]);
1128           for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1129           {
1130             // if (cma.sequenceRef != null)
1131             // {
1132             // int col =
1133             // cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1134             // av.getColumnSelection().addElement(col);
1135             // }
1136             // else
1137             {
1138               av.getColumnSelection().addElement(c);
1139             }
1140           }
1141         }
1142       }
1143       fr = Math.min(lastX, currentX);
1144       to = Math.max(lastX, currentX);
1145
1146       jalview.bin.Console.trace("Marking " + fr + " to " + to);
1147       for (int c = fr; c <= to; c++)
1148       {
1149         av.getColumnSelection().addElement(c);
1150       }
1151     }
1152
1153   }
1154
1155   /**
1156    * Constructs the tooltip, and constructs and displays a status message, for
1157    * the current mouse position
1158    * 
1159    * @param evt
1160    */
1161   @Override
1162   public void mouseMoved(MouseEvent evt)
1163   {
1164     int yPos = evt.getY();
1165     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1166     int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1167     int row = rowAndOffset[0];
1168
1169     if (row == -1)
1170     {
1171       this.setToolTipText(null);
1172       return;
1173     }
1174
1175     int column = getColumnForXPos(evt.getX());
1176
1177     AlignmentAnnotation ann = aa[row];
1178     if (row > -1 && ann.annotations != null
1179             && column < ann.annotations.length)
1180     {
1181       String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1182               ap);
1183       setToolTipText(toolTip == null ? null
1184               : JvSwingUtils.wrapTooltip(true, toolTip));
1185       String msg = getStatusMessage(av.getAlignment(), column, ann,
1186               rowAndOffset[1], av);
1187       ap.alignFrame.setStatus(msg);
1188     }
1189     else
1190     {
1191       this.setToolTipText(null);
1192       ap.alignFrame.setStatus(" ");
1193     }
1194   }
1195
1196   private int getColumnForXPos(int x)
1197   {
1198     int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1199     column = Math.min(column, av.getRanges().getEndRes());
1200
1201     if (av.hasHiddenColumns())
1202     {
1203       column = av.getAlignment().getHiddenColumns()
1204               .visibleToAbsoluteColumn(column);
1205     }
1206     return column;
1207   }
1208
1209   /**
1210    * Answers the index in the annotations array of the visible annotation at the
1211    * given y position. This is done by adding the heights of visible annotations
1212    * until the y position has been exceeded. Answers -1 if no annotations are
1213    * visible, or the y position is below all annotations.
1214    * 
1215    * @param yPos
1216    * @param aa
1217    * @return
1218    */
1219   static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1220   {
1221     if (aa == null)
1222     {
1223       return -1;
1224     }
1225     return getRowIndexAndOffset(yPos, aa)[0];
1226   }
1227
1228   static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1229   {
1230     int[] res = new int[2];
1231     res[0] = -1;
1232     res[1] = 0;
1233     if (aa == null)
1234     {
1235       return res;
1236     }
1237     int row = -1;
1238     int height = 0, lheight = 0;
1239     for (int i = 0; i < aa.length; i++)
1240     {
1241       if (aa[i].visible)
1242       {
1243         lheight = height;
1244         height += aa[i].height;
1245       }
1246
1247       if (height > yPos)
1248       {
1249         row = i;
1250         res[0] = row;
1251         res[1] = yPos-lheight;
1252         break;
1253       }
1254     }
1255     return res;
1256   }
1257
1258   /**
1259    * Answers a tooltip for the annotation at the current mouse position, not
1260    * wrapped in &lt;html&gt; tags (apply if wanted). Answers null if there is no
1261    * tooltip to show.
1262    * 
1263    * @param ann
1264    * @param column
1265    * @param anns
1266    * @param rowAndOffset
1267    */
1268   static String buildToolTip(AlignmentAnnotation ann, int column,
1269           AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1270           AlignmentPanel ap)
1271   {
1272     String tooltip = null;
1273     if (ann.graphGroup > -1)
1274     {
1275       StringBuilder tip = new StringBuilder(32);
1276       boolean first = true;
1277       for (int i = 0; i < anns.length; i++)
1278       {
1279         if (anns[i].graphGroup == ann.graphGroup
1280                 && anns[i].annotations[column] != null)
1281         {
1282           if (!first)
1283           {
1284             tip.append("<br>");
1285           }
1286           first = false;
1287           tip.append(anns[i].label);
1288           String description = anns[i].annotations[column].description;
1289           if (description != null && description.length() > 0)
1290           {
1291             tip.append(" ").append(description);
1292           }
1293         }
1294       }
1295       tooltip = first ? null : tip.toString();
1296     }
1297     else if (column < ann.annotations.length
1298             && ann.annotations[column] != null)
1299     {
1300       tooltip = ann.annotations[column].description;
1301     }
1302     // TODO abstract tooltip generator so different implementations can be built
1303     if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1304     {
1305       if (rowAndOffset>=ann.graphHeight)
1306       {
1307         return null;
1308       }
1309       ContactListI clist = av.getContactList(ann, column);
1310       if (clist != null)
1311       {
1312         ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1313         ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1314         ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1315         StringBuilder tooltipb = new StringBuilder();
1316         tooltipb.append("Contact from ")
1317         .append(clist.getPosition()).append(", [").append(ci.cStart).append(" - ").append(ci.cEnd).append("]").append("<br/>Mean:");
1318         Format.appendPercentage(tooltipb, (float)cr.getMean(),2);
1319         tooltip = tooltipb.toString();
1320         int col = ann.sequenceRef.findPosition(column);
1321         int[][] highlightPos;
1322         int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1323         if (mappedPos != null)
1324         {
1325           highlightPos = new int[1 + mappedPos.length][2];
1326           highlightPos[0] = new int[] { col, col };
1327           for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1328           {
1329             highlightPos[h][0] = ann.sequenceRef
1330                     .findPosition(mappedPos[p] - 1);
1331             highlightPos[h][1] = ann.sequenceRef
1332                     .findPosition(mappedPos[p + 1] - 1);
1333           }
1334         }
1335         else
1336         {
1337           highlightPos = new int[][] { new int[] { col, col } };
1338         }
1339         ap.getStructureSelectionManager()
1340                 .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1341       }
1342     }
1343     return tooltip;
1344   }
1345
1346   /**
1347    * Constructs and returns the status bar message
1348    * 
1349    * @param al
1350    * @param column
1351    * @param ann
1352    * @param rowAndOffset
1353    */
1354   static String getStatusMessage(AlignmentI al, int column,
1355           AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1356   {
1357     /*
1358      * show alignment column and annotation description if any
1359      */
1360     StringBuilder text = new StringBuilder(32);
1361     text.append(MessageManager.getString("label.column")).append(" ")
1362             .append(column + 1);
1363
1364     if (column < ann.annotations.length && ann.annotations[column] != null)
1365     {
1366       String description = ann.annotations[column].description;
1367       if (description != null && description.trim().length() > 0)
1368       {
1369         text.append("  ").append(description);
1370       }
1371     }
1372
1373     /*
1374      * if the annotation is sequence-specific, show the sequence number
1375      * in the alignment, and (if not a gap) the residue and position
1376      */
1377     SequenceI seqref = ann.sequenceRef;
1378     if (seqref != null)
1379     {
1380       int seqIndex = al.findIndex(seqref);
1381       if (seqIndex != -1)
1382       {
1383         text.append(", ").append(MessageManager.getString("label.sequence"))
1384                 .append(" ").append(seqIndex + 1);
1385         char residue = seqref.getCharAt(column);
1386         if (!Comparison.isGap(residue))
1387         {
1388           text.append(" ");
1389           String name;
1390           if (al.isNucleotide())
1391           {
1392             name = ResidueProperties.nucleotideName
1393                     .get(String.valueOf(residue));
1394             text.append(" Nucleotide: ")
1395                     .append(name != null ? name : residue);
1396           }
1397           else
1398           {
1399             name = 'X' == residue ? "X"
1400                     : ('*' == residue ? "STOP"
1401                             : ResidueProperties.aa2Triplet
1402                                     .get(String.valueOf(residue)));
1403             text.append(" Residue: ").append(name != null ? name : residue);
1404           }
1405           int residuePos = seqref.findPosition(column);
1406           text.append(" (").append(residuePos).append(")");
1407         }
1408       }
1409     }
1410
1411     return text.toString();
1412   }
1413
1414   /**
1415    * DOCUMENT ME!
1416    * 
1417    * @param evt
1418    *          DOCUMENT ME!
1419    */
1420   @Override
1421   public void mouseClicked(MouseEvent evt)
1422   {
1423     // if (activeRow != -1)
1424     // {
1425     // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1426     // AlignmentAnnotation anot = aa[activeRow];
1427     // }
1428   }
1429
1430   // TODO mouseClicked-content and drawCursor are quite experimental!
1431   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1432           int y1)
1433   {
1434     int pady = av.getCharHeight() / 5;
1435     int charOffset = 0;
1436     graphics.setColor(Color.black);
1437     graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1438
1439     if (av.validCharWidth)
1440     {
1441       graphics.setColor(Color.white);
1442
1443       char s = seq.getCharAt(res);
1444
1445       charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1446       graphics.drawString(String.valueOf(s), charOffset + x1,
1447               (y1 + av.getCharHeight()) - pady);
1448     }
1449
1450   }
1451
1452   private volatile boolean imageFresh = false;
1453
1454   private Rectangle visibleRect = new Rectangle(),
1455           clipBounds = new Rectangle();
1456
1457   /**
1458    * DOCUMENT ME!
1459    * 
1460    * @param g
1461    *          DOCUMENT ME!
1462    */
1463   @Override
1464   public void paintComponent(Graphics g)
1465   {
1466
1467     // BH: note that this method is generally recommended to
1468     // call super.paintComponent(g). Otherwise, the children of this
1469     // component will not be rendered. That is not needed here
1470     // because AnnotationPanel does not have any children. It is
1471     // just a JPanel contained in a JViewPort.
1472
1473     computeVisibleRect(visibleRect);
1474
1475     g.setColor(Color.white);
1476     g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1477
1478     if (image != null)
1479     {
1480       // BH 2018 optimizing generation of new Rectangle().
1481       if (fastPaint
1482               || (visibleRect.width != (clipBounds = g
1483                       .getClipBounds(clipBounds)).width)
1484               || (visibleRect.height != clipBounds.height))
1485       {
1486
1487         g.drawImage(image, 0, 0, this);
1488         fastPaint = false;
1489         return;
1490       }
1491     }
1492     updateFadedImageWidth();
1493     if (imgWidth < 1)
1494     {
1495       return;
1496     }
1497     Graphics2D gg;
1498     if (image == null || imgWidth != image.getWidth(this)
1499             || image.getHeight(this) != getHeight())
1500     {
1501       boolean tried = false;
1502       image = null;
1503       while (image == null && !tried)
1504       {
1505         try
1506         {
1507           image = new BufferedImage(imgWidth,
1508                   ap.getAnnotationPanel().getHeight(),
1509                   BufferedImage.TYPE_INT_RGB);
1510           tried = true;
1511         } catch (IllegalArgumentException exc)
1512         {
1513           jalview.bin.Console.errPrintln(
1514                   "Serious issue with viewport geometry imgWidth requested was "
1515                           + imgWidth);
1516           return;
1517         } catch (OutOfMemoryError oom)
1518         {
1519           try
1520           {
1521             System.gc();
1522           } catch (Exception x)
1523           {
1524           }
1525           ;
1526           new OOMWarning(
1527                   "Couldn't allocate memory to redraw screen. Please restart Jalview",
1528                   oom);
1529           return;
1530         }
1531
1532       }
1533       gg = (Graphics2D) image.getGraphics();
1534
1535       if (av.antiAlias)
1536       {
1537         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1538                 RenderingHints.VALUE_ANTIALIAS_ON);
1539       }
1540
1541       gg.setFont(av.getFont());
1542       fm = gg.getFontMetrics();
1543       gg.setColor(Color.white);
1544       gg.fillRect(0, 0, imgWidth, image.getHeight());
1545       imageFresh = true;
1546     }
1547     else
1548     {
1549       gg = (Graphics2D) image.getGraphics();
1550
1551     }
1552
1553     drawComponent(gg, av.getRanges().getStartRes(),
1554             av.getRanges().getEndRes() + 1);
1555     gg.dispose();
1556     imageFresh = false;
1557     g.drawImage(image, 0, 0, this);
1558   }
1559
1560   public void updateFadedImageWidth()
1561   {
1562     imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1563             + 1) * av.getCharWidth();
1564
1565   }
1566
1567   /**
1568    * set true to enable redraw timing debug output on stderr
1569    */
1570   private final boolean debugRedraw = false;
1571
1572   /**
1573    * non-Thread safe repaint
1574    * 
1575    * @param horizontal
1576    *          repaint with horizontal shift in alignment
1577    */
1578   public void fastPaint(int horizontal)
1579   {
1580     if ((horizontal == 0) || image == null
1581             || av.getAlignment().getAlignmentAnnotation() == null
1582             || av.getAlignment().getAlignmentAnnotation().length < 1
1583             || av.isCalcInProgress())
1584     {
1585       repaint();
1586       return;
1587     }
1588
1589     int sr = av.getRanges().getStartRes();
1590     int er = av.getRanges().getEndRes() + 1;
1591     int transX = 0;
1592
1593     Graphics2D gg = (Graphics2D) image.getGraphics();
1594
1595     if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1596     {
1597       // scroll is less than imgWidth away so can re-use buffered graphics
1598       gg.copyArea(0, 0, imgWidth, getHeight(),
1599               -horizontal * av.getCharWidth(), 0);
1600
1601       if (horizontal > 0) // scrollbar pulled right, image to the left
1602       {
1603         transX = (er - sr - horizontal) * av.getCharWidth();
1604         sr = er - horizontal;
1605       }
1606       else if (horizontal < 0)
1607       {
1608         er = sr - horizontal;
1609       }
1610     }
1611     gg.translate(transX, 0);
1612
1613     drawComponent(gg, sr, er);
1614
1615     gg.translate(-transX, 0);
1616
1617     gg.dispose();
1618
1619     fastPaint = true;
1620
1621     // Call repaint on alignment panel so that repaints from other alignment
1622     // panel components can be aggregated. Otherwise performance of the overview
1623     // window and others may be adversely affected.
1624     av.getAlignPanel().repaint();
1625   }
1626
1627   private volatile boolean lastImageGood = false;
1628
1629   /**
1630    * DOCUMENT ME!
1631    * 
1632    * @param g
1633    *          DOCUMENT ME!
1634    * @param startRes
1635    *          DOCUMENT ME!
1636    * @param endRes
1637    *          DOCUMENT ME!
1638    */
1639   public void drawComponent(Graphics g, int startRes, int endRes)
1640   {
1641     BufferedImage oldFaded = fadedImage;
1642     if (av.isCalcInProgress())
1643     {
1644       if (image == null)
1645       {
1646         lastImageGood = false;
1647         return;
1648       }
1649       // We'll keep a record of the old image,
1650       // and draw a faded image until the calculation
1651       // has completed
1652       if (lastImageGood
1653               && (fadedImage == null || fadedImage.getWidth() != imgWidth
1654                       || fadedImage.getHeight() != image.getHeight()))
1655       {
1656         // jalview.bin.Console.errPrintln("redraw faded image ("+(fadedImage==null ?
1657         // "null image" : "") + " lastGood="+lastImageGood+")");
1658         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1659                 BufferedImage.TYPE_INT_RGB);
1660
1661         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1662
1663         fadedG.setColor(Color.white);
1664         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1665
1666         fadedG.setComposite(
1667                 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1668         fadedG.drawImage(image, 0, 0, this);
1669
1670       }
1671       // make sure we don't overwrite the last good faded image until all
1672       // calculations have finished
1673       lastImageGood = false;
1674
1675     }
1676     else
1677     {
1678       if (fadedImage != null)
1679       {
1680         oldFaded = fadedImage;
1681       }
1682       fadedImage = null;
1683     }
1684
1685     g.setColor(Color.white);
1686     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1687
1688     g.setFont(av.getFont());
1689     if (fm == null)
1690     {
1691       fm = g.getFontMetrics();
1692     }
1693
1694     if ((av.getAlignment().getAlignmentAnnotation() == null)
1695             || (av.getAlignment().getAlignmentAnnotation().length < 1))
1696     {
1697       g.setColor(Color.white);
1698       g.fillRect(0, 0, getWidth(), getHeight());
1699       g.setColor(Color.black);
1700       if (av.validCharWidth)
1701       {
1702         g.drawString(MessageManager
1703                 .getString("label.alignment_has_no_annotations"), 20, 15);
1704       }
1705
1706       return;
1707     }
1708     lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1709             endRes);
1710     if (!lastImageGood && fadedImage == null)
1711     {
1712       fadedImage = oldFaded;
1713     }
1714     if (dragMode == DragMode.MatrixSelect)
1715     {
1716       g.setColor(Color.yellow);
1717       g.drawRect(Math.min(firstDragX, mouseDragLastX),
1718               Math.min(firstDragY, mouseDragLastY),
1719               Math.max(firstDragX, mouseDragLastX)
1720                       - Math.min(firstDragX, mouseDragLastX),
1721               Math.max(firstDragY, mouseDragLastY)
1722                       - Math.min(firstDragY, mouseDragLastY));
1723
1724     }
1725   }
1726
1727   @Override
1728   public FontMetrics getFontMetrics()
1729   {
1730     return fm;
1731   }
1732
1733   @Override
1734   public Image getFadedImage()
1735   {
1736     return fadedImage;
1737   }
1738
1739   @Override
1740   public int getFadedImageWidth()
1741   {
1742     updateFadedImageWidth();
1743     return imgWidth;
1744   }
1745
1746   private int[] bounds = new int[2];
1747
1748   @Override
1749   public int[] getVisibleVRange()
1750   {
1751     if (ap != null && ap.getAlabels() != null)
1752     {
1753       int sOffset = -ap.getAlabels().getScrollOffset();
1754       int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1755       bounds[0] = sOffset;
1756       bounds[1] = visHeight;
1757       return bounds;
1758     }
1759     else
1760     {
1761       return null;
1762     }
1763   }
1764
1765   /**
1766    * Try to ensure any references held are nulled
1767    */
1768   public void dispose()
1769   {
1770     av = null;
1771     ap = null;
1772     image = null;
1773     fadedImage = null;
1774     // gg = null;
1775     _mwl = null;
1776
1777     /*
1778      * I created the renderer so I will dispose of it
1779      */
1780     if (renderer != null)
1781     {
1782       renderer.dispose();
1783     }
1784   }
1785
1786   @Override
1787   public void propertyChange(PropertyChangeEvent evt)
1788   {
1789     // Respond to viewport range changes (e.g. alignment panel was scrolled)
1790     // Both scrolling and resizing change viewport ranges: scrolling changes
1791     // both start and end points, but resize only changes end values.
1792     // Here we only want to fastpaint on a scroll, with resize using a normal
1793     // paint, so scroll events are identified as changes to the horizontal or
1794     // vertical start value.
1795     if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1796     {
1797       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1798     }
1799     else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1800     {
1801       fastPaint(((int[]) evt.getNewValue())[0]
1802               - ((int[]) evt.getOldValue())[0]);
1803     }
1804     else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1805     {
1806       repaint();
1807     }
1808   }
1809
1810   /**
1811    * computes the visible height of the annotation panel
1812    * 
1813    * @param adjustPanelHeight
1814    *          - when false, just adjust existing height according to other
1815    *          windows
1816    * @param annotationHeight
1817    * @return height to use for the ScrollerPreferredVisibleSize
1818    */
1819   public int adjustForAlignFrame(boolean adjustPanelHeight,
1820           int annotationHeight)
1821   {
1822     /*
1823      * Estimate available height in the AlignFrame for alignment +
1824      * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1825      * hscroll, status bar, insets. 
1826      */
1827     int stuff = (ap.getViewName() != null ? 30 : 0)
1828             + (Platform.isAMacAndNotJS() ? 120 : 140);
1829     int availableHeight = ap.alignFrame.getHeight() - stuff;
1830     int rowHeight = av.getCharHeight();
1831
1832     if (adjustPanelHeight)
1833     {
1834       int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1835
1836       /*
1837        * If not enough vertical space, maximize annotation height while keeping
1838        * at least two rows of alignment visible
1839        */
1840       if (annotationHeight + alignmentHeight > availableHeight)
1841       {
1842         annotationHeight = Math.min(annotationHeight,
1843                 availableHeight - 2 * rowHeight);
1844       }
1845     }
1846     else
1847     {
1848       // maintain same window layout whilst updating sliders
1849       annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1850               availableHeight - 2 * rowHeight);
1851     }
1852     return annotationHeight;
1853   }
1854 }