Clover icon

Coverage Report

  1. Project Clover database Mon Nov 11 2024 17:27:16 GMT
  2. Package jalview.gui

File AnnotationPanel.java

 

Coverage histogram

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

Code metrics

312
599
41
2
1,880
1,325
250
0.42
14.61
20.5
6.1

Classes

Class Line # Actions
AnnotationPanel 86 599 250
0.2237394922.4%
AnnotationPanel.DragMode 90 0 0
-1.0 -
 

Contributing tests

This file is covered by 207 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  477 toggle public AnnotationPanel(AlignmentPanel ap)
168    {
169  477 ToolTipManager.sharedInstance().registerComponent(this);
170  477 ToolTipManager.sharedInstance().setInitialDelay(0);
171  477 ToolTipManager.sharedInstance().setDismissDelay(10000);
172  477 this.ap = ap;
173  477 av = ap.av;
174  477 this.setLayout(null);
175  477 addMouseListener(this);
176  477 addMouseMotionListener(this);
177  477 ap.annotationScroller.getVerticalScrollBar()
178    .addAdjustmentListener(this);
179    // save any wheel listeners on the scroller, so we can propagate scroll
180    // events to them.
181  477 _mwl = ap.annotationScroller.getMouseWheelListeners();
182    // and then set our own listener to consume all mousewheel events
183  477 ap.annotationScroller.addMouseWheelListener(this);
184  477 renderer = new AnnotationRenderer();
185   
186  477 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  5179 toggle @Override
250    public boolean getScrollableTracksViewportHeight()
251    {
252  5179 return false;
253    }
254   
 
255  5179 toggle @Override
256    public boolean getScrollableTracksViewportWidth()
257    {
258  5179 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  908 toggle @Override
276    public void adjustmentValueChanged(AdjustmentEvent evt)
277    {
278    // update annotation label display
279  908 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  3675 toggle public int adjustPanelHeight()
289    {
290  3675 int height = av.calcPanelHeight();
291  3675 this.setPreferredSize(new Dimension(1, height));
292  3675 if (ap != null)
293    {
294    // revalidate only when the alignment panel is fully constructed
295  3194 ap.validate();
296    }
297   
298  3675 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].isForDisplay())
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  1 toggle @Override
913    public void mouseEntered(MouseEvent evt)
914    {
915  1 this.mouseDragging = false;
916  1 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].isForDisplay())
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  0 first = false;
1279    }
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 = getAnnotationBriefSummary(ann.annotations[column]);
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 == null || tooltip.length() == 0 ? null : tooltip;
1338    }
1339   
 
1340  0 toggle private static String getAnnotationBriefSummary(Annotation a)
1341    {
1342  0 StringBuilder ttSB = new StringBuilder();
1343  0 if (a.secondaryStructure != 0 && a.secondaryStructure != ' ')
1344    {
1345  0 ttSB.append(a.secondaryStructure);
1346    }
1347  0 else if (a.description != null && a.description.trim().length() > 0)
1348    {
1349  0 ttSB.append(a.description);
1350    }
1351  0 else if (a.displayCharacter != null
1352    && a.displayCharacter.trim().length() > 0)
1353    {
1354  0 ttSB.append(a.displayCharacter);
1355    }
1356  0 else if (!Float.isNaN(a.value))
1357    {
1358  0 if (a.value == Math.floor(a.value)) // likely integer value
1359    {
1360  0 ttSB.append(String.format("%.0f", a.value));
1361    }
1362    else // display as is
1363    {
1364  0 ttSB.append(String.valueOf(a.value));
1365    }
1366    }
1367  0 return ttSB.toString();
1368    }
1369   
1370    /**
1371    * Constructs and returns the status bar message
1372    *
1373    * @param al
1374    * @param column
1375    * @param ann
1376    * @param rowAndOffset
1377    */
 
