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