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