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