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