Clover icon

Coverage Report

  1. Project Clover database Fri Jul 3 2026 11:38:22 BST
  2. Package jalview.gui

File AnnotationPanel.java

 

Coverage histogram

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

Code metrics

312
603
41
2
1,882
1,326
250
0.41
14.71
20.5
6.1

Classes

Class Line # Actions
AnnotationPanel 86 603 250
0.2196652722%
AnnotationPanel.DragMode 90 0 0
-1.0 -
 

Contributing tests

This file is covered by 239 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  506 toggle public AnnotationPanel(AlignmentPanel ap)
168    {
169  506 ToolTipManager.sharedInstance().registerComponent(this);
170  506 ToolTipManager.sharedInstance().setInitialDelay(0);
171  506 ToolTipManager.sharedInstance().setDismissDelay(10000);
172  506 this.ap = ap;
173  506 av = ap.av;
174  506 this.setLayout(null);
175  506 addMouseListener(this);
176  506 addMouseMotionListener(this);
177  506 ap.annotationScroller.getVerticalScrollBar()
178    .addAdjustmentListener(this);
179    // save any wheel listeners on the scroller, so we can propagate scroll
180    // events to them.
181  506 _mwl = ap.annotationScroller.getMouseWheelListeners();
182    // and then set our own listener to consume all mousewheel events
183  506 ap.annotationScroller.addMouseWheelListener(this);
184  506 renderer = new AnnotationRenderer();
185   
186  506 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  14732 toggle @Override
250    public boolean getScrollableTracksViewportHeight()
251    {
252  14732 return false;
253    }
254   
 
255  14732 toggle @Override
256    public boolean getScrollableTracksViewportWidth()
257    {
258  14732 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  1051 toggle @Override
276    public void adjustmentValueChanged(AdjustmentEvent evt)
277    {
278    // update annotation label display
279  1051 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  8662 toggle public int adjustPanelHeight()
289    {
290  8662 int height = av.calcPanelHeight();
291  8662 this.setPreferredSize(new Dimension(1, height));
292  8662 if (ap != null)
293    {
294    // revalidate only when the alignment panel is fully constructed
295  8059 ap.validate();
296    }
297   
298  8661 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 int ourActive = -1;
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 ourActive = i;
576  0 if (aa[i].editable)
577    {
578  0 activeRow = i;
579    }
580  0 else if (aa[i].graph != 0)
581    {
582    /*
583    * we have clicked on a resizable graph annotation
584    */
585  0 graphStretch = i;
586  0 yOffset = height - y;
587    }
588  0 break;
589    }
590    }
591   
592    /*
593    * isPopupTrigger fires in mousePressed on Mac,
594    * not until mouseRelease on Windows
595    */
596  0 if (evt.isPopupTrigger() && activeRow != -1)
597    {
598  0 showPopupMenu(y, evt.getX());
599  0 return;
600    }
601   
602  0 if (graphStretch != -1)
603    {
604   
605  0 if (aa[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP)
606    {
607    // data in row has position on y as well as x axis
608  0 if (evt.isAltDown() || evt.isAltGraphDown())
609    {
610  0 dragMode = DragMode.MatrixSelect;
611  0 firstDragX = mouseDragLastX;
612  0 firstDragY = mouseDragLastY;
613  0 return;
614    }
615    }
616    }
617   
618    // no row (or row that can be adjusted) was pressed. Simulate a ruler
619    // click
620  0 activeRow = ourActive; // so selection/drag positon is shown
621  0 ap.getScalePanel().mousePressed(evt);
622    }
623   
624    /**
625    * checks whether the annotation row under the mouse click evt's handles the
626    * event
627    *
628    * @param evt
629    * @return false if evt was not handled
630    */
 
631  0 toggle boolean matrix_clicked(MouseEvent evt)
632    {
633  0 int[] rowIndex = getRowIndexAndOffset(evt.getY(),
634    av.getAlignment().getAlignmentAnnotation());
635  0 if (rowIndex == null)
636    {
637  0 jalview.bin.Console
638    .error("IMPLEMENTATION ERROR: matrix click out of range.");
639  0 return false;
640    }
641  0 int yOffset = rowIndex[1];
642  0 AlignmentAnnotation[] allAnnotation = av.getAlignment()
643    .getAlignmentAnnotation();
644  0 if (allAnnotation == null || rowIndex[0] < 0
645    || rowIndex[0] >= allAnnotation.length)
646    {
647  0 return false;
648    }
649  0 AlignmentAnnotation clicked = av.getAlignment()
650    .getAlignmentAnnotation()[rowIndex[0]];
651  0 if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
652    {
653  0 return false;
654    }
655   
656    // TODO - use existing threshold to select related sections of matrix
657  0 GraphLine thr = clicked.getThreshold();
658   
659  0 int currentX = getColumnForXPos(evt.getX());
660  0 ContactListI forCurrentX = av.getContactList(clicked, currentX);
661  0 if (forCurrentX != null)
662    {
663  0 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
664    clicked.graphHeight);
665  0 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset);
666  0 if (cXci != null)
667    {
668    /**
669    * start and end range corresponding to the row range under the mouse at
670    * column currentX
671    */
672  0 int fr, to;
673  0 fr = Math.min(cXci.cStart, cXci.cEnd);
674  0 to = Math.max(cXci.cStart, cXci.cEnd);
675   
676    // double click selects the whole group
677  0 if (evt.getClickCount() == 2)
678    {
679  0 ContactMatrixI matrix = av.getContactMatrix(clicked);
680   
681  0 if (matrix != null)
682    {
683    // simplest approach is to select all group containing column
684  0 if (matrix.hasGroups())
685    {
686  0 SequenceI rseq = clicked.sequenceRef;
687  0 BitSet grp = new BitSet();
688  0 grp.or(matrix.getGroupsFor(forCurrentX.getPosition()));
689    // TODO: cXci needs to be mapped to real groups
690  0 for (int c = fr; c <= to; c++)
691    {
692  0 BitSet additionalGrp = matrix.getGroupsFor(c);
693  0 grp.or(additionalGrp);
694    }
695   
696  0 HiddenColumns hc = av.getAlignment().getHiddenColumns();
697  0 ColumnSelection cs = av.getColumnSelection();
698   
699  0 for (int p = grp.nextSetBit(0); p >= 0; p = grp
700    .nextSetBit(p + 1))
701    {
702  0 if (matrix instanceof MappableContactMatrixI)
703    {
704    // find the end of this run of set bits
705  0 int nextp = grp.nextClearBit(p) - 1;
706  0 int[] pos = ((MappableContactMatrixI) matrix)
707    .getMappedPositionsFor(rseq, p, nextp);
708  0 p = nextp;
709   
710  0 if (pos != null)
711    {
712  0 for (int pos_p = pos[0]; pos_p <= pos[1]; pos_p++)
713    {
714  0 int col = rseq.findIndex(pos_p) - 1;
715  0 if (col >= 0 && (!av.hasHiddenColumns()
716    || hc.isVisible(col)))
717    {
718  0 cs.addElement(col);
719    }
720    }
721    }
722    }
723    else
724    {
725  0 int offp = (rseq != null)
726    ? rseq.findIndex(rseq.getStart() - 1 + p)
727    : p;
728   
729  0 if (!av.hasHiddenColumns() || hc.isVisible(offp))
730    {
731  0 cs.addElement(offp);
732    }
733    }
734    }
735    }
736    // possible alternative for interactive selection - threshold
737    // gives 'ceiling' for forming a cluster
738    // when a row+column is selected, farthest common ancestor less
739    // than thr is used to compute cluster
740   
741    }
742    }
743    else
744    {
745    // select corresponding range in segment under mouse
746    {
747  0 int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
748  0 if (rng != null)
749    {
750  0 av.getColumnSelection().addRangeOfElements(rng, true);
751    }
752  0 av.getColumnSelection().addElement(currentX);
753    }
754    // PAE SPECIFIC
755    // and also select everything lower than the max range adjacent
756    // (kind of works)
757  0 if (evt.isControlDown()
758    && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
759    {
760  0 int c = fr;
761  0 ContactRange cr = forCurrentX.getRangeFor(fr, to);
762  0 double cval;
763    // TODO: could use GraphLine instead of arbitrary picking
764    // TODO: could report mean/median/variance for partitions
765    // (contiguous selected vs unselected regions and inter-contig
766    // regions)
767    // controls feathering - what other elements in row/column
768    // should we select
769  0 double thresh = cr.getMean()
770    + (cr.getMax() - cr.getMean()) * .15;
771  0 while (c >= 0)
772    {
773  0 cval = forCurrentX.getContactAt(c);
774  0 if (// cr.getMin() <= cval &&
775  0 cval <= thresh)
776    {
777  0 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
778  0 if (cols != null)
779    {
780  0 av.getColumnSelection().addRangeOfElements(cols, true);
781    }
782    else
783    {
784  0 break;
785    }
786    }
787  0 c--;
788    }
789  0 c = to;
790  0 while (c < forCurrentX.getContactHeight())
791    {
792  0 cval = forCurrentX.getContactAt(c);
793  0 if (// cr.getMin() <= cval &&
794  0 cval <= thresh)
795    {
796  0 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
797  0 if (cols != null)
798    {
799  0 av.getColumnSelection().addRangeOfElements(cols, true);
800    }
801    }
802    else
803    {
804  0 break;
805    }
806  0 c++;
807   
808    }
809    }
810   
811    }
812    }
813    }
814  0 ap.paintAlignment(false, false);
815  0 PaintRefresher.Refresh(ap, av.getSequenceSetId());
816  0 av.sendSelection();
817  0 return true;
818    }
819   
820    /**
821    * Construct and display a context menu at the right-click position
822    *
823    * @param y
824    * @param x
825    */
 
826  0 toggle void showPopupMenu(final int y, int x)
827    {
828  0 if (av.getColumnSelection() == null
829    || av.getColumnSelection().isEmpty())
830    {
831  0 return;
832    }
833   
834  0 JPopupMenu pop = new JPopupMenu(
835    MessageManager.getString("label.structure_type"));
836  0 JMenuItem item;
837    /*
838    * Just display the needed structure options
839    */
840  0 if (av.getAlignment().isNucleotide())
841    {
842  0 item = new JMenuItem(STEM);
843  0 item.addActionListener(this);
844  0 pop.add(item);
845    }
846    else
847    {
848  0 item = new JMenuItem(HELIX);
849  0 item.addActionListener(this);
850  0 pop.add(item);
851  0 item = new JMenuItem(SHEET);
852  0 item.addActionListener(this);
853  0 pop.add(item);
854    }
855  0 item = new JMenuItem(LABEL);
856  0 item.addActionListener(this);
857  0 pop.add(item);
858  0 item = new JMenuItem(COLOUR);
859  0 item.addActionListener(this);
860  0 pop.add(item);
861  0 item = new JMenuItem(REMOVE);
862  0 item.addActionListener(this);
863  0 pop.add(item);
864  0 pop.show(this, x, y);
865    }
866   
867    /**
868    * Action on mouse up is to clear mouse drag data and call mouseReleased on
869    * ScalePanel, to deal with defining the selection group (if any) defined by
870    * the mouse drag
871    *
872    * @param evt
873    */
 
874  0 toggle @Override
875    public void mouseReleased(MouseEvent evt)
876    {
877  0 if (dragMode == DragMode.MatrixSelect)
878    {
879  0 matrixSelectRange(evt);
880    }
881  0 graphStretch = -1;
882  0 mouseDragLastX = -1;
883  0 mouseDragLastY = -1;
884  0 firstDragX = -1;
885  0 firstDragY = -1;
886  0 mouseDragging = false;
887  0 if (dragMode == DragMode.Resize)
888    {
889  0 ap.adjustAnnotationHeight();
890    }
891  0 dragMode = DragMode.Undefined;
892  0 if (!matrix_clicked(evt))
893    {
894  0 ap.getScalePanel().mouseReleased(evt);
895    }
896   
897    /*
898    * isPopupTrigger is set in mouseReleased on Windows
899    * (in mousePressed on Mac)
900    */
901  0 if (evt.isPopupTrigger() && activeRow != -1)
902    {
903  0 showPopupMenu(evt.getY(), evt.getX());
904    }
905   
906    }
907   
908    /**
909    * DOCUMENT ME!
910    *
911    * @param evt
912    * DOCUMENT ME!
913    */
 
914  0 toggle @Override
915    public void mouseEntered(MouseEvent evt)
916    {
917  0 this.mouseDragging = false;
918  0 ap.getScalePanel().mouseEntered(evt);
919    }
920   
921    /**
922    * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
923    * with column selection on a mouse drag
924    *
925    * @param evt
926    */
 
927  0 toggle @Override
928    public void mouseExited(MouseEvent evt)
929    {
930  0 ap.getScalePanel().mouseExited(evt);
931    }
932   
933    /**
934    * Action on starting or continuing a mouse drag. There are two possible
935    * actions:
936    * <ul>
937    * <li>drag up or down on a graphed annotation increases or decreases the
938    * height of the graph</li>
939    * <li>dragging left or right selects the columns dragged across</li>
940    * </ul>
941    * A drag on a graph annotation is treated as column selection if it starts
942    * with more horizontal than vertical movement, and as resize if it starts
943    * with more vertical than horizontal movement. Once started, the drag does
944    * not change mode.
945    *
946    * @param evt
947    */
 
948  0 toggle @Override
949    public void mouseDragged(MouseEvent evt)
950    {
951    /*
952    * if dragMode is Undefined:
953    * - set to Select if dx > dy
954    * - set to Resize if dy > dx
955    * - do nothing if dx == dy
956    */
957  0 final int x = evt.getX();
958  0 final int y = evt.getY();
959  0 if (dragMode == DragMode.Undefined)
960    {
961  0 int dx = Math.abs(x - mouseDragLastX);
962  0 int dy = Math.abs(y - mouseDragLastY);
963  0 if (graphStretch == -1 || dx > dy)
964    {
965    /*
966    * mostly horizontal drag, or not a graph annotation
967    */
968  0 dragMode = DragMode.Select;
969    }
970  0 else if (dy > dx)
971    {
972    /*
973    * mostly vertical drag
974    */
975  0 dragMode = DragMode.Resize;
976  0 notJustOne = evt.isShiftDown();
977   
978    /*
979    * but could also be a matrix drag
980    */
981  0 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
982    .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
983    {
984    /*
985    * dragging in a matrix
986    */
987  0 dragMode = DragMode.MatrixSelect;
988  0 firstDragX = mouseDragLastX;
989  0 firstDragY = mouseDragLastY;
990    }
991    }
992    }
993   
994  0 if (dragMode == DragMode.Undefined)
995   
996    {
997    /*
998    * drag is diagonal - defer deciding whether to
999    * treat as up/down or left/right
1000    */
1001  0 return;
1002    }
1003   
1004  0 try
1005    {
1006  0 if (dragMode == DragMode.Resize)
1007    {
1008    /*
1009    * resize graph annotation if mouse was dragged up or down
1010    */
1011  0 int deltaY = mouseDragLastY - evt.getY();
1012  0 if (deltaY != 0)
1013    {
1014  0 AlignmentAnnotation graphAnnotation = av.getAlignment()
1015    .getAlignmentAnnotation()[graphStretch];
1016  0 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
1017  0 if (notJustOne)
1018    {
1019  0 for (AlignmentAnnotation similar : av.getAlignment()
1020    .findAnnotations(null, graphAnnotation.getCalcId(),
1021    graphAnnotation.label))
1022    {
1023  0 similar.graphHeight = newHeight;
1024    }
1025   
1026    }
1027    else
1028    {
1029  0 graphAnnotation.graphHeight = newHeight;
1030    }
1031  0 adjustPanelHeight();
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    // TODO abstract tooltip generator so different implementations can be built
1298  0 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1299    {
1300  0 if (rowAndOffset >= ann.graphHeight)
1301    {
1302  0 return null;
1303    }
1304  0 ContactListI clist = av.getContactList(ann, column);
1305  0 if (clist != null)
1306    {
1307  0 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1308  0 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1309  0 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1310  0 StringBuilder tooltipb = new StringBuilder();
1311  0 tooltipb.append("Contact from ").append(clist.getPosition())
1312    .append(", [").append(ci.cStart).append(" - ")
1313    .append(ci.cEnd).append("]").append("<br/>Mean:");
1314  0 Format.appendPercentage(tooltipb, (float) cr.getMean(), 2);
1315  0 tooltip = tooltipb.toString();
1316  0 int col = ann.sequenceRef.findPosition(column);
1317  0 int[][] highlightPos;
1318  0 int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1319  0 if (mappedPos != null)
1320    {
1321  0 highlightPos = new int[1 + mappedPos.length][2];
1322  0 highlightPos[0] = new int[] { col, col };
1323  0 for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1324    {
1325  0 highlightPos[h][0] = ann.sequenceRef
1326    .findPosition(mappedPos[p] - 1);
1327  0 highlightPos[h][1] = ann.sequenceRef
1328    .findPosition(mappedPos[p + 1] - 1);
1329    }
1330    }
1331    else
1332    {
1333  0 highlightPos = new int[][] { new int[] { col, col } };
1334    }
1335  0 ap.getStructureSelectionManager()
1336    .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1337    }
1338    }
1339  0 return tooltip == null || tooltip.length() == 0 ? null : tooltip;
1340    }
1341   
 
1342  0 toggle private static String getAnnotationBriefSummary(Annotation a)
1343    {
1344  0 StringBuilder ttSB = new StringBuilder();
1345  0 if (a.secondaryStructure != 0 && a.secondaryStructure != ' ')
1346    {
1347  0 ttSB.append(a.secondaryStructure);
1348    }
1349  0 else if (a.description != null && a.description.trim().length() > 0)
1350    {
1351  0 ttSB.append(a.description);
1352    }
1353  0 else if (a.displayCharacter != null
1354    && a.displayCharacter.trim().length() > 0)
1355    {
1356  0 ttSB.append(a.displayCharacter);
1357    }
1358  0 else if (!Float.isNaN(a.value))
1359    {
1360  0 if (a.value == Math.floor(a.value)) // likely integer value
1361    {
1362  0 ttSB.append(String.format("%.0f", a.value));
1363    }
1364    else // display as is
1365    {
1366  0 ttSB.append(String.valueOf(a.value));
1367    }
1368    }
1369  0 return ttSB.toString();
1370    }
1371   
1372    /**
1373    * Constructs and returns the status bar message
1374    *
1375    * @param al
1376    * @param column
1377    * @param ann
1378    * @param rowAndOffset
1379    */
 
1380  0 toggle static String getStatusMessage(AlignmentI al, int column,
1381    AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1382    {
1383    /*
1384    * show alignment column and annotation description if any
1385    */
1386  0 StringBuilder text = new StringBuilder(32);
1387  0 text.append(MessageManager.getString("label.column")).append(" ")
1388    .append(column + 1);
1389   
1390  0 if (column < ann.annotations.length && ann.annotations[column] != null)
1391    {
1392  0 String description = getAnnotationBriefSummary(
1393    ann.annotations[column]);
1394  0 if (description != null && description.trim().length() > 0)
1395    {
1396  0 text.append(" ").append(description);
1397    }
1398    }
1399   
1400    /*
1401    * if the annotation is sequence-specific, show the sequence number
1402    * in the alignment, and (if not a gap) the residue and position
1403    */
1404  0 SequenceI seqref = ann.sequenceRef;
1405  0 if (seqref != null)
1406    {
1407  0 int seqIndex = al.findIndex(seqref);
1408  0 if (seqIndex != -1)
1409    {
1410  0 text.append(", ").append(MessageManager.getString("label.sequence"))
1411    .append(" ").append(seqIndex + 1);
1412  0 char residue = seqref.getCharAt(column);
1413  0 if (!Comparison.isGap(residue))
1414    {
1415  0 text.append(" ");
1416  0 String name;
1417  0 if (al.isNucleotide())
1418    {
1419  0 name = ResidueProperties.nucleotideName
1420    .get(String.valueOf(residue));
1421  0 text.append(" Nucleotide: ")
1422  0 .append(name != null ? name : residue);
1423    }
1424    else
1425    {
1426  0 name = 'X' == residue ? "X"
1427  0 : ('*' == residue ? "STOP"
1428    : ResidueProperties.aa2Triplet
1429    .get(String.valueOf(residue)));
1430  0 text.append(" Residue: ").append(name != null ? name : residue);
1431    }
1432  0 int residuePos = seqref.findPosition(column);
1433  0 text.append(" (").append(residuePos).append(")");
1434    }
1435    }
1436    }
1437   
1438  0 return text.toString();
1439    }
1440   
1441    /**
1442    * DOCUMENT ME!
1443    *
1444    * @param evt
1445    * DOCUMENT ME!
1446    */
 
1447  0 toggle @Override
1448    public void mouseClicked(MouseEvent evt)
1449    {
1450    // if (activeRow != -1)
1451    // {
1452    // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1453    // AlignmentAnnotation anot = aa[activeRow];
1454    // }
1455    }
1456   
1457    // TODO mouseClicked-content and drawCursor are quite experimental!
 
1458  0 toggle public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1459    int y1)
1460    {
1461  0 int pady = av.getCharHeight() / 5;
1462  0 int charOffset = 0;
1463  0 graphics.setColor(Color.black);
1464  0 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1465   
1466  0 if (av.validCharWidth)
1467    {
1468  0 graphics.setColor(Color.white);
1469   
1470  0 char s = seq.getCharAt(res);
1471   
1472  0 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1473  0 graphics.drawString(String.valueOf(s), charOffset + x1,
1474    (y1 + av.getCharHeight()) - pady);
1475    }
1476   
1477    }
1478   
1479    private volatile boolean imageFresh = false;
1480   
1481    private Rectangle visibleRect = new Rectangle(),
1482    clipBounds = new Rectangle();
1483   
1484    /**
1485    * DOCUMENT ME!
1486    *
1487    * @param g
1488    * DOCUMENT ME!
1489    */
 
1490  2646 toggle @Override
1491    public void paintComponent(Graphics g)
1492    {
1493   
1494    // BH: note that this method is generally recommended to
1495    // call super.paintComponent(g). Otherwise, the children of this
1496    // component will not be rendered. That is not needed here
1497    // because AnnotationPanel does not have any children. It is
1498    // just a JPanel contained in a JViewPort.
1499   
1500  2646 computeVisibleRect(visibleRect);
1501   
1502  2646 g.setColor(Color.white);
1503  2646 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1504   
1505  2646 if (image != null)
1506    {
1507    // BH 2018 optimizing generation of new Rectangle().
1508  ? if (fastPaint
1509    || (visibleRect.width != (clipBounds = g
1510    .getClipBounds(clipBounds)).width)
1511    || (visibleRect.height != clipBounds.height))
1512    {
1513   
1514  149 g.drawImage(image, 0, 0, this);
1515  149 fastPaint = false;
1516  149 return;
1517    }
1518    }
1519  2497 updateFadedImageWidth();
1520  2497 if (imgWidth < 1)
1521    {
1522  0 return;
1523    }
1524  2497 Graphics2D gg;
1525  2497 if (image == null || imgWidth != image.getWidth(this)
1526    || image.getHeight(this) != getHeight())
1527    {
1528  231 boolean tried = false;
1529  231 image = null;
1530  462 while (image == null && !tried)
1531    {
1532  231 try
1533    {
1534  231 image = new BufferedImage(imgWidth,
1535    ap.getAnnotationPanel().getHeight(),
1536    BufferedImage.TYPE_INT_RGB);
1537  231 tried = true;
1538    } catch (IllegalArgumentException exc)
1539    {
1540  0 jalview.bin.Console.errPrintln(
1541    "Serious issue with viewport geometry imgWidth requested was "
1542    + imgWidth);
1543  0 return;
1544    } catch (OutOfMemoryError oom)
1545    {
1546  0 try
1547    {
1548  0 System.gc();
1549    } catch (Exception x)
1550    {
1551    }
1552  0 ;
1553  0 new OOMWarning(
1554    "Couldn't allocate memory to redraw screen. Please restart Jalview",
1555    oom);
1556  0 return;
1557    }
1558   
1559    }
1560  231 gg = (Graphics2D) image.getGraphics();
1561   
1562  231 if (av.antiAlias)
1563    {
1564  131 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1565    RenderingHints.VALUE_ANTIALIAS_ON);
1566    }
1567   
1568  231 gg.setFont(av.getFont());
1569  231 fm = gg.getFontMetrics();
1570  231 gg.setColor(Color.white);
1571  231 gg.fillRect(0, 0, imgWidth, image.getHeight());
1572  231 imageFresh = true;
1573    }
1574    else
1575    {
1576  2266 gg = (Graphics2D) image.getGraphics();
1577   
1578    }
1579   
1580  2497 drawComponent(gg, av.getRanges().getStartRes(),
1581    av.getRanges().getEndRes() + 1);
1582  2497 gg.dispose();
1583  2497 imageFresh = false;
1584  2497 g.drawImage(image, 0, 0, this);
1585    }
1586   
 
1587  5282 toggle public void updateFadedImageWidth()
1588    {
1589  5282 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1590    + 1) * av.getCharWidth();
1591   
1592    }
1593   
1594    /**
1595    * set true to enable redraw timing debug output on stderr
1596    */
1597    private final boolean debugRedraw = false;
1598   
1599    /**
1600    * non-Thread safe repaint
1601    *
1602    * @param horizontal
1603    * repaint with horizontal shift in alignment
1604    */
 
1605  37 toggle public void fastPaint(int horizontal)
1606    {
1607  37 if ((horizontal == 0) || image == null
1608    || av.getAlignment().getAlignmentAnnotation() == null
1609    || av.getAlignment().getAlignmentAnnotation().length < 1
1610    || av.isCalcInProgress())
1611    {
1612  36 repaint();
1613  36 return;
1614    }
1615   
1616  1 int sr = av.getRanges().getStartRes();
1617  1 int er = av.getRanges().getEndRes() + 1;
1618  1 int transX = 0;
1619   
1620  1 Graphics2D gg = (Graphics2D) image.getGraphics();
1621   
1622  1 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1623    {
1624    // scroll is less than imgWidth away so can re-use buffered graphics
1625  0 gg.copyArea(0, 0, imgWidth, getHeight(),
1626    -horizontal * av.getCharWidth(), 0);
1627   
1628  0 if (horizontal > 0) // scrollbar pulled right, image to the left
1629    {
1630  0 transX = (er - sr - horizontal) * av.getCharWidth();
1631  0 sr = er - horizontal;
1632    }
1633  0 else if (horizontal < 0)
1634    {
1635  0 er = sr - horizontal;
1636    }
1637    }
1638  1 gg.translate(transX, 0);
1639   
1640  1 drawComponent(gg, sr, er);
1641   
1642  1 gg.translate(-transX, 0);
1643   
1644  1 gg.dispose();
1645   
1646  1 fastPaint = true;
1647   
1648    // Call repaint on alignment panel so that repaints from other alignment
1649    // panel components can be aggregated. Otherwise performance of the overview
1650    // window and others may be adversely affected.
1651  1 av.getAlignPanel().repaint();
1652    }
1653   
1654    private volatile boolean lastImageGood = false;
1655   
1656    /**
1657    * DOCUMENT ME!
1658    *
1659    * @param g
1660    * DOCUMENT ME!
1661    * @param startRes
1662    * DOCUMENT ME!
1663    * @param endRes
1664    * DOCUMENT ME!
1665    */
 
1666  2498 toggle public void drawComponent(Graphics g, int startRes, int endRes)
1667    {
1668  2498 BufferedImage oldFaded = fadedImage;
1669  2498 if (av.isCalcInProgress())
1670    {
1671  421 if (image == null)
1672    {
1673  0 lastImageGood = false;
1674  0 return;
1675    }
1676    // We'll keep a record of the old image,
1677    // and draw a faded image until the calculation
1678    // has completed
1679  421 if (lastImageGood
1680    && (fadedImage == null || fadedImage.getWidth() != imgWidth
1681    || fadedImage.getHeight() != image.getHeight()))
1682    {
1683    // jalview.bin.Console.errPrintln("redraw faded image
1684    // ("+(fadedImage==null ?
1685    // "null image" : "") + " lastGood="+lastImageGood+")");
1686  160 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1687    BufferedImage.TYPE_INT_RGB);
1688   
1689  160 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1690   
1691  160 fadedG.setColor(Color.white);
1692  160 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1693   
1694  160 fadedG.setComposite(
1695    AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1696  160 fadedG.drawImage(image, 0, 0, this);
1697   
1698    }
1699    // make sure we don't overwrite the last good faded image until all
1700    // calculations have finished
1701  421 lastImageGood = false;
1702   
1703    }
1704    else
1705    {
1706  2077 if (fadedImage != null)
1707    {
1708  144 oldFaded = fadedImage;
1709    }
1710  2077 fadedImage = null;
1711    }
1712   
1713  2498 g.setColor(Color.white);
1714  2498 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1715   
1716  2498 g.setFont(av.getFont());
1717  2498 if (fm == null)
1718    {
1719  0 fm = g.getFontMetrics();
1720    }
1721   
1722  2498 if ((av.getAlignment().getAlignmentAnnotation() == null)
1723    || (av.getAlignment().getAlignmentAnnotation().length < 1))
1724    {
1725  0 g.setColor(Color.white);
1726  0 g.fillRect(0, 0, getWidth(), getHeight());
1727  0 g.setColor(Color.black);
1728  0 if (av.validCharWidth)
1729    {
1730  0 g.drawString(MessageManager
1731    .getString("label.alignment_has_no_annotations"), 20, 15);
1732    }
1733   
1734  0 return;
1735    }
1736  2498 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1737    endRes);
1738  2498 if (!lastImageGood && fadedImage == null)
1739    {
1740  50 fadedImage = oldFaded;
1741    }
1742  2498 if (dragMode == DragMode.MatrixSelect)
1743    {
1744  0 g.setColor(Color.yellow);
1745  0 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1746    Math.min(firstDragY, mouseDragLastY),
1747    Math.max(firstDragX, mouseDragLastX)
1748    - Math.min(firstDragX, mouseDragLastX),
1749    Math.max(firstDragY, mouseDragLastY)
1750    - Math.min(firstDragY, mouseDragLastY));
1751   
1752    }
1753    }
1754   
 