1378  0 toggle static String getStatusMessage(AlignmentI al, int column,
1379    AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1380    {
1381    /*
1382    * show alignment column and annotation description if any
1383    */
1384  0 StringBuilder text = new StringBuilder(32);
1385  0 text.append(MessageManager.getString("label.column")).append(" ")
1386    .append(column + 1);
1387   
1388  0 if (column < ann.annotations.length && ann.annotations[column] != null)
1389    {
1390  0 String description = getAnnotationBriefSummary(
1391    ann.annotations[column]);
1392  0 if (description != null && description.trim().length() > 0)
1393    {
1394  0 text.append(" ").append(description);
1395    }
1396    }
1397   
1398    /*
1399    * if the annotation is sequence-specific, show the sequence number
1400    * in the alignment, and (if not a gap) the residue and position
1401    */
1402  0 SequenceI seqref = ann.sequenceRef;
1403  0 if (seqref != null)
1404    {
1405  0 int seqIndex = al.findIndex(seqref);
1406  0 if (seqIndex != -1)
1407    {
1408  0 text.append(", ").append(MessageManager.getString("label.sequence"))
1409    .append(" ").append(seqIndex + 1);
1410  0 char residue = seqref.getCharAt(column);
1411  0 if (!Comparison.isGap(residue))
1412    {
1413  0 text.append(" ");
1414  0 String name;
1415  0 if (al.isNucleotide())
1416    {
1417  0 name = ResidueProperties.nucleotideName
1418    .get(String.valueOf(residue));
1419  0 text.append(" Nucleotide: ")
1420  0 .append(name != null ? name : residue);
1421    }
1422    else
1423    {
1424  0 name = 'X' == residue ? "X"
1425  0 : ('*' == residue ? "STOP"
1426    : ResidueProperties.aa2Triplet
1427    .get(String.valueOf(residue)));
1428  0 text.append(" Residue: ").append(name != null ? name : residue);
1429    }
1430  0 int residuePos = seqref.findPosition(column);
1431  0 text.append(" (").append(residuePos).append(")");
1432    }
1433    }
1434    }
1435   
1436  0 return text.toString();
1437    }
1438   
1439    /**
1440    * DOCUMENT ME!
1441    *
1442    * @param evt
1443    * DOCUMENT ME!
1444    */
 
1445  0 toggle @Override
1446    public void mouseClicked(MouseEvent evt)
1447    {
1448    // if (activeRow != -1)
1449    // {
1450    // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1451    // AlignmentAnnotation anot = aa[activeRow];
1452    // }
1453    }
1454   
1455    // TODO mouseClicked-content and drawCursor are quite experimental!
 
1456  0 toggle public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1457    int y1)
1458    {
1459  0 int pady = av.getCharHeight() / 5;
1460  0 int charOffset = 0;
1461  0 graphics.setColor(Color.black);
1462  0 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1463   
1464  0 if (av.validCharWidth)
1465    {
1466  0 graphics.setColor(Color.white);
1467   
1468  0 char s = seq.getCharAt(res);
1469   
1470  0 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1471  0 graphics.drawString(String.valueOf(s), charOffset + x1,
1472    (y1 + av.getCharHeight()) - pady);
1473    }
1474   
1475    }
1476   
1477    private volatile boolean imageFresh = false;
1478   
1479    private Rectangle visibleRect = new Rectangle(),
1480    clipBounds = new Rectangle();
1481   
1482    /**
1483    * DOCUMENT ME!
1484    *
1485    * @param g
1486    * DOCUMENT ME!
1487    */
 
