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