Clover icon

Coverage Report

  1. Project Clover database Fri Dec 6 2024 13:47:14 GMT
  2. Package jalview.gui

File AnnotationPanel.java

 

Coverage histogram

../../img/srcFileCovDistChart3.png
52% of files have more coverage

Code metrics

300
587
40
2
1,849
1,295
239
0.41
14.68
20
5.97

Classes

Class Line # Actions
AnnotationPanel 86 587 239
0.2265372122.7%
AnnotationPanel.DragMode 90 0 0
-1.0 -
 

Contributing tests

This file is covered by 209 tests. .

Source view

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  478 toggle public AnnotationPanel(AlignmentPanel ap)
168    {
169  478 ToolTipManager.sharedInstance().registerComponent(this);
170  478 ToolTipManager.sharedInstance().setInitialDelay(0);
171  478 ToolTipManager.sharedInstance().setDismissDelay(10000);
172  478 this.ap = ap;
173  478 av = ap.av;
174  478 this.setLayout(null);
175  478 addMouseListener(this);
176  478 addMouseMotionListener(this);
177  478 ap.annotationScroller.getVerticalScrollBar()
178    .addAdjustmentListener(this);
179    // save any wheel listeners on the scroller, so we can propagate scroll
180    // events to them.
181  478 _mwl = ap.annotationScroller.getMouseWheelListeners();
182    // and then set our own listener to consume all mousewheel events
183  478 ap.annotationScroller.addMouseWheelListener(this);
184  478 renderer = new AnnotationRenderer();
185   
186  478 av.getRanges().addPropertyChangeListener(this);
187    }
188   
 
