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.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;
80 * AnnotationPanel displays visible portion of annotation rows below unwrapped
86 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
87 MouseListener, MouseWheelListener, MouseMotionListener,
88 ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
92 Select, Resize, Undefined, MatrixSelect
95 String HELIX = MessageManager.getString("label.helix");
97 String SHEET = MessageManager.getString("label.sheet");
100 * For RNA secondary structure "stems" aka helices
102 String STEM = MessageManager.getString("label.rna_helix");
104 String LABEL = MessageManager.getString("label.label");
106 String REMOVE = MessageManager.getString("label.remove_annotation");
108 String COLOUR = MessageManager.getString("action.colour");
110 public final Color HELIX_COLOUR = Color.red.darker();
112 public final Color SHEET_COLOUR = Color.green.darker().darker();
114 public final Color STEM_COLOUR = Color.blue.darker();
117 public AlignViewport av;
121 public int activeRow = -1;
123 public BufferedImage image;
125 public volatile BufferedImage fadedImage;
127 // private Graphics2D gg;
129 public FontMetrics fm;
131 public int imgWidth = 0;
133 boolean fastPaint = false;
135 // Used For mouse Dragging and resizing graphs
136 int graphStretch = -1;
138 int mouseDragLastX = -1;
140 int mouseDragLastY = -1;
146 DragMode dragMode = DragMode.Undefined;
148 boolean mouseDragging = false;
150 // for editing cursor
155 public final AnnotationRenderer renderer;
157 private MouseWheelListener[] _mwl;
159 private boolean notJustOne;
162 * Creates a new AnnotationPanel object.
167 public AnnotationPanel(AlignmentPanel ap)
169 ToolTipManager.sharedInstance().registerComponent(this);
170 ToolTipManager.sharedInstance().setInitialDelay(0);
171 ToolTipManager.sharedInstance().setDismissDelay(10000);
174 this.setLayout(null);
175 addMouseListener(this);
176 addMouseMotionListener(this);
177 ap.annotationScroller.getVerticalScrollBar()
178 .addAdjustmentListener(this);
179 // save any wheel listeners on the scroller, so we can propagate scroll
181 _mwl = ap.annotationScroller.getMouseWheelListeners();
182 // and then set our own listener to consume all mousewheel events
183 ap.annotationScroller.addMouseWheelListener(this);
184 renderer = new AnnotationRenderer();
186 av.getRanges().addPropertyChangeListener(this);
189 public AnnotationPanel(AlignViewport av)
192 renderer = new AnnotationRenderer();
196 public void mouseWheelMoved(MouseWheelEvent e)
201 double wheelRotation = e.getPreciseWheelRotation();
202 if (wheelRotation > 0)
204 av.getRanges().scrollRight(true);
206 else if (wheelRotation < 0)
208 av.getRanges().scrollRight(false);
213 // TODO: find the correct way to let the event bubble up to
214 // ap.annotationScroller
215 for (MouseWheelListener mwl : _mwl)
219 mwl.mouseWheelMoved(e);
230 public Dimension getPreferredScrollableViewportSize()
232 Dimension ps = getPreferredSize();
233 return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
237 public int getScrollableBlockIncrement(Rectangle visibleRect,
238 int orientation, int direction)
244 public boolean getScrollableTracksViewportHeight()
250 public boolean getScrollableTracksViewportWidth()
256 public int getScrollableUnitIncrement(Rectangle visibleRect,
257 int orientation, int direction)
266 * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
270 public void adjustmentValueChanged(AdjustmentEvent evt)
272 // update annotation label display
273 ap.getAlabels().setScrollOffset(-evt.getValue());
277 * Calculates the height of the annotation displayed in the annotation panel.
278 * Callers should normally call the ap.adjustAnnotationHeight method to ensure
279 * all annotation associated components are updated correctly.
282 public int adjustPanelHeight()
284 int height = av.calcPanelHeight();
285 this.setPreferredSize(new Dimension(1, height));
288 // revalidate only when the alignment panel is fully constructed
302 public void actionPerformed(ActionEvent evt)
304 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
309 Annotation[] anot = aa[activeRow].annotations;
311 if (anot.length < av.getColumnSelection().getMax())
313 Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
315 System.arraycopy(anot, 0, temp, 0, anot.length);
317 aa[activeRow].annotations = anot;
320 String action = evt.getActionCommand();
321 if (action.equals(REMOVE))
323 for (int index : av.getColumnSelection().getSelected())
325 if (av.getAlignment().getHiddenColumns().isVisible(index))
331 else if (action.equals(LABEL))
333 String exMesg = collectAnnotVals(anot, LABEL);
334 String label = JvOptionPane.showInputDialog(
335 MessageManager.getString("label.enter_label"), exMesg);
342 if ((label.length() > 0) && !aa[activeRow].hasText)
344 aa[activeRow].hasText = true;
347 for (int index : av.getColumnSelection().getSelected())
349 if (!av.getAlignment().getHiddenColumns().isVisible(index))
354 if (anot[index] == null)
356 anot[index] = new Annotation(label, "", ' ', 0);
360 anot[index].displayCharacter = label;
364 else if (action.equals(COLOUR))
366 final Annotation[] fAnot = anot;
367 String title = MessageManager
368 .getString("label.select_foreground_colour");
369 ColourChooserListener listener = new ColourChooserListener()
372 public void colourSelected(Color c)
374 HiddenColumns hiddenColumns = av.getAlignment()
376 for (int index : av.getColumnSelection().getSelected())
378 if (hiddenColumns.isVisible(index))
380 if (fAnot[index] == null)
382 fAnot[index] = new Annotation("", "", ' ', 0);
384 fAnot[index].colour = c;
389 JalviewColourChooser.showColourChooser(this, title, Color.black,
393 // HELIX, SHEET or STEM
396 String symbol = "\u03B1"; // alpha
398 if (action.equals(HELIX))
402 else if (action.equals(SHEET))
405 symbol = "\u03B2"; // beta
408 // Added by LML to color stems
409 else if (action.equals(STEM))
412 int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
413 symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
416 if (!aa[activeRow].hasIcons)
418 aa[activeRow].hasIcons = true;
421 String label = JvOptionPane.showInputDialog(MessageManager
422 .getString("label.enter_label_for_the_structure"), symbol);
429 if ((label.length() > 0) && !aa[activeRow].hasText)
431 aa[activeRow].hasText = true;
432 if (action.equals(STEM))
434 aa[activeRow].showAllColLabels = true;
437 for (int index : av.getColumnSelection().getSelected())
439 if (!av.getAlignment().getHiddenColumns().isVisible(index))
444 if (anot[index] == null)
446 anot[index] = new Annotation(label, "", type, 0);
449 anot[index].secondaryStructure = type != 'S' ? type
450 : label.length() == 0 ? ' ' : label.charAt(0);
451 anot[index].displayCharacter = label;
456 av.getAlignment().validateAnnotation(aa[activeRow]);
457 ap.alignmentChanged();
458 ap.alignFrame.setMenusForViewport();
466 * Returns any existing annotation concatenated as a string. For each
467 * annotation, takes the description, if any, else the secondary structure
468 * character (if type is HELIX, SHEET or STEM), else the display character (if
475 private String collectAnnotVals(Annotation[] anots, String type)
477 // TODO is this method wanted? why? 'last' is not used
479 StringBuilder collatedInput = new StringBuilder(64);
481 ColumnSelection viscols = av.getColumnSelection();
482 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
485 * the selection list (read-only view) is in selection order, not
486 * column order; make a copy so we can sort it
488 List<Integer> selected = new ArrayList<>(viscols.getSelected());
489 Collections.sort(selected);
490 for (int index : selected)
492 // always check for current display state - just in case
493 if (!hidden.isVisible(index))
497 String tlabel = null;
498 if (anots[index] != null)
499 { // LML added stem code
500 if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
501 || type.equals(LABEL))
503 tlabel = anots[index].description;
504 if (tlabel == null || tlabel.length() < 1)
506 if (type.equals(HELIX) || type.equals(SHEET)
507 || type.equals(STEM))
509 tlabel = "" + anots[index].secondaryStructure;
513 tlabel = "" + anots[index].displayCharacter;
517 if (tlabel != null && !tlabel.equals(last))
519 if (last.length() > 0)
521 collatedInput.append(" ");
523 collatedInput.append(tlabel);
527 return collatedInput.toString();
531 * Action on right mouse pressed on Mac is to show a pop-up menu for the
532 * annotation. Action on left mouse pressed is to find which annotation is
533 * pressed and mark the start of a column selection or graph resize operation.
538 public void mousePressed(MouseEvent evt)
541 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
546 mouseDragLastX = evt.getX();
547 mouseDragLastY = evt.getY();
550 * add visible annotation heights until we reach the y
551 * position, to find which annotation it is in
556 // todo could reuse getRowIndexAndOffset ?
557 final int y = evt.getY();
559 for (int i = 0; i < aa.length; i++)
563 height += aa[i].height;
572 else if (aa[i].graph != 0)
575 * we have clicked on a resizable graph annotation
578 yOffset = height - y;
585 * isPopupTrigger fires in mousePressed on Mac,
586 * not until mouseRelease on Windows
588 if (evt.isPopupTrigger() && activeRow != -1)
590 showPopupMenu(y, evt.getX());
594 if (graphStretch != -1)
597 if (aa[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP)
599 // data in row has position on y as well as x axis
600 if (evt.isAltDown() || evt.isAltGraphDown())
602 dragMode = DragMode.MatrixSelect;
603 firstDragX = mouseDragLastX;
604 firstDragY = mouseDragLastY;
610 // no row (or row that can be adjusted) was pressed. Simulate a ruler
612 ap.getScalePanel().mousePressed(evt);
617 * checks whether the annotation row under the mouse click evt's handles the
621 * @return false if evt was not handled
623 boolean matrix_clicked(MouseEvent evt)
625 int[] rowIndex = getRowIndexAndOffset(evt.getY(),
626 av.getAlignment().getAlignmentAnnotation());
627 if (rowIndex == null)
630 .error("IMPLEMENTATION ERROR: matrix click out of range.");
633 int yOffset = rowIndex[1];
635 AlignmentAnnotation clicked = av.getAlignment()
636 .getAlignmentAnnotation()[rowIndex[0]];
637 if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
642 // TODO - use existing threshold to select related sections of matrix
643 GraphLine thr = clicked.getThreshold();
645 int currentX = getColumnForXPos(evt.getX());
646 ContactListI forCurrentX = av.getContactList(clicked, currentX);
647 if (forCurrentX != null)
649 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
650 clicked.graphHeight);
651 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset);
655 * start and end range corresponding to the row range under the mouse at
659 fr = Math.min(cXci.cStart, cXci.cEnd);
660 to = Math.max(cXci.cStart, cXci.cEnd);
662 // double click selects the whole group
663 if (evt.getClickCount() == 2)
665 ContactMatrixI matrix = av.getContactMatrix(clicked);
669 // simplest approach is to select all group containing column
670 if (matrix.hasGroups())
672 SequenceI rseq = clicked.sequenceRef;
673 BitSet grp = new BitSet();
674 grp.or(matrix.getGroupsFor(currentX));
675 // TODO: cXci needs to be mapped to real groups
676 for (int c = fr; c <= to; c++)
678 BitSet additionalGrp = matrix.getGroupsFor(c);
679 grp.or(additionalGrp);
682 HiddenColumns hc = av.getAlignment().getHiddenColumns();
683 ColumnSelection cs = av.getColumnSelection();
685 for (int p = grp.nextSetBit(0); p >= 0; p = grp
688 if (matrix instanceof MappableContactMatrixI)
690 // find the end of this run of set bits
691 int nextp = grp.nextClearBit(p) - 1;
692 int[] pos = ((MappableContactMatrixI) matrix)
693 .getMappedPositionsFor(rseq, p, nextp);
698 for (int pos_p = pos[0]; pos_p <= pos[1]; pos_p++)
700 int col = rseq.findIndex(pos_p) - 1;
701 if (col >= 0 && (!av.hasHiddenColumns()
702 || hc.isVisible(col)))
711 int offp = (rseq != null)
712 ? rseq.findIndex(rseq.getStart() - 1 + p)
715 if (!av.hasHiddenColumns() || hc.isVisible(offp))
722 // possible alternative for interactive selection - threshold
723 // gives 'ceiling' for forming a cluster
724 // when a row+column is selected, farthest common ancestor less
725 // than thr is used to compute cluster
731 // select corresponding range in segment under mouse
733 int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
736 av.getColumnSelection().addRangeOfElements(rng, true);
738 av.getColumnSelection().addElement(currentX);
741 // and also select everything lower than the max range adjacent
743 if (evt.isControlDown()
744 && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
747 ContactRange cr = forCurrentX.getRangeFor(fr, to);
749 // TODO: could use GraphLine instead of arbitrary picking
750 // TODO: could report mean/median/variance for partitions
751 // (contiguous selected vs unselected regions and inter-contig
753 // controls feathering - what other elements in row/column
755 double thresh = cr.getMean()
756 + (cr.getMax() - cr.getMean()) * .15;
759 cval = forCurrentX.getContactAt(c);
760 if (// cr.getMin() <= cval &&
763 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
766 av.getColumnSelection().addRangeOfElements(cols, true);
776 while (c < forCurrentX.getContactHeight())
778 cval = forCurrentX.getContactAt(c);
779 if (// cr.getMin() <= cval &&
782 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
785 av.getColumnSelection().addRangeOfElements(cols, true);
800 ap.paintAlignment(false, false);
801 PaintRefresher.Refresh(ap, av.getSequenceSetId());
807 * Construct and display a context menu at the right-click position
812 void showPopupMenu(final int y, int x)
814 if (av.getColumnSelection() == null
815 || av.getColumnSelection().isEmpty())
820 JPopupMenu pop = new JPopupMenu(
821 MessageManager.getString("label.structure_type"));
824 * Just display the needed structure options
826 if (av.getAlignment().isNucleotide())
828 item = new JMenuItem(STEM);
829 item.addActionListener(this);
834 item = new JMenuItem(HELIX);
835 item.addActionListener(this);
837 item = new JMenuItem(SHEET);
838 item.addActionListener(this);
841 item = new JMenuItem(LABEL);
842 item.addActionListener(this);
844 item = new JMenuItem(COLOUR);
845 item.addActionListener(this);
847 item = new JMenuItem(REMOVE);
848 item.addActionListener(this);
850 pop.show(this, x, y);
854 * Action on mouse up is to clear mouse drag data and call mouseReleased on
855 * ScalePanel, to deal with defining the selection group (if any) defined by
861 public void mouseReleased(MouseEvent evt)
863 if (dragMode == DragMode.MatrixSelect)
865 matrixSelectRange(evt);
872 mouseDragging = false;
873 if (dragMode == DragMode.Resize)
875 ap.adjustAnnotationHeight();
877 dragMode = DragMode.Undefined;
878 if (!matrix_clicked(evt))
880 ap.getScalePanel().mouseReleased(evt);
884 * isPopupTrigger is set in mouseReleased on Windows
885 * (in mousePressed on Mac)
887 if (evt.isPopupTrigger() && activeRow != -1)
889 showPopupMenu(evt.getY(), evt.getX());
901 public void mouseEntered(MouseEvent evt)
903 this.mouseDragging = false;
904 ap.getScalePanel().mouseEntered(evt);
908 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
909 * with column selection on a mouse drag
914 public void mouseExited(MouseEvent evt)
916 ap.getScalePanel().mouseExited(evt);
920 * Action on starting or continuing a mouse drag. There are two possible
923 * <li>drag up or down on a graphed annotation increases or decreases the
924 * height of the graph</li>
925 * <li>dragging left or right selects the columns dragged across</li>
927 * A drag on a graph annotation is treated as column selection if it starts
928 * with more horizontal than vertical movement, and as resize if it starts
929 * with more vertical than horizontal movement. Once started, the drag does
935 public void mouseDragged(MouseEvent evt)
938 * if dragMode is Undefined:
939 * - set to Select if dx > dy
940 * - set to Resize if dy > dx
941 * - do nothing if dx == dy
943 final int x = evt.getX();
944 final int y = evt.getY();
945 if (dragMode == DragMode.Undefined)
947 int dx = Math.abs(x - mouseDragLastX);
948 int dy = Math.abs(y - mouseDragLastY);
949 if (graphStretch == -1 || dx > dy)
952 * mostly horizontal drag, or not a graph annotation
954 dragMode = DragMode.Select;
959 * mostly vertical drag
961 dragMode = DragMode.Resize;
962 notJustOne = evt.isShiftDown();
965 * but could also be a matrix drag
967 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
968 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
971 * dragging in a matrix
973 dragMode = DragMode.MatrixSelect;
974 firstDragX = mouseDragLastX;
975 firstDragY = mouseDragLastY;
980 if (dragMode == DragMode.Undefined)
984 * drag is diagonal - defer deciding whether to
985 * treat as up/down or left/right
992 if (dragMode == DragMode.Resize)
995 * resize graph annotation if mouse was dragged up or down
997 int deltaY = mouseDragLastY - evt.getY();
1000 AlignmentAnnotation graphAnnotation = av.getAlignment()
1001 .getAlignmentAnnotation()[graphStretch];
1002 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
1005 for (AlignmentAnnotation similar : av.getAlignment()
1006 .findAnnotations(null, graphAnnotation.getCalcId(),
1007 graphAnnotation.label))
1009 similar.graphHeight = newHeight;
1015 graphAnnotation.graphHeight = newHeight;
1017 adjustPanelHeight();
1018 ap.paintAlignment(false, false);
1021 else if (dragMode == DragMode.MatrixSelect)
1024 * TODO draw a rubber band for range
1028 ap.paintAlignment(false, false);
1033 * for mouse drag left or right, delegate to
1034 * ScalePanel to adjust the column selection
1036 ap.getScalePanel().mouseDragged(evt);
1045 public void matrixSelectRange(MouseEvent evt)
1048 * get geometry of drag
1050 int fromY = Math.min(firstDragY, evt.getY());
1051 int toY = Math.max(firstDragY, evt.getY());
1052 int fromX = Math.min(firstDragX, evt.getX());
1053 int toX = Math.max(firstDragX, evt.getX());
1055 int deltaY = toY - fromY;
1056 int deltaX = toX - fromX;
1058 int[] rowIndex = getRowIndexAndOffset(fromY,
1059 av.getAlignment().getAlignmentAnnotation());
1060 int[] toRowIndex = getRowIndexAndOffset(toY,
1061 av.getAlignment().getAlignmentAnnotation());
1063 if (rowIndex == null || toRowIndex == null)
1065 jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1068 if (rowIndex[0] != toRowIndex[0])
1071 .trace("Drag went to another row. needs to be clipped");
1074 // rectangular selection on matrix style annotation
1075 AlignmentAnnotation cma = av.getAlignment()
1076 .getAlignmentAnnotation()[rowIndex[0]];
1078 int lastX = getColumnForXPos(fromX);
1079 int currentX = getColumnForXPos(toX);
1080 int fromXc = Math.min(lastX, currentX);
1081 int toXc = Math.max(lastX, currentX);
1082 ContactListI forFromX = av.getContactList(cma, fromXc);
1083 ContactListI forToX = av.getContactList(cma, toXc);
1085 if (forFromX != null && forToX != null)
1087 // FIXME will need two ContactGeometry objects when handling contact matrices with differing numbers of rows at each
1089 ContactGeometry xcgeom = new ContactGeometry(forFromX,
1091 ContactGeometry.contactInterval lastXci = xcgeom
1092 .mapFor(rowIndex[1]);
1093 ContactGeometry.contactInterval cXci = xcgeom.mapFor(rowIndex[1] + deltaY);
1095 // mark rectangular region formed by drag
1096 jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
1097 + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
1098 + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1100 fr = Math.min(lastXci.cStart, cXci.cStart);
1101 to = Math.max(lastXci.cEnd, cXci.cEnd);
1102 int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
1103 if (mappedPos != null)
1105 jalview.bin.Console.trace("Marking " + fr + " to " + to
1106 + " mapping to sequence positions " + mappedPos[0] + " to "
1108 for (int pair = 0; pair < mappedPos.length; pair += 2)
1110 for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1112 // if (cma.sequenceRef != null)
1114 // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1115 // av.getColumnSelection().addElement(col);
1119 av.getColumnSelection().addElement(c-1);
1123 fr = Math.min(lastX, currentX);
1124 to = Math.max(lastX, currentX);
1126 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1127 for (int c = fr; c <= to; c++)
1129 av.getColumnSelection().addElement(c);
1136 * Constructs the tooltip, and constructs and displays a status message, for
1137 * the current mouse position
1142 public void mouseMoved(MouseEvent evt)
1144 int yPos = evt.getY();
1145 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1146 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1147 int row = rowAndOffset[0];
1151 this.setToolTipText(null);
1155 int column = getColumnForXPos(evt.getX());
1157 AlignmentAnnotation ann = aa[row];
1158 if (row > -1 && ann.annotations != null
1159 && column < ann.annotations.length)
1161 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1163 setToolTipText(toolTip == null ? null
1164 : JvSwingUtils.wrapTooltip(true, toolTip));
1165 String msg = getStatusMessage(av.getAlignment(), column, ann,
1166 rowAndOffset[1], av);
1167 ap.alignFrame.setStatus(msg);
1171 this.setToolTipText(null);
1172 ap.alignFrame.setStatus(" ");
1176 private int getColumnForXPos(int x)
1178 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1179 column = Math.min(column, av.getRanges().getEndRes());
1181 if (av.hasHiddenColumns())
1183 column = av.getAlignment().getHiddenColumns()
1184 .visibleToAbsoluteColumn(column);
1190 * Answers the index in the annotations array of the visible annotation at the
1191 * given y position. This is done by adding the heights of visible annotations
1192 * until the y position has been exceeded. Answers -1 if no annotations are
1193 * visible, or the y position is below all annotations.
1199 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1205 return getRowIndexAndOffset(yPos, aa)[0];
1208 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1210 int[] res = new int[2];
1218 int height = 0, lheight = 0;
1219 for (int i = 0; i < aa.length; i++)
1224 height += aa[i].height;
1231 res[1] = yPos-lheight;
1239 * Answers a tooltip for the annotation at the current mouse position, not
1240 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1246 * @param rowAndOffset
1248 static String buildToolTip(AlignmentAnnotation ann, int column,
1249 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1252 String tooltip = null;
1253 if (ann.graphGroup > -1)
1255 StringBuilder tip = new StringBuilder(32);
1256 boolean first = true;
1257 for (int i = 0; i < anns.length; i++)
1259 if (anns[i].graphGroup == ann.graphGroup
1260 && anns[i].annotations[column] != null)
1267 tip.append(anns[i].label);
1268 String description = anns[i].annotations[column].description;
1269 if (description != null && description.length() > 0)
1271 tip.append(" ").append(description);
1275 tooltip = first ? null : tip.toString();
1277 else if (column < ann.annotations.length
1278 && ann.annotations[column] != null)
1280 tooltip = ann.annotations[column].description;
1282 // TODO abstract tooltip generator so different implementations can be built
1283 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1285 if (rowAndOffset>=ann.graphHeight)
1289 ContactListI clist = av.getContactList(ann, column);
1292 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1293 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1294 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1295 StringBuilder tooltipb = new StringBuilder();
1296 tooltipb.append("Contact from ")
1297 .append(clist.getPosition()).append(", [").append(ci.cStart).append(" - ").append(ci.cEnd).append("]").append("<br/>Mean:");
1298 Format.appendPercentage(tooltipb, (float)cr.getMean(),2);
1299 tooltip = tooltipb.toString();
1300 int col = ann.sequenceRef.findPosition(column);
1301 int[][] highlightPos;
1302 int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1303 if (mappedPos != null)
1305 highlightPos = new int[1 + mappedPos.length][2];
1306 highlightPos[0] = new int[] { col, col };
1307 for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1309 highlightPos[h][0] = ann.sequenceRef
1310 .findPosition(mappedPos[p] - 1);
1311 highlightPos[h][1] = ann.sequenceRef
1312 .findPosition(mappedPos[p + 1] - 1);
1317 highlightPos = new int[][] { new int[] { col, col } };
1319 ap.getStructureSelectionManager()
1320 .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1327 * Constructs and returns the status bar message
1332 * @param rowAndOffset
1334 static String getStatusMessage(AlignmentI al, int column,
1335 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1338 * show alignment column and annotation description if any
1340 StringBuilder text = new StringBuilder(32);
1341 text.append(MessageManager.getString("label.column")).append(" ")
1342 .append(column + 1);
1344 if (column < ann.annotations.length && ann.annotations[column] != null)
1346 String description = ann.annotations[column].description;
1347 if (description != null && description.trim().length() > 0)
1349 text.append(" ").append(description);
1354 * if the annotation is sequence-specific, show the sequence number
1355 * in the alignment, and (if not a gap) the residue and position
1357 SequenceI seqref = ann.sequenceRef;
1360 int seqIndex = al.findIndex(seqref);
1363 text.append(", ").append(MessageManager.getString("label.sequence"))
1364 .append(" ").append(seqIndex + 1);
1365 char residue = seqref.getCharAt(column);
1366 if (!Comparison.isGap(residue))
1370 if (al.isNucleotide())
1372 name = ResidueProperties.nucleotideName
1373 .get(String.valueOf(residue));
1374 text.append(" Nucleotide: ")
1375 .append(name != null ? name : residue);
1379 name = 'X' == residue ? "X"
1380 : ('*' == residue ? "STOP"
1381 : ResidueProperties.aa2Triplet
1382 .get(String.valueOf(residue)));
1383 text.append(" Residue: ").append(name != null ? name : residue);
1385 int residuePos = seqref.findPosition(column);
1386 text.append(" (").append(residuePos).append(")");
1391 return text.toString();
1401 public void mouseClicked(MouseEvent evt)
1403 // if (activeRow != -1)
1405 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1406 // AlignmentAnnotation anot = aa[activeRow];
1410 // TODO mouseClicked-content and drawCursor are quite experimental!
1411 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1414 int pady = av.getCharHeight() / 5;
1416 graphics.setColor(Color.black);
1417 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1419 if (av.validCharWidth)
1421 graphics.setColor(Color.white);
1423 char s = seq.getCharAt(res);
1425 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1426 graphics.drawString(String.valueOf(s), charOffset + x1,
1427 (y1 + av.getCharHeight()) - pady);
1432 private volatile boolean imageFresh = false;
1434 private Rectangle visibleRect = new Rectangle(),
1435 clipBounds = new Rectangle();
1444 public void paintComponent(Graphics g)
1447 // BH: note that this method is generally recommended to
1448 // call super.paintComponent(g). Otherwise, the children of this
1449 // component will not be rendered. That is not needed here
1450 // because AnnotationPanel does not have any children. It is
1451 // just a JPanel contained in a JViewPort.
1453 computeVisibleRect(visibleRect);
1455 g.setColor(Color.white);
1456 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1460 // BH 2018 optimizing generation of new Rectangle().
1462 || (visibleRect.width != (clipBounds = g
1463 .getClipBounds(clipBounds)).width)
1464 || (visibleRect.height != clipBounds.height))
1467 g.drawImage(image, 0, 0, this);
1472 updateFadedImageWidth();
1478 if (image == null || imgWidth != image.getWidth(this)
1479 || image.getHeight(this) != getHeight())
1481 boolean tried = false;
1483 while (image == null && !tried)
1487 image = new BufferedImage(imgWidth,
1488 ap.getAnnotationPanel().getHeight(),
1489 BufferedImage.TYPE_INT_RGB);
1491 } catch (IllegalArgumentException exc)
1493 jalview.bin.Console.errPrintln(
1494 "Serious issue with viewport geometry imgWidth requested was "
1497 } catch (OutOfMemoryError oom)
1502 } catch (Exception x)
1507 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1513 gg = (Graphics2D) image.getGraphics();
1517 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1518 RenderingHints.VALUE_ANTIALIAS_ON);
1521 gg.setFont(av.getFont());
1522 fm = gg.getFontMetrics();
1523 gg.setColor(Color.white);
1524 gg.fillRect(0, 0, imgWidth, image.getHeight());
1529 gg = (Graphics2D) image.getGraphics();
1533 drawComponent(gg, av.getRanges().getStartRes(),
1534 av.getRanges().getEndRes() + 1);
1537 g.drawImage(image, 0, 0, this);
1540 public void updateFadedImageWidth()
1542 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1543 + 1) * av.getCharWidth();
1548 * set true to enable redraw timing debug output on stderr
1550 private final boolean debugRedraw = false;
1553 * non-Thread safe repaint
1556 * repaint with horizontal shift in alignment
1558 public void fastPaint(int horizontal)
1560 if ((horizontal == 0) || image == null
1561 || av.getAlignment().getAlignmentAnnotation() == null
1562 || av.getAlignment().getAlignmentAnnotation().length < 1
1563 || av.isCalcInProgress())
1569 int sr = av.getRanges().getStartRes();
1570 int er = av.getRanges().getEndRes() + 1;
1573 Graphics2D gg = (Graphics2D) image.getGraphics();
1575 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1577 // scroll is less than imgWidth away so can re-use buffered graphics
1578 gg.copyArea(0, 0, imgWidth, getHeight(),
1579 -horizontal * av.getCharWidth(), 0);
1581 if (horizontal > 0) // scrollbar pulled right, image to the left
1583 transX = (er - sr - horizontal) * av.getCharWidth();
1584 sr = er - horizontal;
1586 else if (horizontal < 0)
1588 er = sr - horizontal;
1591 gg.translate(transX, 0);
1593 drawComponent(gg, sr, er);
1595 gg.translate(-transX, 0);
1601 // Call repaint on alignment panel so that repaints from other alignment
1602 // panel components can be aggregated. Otherwise performance of the overview
1603 // window and others may be adversely affected.
1604 av.getAlignPanel().repaint();
1607 private volatile boolean lastImageGood = false;
1619 public void drawComponent(Graphics g, int startRes, int endRes)
1621 BufferedImage oldFaded = fadedImage;
1622 if (av.isCalcInProgress())
1626 lastImageGood = false;
1629 // We'll keep a record of the old image,
1630 // and draw a faded image until the calculation
1633 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1634 || fadedImage.getHeight() != image.getHeight()))
1636 // jalview.bin.Console.errPrintln("redraw faded image ("+(fadedImage==null ?
1637 // "null image" : "") + " lastGood="+lastImageGood+")");
1638 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1639 BufferedImage.TYPE_INT_RGB);
1641 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1643 fadedG.setColor(Color.white);
1644 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1646 fadedG.setComposite(
1647 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1648 fadedG.drawImage(image, 0, 0, this);
1651 // make sure we don't overwrite the last good faded image until all
1652 // calculations have finished
1653 lastImageGood = false;
1658 if (fadedImage != null)
1660 oldFaded = fadedImage;
1665 g.setColor(Color.white);
1666 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1668 g.setFont(av.getFont());
1671 fm = g.getFontMetrics();
1674 if ((av.getAlignment().getAlignmentAnnotation() == null)
1675 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1677 g.setColor(Color.white);
1678 g.fillRect(0, 0, getWidth(), getHeight());
1679 g.setColor(Color.black);
1680 if (av.validCharWidth)
1682 g.drawString(MessageManager
1683 .getString("label.alignment_has_no_annotations"), 20, 15);
1688 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1690 if (!lastImageGood && fadedImage == null)
1692 fadedImage = oldFaded;
1694 if (dragMode == DragMode.MatrixSelect)
1696 g.setColor(Color.yellow);
1697 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1698 Math.min(firstDragY, mouseDragLastY),
1699 Math.max(firstDragX, mouseDragLastX)
1700 - Math.min(firstDragX, mouseDragLastX),
1701 Math.max(firstDragY, mouseDragLastY)
1702 - Math.min(firstDragY, mouseDragLastY));
1708 public FontMetrics getFontMetrics()
1714 public Image getFadedImage()
1720 public int getFadedImageWidth()
1722 updateFadedImageWidth();
1726 private int[] bounds = new int[2];
1729 public int[] getVisibleVRange()
1731 if (ap != null && ap.getAlabels() != null)
1733 int sOffset = -ap.getAlabels().getScrollOffset();
1734 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1735 bounds[0] = sOffset;
1736 bounds[1] = visHeight;
1746 * Try to ensure any references held are nulled
1748 public void dispose()
1758 * I created the renderer so I will dispose of it
1760 if (renderer != null)
1767 public void propertyChange(PropertyChangeEvent evt)
1769 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1770 // Both scrolling and resizing change viewport ranges: scrolling changes
1771 // both start and end points, but resize only changes end values.
1772 // Here we only want to fastpaint on a scroll, with resize using a normal
1773 // paint, so scroll events are identified as changes to the horizontal or
1774 // vertical start value.
1775 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1777 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1779 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1781 fastPaint(((int[]) evt.getNewValue())[0]
1782 - ((int[]) evt.getOldValue())[0]);
1784 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1791 * computes the visible height of the annotation panel
1793 * @param adjustPanelHeight
1794 * - when false, just adjust existing height according to other
1796 * @param annotationHeight
1797 * @return height to use for the ScrollerPreferredVisibleSize
1799 public int adjustForAlignFrame(boolean adjustPanelHeight,
1800 int annotationHeight)
1803 * Estimate available height in the AlignFrame for alignment +
1804 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1805 * hscroll, status bar, insets.
1807 int stuff = (ap.getViewName() != null ? 30 : 0)
1808 + (Platform.isAMacAndNotJS() ? 120 : 140);
1809 int availableHeight = ap.alignFrame.getHeight() - stuff;
1810 int rowHeight = av.getCharHeight();
1812 if (adjustPanelHeight)
1814 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1817 * If not enough vertical space, maximize annotation height while keeping
1818 * at least two rows of alignment visible
1820 if (annotationHeight + alignmentHeight > availableHeight)
1822 annotationHeight = Math.min(annotationHeight,
1823 availableHeight - 2 * rowHeight);
1828 // maintain same window layout whilst updating sliders
1829 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1830 availableHeight - 2 * rowHeight);
1832 return annotationHeight;