1488  1884 toggle @Override
1489    public void paintComponent(Graphics g)
1490    {
1491   
1492    // BH: note that this method is generally recommended to
1493    // call super.paintComponent(g). Otherwise, the children of this
1494    // component will not be rendered. That is not needed here
1495    // because AnnotationPanel does not have any children. It is
1496    // just a JPanel contained in a JViewPort.
1497   
1498  1884 computeVisibleRect(visibleRect);
1499   
1500  1884 g.setColor(Color.white);
1501  1884 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1502   
1503  1884 if (image != null)
1504    {
1505    // BH 2018 optimizing generation of new Rectangle().
1506  ? if (fastPaint
1507    || (visibleRect.width != (clipBounds = g
1508    .getClipBounds(clipBounds)).width)
1509    || (visibleRect.height != clipBounds.height))
1510    {
1511   
1512  112 g.drawImage(image, 0, 0, this);
1513  112 fastPaint = false;
1514  112 return;
1515    }
1516    }
1517  1772 updateFadedImageWidth();
1518  1772 if (imgWidth < 1)
1519    {
1520  0 return;
1521    }
1522  1772 Graphics2D gg;
1523  1772 if (image == null || imgWidth != image.getWidth(this)
1524    || image.getHeight(this) != getHeight())
1525    {
1526  197 boolean tried = false;
1527  197 image = null;
1528  394 while (image == null && !tried)
1529    {
1530  197 try
1531    {
1532  197 image = new BufferedImage(imgWidth,
1533    ap.getAnnotationPanel().getHeight(),
1534    BufferedImage.TYPE_INT_RGB);
1535  197 tried = true;
1536    } catch (IllegalArgumentException exc)
1537    {
1538  0 jalview.bin.Console.errPrintln(
1539    "Serious issue with viewport geometry imgWidth requested was "
1540    + imgWidth);
1541  0 return;
1542    } catch (OutOfMemoryError oom)
1543    {
1544  0 try
1545    {
1546  0 System.gc();
1547    } catch (Exception x)
1548    {
1549    }
1550  0 ;
1551  0 new OOMWarning(
1552    "Couldn't allocate memory to redraw screen. Please restart Jalview",
1553    oom);
1554  0 return;
1555    }
1556   
1557    }
1558  197 gg = (Graphics2D) image.getGraphics();
1559   
1560  197 if (av.antiAlias)
1561    {
1562  122 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1563    RenderingHints.VALUE_ANTIALIAS_ON);
1564    }
1565   
1566  197 gg.setFont(av.getFont());
1567  197 fm = gg.getFontMetrics();
1568  197 gg.setColor(Color.white);
1569  197 gg.fillRect(0, 0, imgWidth, image.getHeight());
1570  197 imageFresh = true;
1571    }
1572    else
1573    {
1574  1575 gg = (Graphics2D) image.getGraphics();
1575   
1576    }
1577   
1578  1772 drawComponent(gg, av.getRanges().getStartRes(),
1579    av.getRanges().getEndRes() + 1);
1580  1772 gg.dispose();
1581  1772 imageFresh = false;
1582  1772 g.drawImage(image, 0, 0, this);
1583    }
1584   
 
1585  3771 toggle public void updateFadedImageWidth()
1586    {
1587  3771 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1588    + 1) * av.getCharWidth();
1589   
1590    }
1591   
1592    /**
1593    * set true to enable redraw timing debug output on stderr
1594    */
1595    private final boolean debugRedraw = false;
1596   
1597    /**
1598    * non-Thread safe repaint
1599    *
1600    * @param horizontal
1601    * repaint with horizontal shift in alignment
1602    */
 
