2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
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;
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;
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.MessageManager;
72 import jalview.util.Platform;
73 import jalview.viewmodel.ViewportListenerI;
74 import jalview.viewmodel.ViewportRanges;
75 import jalview.ws.datamodel.alphafold.PAEContactMatrix;
78 * AnnotationPanel displays visible portion of annotation rows below unwrapped
84 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
85 MouseListener, MouseWheelListener, MouseMotionListener,
86 ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
90 Select, Resize, Undefined, MatrixSelect
93 String HELIX = MessageManager.getString("label.helix");
95 String SHEET = MessageManager.getString("label.sheet");
98 * For RNA secondary structure "stems" aka helices
100 String STEM = MessageManager.getString("label.rna_helix");
102 String LABEL = MessageManager.getString("label.label");
104 String REMOVE = MessageManager.getString("label.remove_annotation");
106 String COLOUR = MessageManager.getString("action.colour");
108 public final Color HELIX_COLOUR = Color.red.darker();
110 public final Color SHEET_COLOUR = Color.green.darker().darker();
112 public final Color STEM_COLOUR = Color.blue.darker();
115 public AlignViewport av;
119 public int activeRow = -1;
121 public BufferedImage image;
123 public volatile BufferedImage fadedImage;
125 // private Graphics2D gg;
127 public FontMetrics fm;
129 public int imgWidth = 0;
131 boolean fastPaint = false;
133 // Used For mouse Dragging and resizing graphs
134 int graphStretch = -1;
136 int mouseDragLastX = -1;
138 int mouseDragLastY = -1;
144 DragMode dragMode = DragMode.Undefined;
146 boolean mouseDragging = false;
148 // for editing cursor
153 public final AnnotationRenderer renderer;
155 private MouseWheelListener[] _mwl;
157 private boolean notJustOne;
160 * Creates a new AnnotationPanel object.
165 public AnnotationPanel(AlignmentPanel ap)
167 ToolTipManager.sharedInstance().registerComponent(this);
168 ToolTipManager.sharedInstance().setInitialDelay(0);
169 ToolTipManager.sharedInstance().setDismissDelay(10000);
172 this.setLayout(null);
173 addMouseListener(this);
174 addMouseMotionListener(this);
175 ap.annotationScroller.getVerticalScrollBar()
176 .addAdjustmentListener(this);
177 // save any wheel listeners on the scroller, so we can propagate scroll
179 _mwl = ap.annotationScroller.getMouseWheelListeners();
180 // and then set our own listener to consume all mousewheel events
181 ap.annotationScroller.addMouseWheelListener(this);
182 renderer = new AnnotationRenderer();
184 av.getRanges().addPropertyChangeListener(this);
187 public AnnotationPanel(AlignViewport av)
190 renderer = new AnnotationRenderer();
194 public void mouseWheelMoved(MouseWheelEvent e)
199 double wheelRotation = e.getPreciseWheelRotation();
200 if (wheelRotation > 0)
202 av.getRanges().scrollRight(true);
204 else if (wheelRotation < 0)
206 av.getRanges().scrollRight(false);
211 // TODO: find the correct way to let the event bubble up to
212 // ap.annotationScroller
213 for (MouseWheelListener mwl : _mwl)
217 mwl.mouseWheelMoved(e);
228 public Dimension getPreferredScrollableViewportSize()
230 Dimension ps = getPreferredSize();
231 return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
235 public int getScrollableBlockIncrement(Rectangle visibleRect,
236 int orientation, int direction)
242 public boolean getScrollableTracksViewportHeight()
248 public boolean getScrollableTracksViewportWidth()
254 public int getScrollableUnitIncrement(Rectangle visibleRect,
255 int orientation, int direction)
264 * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
268 public void adjustmentValueChanged(AdjustmentEvent evt)
270 // update annotation label display
271 ap.getAlabels().setScrollOffset(-evt.getValue());
275 * Calculates the height of the annotation displayed in the annotation panel.
276 * Callers should normally call the ap.adjustAnnotationHeight method to ensure
277 * all annotation associated components are updated correctly.
280 public int adjustPanelHeight()
282 int height = av.calcPanelHeight();
283 this.setPreferredSize(new Dimension(1, height));
286 // revalidate only when the alignment panel is fully constructed
300 public void actionPerformed(ActionEvent evt)
302 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
307 Annotation[] anot = aa[activeRow].annotations;
309 if (anot.length < av.getColumnSelection().getMax())
311 Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
313 System.arraycopy(anot, 0, temp, 0, anot.length);
315 aa[activeRow].annotations = anot;
318 String action = evt.getActionCommand();
319 if (action.equals(REMOVE))
321 for (int index : av.getColumnSelection().getSelected())
323 if (av.getAlignment().getHiddenColumns().isVisible(index))
329 else if (action.equals(LABEL))
331 String exMesg = collectAnnotVals(anot, LABEL);
332 String label = JvOptionPane.showInputDialog(
333 MessageManager.getString("label.enter_label"), exMesg);
340 if ((label.length() > 0) && !aa[activeRow].hasText)
342 aa[activeRow].hasText = true;
345 for (int index : av.getColumnSelection().getSelected())
347 if (!av.getAlignment().getHiddenColumns().isVisible(index))
352 if (anot[index] == null)
354 anot[index] = new Annotation(label, "", ' ', 0);
358 anot[index].displayCharacter = label;
362 else if (action.equals(COLOUR))
364 final Annotation[] fAnot = anot;
365 String title = MessageManager
366 .getString("label.select_foreground_colour");
367 ColourChooserListener listener = new ColourChooserListener()
370 public void colourSelected(Color c)
372 HiddenColumns hiddenColumns = av.getAlignment()
374 for (int index : av.getColumnSelection().getSelected())
376 if (hiddenColumns.isVisible(index))
378 if (fAnot[index] == null)
380 fAnot[index] = new Annotation("", "", ' ', 0);
382 fAnot[index].colour = c;
387 JalviewColourChooser.showColourChooser(this, title, Color.black,
391 // HELIX, SHEET or STEM
394 String symbol = "\u03B1"; // alpha
396 if (action.equals(HELIX))
400 else if (action.equals(SHEET))
403 symbol = "\u03B2"; // beta
406 // Added by LML to color stems
407 else if (action.equals(STEM))
410 int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
411 symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
414 if (!aa[activeRow].hasIcons)
416 aa[activeRow].hasIcons = true;
419 String label = JvOptionPane.showInputDialog(MessageManager
420 .getString("label.enter_label_for_the_structure"), symbol);
427 if ((label.length() > 0) && !aa[activeRow].hasText)
429 aa[activeRow].hasText = true;
430 if (action.equals(STEM))
432 aa[activeRow].showAllColLabels = true;
435 for (int index : av.getColumnSelection().getSelected())
437 if (!av.getAlignment().getHiddenColumns().isVisible(index))
442 if (anot[index] == null)
444 anot[index] = new Annotation(label, "", type, 0);
447 anot[index].secondaryStructure = type != 'S' ? type
448 : label.length() == 0 ? ' ' : label.charAt(0);
449 anot[index].displayCharacter = label;
454 av.getAlignment().validateAnnotation(aa[activeRow]);
455 ap.alignmentChanged();
456 ap.alignFrame.setMenusForViewport();
464 * Returns any existing annotation concatenated as a string. For each
465 * annotation, takes the description, if any, else the secondary structure
466 * character (if type is HELIX, SHEET or STEM), else the display character (if
473 private String collectAnnotVals(Annotation[] anots, String type)
475 // TODO is this method wanted? why? 'last' is not used
477 StringBuilder collatedInput = new StringBuilder(64);
479 ColumnSelection viscols = av.getColumnSelection();
480 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
483 * the selection list (read-only view) is in selection order, not
484 * column order; make a copy so we can sort it
486 List<Integer> selected = new ArrayList<>(viscols.getSelected());
487 Collections.sort(selected);
488 for (int index : selected)
490 // always check for current display state - just in case
491 if (!hidden.isVisible(index))
495 String tlabel = null;
496 if (anots[index] != null)
497 { // LML added stem code
498 if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
499 || type.equals(LABEL))
501 tlabel = anots[index].description;
502 if (tlabel == null || tlabel.length() < 1)
504 if (type.equals(HELIX) || type.equals(SHEET)
505 || type.equals(STEM))
507 tlabel = "" + anots[index].secondaryStructure;
511 tlabel = "" + anots[index].displayCharacter;
515 if (tlabel != null && !tlabel.equals(last))
517 if (last.length() > 0)
519 collatedInput.append(" ");
521 collatedInput.append(tlabel);
525 return collatedInput.toString();
529 * Action on right mouse pressed on Mac is to show a pop-up menu for the
530 * annotation. Action on left mouse pressed is to find which annotation is
531 * pressed and mark the start of a column selection or graph resize operation.
536 public void mousePressed(MouseEvent evt)
539 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
544 mouseDragLastX = evt.getX();
545 mouseDragLastY = evt.getY();
548 * add visible annotation heights until we reach the y
549 * position, to find which annotation it is in
554 // todo could reuse getRowIndexAndOffset ?
555 final int y = evt.getY();
557 for (int i = 0; i < aa.length; i++)
561 height += aa[i].height;
570 else if (aa[i].graph != 0)
573 * we have clicked on a resizable graph annotation
576 yOffset = height - y;
583 * isPopupTrigger fires in mousePressed on Mac,
584 * not until mouseRelease on Windows
586 if (evt.isPopupTrigger() && activeRow != -1)
588 showPopupMenu(y, evt.getX());
592 if (graphStretch != -1)
595 if (aa[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP)
597 // data in row has position on y as well as x axis
598 if (evt.isAltDown() || evt.isAltGraphDown())
600 dragMode = DragMode.MatrixSelect;
601 firstDragX = mouseDragLastX;
602 firstDragY = mouseDragLastY;
608 // no row (or row that can be adjusted) was pressed. Simulate a ruler click
609 ap.getScalePanel().mousePressed(evt);
614 * checks whether the annotation row under the mouse click evt's handles the
618 * @return false if evt was not handled
620 boolean matrix_clicked(MouseEvent evt)
622 int[] rowIndex = getRowIndexAndOffset(evt.getY(),
623 av.getAlignment().getAlignmentAnnotation());
624 if (rowIndex == null)
627 .error("IMPLEMENTATION ERROR: matrix click out of range.");
630 int yOffset = rowIndex[1];
632 AlignmentAnnotation clicked = av.getAlignment()
633 .getAlignmentAnnotation()[rowIndex[0]];
634 if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
639 // TODO - use existing threshold to select related sections of matrix
640 GraphLine thr = clicked.getThreshold();
642 int currentX = getColumnForXPos(evt.getX());
643 ContactListI forCurrentX = av.getContactList(clicked, currentX);
644 if (forCurrentX != null)
646 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
647 clicked.graphHeight);
648 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset,
651 * start and end range corresponding to the row range under the mouse at
655 fr = Math.min(cXci.cStart, cXci.cEnd);
656 to = Math.max(cXci.cStart, cXci.cEnd);
658 // double click selects the whole group
659 if (evt.getClickCount() == 2)
661 ContactMatrixI matrix = av.getContactMatrix(clicked);
665 // simplest approach is to select all group containing column
666 if (matrix.hasGroups())
668 SequenceI rseq = clicked.sequenceRef;
669 BitSet grp = matrix.getGroupsFor(currentX);
670 // TODO: cXci needs to be mapped to real groups
671 for (int c = fr; c <= to; c++)
673 BitSet additionalGrp = matrix.getGroupsFor(c);
674 grp.or(additionalGrp);
676 HiddenColumns hc = av.getAlignment().getHiddenColumns();
677 for (int p = grp.nextSetBit(0); p >= 0; p = grp
680 int offp = (rseq != null)
681 ? rseq.findIndex(rseq.getStart() - 1 + p)
684 if (!av.hasHiddenColumns() || hc.isVisible(offp))
686 av.getColumnSelection().addElement(offp);
690 // possible alternative for interactive selection - threshold
691 // gives 'ceiling' for forming a cluster
692 // when a row+column is selected, farthest common ancestor less
693 // than thr is used to compute cluster
699 // select corresponding range in segment under mouse
701 int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
704 av.getColumnSelection().addRangeOfElements(rng, true);
706 av.getColumnSelection().addElement(currentX);
709 // and also select everything lower than the max range adjacent
711 if (evt.isControlDown()
712 && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
715 ContactRange cr = forCurrentX.getRangeFor(fr, to);
717 // TODO: could use GraphLine instead of arbitrary picking
718 // TODO: could report mean/median/variance for partitions
719 // (contiguous selected vs unselected regions and inter-contig
721 // controls feathering - what other elements in row/column
723 double thresh = cr.getMean() + (cr.getMax() - cr.getMean()) * .15;
726 cval = forCurrentX.getContactAt(c);
727 if (// cr.getMin() <= cval &&
730 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
733 av.getColumnSelection().addRangeOfElements(cols, true);
743 while (c < forCurrentX.getContactHeight())
745 cval = forCurrentX.getContactAt(c);
746 if (// cr.getMin() <= cval &&
749 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
752 av.getColumnSelection().addRangeOfElements(cols, true);
765 ap.paintAlignment(false, false);
766 PaintRefresher.Refresh(ap, av.getSequenceSetId());
771 * Construct and display a context menu at the right-click position
776 void showPopupMenu(final int y, int x)
778 if (av.getColumnSelection() == null
779 || av.getColumnSelection().isEmpty())
784 JPopupMenu pop = new JPopupMenu(
785 MessageManager.getString("label.structure_type"));
788 * Just display the needed structure options
790 if (av.getAlignment().isNucleotide())
792 item = new JMenuItem(STEM);
793 item.addActionListener(this);
798 item = new JMenuItem(HELIX);
799 item.addActionListener(this);
801 item = new JMenuItem(SHEET);
802 item.addActionListener(this);
805 item = new JMenuItem(LABEL);
806 item.addActionListener(this);
808 item = new JMenuItem(COLOUR);
809 item.addActionListener(this);
811 item = new JMenuItem(REMOVE);
812 item.addActionListener(this);
814 pop.show(this, x, y);
818 * Action on mouse up is to clear mouse drag data and call mouseReleased on
819 * ScalePanel, to deal with defining the selection group (if any) defined by
825 public void mouseReleased(MouseEvent evt)
827 if (dragMode == DragMode.MatrixSelect)
829 matrixSelectRange(evt);
836 mouseDragging = false;
837 if (dragMode == DragMode.Resize)
839 ap.adjustAnnotationHeight();
841 dragMode = DragMode.Undefined;
842 if (!matrix_clicked(evt))
844 ap.getScalePanel().mouseReleased(evt);
848 * isPopupTrigger is set in mouseReleased on Windows
849 * (in mousePressed on Mac)
851 if (evt.isPopupTrigger() && activeRow != -1)
853 showPopupMenu(evt.getY(), evt.getX());
865 public void mouseEntered(MouseEvent evt)
867 this.mouseDragging = false;
868 ap.getScalePanel().mouseEntered(evt);
872 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
873 * with column selection on a mouse drag
878 public void mouseExited(MouseEvent evt)
880 ap.getScalePanel().mouseExited(evt);
884 * Action on starting or continuing a mouse drag. There are two possible
887 * <li>drag up or down on a graphed annotation increases or decreases the
888 * height of the graph</li>
889 * <li>dragging left or right selects the columns dragged across</li>
891 * A drag on a graph annotation is treated as column selection if it starts
892 * with more horizontal than vertical movement, and as resize if it starts
893 * with more vertical than horizontal movement. Once started, the drag does
899 public void mouseDragged(MouseEvent evt)
902 * if dragMode is Undefined:
903 * - set to Select if dx > dy
904 * - set to Resize if dy > dx
905 * - do nothing if dx == dy
907 final int x = evt.getX();
908 final int y = evt.getY();
909 if (dragMode == DragMode.Undefined)
911 int dx = Math.abs(x - mouseDragLastX);
912 int dy = Math.abs(y - mouseDragLastY);
913 if (graphStretch == -1 || dx > dy)
916 * mostly horizontal drag, or not a graph annotation
918 dragMode = DragMode.Select;
923 * mostly vertical drag
925 dragMode = DragMode.Resize;
926 notJustOne = evt.isShiftDown();
929 * but could also be a matrix drag
931 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
932 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
935 * dragging in a matrix
937 dragMode = DragMode.MatrixSelect;
938 firstDragX = mouseDragLastX;
939 firstDragY = mouseDragLastY;
944 if (dragMode == DragMode.Undefined)
948 * drag is diagonal - defer deciding whether to
949 * treat as up/down or left/right
956 if (dragMode == DragMode.Resize)
959 * resize graph annotation if mouse was dragged up or down
961 int deltaY = mouseDragLastY - evt.getY();
964 AlignmentAnnotation graphAnnotation = av.getAlignment()
965 .getAlignmentAnnotation()[graphStretch];
966 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
969 for (AlignmentAnnotation similar : av.getAlignment()
970 .findAnnotations(null, graphAnnotation.getCalcId(),
971 graphAnnotation.label))
973 similar.graphHeight = newHeight;
979 graphAnnotation.graphHeight = newHeight;
982 ap.paintAlignment(false, false);
985 else if (dragMode == DragMode.MatrixSelect)
988 * TODO draw a rubber band for range
992 ap.paintAlignment(false, false);
997 * for mouse drag left or right, delegate to
998 * ScalePanel to adjust the column selection
1000 ap.getScalePanel().mouseDragged(evt);
1009 public void matrixSelectRange(MouseEvent evt)
1012 * get geometry of drag
1014 int fromY = Math.min(firstDragY, evt.getY());
1015 int toY = Math.max(firstDragY, evt.getY());
1016 int fromX = Math.min(firstDragX, evt.getX());
1017 int toX = Math.max(firstDragX, evt.getX());
1019 int deltaY = toY - fromY;
1020 int deltaX = toX - fromX;
1022 int[] rowIndex = getRowIndexAndOffset(fromY,
1023 av.getAlignment().getAlignmentAnnotation());
1024 int[] toRowIndex = getRowIndexAndOffset(toY,
1025 av.getAlignment().getAlignmentAnnotation());
1027 if (rowIndex == null || toRowIndex == null)
1029 jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1032 if (rowIndex[0] != toRowIndex[0])
1034 jalview.bin.Console.trace("Drag went to another row. needs to be clipped");
1037 // rectangular selection on matrix style annotation
1038 AlignmentAnnotation cma = av.getAlignment()
1039 .getAlignmentAnnotation()[rowIndex[0]];
1041 int lastX = getColumnForXPos(fromX);
1042 int currentX = getColumnForXPos(toX);
1043 int fromXc = Math.min(lastX, currentX);
1044 int toXc = Math.max(lastX, currentX);
1045 ContactListI forFromX = av.getContactList(cma, fromXc);
1046 ContactListI forToX = av.getContactList(cma, toXc);
1048 if (forFromX != null && forToX != null)
1050 ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
1052 ContactGeometry.contactInterval lastXci = lastXcgeom
1053 .mapFor(rowIndex[1], rowIndex[1] - deltaY);
1055 ContactGeometry cXcgeom = new ContactGeometry(forToX,
1057 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
1058 rowIndex[1] - deltaY);
1060 // mark rectangular region formed by drag
1061 jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
1062 + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
1063 + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1065 fr = Math.min(lastXci.cStart, lastXci.cEnd);
1066 to = Math.max(lastXci.cStart, lastXci.cEnd);
1067 int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
1068 if (mappedPos != null)
1070 jalview.bin.Console.trace("Marking " + fr + " to " + to
1071 + " mapping to sequence positions " + mappedPos[0] + " to "
1073 for (int pair = 0; pair < mappedPos.length; pair += 2)
1075 for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1077 // if (cma.sequenceRef != null)
1079 // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1080 // av.getColumnSelection().addElement(col);
1084 av.getColumnSelection().addElement(c);
1088 // and again for most recent corner of drag
1089 fr = Math.min(cXci.cStart, cXci.cEnd);
1090 to = Math.max(cXci.cStart, cXci.cEnd);
1091 mappedPos = forFromX.getMappedPositionsFor(fr, to);
1092 if (mappedPos != null)
1094 for (int pair = 0; pair < mappedPos.length; pair += 2)
1096 jalview.bin.Console.trace("Marking " + fr + " to " + to
1097 + " mapping to sequence positions " + mappedPos[pair] + " to "
1098 + mappedPos[pair+1]);
1099 for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1101 // if (cma.sequenceRef != null)
1103 // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1104 // av.getColumnSelection().addElement(col);
1108 av.getColumnSelection().addElement(c);
1113 fr = Math.min(lastX, currentX);
1114 to = Math.max(lastX, currentX);
1116 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1117 for (int c = fr; c <= to; c++)
1119 av.getColumnSelection().addElement(c);
1126 * Constructs the tooltip, and constructs and displays a status message, for
1127 * the current mouse position
1132 public void mouseMoved(MouseEvent evt)
1134 int yPos = evt.getY();
1135 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1136 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1137 int row = rowAndOffset[0];
1141 this.setToolTipText(null);
1145 int column = getColumnForXPos(evt.getX());
1147 AlignmentAnnotation ann = aa[row];
1148 if (row > -1 && ann.annotations != null
1149 && column < ann.annotations.length)
1151 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1153 setToolTipText(toolTip == null ? null
1154 : JvSwingUtils.wrapTooltip(true, toolTip));
1155 String msg = getStatusMessage(av.getAlignment(), column, ann,
1156 rowAndOffset[1], av);
1157 ap.alignFrame.setStatus(msg);
1161 this.setToolTipText(null);
1162 ap.alignFrame.setStatus(" ");
1166 private int getColumnForXPos(int x)
1168 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1169 column = Math.min(column, av.getRanges().getEndRes());
1171 if (av.hasHiddenColumns())
1173 column = av.getAlignment().getHiddenColumns()
1174 .visibleToAbsoluteColumn(column);
1180 * Answers the index in the annotations array of the visible annotation at the
1181 * given y position. This is done by adding the heights of visible annotations
1182 * until the y position has been exceeded. Answers -1 if no annotations are
1183 * visible, or the y position is below all annotations.
1189 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1195 return getRowIndexAndOffset(yPos, aa)[0];
1198 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1200 int[] res = new int[2];
1208 int height = 0, lheight = 0;
1209 for (int i = 0; i < aa.length; i++)
1214 height += aa[i].height;
1221 res[1] = height - yPos;
1229 * Answers a tooltip for the annotation at the current mouse position, not
1230 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1236 * @param rowAndOffset
1238 static String buildToolTip(AlignmentAnnotation ann, int column,
1239 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1242 String tooltip = null;
1243 if (ann.graphGroup > -1)
1245 StringBuilder tip = new StringBuilder(32);
1246 boolean first = true;
1247 for (int i = 0; i < anns.length; i++)
1249 if (anns[i].graphGroup == ann.graphGroup
1250 && anns[i].annotations[column] != null)
1257 tip.append(anns[i].label);
1258 String description = anns[i].annotations[column].description;
1259 if (description != null && description.length() > 0)
1261 tip.append(" ").append(description);
1265 tooltip = first ? null : tip.toString();
1267 else if (column < ann.annotations.length
1268 && ann.annotations[column] != null)
1270 tooltip = ann.annotations[column].description;
1272 // TODO abstract tooltip generator so different implementations can be built
1273 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1275 ContactListI clist = av.getContactList(ann, column);
1278 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1279 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1280 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1281 tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
1282 + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
1284 int col = ann.sequenceRef.findPosition(column);
1285 int[][] highlightPos;
1286 int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1287 if (mappedPos != null)
1289 highlightPos = new int[1 + mappedPos.length][2];
1290 highlightPos[0] = new int[] { col, col };
1291 for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1293 highlightPos[h][0] = ann.sequenceRef
1294 .findPosition(mappedPos[p] - 1);
1295 highlightPos[h][1] = ann.sequenceRef
1296 .findPosition(mappedPos[p + 1] - 1);
1301 highlightPos = new int[][] { new int[] { col, col } };
1303 ap.getStructureSelectionManager()
1304 .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1311 * Constructs and returns the status bar message
1316 * @param rowAndOffset
1318 static String getStatusMessage(AlignmentI al, int column,
1319 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1322 * show alignment column and annotation description if any
1324 StringBuilder text = new StringBuilder(32);
1325 text.append(MessageManager.getString("label.column")).append(" ")
1326 .append(column + 1);
1328 if (column < ann.annotations.length && ann.annotations[column] != null)
1330 String description = ann.annotations[column].description;
1331 if (description != null && description.trim().length() > 0)
1333 text.append(" ").append(description);
1338 * if the annotation is sequence-specific, show the sequence number
1339 * in the alignment, and (if not a gap) the residue and position
1341 SequenceI seqref = ann.sequenceRef;
1344 int seqIndex = al.findIndex(seqref);
1347 text.append(", ").append(MessageManager.getString("label.sequence"))
1348 .append(" ").append(seqIndex + 1);
1349 char residue = seqref.getCharAt(column);
1350 if (!Comparison.isGap(residue))
1354 if (al.isNucleotide())
1356 name = ResidueProperties.nucleotideName
1357 .get(String.valueOf(residue));
1358 text.append(" Nucleotide: ")
1359 .append(name != null ? name : residue);
1363 name = 'X' == residue ? "X"
1364 : ('*' == residue ? "STOP"
1365 : ResidueProperties.aa2Triplet
1366 .get(String.valueOf(residue)));
1367 text.append(" Residue: ").append(name != null ? name : residue);
1369 int residuePos = seqref.findPosition(column);
1370 text.append(" (").append(residuePos).append(")");
1375 return text.toString();
1385 public void mouseClicked(MouseEvent evt)
1387 // if (activeRow != -1)
1389 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1390 // AlignmentAnnotation anot = aa[activeRow];
1394 // TODO mouseClicked-content and drawCursor are quite experimental!
1395 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1398 int pady = av.getCharHeight() / 5;
1400 graphics.setColor(Color.black);
1401 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1403 if (av.validCharWidth)
1405 graphics.setColor(Color.white);
1407 char s = seq.getCharAt(res);
1409 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1410 graphics.drawString(String.valueOf(s), charOffset + x1,
1411 (y1 + av.getCharHeight()) - pady);
1416 private volatile boolean imageFresh = false;
1418 private Rectangle visibleRect = new Rectangle(),
1419 clipBounds = new Rectangle();
1428 public void paintComponent(Graphics g)
1431 // BH: note that this method is generally recommended to
1432 // call super.paintComponent(g). Otherwise, the children of this
1433 // component will not be rendered. That is not needed here
1434 // because AnnotationPanel does not have any children. It is
1435 // just a JPanel contained in a JViewPort.
1437 computeVisibleRect(visibleRect);
1439 g.setColor(Color.white);
1440 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1444 // BH 2018 optimizing generation of new Rectangle().
1446 || (visibleRect.width != (clipBounds = g
1447 .getClipBounds(clipBounds)).width)
1448 || (visibleRect.height != clipBounds.height))
1451 g.drawImage(image, 0, 0, this);
1456 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1457 + 1) * av.getCharWidth();
1463 if (image == null || imgWidth != image.getWidth(this)
1464 || image.getHeight(this) != getHeight())
1466 boolean tried = false;
1468 while (image == null && !tried)
1472 image = new BufferedImage(imgWidth,
1473 ap.getAnnotationPanel().getHeight(),
1474 BufferedImage.TYPE_INT_RGB);
1476 } catch (IllegalArgumentException exc)
1479 "Serious issue with viewport geometry imgWidth requested was "
1482 } catch (OutOfMemoryError oom)
1487 } catch (Exception x)
1492 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1498 gg = (Graphics2D) image.getGraphics();
1502 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1503 RenderingHints.VALUE_ANTIALIAS_ON);
1506 gg.setFont(av.getFont());
1507 fm = gg.getFontMetrics();
1508 gg.setColor(Color.white);
1509 gg.fillRect(0, 0, imgWidth, image.getHeight());
1514 gg = (Graphics2D) image.getGraphics();
1518 drawComponent(gg, av.getRanges().getStartRes(),
1519 av.getRanges().getEndRes() + 1);
1522 g.drawImage(image, 0, 0, this);
1526 * set true to enable redraw timing debug output on stderr
1528 private final boolean debugRedraw = false;
1531 * non-Thread safe repaint
1534 * repaint with horizontal shift in alignment
1536 public void fastPaint(int horizontal)
1538 if ((horizontal == 0) || image == null
1539 || av.getAlignment().getAlignmentAnnotation() == null
1540 || av.getAlignment().getAlignmentAnnotation().length < 1
1541 || av.isCalcInProgress())
1547 int sr = av.getRanges().getStartRes();
1548 int er = av.getRanges().getEndRes() + 1;
1551 Graphics2D gg = (Graphics2D) image.getGraphics();
1553 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1555 // scroll is less than imgWidth away so can re-use buffered graphics
1556 gg.copyArea(0, 0, imgWidth, getHeight(),
1557 -horizontal * av.getCharWidth(), 0);
1559 if (horizontal > 0) // scrollbar pulled right, image to the left
1561 transX = (er - sr - horizontal) * av.getCharWidth();
1562 sr = er - horizontal;
1564 else if (horizontal < 0)
1566 er = sr - horizontal;
1569 gg.translate(transX, 0);
1571 drawComponent(gg, sr, er);
1573 gg.translate(-transX, 0);
1579 // Call repaint on alignment panel so that repaints from other alignment
1580 // panel components can be aggregated. Otherwise performance of the overview
1581 // window and others may be adversely affected.
1582 av.getAlignPanel().repaint();
1585 private volatile boolean lastImageGood = false;
1597 public void drawComponent(Graphics g, int startRes, int endRes)
1599 BufferedImage oldFaded = fadedImage;
1600 if (av.isCalcInProgress())
1604 lastImageGood = false;
1607 // We'll keep a record of the old image,
1608 // and draw a faded image until the calculation
1611 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1612 || fadedImage.getHeight() != image.getHeight()))
1614 // System.err.println("redraw faded image ("+(fadedImage==null ?
1615 // "null image" : "") + " lastGood="+lastImageGood+")");
1616 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1617 BufferedImage.TYPE_INT_RGB);
1619 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1621 fadedG.setColor(Color.white);
1622 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1624 fadedG.setComposite(
1625 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1626 fadedG.drawImage(image, 0, 0, this);
1629 // make sure we don't overwrite the last good faded image until all
1630 // calculations have finished
1631 lastImageGood = false;
1636 if (fadedImage != null)
1638 oldFaded = fadedImage;
1643 g.setColor(Color.white);
1644 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1646 g.setFont(av.getFont());
1649 fm = g.getFontMetrics();
1652 if ((av.getAlignment().getAlignmentAnnotation() == null)
1653 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1655 g.setColor(Color.white);
1656 g.fillRect(0, 0, getWidth(), getHeight());
1657 g.setColor(Color.black);
1658 if (av.validCharWidth)
1660 g.drawString(MessageManager
1661 .getString("label.alignment_has_no_annotations"), 20, 15);
1666 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1668 if (!lastImageGood && fadedImage == null)
1670 fadedImage = oldFaded;
1672 if (dragMode == DragMode.MatrixSelect)
1674 g.setColor(Color.yellow);
1675 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1676 Math.min(firstDragY, mouseDragLastY),
1677 Math.max(firstDragX, mouseDragLastX)
1678 - Math.min(firstDragX, mouseDragLastX),
1679 Math.max(firstDragY, mouseDragLastY)
1680 - Math.min(firstDragY, mouseDragLastY));
1686 public FontMetrics getFontMetrics()
1692 public Image getFadedImage()
1698 public int getFadedImageWidth()
1703 private int[] bounds = new int[2];
1706 public int[] getVisibleVRange()
1708 if (ap != null && ap.getAlabels() != null)
1710 int sOffset = -ap.getAlabels().getScrollOffset();
1711 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1712 bounds[0] = sOffset;
1713 bounds[1] = visHeight;
1723 * Try to ensure any references held are nulled
1725 public void dispose()
1735 * I created the renderer so I will dispose of it
1737 if (renderer != null)
1744 public void propertyChange(PropertyChangeEvent evt)
1746 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1747 // Both scrolling and resizing change viewport ranges: scrolling changes
1748 // both start and end points, but resize only changes end values.
1749 // Here we only want to fastpaint on a scroll, with resize using a normal
1750 // paint, so scroll events are identified as changes to the horizontal or
1751 // vertical start value.
1752 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1754 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1756 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1758 fastPaint(((int[]) evt.getNewValue())[0]
1759 - ((int[]) evt.getOldValue())[0]);
1761 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1768 * computes the visible height of the annotation panel
1770 * @param adjustPanelHeight
1771 * - when false, just adjust existing height according to other
1773 * @param annotationHeight
1774 * @return height to use for the ScrollerPreferredVisibleSize
1776 public int adjustForAlignFrame(boolean adjustPanelHeight,
1777 int annotationHeight)
1780 * Estimate available height in the AlignFrame for alignment +
1781 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1782 * hscroll, status bar, insets.
1784 int stuff = (ap.getViewName() != null ? 30 : 0)
1785 + (Platform.isAMacAndNotJS() ? 120 : 140);
1786 int availableHeight = ap.alignFrame.getHeight() - stuff;
1787 int rowHeight = av.getCharHeight();
1789 if (adjustPanelHeight)
1791 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1794 * If not enough vertical space, maximize annotation height while keeping
1795 * at least two rows of alignment visible
1797 if (annotationHeight + alignmentHeight > availableHeight)
1799 annotationHeight = Math.min(annotationHeight,
1800 availableHeight - 2 * rowHeight);
1805 // maintain same window layout whilst updating sliders
1806 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1807 availableHeight - 2 * rowHeight);
1809 return annotationHeight;