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