1755  2785 toggle @Override
1756    public FontMetrics getFontMetrics()
1757    {
1758  2785 return fm;
1759    }
1760   
 
1761  2785 toggle @Override
1762    public Image getFadedImage()
1763    {
1764  2785 return fadedImage;
1765    }
1766   
 
1767  2785 toggle @Override
1768    public int getFadedImageWidth()
1769    {
1770  2785 updateFadedImageWidth();
1771  2785 return imgWidth;
1772    }
1773   
1774    private int[] bounds = new int[2];
1775   
 
1776  8129 toggle @Override
1777    public int[] getVisibleVRange()
1778    {
1779  8129 if (ap != null && ap.getAlabels() != null)
1780    {
1781  7880 int sOffset = -ap.getAlabels().getScrollOffset();
1782  7880 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1783  7880 bounds[0] = sOffset;
1784  7880 bounds[1] = visHeight;
1785  7880 return bounds;
1786    }
1787    else
1788    {
1789  249 return null;
1790    }
1791    }
1792   
1793    /**
1794    * Try to ensure any references held are nulled
1795    */
 
1796  270 toggle public void dispose()
1797    {
1798  270 av = null;
1799  270 ap = null;
1800  270 image = null;
1801  270 fadedImage = null;
1802    // gg = null;
1803  270 _mwl = null;
1804   
1805    /*
1806    * I created the renderer so I will dispose of it
1807    */
1808  270 if (renderer != null)
1809    {
1810  270 renderer.dispose();
1811    }
1812    }
1813   
 