1603  36 toggle public void fastPaint(int horizontal)
1604    {
1605  36 if ((horizontal == 0) || image == null
1606    || av.getAlignment().getAlignmentAnnotation() == null
1607    || av.getAlignment().getAlignmentAnnotation().length < 1
1608    || av.isCalcInProgress())
1609    {
1610  35 repaint();
1611  35 return;
1612    }
1613   
1614  1 int sr = av.getRanges().getStartRes();
1615  1 int er = av.getRanges().getEndRes() + 1;
1616  1 int transX = 0;
1617   
1618  1 Graphics2D gg = (Graphics2D) image.getGraphics();
1619   
1620  1 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1621    {
1622    // scroll is less than imgWidth away so can re-use buffered graphics
1623  0 gg.copyArea(0, 0, imgWidth, getHeight(),
1624    -horizontal * av.getCharWidth(), 0);
1625   
1626  0 if (horizontal > 0) // scrollbar pulled right, image to the left
1627    {
1628  0 transX = (er - sr - horizontal) * av.getCharWidth();
1629  0 sr = er - horizontal;
1630    }
1631  0 else if (horizontal < 0)
1632    {
1633  0 er = sr - horizontal;
1634    }
1635    }
1636  1 gg.translate(transX, 0);
1637   
1638  1 drawComponent(gg, sr, er);
1639   
1640  1 gg.translate(-transX, 0);
1641   
1642  1 gg.dispose();
1643   
1644  1 fastPaint = true;
1645   
1646    // Call repaint on alignment panel so that repaints from other alignment
1647    // panel components can be aggregated. Otherwise performance of the overview
1648    // window and others may be adversely affected.
1649  1 av.getAlignPanel().repaint();
1650    }
1651   
1652    private volatile boolean lastImageGood = false;
1653   
1654    /**
1655    * DOCUMENT ME!
1656    *
1657    * @param g
1658    * DOCUMENT ME!
1659    * @param startRes
1660    * DOCUMENT ME!
1661    * @param endRes
1662    * DOCUMENT ME!
1663    */
 
1664  1773 toggle public void drawComponent(Graphics g, int startRes, int endRes)
1665    {
1666  1773 BufferedImage oldFaded = fadedImage;
1667  1773 if (av.isCalcInProgress())
1668    {
1669  163 if (image == null)
1670    {
1671  0 lastImageGood = false;
1672  0 return;
1673    }
1674    // We'll keep a record of the old image,
1675    // and draw a faded image until the calculation
1676    // has completed
1677  163 if (lastImageGood
1678    && (fadedImage == null || fadedImage.getWidth() != imgWidth
1679    || fadedImage.getHeight() != image.getHeight()))
1680    {
1681    // jalview.bin.Console.errPrintln("redraw faded image
1682    // ("+(fadedImage==null ?
1683    // "null image" : "") + " lastGood="+lastImageGood+")");
1684  54 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1685    BufferedImage.TYPE_INT_RGB);
1686   
1687  54 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1688   
1689  54 fadedG.setColor(Color.white);
1690  54 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1691   
1692  54 fadedG.setComposite(
1693    AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1694  54 fadedG.drawImage(image, 0, 0, this);
1695   
1696    }
1697    // make sure we don't overwrite the last good faded image until all
1698    // calculations have finished
1699  163 lastImageGood = false;
1700   
1701    }
1702    else
1703    {
1704  1610 if (fadedImage != null)
1705    {
1706  52 oldFaded = fadedImage;
1707    }
1708  1610 fadedImage = null;
1709    }
1710   
1711  1773 g.setColor(Color.white);
1712  1773 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1713   
1714  1773 g.setFont(av.getFont());
1715  1773 if (fm == null)
1716    {
1717  0 fm = g.getFontMetrics();
1718    }
1719   
1720  1773 if ((av.getAlignment().getAlignmentAnnotation() == null)
1721    || (av.getAlignment().getAlignmentAnnotation().length < 1))
1722    {
1723  0 g.setColor(Color.white);
1724  0 g.fillRect(0, 0, getWidth(), getHeight());
1725  0 g.setColor(Color.black);
1726  0 if (av.validCharWidth)
1727    {
1728  0 g.drawString(MessageManager
1729    .getString("label.alignment_has_no_annotations"), 20, 15);
1730    }
1731   
1732  0 return;
1733    }
1734  1773 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1735    endRes);
1736  1773 if (!lastImageGood && fadedImage == null)
1737    {
1738  78 fadedImage = oldFaded;
1739    }
1740  1773 if (dragMode == DragMode.MatrixSelect)
1741    {
1742  0 g.setColor(Color.yellow);
1743  0 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1744    Math.min(firstDragY, mouseDragLastY),
1745    Math.max(firstDragX, mouseDragLastX)
1746    - Math.min(firstDragX, mouseDragLastX),
1747    Math.max(firstDragY, mouseDragLastY)
1748    - Math.min(firstDragY, mouseDragLastY));
1749   
1750    }
1751    }
1752   
 
