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