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