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