1814  763 toggle @Override
1815    public void propertyChange(PropertyChangeEvent evt)
1816    {
1817    // Respond to viewport range changes (e.g. alignment panel was scrolled)
1818    // Both scrolling and resizing change viewport ranges: scrolling changes
1819    // both start and end points, but resize only changes end values.
1820    // Here we only want to fastpaint on a scroll, with resize using a normal
1821    // paint, so scroll events are identified as changes to the horizontal or
1822    // vertical start value.
1823  763 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1824    {
1825  37 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1826    }
1827  726 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1828    {
1829  0 fastPaint(((int[]) evt.getNewValue())[0]
1830    - ((int[]) evt.getOldValue())[0]);
1831    }
1832  726 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1833    {
1834  0 repaint();
1835    }
1836    }
1837   
1838    /**
1839    * computes the visible height of the annotation panel
1840    *
1841    * @param adjustPanelHeight
1842    * - when false, just adjust existing height according to other
1843    * windows
1844    * @param annotationHeight
1845    * @return height to use for the ScrollerPreferredVisibleSize
1846    */
 
1847  6392 toggle public int adjustForAlignFrame(boolean adjustPanelHeight,
1848    int annotationHeight)
1849    {
1850    /*
1851    * Estimate available height in the AlignFrame for alignment +
1852    * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1853    * hscroll, status bar, insets.
1854    */
1855  6386 int stuff = (ap.getViewName() != null ? 30 : 0)
1856  6385 + (Platform.isAMacAndNotJS() ? 120 : 140);
1857  6385 int availableHeight = ap.alignFrame.getHeight() - stuff;
1858  6385 int rowHeight = av.getCharHeight();
1859   
1860  6386 if (adjustPanelHeight)
1861    {
1862  5766 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1863   
1864    /*
1865    * If not enough vertical space, maximize annotation height while keeping
1866    * at least two rows of alignment visible
1867    */
1868  5766 if (annotationHeight + alignmentHeight > availableHeight)
1869    {
1870  3447 annotationHeight = Math.min(annotationHeight,
1871    availableHeight - 2 * rowHeight);
1872    }
1873    }
1874    else
1875    {
1876    // maintain same window layout whilst updating sliders
1877  620 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1878    availableHeight - 2 * rowHeight);
1879    }
1880  6386 return annotationHeight;
1881    }
1882    }