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