1753  1999 toggle @Override
1754    public FontMetrics getFontMetrics()
1755    {
1756  1999 return fm;
1757    }
1758   
 
1759  1999 toggle @Override
1760    public Image getFadedImage()
1761    {
1762  1999 return fadedImage;
1763    }
1764   
 
1765  1999 toggle @Override
1766    public int getFadedImageWidth()
1767    {
1768  1999 updateFadedImageWidth();
1769  1999 return imgWidth;
1770    }
1771   
1772    private int[] bounds = new int[2];
1773   
 
1774  5829 toggle @Override
1775    public int[] getVisibleVRange()
1776    {
1777  5829 if (ap != null && ap.getAlabels() != null)
1778    {
1779  5641 int sOffset = -ap.getAlabels().getScrollOffset();
1780  5641 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1781  5641 bounds[0] = sOffset;
1782  5641 bounds[1] = visHeight;
1783  5641 return bounds;
1784    }
1785    else
1786    {
1787  188 return null;
1788    }
1789    }
1790   
1791    /**
1792    * Try to ensure any references held are nulled
1793    */
 
1794  252 toggle public void dispose()
1795    {
1796  252 av = null;
1797  252 ap = null;
1798  252 image = null;
1799  252 fadedImage = null;
1800    // gg = null;
1801  252 _mwl = null;
1802   
1803    /*
1804    * I created the renderer so I will dispose of it
1805    */
1806  252 if (renderer != null)
1807    {
1808  252 renderer.dispose();
1809    }
1810    }
1811   
 
1812  615 toggle @Override
1813    public void propertyChange(PropertyChangeEvent evt)
1814    {
1815    // Respond to viewport range changes (e.g. alignment panel was scrolled)
1816    // Both scrolling and resizing change viewport ranges: scrolling changes
1817    // both start and end points, but resize only changes end values.
1818    // Here we only want to fastpaint on a scroll, with resize using a normal
1819    // paint, so scroll events are identified as changes to the horizontal or
1820    // vertical start value.
1821  615 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1822    {
1823  36 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1824    }
1825  579 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1826    {
1827  0 fastPaint(((int[]) evt.getNewValue())[0]
1828    - ((int[]) evt.getOldValue())[0]);
1829    }
1830  579 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1831    {
1832  0 repaint();
1833    }
1834    }
1835   
1836    /**
1837    * computes the visible height of the annotation panel
1838    *
1839    * @param adjustPanelHeight
1840    * - when false, just adjust existing height according to other
1841    * windows
1842    * @param annotationHeight
1843    * @return height to use for the ScrollerPreferredVisibleSize
1844    */
 
1845  1629 toggle public int adjustForAlignFrame(boolean adjustPanelHeight,
1846    int annotationHeight)
1847    {
1848    /*
1849    * Estimate available height in the AlignFrame for alignment +
1850    * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1851    * hscroll, status bar, insets.
1852    */
1853  1629 int stuff = (ap.getViewName() != null ? 30 : 0)
1854  1629 + (Platform.isAMacAndNotJS() ? 120 : 140);
1855  1629 int availableHeight = ap.alignFrame.getHeight() - stuff;
1856  1629 int rowHeight = av.getCharHeight();
1857   
1858  1629 if (adjustPanelHeight)
1859    {
1860  1042 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1861   
1862    /*
1863    * If not enough vertical space, maximize annotation height while keeping
1864    * at least two rows of alignment visible
1865    */
1866  1042 if (annotationHeight + alignmentHeight > availableHeight)
1867    {
1868  639 annotationHeight = Math.min(annotationHeight,
1869    availableHeight - 2 * rowHeight);
1870    }
1871    }
1872    else
1873    {
1874    // maintain same window layout whilst updating sliders
1875  587 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1876    availableHeight - 2 * rowHeight);
1877    }
1878  1629 return annotationHeight;
1879    }
1880    }