189  13 toggle public AnnotationPanel(AlignViewport av)
190    {
191  13 this.av = av;
192  13 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  0 toggle @Override
211    public void mouseWheelMoved(MouseWheelEvent e)
212    {
213  0 if (e.isShiftDown())
214    {
215  0 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  0 for (MouseWheelListener mwl : _mwl)
222    {
223  0 if (mwl != null)
224    {
225  0 mwl.mouseWheelMoved(e);
226    }
227  0 if (e.isConsumed())
228    {
229  0 break;
230    }
231    }
232    }
233    }
234   
 
235  0 toggle @Override
236    public Dimension getPreferredScrollableViewportSize()
237    {
238  0 Dimension ps = getPreferredSize();
239  0 return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
240    }
241   
 
242  0 toggle @Override
243    public int getScrollableBlockIncrement(Rectangle visibleRect,
244    int orientation, int direction)
245    {
246  0 return 30;
247    }
248   
 
249  7035 toggle @Override
250    public boolean getScrollableTracksViewportHeight()
251    {
252  7035 return false;
253    }
254   
 
255  7035 toggle @Override
256    public boolean getScrollableTracksViewportWidth()
257    {
258  7035 return true;
259    }
260   
 
261  0 toggle @Override
262    public int getScrollableUnitIncrement(Rectangle visibleRect,
263    int orientation, int direction)
264    {
265  0 return 30;
266    }
267   
268    /*
269    * (non-Javadoc)
270    *
271    * @see
272    * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
273    * .AdjustmentEvent)
274    */
 
275  905 toggle @Override
276    public void adjustmentValueChanged(AdjustmentEvent evt)
277    {
278    // update annotation label display
279  905 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  4820 toggle public int adjustPanelHeight()
289    {
290  4820 int height = av.calcPanelHeight();
291  4820 this.setPreferredSize(new Dimension(1, height));
292  4820 if (ap != null)
293    {
294    // revalidate only when the alignment panel is fully constructed
295  4207 ap.validate();
296    }
297   
298  4820 return height;
299    }
300   
301    /**
302    * DOCUMENT ME!
303    *
304    * @param evt
305    * DOCUMENT ME!
306    */
 
307  0 toggle @Override
308    public void actionPerformed(ActionEvent evt)
309    {
310  0 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
311  0 if (aa == null)
312    {
313  0 return;
314    }
315  0 Annotation[] anot = aa[activeRow].annotations;
316   
317  0 if (anot.length < av.getColumnSelection().getMax())
318    {
319  0 Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
320    + 2];
321  0 System.arraycopy(anot, 0, temp, 0, anot.length);
322  0 anot = temp;
323  0 aa[activeRow].annotations = anot;
324    }
325   
326  0 String action = evt.getActionCommand();
327  0 if (action.equals(REMOVE))
328    {
329  0 for (int index : av.getColumnSelection().getSelected())
330    {
331  0 if (av.getAlignment().getHiddenColumns().isVisible(index))
332    {
333  0 anot[index] = null;
334    }
335    }
336    }
337  0 else if (action.equals(LABEL))
338    {
339  0 String exMesg = collectAnnotVals(anot, LABEL);
340  0 String label = JvOptionPane.showInputDialog(
341    MessageManager.getString("label.enter_label"), exMesg);
342   
343  0 if (label == null)
344    {
345  0 return;
346    }
347   
348  0 if ((label.length() > 0) && !aa[activeRow].hasText)
349    {
350  0 aa[activeRow].hasText = true;
351    }
352   
353  0 for (int index : av.getColumnSelection().getSelected())
354    {
355  0 if (!av.getAlignment().getHiddenColumns().isVisible(index))
356    {
357  0 continue;
358    }
359   
360  0 if (anot[index] == null)
361    {
362  0 anot[index] = new Annotation(label, "", ' ', 0);
363    }
364    else
365    {
366  0 anot[index].displayCharacter = label;
367    }
368    }
369    }
370  0 else if (action.equals(COLOUR))
371    {
372  0 final Annotation[] fAnot = anot;
373  0 String title = MessageManager
374    .getString("label.select_foreground_colour");
375  0 ColourChooserListener listener = new ColourChooserListener()
376    {
 
377  0 toggle @Override
378    public void colourSelected(Color c)
379    {
380  0 HiddenColumns hiddenColumns = av.getAlignment()
381    .getHiddenColumns();
382  0 for (int index : av.getColumnSelection().getSelected())
383    {
384  0 if (hiddenColumns.isVisible(index))
385    {
386  0 if (fAnot[index] == null)
387    {
388  0 fAnot[index] = new Annotation("", "", ' ', 0);
389    }
390  0 fAnot[index].colour = c;
391    }
392    }
393    };
394    };
395  0 JalviewColourChooser.showColourChooser(this, title, Color.black,
396    listener);
397    }
398    else
399    // HELIX, SHEET or STEM
400    {
401  0 char type = 0;
402  0 String symbol = "\u03B1"; // alpha
403   
404  0 if (action.equals(HELIX))
405    {
406  0 type = 'H';
407    }
408  0 else if (action.equals(SHEET))
409    {
410  0 type = 'E';
411  0 symbol = "\u03B2"; // beta
412    }
413   
414    // Added by LML to color stems
415  0 else if (action.equals(STEM))
416    {
417  0 type = 'S';
418  0 int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
419  0 symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
420    }
421   
422  0 if (!aa[activeRow].hasIcons)
423    {
424  0 aa[activeRow].hasIcons = true;
425    }
426   
427  0 String label = JvOptionPane.showInputDialog(MessageManager
428    .getString("label.enter_label_for_the_structure"), symbol);
429   
430  0 if (label == null)
431    {
432  0 return;
433    }
434   
435  0 if ((label.length() > 0) && !aa[activeRow].hasText)
436    {
437  0 aa[activeRow].hasText = true;
438  0 if (action.equals(STEM))
439    {
440  0 aa[activeRow].showAllColLabels = true;
441    }
442    }
443  0 for (int index : av.getColumnSelection().getSelected())
444    {
445  0 if (!av.getAlignment().getHiddenColumns().isVisible(index))
446    {
447  0 continue;
448    }
449   
450  0 if (anot[index] == null)
451    {
452  0 anot[index] = new Annotation(label, "", type, 0);
453    }
454   
455  0 anot[index].secondaryStructure = type != 'S' ? type
456  0 : label.length() == 0 ? ' ' : label.charAt(0);
457  0 anot[index].displayCharacter = label;
458   
459    }
460    }
461   
462  0 av.getAlignment().validateAnnotation(aa[activeRow]);
463  0 ap.alignmentChanged();
464  0 ap.alignFrame.setMenusForViewport();
465  0 adjustPanelHeight();
466  0 repaint();
467   
468  0 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  0 toggle private String collectAnnotVals(Annotation[] anots, String type)
482    {
483    // TODO is this method wanted? why? 'last' is not used
484   
485  0 StringBuilder collatedInput = new StringBuilder(64);
486  0 String last = "";
487  0 ColumnSelection viscols = av.getColumnSelection();
488  0 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  0 List<Integer> selected = new ArrayList<>(viscols.getSelected());
495  0 Collections.sort(selected);
496  0 for (int index : selected)
497    {
498    // always check for current display state - just in case
499  0 if (!hidden.isVisible(index))
500    {
501  0 continue;
502    }
503  0 String tlabel = null;
504  0 if (anots[index] != null)
505    { // LML added stem code
506  0 if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
507    || type.equals(LABEL))
508    {
509  0 tlabel = anots[index].description;
510  0 if (tlabel == null || tlabel.length() < 1)
511    {
512  0 if (type.equals(HELIX) || type.equals(SHEET)
513    || type.equals(STEM))
514    {
515  0 tlabel = "" + anots[index].secondaryStructure;
516    }
517    else
518    {
519  0 tlabel = "" + anots[index].displayCharacter;
520    }
521    }
522    }
523  0 if (tlabel != null && !tlabel.equals(last))
524    {
525  0 if (last.length() > 0)
526    {
527  0 collatedInput.append(" ");
528    }
529  0 collatedInput.append(tlabel);
530    }
531    }
532    }
533  0 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  0 toggle @Override
544    public void mousePressed(MouseEvent evt)
545    {
546   
547  0 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
548  0 if (aa == null)
549    {
550  0 return;
551    }
552  0 mouseDragLastX = evt.getX();
553  0 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  0 int height = 0;
560  0 activeRow = -1;
561  0 int yOffset = 0;
562    // todo could reuse getRowIndexAndOffset ?
563  0 final int y = evt.getY();
564   
565  0 for (int i = 0; i < aa.length; i++)
566    {
567  0 if (aa[i].visible)
568    {
569  0 height += aa[i].height;
570    }
571   
572  0 if (y < height)
573    {
574  0 if (aa[i].editable)
575    {
576  0 activeRow = i;
577    }
578  0 else if (aa[i].graph != 0)
579    {
580    /*
581    * we have clicked on a resizable graph annotation
582    */
583  0 graphStretch = i;
584  0 yOffset = height - y;
585    }
586  0 break;
587    }
588    }
589   
590    /*
591    * isPopupTrigger fires in mousePressed on Mac,
592    * not until mouseRelease on Windows
593    */
594  0 if (evt.isPopupTrigger() && activeRow != -1)
595    {
596  0 showPopupMenu(y, evt.getX());
597  0 return;
598    }
599   
600  0 if (graphStretch != -1)
601    {
602   
603  0 if (aa[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP)
604    {
605    // data in row has position on y as well as x axis
606  0 if (evt.isAltDown() || evt.isAltGraphDown())
607    {
608  0 dragMode = DragMode.MatrixSelect;
609  0 firstDragX = mouseDragLastX;
610  0 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  0 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  0 toggle boolean matrix_clicked(MouseEvent evt)
630    {
631  0 int[] rowIndex = getRowIndexAndOffset(evt.getY(),
632    av.getAlignment().getAlignmentAnnotation());
633  0 if (rowIndex == null)
634    {
635  0 jalview.bin.Console
636    .error("IMPLEMENTATION ERROR: matrix click out of range.");
637  0 return false;
638    }
639  0 int yOffset = rowIndex[1];
640  0 AlignmentAnnotation[] allAnnotation = av.getAlignment()
641    .getAlignmentAnnotation();
642  0 if (allAnnotation == null || rowIndex[0] < 0
643    || rowIndex[0] >= allAnnotation.length)
644    {
645  0 return false;
646    }
647  0 AlignmentAnnotation clicked = av.getAlignment()
648    .getAlignmentAnnotation()[rowIndex[0]];
649  0 if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
650    {
651  0 return false;
652    }
653   
654    // TODO - use existing threshold to select related sections of matrix
655  0 GraphLine thr = clicked.getThreshold();
656   
657  0 int currentX = getColumnForXPos(evt.getX());
658  0 ContactListI forCurrentX = av.getContactList(clicked, currentX);
659  0 if (forCurrentX != null)
660    {
661  0 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
662    clicked.graphHeight);
663  0 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset);
664  0 if (cXci != null)
665    {
666    /**
667    * start and end range corresponding to the row range under the mouse at
668    * column currentX
669    */
670  0 int fr, to;
671  0 fr = Math.min(cXci.cStart, cXci.cEnd);
672  0 to = Math.max(cXci.cStart, cXci.cEnd);
673   
674    // double click selects the whole group
675  0 if (evt.getClickCount() == 2)
676    {
677  0 ContactMatrixI matrix = av.getContactMatrix(clicked);
678   
679  0 if (matrix != null)
680    {
681    // simplest approach is to select all group containing column
682  0 if (matrix.hasGroups())
683    {
684  0 SequenceI rseq = clicked.sequenceRef;
685  0 BitSet grp = new BitSet();
686  0 grp.or(matrix.getGroupsFor(forCurrentX.getPosition()));
687    // TODO: cXci needs to be mapped to real groups
688  0 for (int c = fr; c <= to; c++)
689    {
690  0 BitSet additionalGrp = matrix.getGroupsFor(c);
691  0 grp.or(additionalGrp);
692    }
693   
694  0 HiddenColumns hc = av.getAlignment().getHiddenColumns();
695  0 ColumnSelection cs = av.getColumnSelection();
696   
697  0 for (int p = grp.nextSetBit(0); p >= 0; p = grp
698    .nextSetBit(p + 1))
699    {
700  0 if (matrix instanceof MappableContactMatrixI)
701    {
702    // find the end of this run of set bits
703  0 int nextp = grp.nextClearBit(p) - 1;
704  0 int[] pos = ((MappableContactMatrixI) matrix)
705    .getMappedPositionsFor(rseq, p, nextp);
706  0 p = nextp;
707   
708  0 if (pos != null)
709    {
710  0 for (int pos_p = pos[0]; pos_p <= pos[1]; pos_p++)
711    {
712  0 int col = rseq.findIndex(pos_p) - 1;
713  0 if (col >= 0 && (!av.hasHiddenColumns()
714    || hc.isVisible(col)))
715    {
716  0 cs.addElement(col);
717    }
718    }
719    }
720    }
721    else
722    {
723  0 int offp = (rseq != null)
724    ? rseq.findIndex(rseq.getStart() - 1 + p)
725    : p;
726   
727  0 if (!av.hasHiddenColumns() || hc.isVisible(offp))
728    {
729  0 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  0 int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
746  0 if (rng != null)
747    {
748  0 av.getColumnSelection().addRangeOfElements(rng, true);
749    }
750  0 av.getColumnSelection().addElement(currentX);
751    }
752    // PAE SPECIFIC
753    // and also select everything lower than the max range adjacent
754    // (kind of works)
755  0 if (evt.isControlDown()
756    && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
757    {
758  0 int c = fr;
759  0 ContactRange cr = forCurrentX.getRangeFor(fr, to);
760  0 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  0 double thresh = cr.getMean()
768    + (cr.getMax() - cr.getMean()) * .15;
769  0 while (c >= 0)
770    {
771  0 cval = forCurrentX.getContactAt(c);
772  0 if (// cr.getMin() <= cval &&
773  0 cval <= thresh)
774    {
775  0 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
776  0 if (cols != null)
777    {
778  0 av.getColumnSelection().addRangeOfElements(cols, true);
779    }
780    else
781    {
782  0 break;
783    }
784    }
785  0 c--;
786    }
787  0 c = to;
788  0 while (c < forCurrentX.getContactHeight())
789    {
790  0 cval = forCurrentX.getContactAt(c);
791  0 if (// cr.getMin() <= cval &&
792  0 cval <= thresh)
793    {
794  0 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
795  0 if (cols != null)
796    {
797  0 av.getColumnSelection().addRangeOfElements(cols, true);
798    }
799    }
800    else
801    {
802  0 break;
803    }
804  0 c++;
805   
806    }
807    }
808   
809    }
810    }
811    }
812  0 ap.paintAlignment(false, false);
813  0 PaintRefresher.Refresh(ap, av.getSequenceSetId());
814  0 av.sendSelection();
815  0 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  0 toggle void showPopupMenu(final int y, int x)
825    {
826  0 if (av.getColumnSelection() == null
827    || av.getColumnSelection().isEmpty())
828    {
829  0 return;
830    }
831   
832  0 JPopupMenu pop = new JPopupMenu(
833    MessageManager.getString("label.structure_type"));
834  0 JMenuItem item;
835    /*
836    * Just display the needed structure options
837    */
838  0 if (av.getAlignment().isNucleotide())
839    {
840  0 item = new JMenuItem(STEM);
841  0 item.addActionListener(this);
842  0 pop.add(item);
843    }
844    else
845    {
846  0 item = new JMenuItem(HELIX);
847  0 item.addActionListener(this);
848  0 pop.add(item);
849  0 item = new JMenuItem(SHEET);
850  0 item.addActionListener(this);
851  0 pop.add(item);
852    }
853  0 item = new JMenuItem(LABEL);
854  0 item.addActionListener(this);
855  0 pop.add(item);
856  0 item = new JMenuItem(COLOUR);
857  0 item.addActionListener(this);
858  0 pop.add(item);
859  0 item = new JMenuItem(REMOVE);
860  0 item.addActionListener(this);
861  0 pop.add(item);
862  0 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  0 toggle @Override
873    public void mouseReleased(MouseEvent evt)
874    {
875  0 if (dragMode == DragMode.MatrixSelect)
876    {
877  0 matrixSelectRange(evt);
878    }
879  0 graphStretch = -1;
880  0 mouseDragLastX = -1;
881  0 mouseDragLastY = -1;
882  0 firstDragX = -1;
883  0 firstDragY = -1;
884  0 mouseDragging = false;
885  0 if (dragMode == DragMode.Resize)
886    {
887  0 ap.adjustAnnotationHeight();
888    }
889  0 dragMode = DragMode.Undefined;
890  0 if (!matrix_clicked(evt))
891    {
892  0 ap.getScalePanel().mouseReleased(evt);
893    }
894   
895    /*
896    * isPopupTrigger is set in mouseReleased on Windows
897    * (in mousePressed on Mac)
898    */
899  0 if (evt.isPopupTrigger() && activeRow != -1)
900    {
901  0 showPopupMenu(evt.getY(), evt.getX());
902    }
903   
904    }
905   
906    /**
907    * DOCUMENT ME!
908    *
909    * @param evt
910    * DOCUMENT ME!
911    */
 
912  0 toggle @Override
913    public void mouseEntered(MouseEvent evt)
914    {
915  0 this.mouseDragging = false;
916  0 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  0 toggle @Override
926    public void mouseExited(MouseEvent evt)
927    {
928  0 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  0 toggle @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  0 final int x = evt.getX();
956  0 final int y = evt.getY();
957  0 if (dragMode == DragMode.Undefined)
958    {
959  0 int dx = Math.abs(x - mouseDragLastX);
960  0 int dy = Math.abs(y - mouseDragLastY);
961  0 if (graphStretch == -1 || dx > dy)
962    {
963    /*
964    * mostly horizontal drag, or not a graph annotation
965    */
966  0 dragMode = DragMode.Select;
967    }
968  0 else if (dy > dx)
969    {
970    /*
971    * mostly vertical drag
972    */
973  0 dragMode = DragMode.Resize;
974  0 notJustOne = evt.isShiftDown();
975   
976    /*
977    * but could also be a matrix drag
978    */
979  0 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
980    .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
981    {
982    /*
983    * dragging in a matrix
984    */
985  0 dragMode = DragMode.MatrixSelect;
986  0 firstDragX = mouseDragLastX;
987  0 firstDragY = mouseDragLastY;
988    }
989    }
990    }
991   
992  0 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  0 return;
1000    }
1001   
1002  0 try
1003    {
1004  0 if (dragMode == DragMode.Resize)
1005    {
1006    /*
1007    * resize graph annotation if mouse was dragged up or down
1008    */
1009  0 int deltaY = mouseDragLastY - evt.getY();
1010  0 if (deltaY != 0)
1011    {
1012  0 AlignmentAnnotation graphAnnotation = av.getAlignment()
1013    .getAlignmentAnnotation()[graphStretch];
1014  0 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
1015  0 if (notJustOne)
1016    {
1017  0 for (AlignmentAnnotation similar : av.getAlignment()
1018    .findAnnotations(null, graphAnnotation.getCalcId(),
1019    graphAnnotation.label))
1020    {
1021  0 similar.graphHeight = newHeight;
1022    }
1023   
1024    }
1025    else
1026    {
1027  0 graphAnnotation.graphHeight = newHeight;
1028    }
1029  0 adjustPanelHeight();
1030  0 ap.paintAlignment(false, false);
1031    }
1032    }
1033  0 else if (dragMode == DragMode.MatrixSelect)
1034    {
1035    /*
1036    * TODO draw a rubber band for range
1037    */
1038  0 mouseDragLastX = x;
1039  0 mouseDragLastY = y;
1040  0 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  0 ap.getScalePanel().mouseDragged(evt);
1049    }
1050    } finally
1051    {
1052  0 mouseDragLastX = x;
1053  0 mouseDragLastY = y;
1054    }
1055    }
1056   
 
1057  0 toggle public void matrixSelectRange(MouseEvent evt)
1058    {
1059    /*
1060    * get geometry of drag
1061    */
1062  0 int fromY = Math.min(firstDragY, evt.getY());
1063  0 int toY = Math.max(firstDragY, evt.getY());
1064  0 int fromX = Math.min(firstDragX, evt.getX());
1065  0 int toX = Math.max(firstDragX, evt.getX());
1066   
1067  0 int deltaY = toY - fromY;
1068  0 int deltaX = toX - fromX;
1069   
1070  0 int[] rowIndex = getRowIndexAndOffset(fromY,
1071    av.getAlignment().getAlignmentAnnotation());
1072  0 int[] toRowIndex = getRowIndexAndOffset(toY,
1073    av.getAlignment().getAlignmentAnnotation());
1074   
1075  0 if (rowIndex == null || toRowIndex == null)
1076    {
1077  0 jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1078   
1079    }
1080  0 if (rowIndex[0] != toRowIndex[0])
1081    {
1082  0 jalview.bin.Console
1083    .trace("Drag went to another row. needs to be clipped");
1084    }
1085   
1086    // rectangular selection on matrix style annotation
1087  0 AlignmentAnnotation cma = av.getAlignment()
1088    .getAlignmentAnnotation()[rowIndex[0]];
1089   
1090  0 int lastX = getColumnForXPos(fromX);
1091  0 int currentX = getColumnForXPos(toX);
1092  0 int fromXc = Math.min(lastX, currentX);
1093  0 int toXc = Math.max(lastX, currentX);
1094  0 ContactListI forFromX = av.getContactList(cma, fromXc);
1095  0 ContactListI forToX = av.getContactList(cma, toXc);
1096   
1097  0 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  0 ContactGeometry xcgeom = new ContactGeometry(forFromX,
1103    cma.graphHeight);
1104  0 ContactGeometry.contactInterval lastXci = xcgeom.mapFor(rowIndex[1]);
1105  0 ContactGeometry.contactInterval cXci = xcgeom
1106    .mapFor(rowIndex[1] + deltaY);
1107   
1108    // mark rectangular region formed by drag
1109  0 jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
1110    + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
1111    + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1112  0 int fr, to;
1113  0 fr = Math.min(lastXci.cStart, cXci.cStart);
1114  0 to = Math.max(lastXci.cEnd, cXci.cEnd);
1115  0 int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
1116  0 if (mappedPos != null)
1117    {
1118  0 jalview.bin.Console.trace("Marking " + fr + " to " + to
1119    + " mapping to sequence positions " + mappedPos[0] + " to "
1120    + mappedPos[1]);
1121  0 for (int pair = 0; pair < mappedPos.length; pair += 2)
1122    {
1123  0 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  0 av.getColumnSelection().addElement(c - 1);
1133    }
1134    }
1135    }
1136  0 fr = Math.min(lastX, currentX);
1137  0 to = Math.max(lastX, currentX);
1138   
1139  0 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1140  0 for (int c = fr; c <= to; c++)
1141    {
1142  0 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  0 toggle @Override
1155    public void mouseMoved(MouseEvent evt)
1156    {
1157  0 int yPos = evt.getY();
1158  0 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1159  0 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1160  0 int row = rowAndOffset[0];
1161   
1162  0 if (row == -1)
1163    {
1164  0 this.setToolTipText(null);
1165  0 return;
1166    }
1167   
1168  0 int column = getColumnForXPos(evt.getX());
1169   
1170  0 AlignmentAnnotation ann = aa[row];
1171  0 if (row > -1 && ann.annotations != null
1172    && column < ann.annotations.length)
1173    {
1174  0 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1175    ap);
1176  0 setToolTipText(toolTip == null ? null
1177    : JvSwingUtils.wrapTooltip(true, toolTip));
1178  0 String msg = getStatusMessage(av.getAlignment(), column, ann,
1179    rowAndOffset[1], av);
1180  0 ap.alignFrame.setStatus(msg);
1181    }
1182    else
1183    {
1184  0 this.setToolTipText(null);
1185  0 ap.alignFrame.setStatus(" ");
1186    }
1187    }
1188   
 
1189  0 toggle private int getColumnForXPos(int x)
1190    {
1191  0 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1192  0 column = Math.min(column, av.getRanges().getEndRes());
1193   
1194  0 if (av.hasHiddenColumns())
1195    {
1196  0 column = av.getAlignment().getHiddenColumns()
1197    .visibleToAbsoluteColumn(column);
1198    }
1199  0 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  33 toggle static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1213    {
1214  33 if (aa == null)
1215    {
1216  1 return -1;
1217    }
1218  32 return getRowIndexAndOffset(yPos, aa)[0];
1219    }
1220   
 
1221  32 toggle static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1222    {
1223  32 int[] res = new int[2];
1224  32 res[0] = -1;
1225  32 res[1] = 0;
1226  32 if (aa == null)
1227    {
1228  0 return res;
1229    }
1230  32 int row = -1;
1231  32 int height = 0, lheight = 0;
1232  79 for (int i = 0; i < aa.length; i++)
1233    {
1234  75 if (aa[i].visible)
1235    {
1236  66 lheight = height;
1237  66 height += aa[i].height;
1238    }
1239   
1240  75 if (height > yPos)
1241    {
1242  28 row = i;
1243  28 res[0] = row;
1244  28 res[1] = yPos - lheight;
1245  28 break;
1246    }
1247    }
1248  32 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  0 toggle static String buildToolTip(AlignmentAnnotation ann, int column,
1262    AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1263    AlignmentPanel ap)
1264    {
1265  0 String tooltip = null;
1266  0 if (ann.graphGroup > -1)
1267    {
1268  0 StringBuilder tip = new StringBuilder(32);
1269  0 boolean first = true;
1270  0 for (int i = 0; i < anns.length; i++)
1271    {
1272  0 if (anns[i].graphGroup == ann.graphGroup
1273    && anns[i].annotations[column] != null)
1274    {
1275  0 if (!first)
1276    {
1277  0 tip.append("<br>");
1278    }
1279  0 first = false;
1280  0 tip.append(anns[i].label);
1281  0 String description = anns[i].annotations[column].description;
1282  0 if (description != null && description.length() > 0)
1283    {
1284  0 tip.append(" ").append(description);
1285    }
1286    }
1287    }
1288  0 tooltip = first ? null : tip.toString();
1289    }
1290  0 else if (column < ann.annotations.length
1291    && ann.annotations[column] != null)
1292    {
1293  0 tooltip = ann.annotations[column].description;
1294    }
1295    // TODO abstract tooltip generator so different implementations can be built
1296  0 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1297    {
1298  0 if (rowAndOffset >= ann.graphHeight)
1299    {
1300  0 return null;
1301    }
1302  0 ContactListI clist = av.getContactList(ann, column);
1303  0 if (clist != null)
1304    {
1305  0 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1306  0 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1307  0 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1308  0 StringBuilder tooltipb = new StringBuilder();
1309  0 tooltipb.append("Contact from ").append(clist.getPosition())
1310    .append(", [").append(ci.cStart).append(" - ")
1311    .append(ci.cEnd).append("]").append("<br/>Mean:");
1312  0 Format.appendPercentage(tooltipb, (float) cr.getMean(), 2);
1313  0 tooltip = tooltipb.toString();
1314  0 int col = ann.sequenceRef.findPosition(column);
1315  0 int[][] highlightPos;
1316  0 int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1317  0 if (mappedPos != null)
1318    {
1319  0 highlightPos = new int[1 + mappedPos.length][2];
1320  0 highlightPos[0] = new int[] { col, col };
1321  0 for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1322    {
1323  0 highlightPos[h][0] = ann.sequenceRef
1324    .findPosition(mappedPos[p] - 1);
1325  0 highlightPos[h][1] = ann.sequenceRef
1326    .findPosition(mappedPos[p + 1] - 1);
1327    }
1328    }
1329    else
1330    {
1331  0 highlightPos = new int[][] { new int[] { col, col } };
1332    }
1333  0 ap.getStructureSelectionManager()
1334    .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1335    }
1336    }
1337  0 return tooltip;
1338    }
1339   
1340    /**
1341    * Constructs and returns the status bar message
1342    *
1343    * @param al
1344    * @param column
1345    * @param ann
1346    * @param rowAndOffset
1347    */
 
1348  0 toggle static String getStatusMessage(AlignmentI al, int column,
1349    AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1350    {
1351    /*
1352    * show alignment column and annotation description if any
1353    */
1354  0 StringBuilder text = new StringBuilder(32);
1355  0 text.append(MessageManager.getString("label.column")).append(" ")
1356    .append(column + 1);
1357   
1358  0 if (column < ann.annotations.length && ann.annotations[column] != null)
1359    {
1360  0 String description = ann.annotations[column].description;
1361  0 if (description != null && description.trim().length() > 0)
1362    {
1363  0 text.append(" ").append(description);
1364    }
1365    }
1366   
1367    /*
1368    * if the annotation is sequence-specific, show the sequence number
1369    * in the alignment, and (if not a gap) the residue and position
1370    */
1371  0 SequenceI seqref = ann.sequenceRef;
1372  0 if (seqref != null)
1373    {
1374  0 int seqIndex = al.findIndex(seqref);
1375  0 if (seqIndex != -1)
1376    {
1377  0 text.append(", ").append(MessageManager.getString("label.sequence"))
1378    .append(" ").append(seqIndex + 1);
1379  0 char residue = seqref.getCharAt(column);
1380  0 if (!Comparison.isGap(residue))
1381    {
1382  0 text.append(" ");
1383  0 String name;
1384  0 if (al.isNucleotide())
1385    {
1386  0 name = ResidueProperties.nucleotideName
1387    .get(String.valueOf(residue));
1388  0 text.append(" Nucleotide: ")
1389  0 .append(name != null ? name : residue);
1390    }
1391    else
1392    {
1393  0 name = 'X' == residue ? "X"
1394  0 : ('*' == residue ? "STOP"
1395    : ResidueProperties.aa2Triplet
1396    .get(String.valueOf(residue)));
1397  0 text.append(" Residue: ").append(name != null ? name : residue);
1398    }
1399  0 int residuePos = seqref.findPosition(column);
1400  0 text.append(" (").append(residuePos).append(")");
1401    }
1402    }
1403    }
1404   
1405  0 return text.toString();
1406    }
1407   
1408    /**
1409    * DOCUMENT ME!
1410    *
1411    * @param evt
1412    * DOCUMENT ME!
1413    */
 
1414  0 toggle @Override
1415    public void mouseClicked(MouseEvent evt)
1416    {
1417    // if (activeRow != -1)
1418    // {
1419    // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1420    // AlignmentAnnotation anot = aa[activeRow];
1421    // }
1422    }
1423   
1424    // TODO mouseClicked-content and drawCursor are quite experimental!
 
1425  0 toggle public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1426    int y1)
1427    {
1428  0 int pady = av.getCharHeight() / 5;
1429  0 int charOffset = 0;
1430  0 graphics.setColor(Color.black);
1431  0 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1432   
1433  0 if (av.validCharWidth)
1434    {
1435  0 graphics.setColor(Color.white);
1436   
1437  0 char s = seq.getCharAt(res);
1438   
1439  0 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1440  0 graphics.drawString(String.valueOf(s), charOffset + x1,
1441    (y1 + av.getCharHeight()) - pady);
1442    }
1443   
1444    }
1445   
1446    private volatile boolean imageFresh = false;
1447   
1448    private Rectangle visibleRect = new Rectangle(),
1449    clipBounds = new Rectangle();
1450   
1451    /**
1452    * DOCUMENT ME!
1453    *
1454    * @param g
1455    * DOCUMENT ME!
1456    */
 
1457  2208 toggle @Override
1458    public void paintComponent(Graphics g)
1459    {
1460   
1461    // BH: note that this method is generally recommended to
1462    // call super.paintComponent(g). Otherwise, the children of this
1463    // component will not be rendered. That is not needed here
1464    // because AnnotationPanel does not have any children. It is
1465    // just a JPanel contained in a JViewPort.
1466   
1467  2208 computeVisibleRect(visibleRect);
1468   
1469  2208 g.setColor(Color.white);
1470  2208 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1471   
1472  2208 if (image != null)
1473    {
1474    // BH 2018 optimizing generation of new Rectangle().
1475  ? if (fastPaint
1476    || (visibleRect.width != (clipBounds = g
1477    .getClipBounds(clipBounds)).width)
1478    || (visibleRect.height != clipBounds.height))
1479    {
1480   
1481  127 g.drawImage(image, 0, 0, this);
1482  127 fastPaint = false;
1483  127 return;
1484    }
1485    }
1486  2081 updateFadedImageWidth();
1487  2081 if (imgWidth < 1)
1488    {
1489  0 return;
1490    }
1491  2081 Graphics2D gg;
1492  2081 if (image == null || imgWidth != image.getWidth(this)
1493    || image.getHeight(this) != getHeight())
1494    {
1495  198 boolean tried = false;
1496  198 image = null;
1497  396 while (image == null && !tried)
1498    {
1499  198 try
1500    {
1501  198 image = new BufferedImage(imgWidth,
1502    ap.getAnnotationPanel().getHeight(),
1503    BufferedImage.TYPE_INT_RGB);
1504  198 tried = true;
1505    } catch (IllegalArgumentException exc)
1506    {
1507  0 jalview.bin.Console.errPrintln(
1508    "Serious issue with viewport geometry imgWidth requested was "
1509    + imgWidth);
1510  0 return;
1511    } catch (OutOfMemoryError oom)
1512    {
1513  0 try
1514    {
1515  0 System.gc();
1516    } catch (Exception x)
1517    {
1518    }
1519  0 ;
1520  0 new OOMWarning(
1521    "Couldn't allocate memory to redraw screen. Please restart Jalview",
1522    oom);
1523  0 return;
1524    }
1525   
1526    }
1527  198 gg = (Graphics2D) image.getGraphics();
1528   
1529  198 if (av.antiAlias)
1530    {
1531  119 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1532    RenderingHints.VALUE_ANTIALIAS_ON);
1533    }
1534   
1535  198 gg.setFont(av.getFont());
1536  198 fm = gg.getFontMetrics();
1537  198 gg.setColor(Color.white);
1538  198 gg.fillRect(0, 0, imgWidth, image.getHeight());
1539  198 imageFresh = true;
1540    }
1541    else
1542    {
1543  1883 gg = (Graphics2D) image.getGraphics();
1544   
1545    }
1546   
1547  2081 drawComponent(gg, av.getRanges().getStartRes(),
1548    av.getRanges().getEndRes() + 1);
1549  2081 gg.dispose();
1550  2081 imageFresh = false;
1551  2081 g.drawImage(image, 0, 0, this);
1552    }
1553   
 
1554  4455 toggle public void updateFadedImageWidth()
1555    {
1556  4455 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1557    + 1) * av.getCharWidth();
1558   
1559    }
1560   
1561    /**
1562    * set true to enable redraw timing debug output on stderr
1563    */
1564    private final boolean debugRedraw = false;
1565   
1566    /**
1567    * non-Thread safe repaint
1568    *
1569    * @param horizontal
1570    * repaint with horizontal shift in alignment
1571    */
 
1572  36 toggle public void fastPaint(int horizontal)
1573    {
1574  36 if ((horizontal == 0) || image == null
1575    || av.getAlignment().getAlignmentAnnotation() == null
1576    || av.getAlignment().getAlignmentAnnotation().length < 1
1577    || av.isCalcInProgress())
1578    {
1579  35 repaint();
1580  35 return;
1581    }
1582   
1583  1 int sr = av.getRanges().getStartRes();
1584  1 int er = av.getRanges().getEndRes() + 1;
1585  1 int transX = 0;
1586   
1587  1 Graphics2D gg = (Graphics2D) image.getGraphics();
1588   
1589  1 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1590    {
1591    // scroll is less than imgWidth away so can re-use buffered graphics
1592  0 gg.copyArea(0, 0, imgWidth, getHeight(),
1593    -horizontal * av.getCharWidth(), 0);
1594   
1595  0 if (horizontal > 0) // scrollbar pulled right, image to the left
1596    {
1597  0 transX = (er - sr - horizontal) * av.getCharWidth();
1598  0 sr = er - horizontal;
1599    }
1600  0 else if (horizontal < 0)
1601    {
1602  0 er = sr - horizontal;
1603    }
1604    }
1605  1 gg.translate(transX, 0);
1606   
1607  1 drawComponent(gg, sr, er);
1608   
1609  1 gg.translate(-transX, 0);
1610   
1611  1 gg.dispose();
1612   
1613  1 fastPaint = true;
1614   
1615    // Call repaint on alignment panel so that repaints from other alignment
1616    // panel components can be aggregated. Otherwise performance of the overview
1617    // window and others may be adversely affected.
1618  1 av.getAlignPanel().repaint();
1619    }
1620   
1621    private volatile boolean lastImageGood = false;
1622   
1623    /**
1624    * DOCUMENT ME!
1625    *
1626    * @param g
1627    * DOCUMENT ME!
1628    * @param startRes
1629    * DOCUMENT ME!
1630    * @param endRes
1631    * DOCUMENT ME!
1632    */
 
1633  2082 toggle public void drawComponent(Graphics g, int startRes, int endRes)
1634    {
1635  2082 BufferedImage oldFaded = fadedImage;
1636  2082 if (av.isCalcInProgress())
1637    {
1638  183 if (image == null)
1639    {
1640  0 lastImageGood = false;
1641  0 return;
1642    }
1643    // We'll keep a record of the old image,
1644    // and draw a faded image until the calculation
1645    // has completed
1646  183 if (lastImageGood
1647    && (fadedImage == null || fadedImage.getWidth() != imgWidth
1648    || fadedImage.getHeight() != image.getHeight()))
1649    {
1650    // jalview.bin.Console.errPrintln("redraw faded image
1651    // ("+(fadedImage==null ?
1652    // "null image" : "") + " lastGood="+lastImageGood+")");
1653  63 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1654    BufferedImage.TYPE_INT_RGB);
1655   
1656  63 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1657   
1658  63 fadedG.setColor(Color.white);
1659  63 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1660   
1661  63 fadedG.setComposite(
1662    AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1663  63 fadedG.drawImage(image, 0, 0, this);
1664   
1665    }
1666    // make sure we don't overwrite the last good faded image until all
1667    // calculations have finished
1668  183 lastImageGood = false;
1669   
1670    }
1671    else
1672    {
1673  1899 if (fadedImage != null)
1674    {
1675  63 oldFaded = fadedImage;
1676    }
1677  1899 fadedImage = null;
1678    }
1679   
1680  2082 g.setColor(Color.white);
1681  2082 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1682   
1683  2082 g.setFont(av.getFont());
1684  2082 if (fm == null)
1685    {
1686  0 fm = g.getFontMetrics();
1687    }
1688   
1689  2082 if ((av.getAlignment().getAlignmentAnnotation() == null)
1690    || (av.getAlignment().getAlignmentAnnotation().length < 1))
1691    {
1692  0 g.setColor(Color.white);
1693  0 g.fillRect(0, 0, getWidth(), getHeight());
1694  0 g.setColor(Color.black);
1695  0 if (av.validCharWidth)
1696    {
1697  0 g.drawString(MessageManager
1698    .getString("label.alignment_has_no_annotations"), 20, 15);
1699    }
1700   
1701  0 return;
1702    }
1703  2082 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1704    endRes);
1705  2082 if (!lastImageGood && fadedImage == null)
1706    {
1707  92 fadedImage = oldFaded;
1708    }
1709  2082 if (dragMode == DragMode.MatrixSelect)
1710    {
1711  0 g.setColor(Color.yellow);
1712  0 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1713    Math.min(firstDragY, mouseDragLastY),
1714    Math.max(firstDragX, mouseDragLastX)
1715    - Math.min(firstDragX, mouseDragLastX),
1716    Math.max(firstDragY, mouseDragLastY)
1717    - Math.min(firstDragY, mouseDragLastY));
1718   
1719    }
1720    }
1721   
 
1722  2374 toggle @Override
1723    public FontMetrics getFontMetrics()
1724    {
1725  2374 return fm;
1726    }
1727   
 
1728  2374 toggle @Override
1729    public Image getFadedImage()
1730    {
1731  2374 return fadedImage;
1732    }
1733   
 
1734  2374 toggle @Override
1735    public int getFadedImageWidth()
1736    {
1737  2374 updateFadedImageWidth();
1738  2374 return imgWidth;
1739    }
1740   
1741    private int[] bounds = new int[2];
1742   
 
1743  6850 toggle @Override
1744    public int[] getVisibleVRange()
1745    {
1746  6850 if (ap != null && ap.getAlabels() != null)
1747    {
1748  6596 int sOffset = -ap.getAlabels().getScrollOffset();
1749  6596 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1750  6596 bounds[0] = sOffset;
1751  6596 bounds[1] = visHeight;
1752  6596 return bounds;
1753    }
1754    else
1755    {
1756  254 return null;
1757    }
1758    }
1759   
1760    /**
1761    * Try to ensure any references held are nulled
1762    */
 
1763  253 toggle public void dispose()
1764    {
1765  253 av = null;
1766  253 ap = null;
1767  253 image = null;
1768  253 fadedImage = null;
1769    // gg = null;
1770  253 _mwl = null;
1771   
1772    /*
1773    * I created the renderer so I will dispose of it
1774    */
1775  253 if (renderer != null)
1776    {
1777  253 renderer.dispose();
1778    }
1779    }
1780   
 
1781  644 toggle @Override
1782    public void propertyChange(PropertyChangeEvent evt)
1783    {
1784    // Respond to viewport range changes (e.g. alignment panel was scrolled)
1785    // Both scrolling and resizing change viewport ranges: scrolling changes
1786    // both start and end points, but resize only changes end values.
1787    // Here we only want to fastpaint on a scroll, with resize using a normal
1788    // paint, so scroll events are identified as changes to the horizontal or
1789    // vertical start value.
1790  644 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1791    {
1792  36 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1793    }
1794  608 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1795    {
1796  0 fastPaint(((int[]) evt.getNewValue())[0]
1797    - ((int[]) evt.getOldValue())[0]);
1798    }
1799  608 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1800    {
1801  0 repaint();
1802    }
1803    }
1804   
1805    /**
1806    * computes the visible height of the annotation panel
1807    *
1808    * @param adjustPanelHeight
1809    * - when false, just adjust existing height according to other
1810    * windows
1811    * @param annotationHeight
1812    * @return height to use for the ScrollerPreferredVisibleSize
1813    */
 
1814  2640 toggle public int adjustForAlignFrame(boolean adjustPanelHeight,
1815    int annotationHeight)
1816    {
1817    /*
1818    * Estimate available height in the AlignFrame for alignment +
1819    * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1820    * hscroll, status bar, insets.
1821    */
1822  2640 int stuff = (ap.getViewName() != null ? 30 : 0)
1823  2640 + (Platform.isAMacAndNotJS() ? 120 : 140);
1824  2640 int availableHeight = ap.alignFrame.getHeight() - stuff;
1825  2640 int rowHeight = av.getCharHeight();
1826   
1827  2640 if (adjustPanelHeight)
1828    {
1829  2052 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1830   
1831    /*
1832    * If not enough vertical space, maximize annotation height while keeping
1833    * at least two rows of alignment visible
1834    */
1835  2052 if (annotationHeight + alignmentHeight > availableHeight)
1836    {
1837  1261 annotationHeight = Math.min(annotationHeight,
1838    availableHeight - 2 * rowHeight);
1839    }
1840    }
1841    else
1842    {
1843    // maintain same window layout whilst updating sliders
1844  588 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1845    availableHeight - 2 * rowHeight);
1846    }
1847  2640 return annotationHeight;
1848    }
1849    }