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