Clover icon

Coverage Report

  1. Project Clover database Thu Dec 4 2025 16:11:35 GMT
  2. Package jalview.gui

File AnnotationPanel.java

 

Coverage histogram

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

Code metrics

316
609
42
2
1,902
1,343
254
0.42
14.5
21
6.05

Classes

Class Line # Actions
AnnotationPanel 86 609 254
0.2347466323.5%
AnnotationPanel.DragMode 90 0 0
-1.0 -
 

Contributing tests

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