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 * 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.
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
204 * This object is a MouseWheelListener to AnnotationLabels, so mouse wheel
205 * events over the labels are delegated to this method.
207 * Note that this method may also be fired by scrolling with a gesture on a
211 public void mouseWheelMoved(MouseWheelEvent e)
215 ap.getSeqPanel().mouseWheelMoved(e);
219 // TODO: find the correct way to let the event bubble up to
220 // ap.annotationScroller
221 for (MouseWheelListener mwl : _mwl)
225 mwl.mouseWheelMoved(e);
236 public Dimension getPreferredScrollableViewportSize()
238 Dimension ps = getPreferredSize();
239 return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
243 public int getScrollableBlockIncrement(Rectangle visibleRect,
244 int orientation, int direction)
250 public boolean getScrollableTracksViewportHeight()
256 public boolean getScrollableTracksViewportWidth()
262 public int getScrollableUnitIncrement(Rectangle visibleRect,
263 int orientation, int direction)
272 * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
276 public void adjustmentValueChanged(AdjustmentEvent evt)
278 // update annotation label display
279 ap.getAlabels().setScrollOffset(-evt.getValue());
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.
288 public int adjustPanelHeight()
290 int height = av.calcPanelHeight();
291 this.setPreferredSize(new Dimension(1, height));
294 // revalidate only when the alignment panel is fully constructed
308 public void actionPerformed(ActionEvent evt)
310 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
315 Annotation[] anot = aa[activeRow].annotations;
317 if (anot.length < av.getColumnSelection().getMax())
319 Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
321 System.arraycopy(anot, 0, temp, 0, anot.length);
323 aa[activeRow].annotations = anot;
326 String action = evt.getActionCommand();
327 if (action.equals(REMOVE))
329 for (int index : av.getColumnSelection().getSelected())
331 if (av.getAlignment().getHiddenColumns().isVisible(index))
337 else if (action.equals(LABEL))
339 String exMesg = collectAnnotVals(anot, LABEL);
340 String label = JvOptionPane.showInputDialog(
341 MessageManager.getString("label.enter_label"), exMesg);
348 if ((label.length() > 0) && !aa[activeRow].hasText)
350 aa[activeRow].hasText = true;
353 for (int index : av.getColumnSelection().getSelected())
355 if (!av.getAlignment().getHiddenColumns().isVisible(index))
360 if (anot[index] == null)
362 anot[index] = new Annotation(label, "", ' ', 0);
366 anot[index].displayCharacter = label;
370 else if (action.equals(COLOUR))
372 final Annotation[] fAnot = anot;
373 String title = MessageManager
374 .getString("label.select_foreground_colour");
375 ColourChooserListener listener = new ColourChooserListener()
378 public void colourSelected(Color c)
380 HiddenColumns hiddenColumns = av.getAlignment()
382 for (int index : av.getColumnSelection().getSelected())
384 if (hiddenColumns.isVisible(index))
386 if (fAnot[index] == null)
388 fAnot[index] = new Annotation("", "", ' ', 0);
390 fAnot[index].colour = c;
395 JalviewColourChooser.showColourChooser(this, title, Color.black,
399 // HELIX, SHEET or STEM
402 String symbol = "\u03B1"; // alpha
404 if (action.equals(HELIX))
408 else if (action.equals(SHEET))
411 symbol = "\u03B2"; // beta
414 // Added by LML to color stems
415 else if (action.equals(STEM))
418 int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
419 symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
422 if (!aa[activeRow].hasIcons)
424 aa[activeRow].hasIcons = true;
427 String label = JvOptionPane.showInputDialog(MessageManager
428 .getString("label.enter_label_for_the_structure"), symbol);
435 if ((label.length() > 0) && !aa[activeRow].hasText)
437 aa[activeRow].hasText = true;
438 if (action.equals(STEM))
440 aa[activeRow].showAllColLabels = true;
443 for (int index : av.getColumnSelection().getSelected())
445 if (!av.getAlignment().getHiddenColumns().isVisible(index))
450 if (anot[index] == null)
452 anot[index] = new Annotation(label, "", type, 0);
455 anot[index].secondaryStructure = type != 'S' ? type
456 : label.length() == 0 ? ' ' : label.charAt(0);
457 anot[index].displayCharacter = label;
462 av.getAlignment().validateAnnotation(aa[activeRow]);
463 ap.alignmentChanged();
464 ap.alignFrame.setMenusForViewport();
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
481 private String collectAnnotVals(Annotation[] anots, String type)
483 // TODO is this method wanted? why? 'last' is not used
485 StringBuilder collatedInput = new StringBuilder(64);
487 ColumnSelection viscols = av.getColumnSelection();
488 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
491 * the selection list (read-only view) is in selection order, not
492 * column order; make a copy so we can sort it
494 List<Integer> selected = new ArrayList<>(viscols.getSelected());
495 Collections.sort(selected);
496 for (int index : selected)
498 // always check for current display state - just in case
499 if (!hidden.isVisible(index))
503 String tlabel = null;
504 if (anots[index] != null)
505 { // LML added stem code
506 if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
507 || type.equals(LABEL))
509 tlabel = anots[index].description;
510 if (tlabel == null || tlabel.length() < 1)
512 if (type.equals(HELIX) || type.equals(SHEET)
513 || type.equals(STEM))
515 tlabel = "" + anots[index].secondaryStructure;
519 tlabel = "" + anots[index].displayCharacter;
523 if (tlabel != null && !tlabel.equals(last))
525 if (last.length() > 0)
527 collatedInput.append(" ");
529 collatedInput.append(tlabel);
533 return collatedInput.toString();
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.
544 public void mousePressed(MouseEvent evt)
547 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
552 mouseDragLastX = evt.getX();
553 mouseDragLastY = evt.getY();
556 * add visible annotation heights until we reach the y
557 * position, to find which annotation it is in
562 // todo could reuse getRowIndexAndOffset ?
563 final int y = evt.getY();
565 for (int i = 0; i < aa.length; i++)
569 height += aa[i].height;
578 else if (aa[i].graph != 0)
581 * we have clicked on a resizable graph annotation
584 yOffset = height - y;
591 * isPopupTrigger fires in mousePressed on Mac,
592 * not until mouseRelease on Windows
594 if (evt.isPopupTrigger() && activeRow != -1)
596 showPopupMenu(y, evt.getX());
600 if (graphStretch != -1)
603 if (aa[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP)
605 // data in row has position on y as well as x axis
606 if (evt.isAltDown() || evt.isAltGraphDown())
608 dragMode = DragMode.MatrixSelect;
609 firstDragX = mouseDragLastX;
610 firstDragY = mouseDragLastY;
616 // no row (or row that can be adjusted) was pressed. Simulate a ruler
618 ap.getScalePanel().mousePressed(evt);
623 * checks whether the annotation row under the mouse click evt's handles the
627 * @return false if evt was not handled
629 boolean matrix_clicked(MouseEvent evt)
631 int[] rowIndex = getRowIndexAndOffset(evt.getY(),
632 av.getAlignment().getAlignmentAnnotation());
633 if (rowIndex == null)
636 .error("IMPLEMENTATION ERROR: matrix click out of range.");
639 int yOffset = rowIndex[1];
640 AlignmentAnnotation[] allAnnotation = av.getAlignment()
641 .getAlignmentAnnotation();
642 if (allAnnotation == null || rowIndex[0] < 0
643 || rowIndex[0] >= allAnnotation.length)
647 AlignmentAnnotation clicked = av.getAlignment()
648 .getAlignmentAnnotation()[rowIndex[0]];
649 if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
654 // TODO - use existing threshold to select related sections of matrix
655 GraphLine thr = clicked.getThreshold();
657 int currentX = getColumnForXPos(evt.getX());
658 ContactListI forCurrentX = av.getContactList(clicked, currentX);
659 if (forCurrentX != null)
661 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
662 clicked.graphHeight);
663 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset);
667 * start and end range corresponding to the row range under the mouse at
671 fr = Math.min(cXci.cStart, cXci.cEnd);
672 to = Math.max(cXci.cStart, cXci.cEnd);
674 // double click selects the whole group
675 if (evt.getClickCount() == 2)
677 ContactMatrixI matrix = av.getContactMatrix(clicked);
681 // simplest approach is to select all group containing column
682 if (matrix.hasGroups())
684 SequenceI rseq = clicked.sequenceRef;
685 BitSet grp = new BitSet();
686 grp.or(matrix.getGroupsFor(forCurrentX.getPosition()));
687 // TODO: cXci needs to be mapped to real groups
688 for (int c = fr; c <= to; c++)
690 BitSet additionalGrp = matrix.getGroupsFor(c);
691 grp.or(additionalGrp);
694 HiddenColumns hc = av.getAlignment().getHiddenColumns();
695 ColumnSelection cs = av.getColumnSelection();
697 for (int p = grp.nextSetBit(0); p >= 0; p = grp
700 if (matrix instanceof MappableContactMatrixI)
702 // find the end of this run of set bits
703 int nextp = grp.nextClearBit(p) - 1;
704 int[] pos = ((MappableContactMatrixI) matrix)
705 .getMappedPositionsFor(rseq, p, nextp);
710 for (int pos_p = pos[0]; pos_p <= pos[1]; pos_p++)
712 int col = rseq.findIndex(pos_p) - 1;
713 if (col >= 0 && (!av.hasHiddenColumns()
714 || hc.isVisible(col)))
723 int offp = (rseq != null)
724 ? rseq.findIndex(rseq.getStart() - 1 + p)
727 if (!av.hasHiddenColumns() || hc.isVisible(offp))
734 // possible alternative for interactive selection - threshold
735 // gives 'ceiling' for forming a cluster
736 // when a row+column is selected, farthest common ancestor less
737 // than thr is used to compute cluster
743 // select corresponding range in segment under mouse
745 int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
748 av.getColumnSelection().addRangeOfElements(rng, true);
750 av.getColumnSelection().addElement(currentX);
753 // and also select everything lower than the max range adjacent
755 if (evt.isControlDown()
756 && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
759 ContactRange cr = forCurrentX.getRangeFor(fr, to);
761 // TODO: could use GraphLine instead of arbitrary picking
762 // TODO: could report mean/median/variance for partitions
763 // (contiguous selected vs unselected regions and inter-contig
765 // controls feathering - what other elements in row/column
767 double thresh = cr.getMean()
768 + (cr.getMax() - cr.getMean()) * .15;
771 cval = forCurrentX.getContactAt(c);
772 if (// cr.getMin() <= cval &&
775 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
778 av.getColumnSelection().addRangeOfElements(cols, true);
788 while (c < forCurrentX.getContactHeight())
790 cval = forCurrentX.getContactAt(c);
791 if (// cr.getMin() <= cval &&
794 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
797 av.getColumnSelection().addRangeOfElements(cols, true);
812 ap.paintAlignment(false, false);
813 PaintRefresher.Refresh(ap, av.getSequenceSetId());
819 * Construct and display a context menu at the right-click position
824 void showPopupMenu(final int y, int x)
826 if (av.getColumnSelection() == null
827 || av.getColumnSelection().isEmpty())
832 JPopupMenu pop = new JPopupMenu(
833 MessageManager.getString("label.structure_type"));
836 * Just display the needed structure options
838 if (av.getAlignment().isNucleotide())
840 item = new JMenuItem(STEM);
841 item.addActionListener(this);
846 item = new JMenuItem(HELIX);
847 item.addActionListener(this);
849 item = new JMenuItem(SHEET);
850 item.addActionListener(this);
853 item = new JMenuItem(LABEL);
854 item.addActionListener(this);
856 item = new JMenuItem(COLOUR);
857 item.addActionListener(this);
859 item = new JMenuItem(REMOVE);
860 item.addActionListener(this);
862 pop.show(this, x, y);
866 * Action on mouse up is to clear mouse drag data and call mouseReleased on
867 * ScalePanel, to deal with defining the selection group (if any) defined by
873 public void mouseReleased(MouseEvent evt)
875 if (dragMode == DragMode.MatrixSelect)
877 matrixSelectRange(evt);
884 mouseDragging = false;
885 if (dragMode == DragMode.Resize)
887 ap.adjustAnnotationHeight();
889 dragMode = DragMode.Undefined;
890 if (!matrix_clicked(evt))
892 ap.getScalePanel().mouseReleased(evt);
896 * isPopupTrigger is set in mouseReleased on Windows
897 * (in mousePressed on Mac)
899 if (evt.isPopupTrigger() && activeRow != -1)
901 showPopupMenu(evt.getY(), evt.getX());
913 public void mouseEntered(MouseEvent evt)
915 this.mouseDragging = false;
916 ap.getScalePanel().mouseEntered(evt);
920 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
921 * with column selection on a mouse drag
926 public void mouseExited(MouseEvent evt)
928 ap.getScalePanel().mouseExited(evt);
932 * Action on starting or continuing a mouse drag. There are two possible
935 * <li>drag up or down on a graphed annotation increases or decreases the
936 * height of the graph</li>
937 * <li>dragging left or right selects the columns dragged across</li>
939 * A drag on a graph annotation is treated as column selection if it starts
940 * with more horizontal than vertical movement, and as resize if it starts
941 * with more vertical than horizontal movement. Once started, the drag does
947 public void mouseDragged(MouseEvent evt)
950 * if dragMode is Undefined:
951 * - set to Select if dx > dy
952 * - set to Resize if dy > dx
953 * - do nothing if dx == dy
955 final int x = evt.getX();
956 final int y = evt.getY();
957 if (dragMode == DragMode.Undefined)
959 int dx = Math.abs(x - mouseDragLastX);
960 int dy = Math.abs(y - mouseDragLastY);
961 if (graphStretch == -1 || dx > dy)
964 * mostly horizontal drag, or not a graph annotation
966 dragMode = DragMode.Select;
971 * mostly vertical drag
973 dragMode = DragMode.Resize;
974 notJustOne = evt.isShiftDown();
977 * but could also be a matrix drag
979 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
980 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
983 * dragging in a matrix
985 dragMode = DragMode.MatrixSelect;
986 firstDragX = mouseDragLastX;
987 firstDragY = mouseDragLastY;
992 if (dragMode == DragMode.Undefined)
996 * drag is diagonal - defer deciding whether to
997 * treat as up/down or left/right
1004 if (dragMode == DragMode.Resize)
1007 * resize graph annotation if mouse was dragged up or down
1009 int deltaY = mouseDragLastY - evt.getY();
1012 AlignmentAnnotation graphAnnotation = av.getAlignment()
1013 .getAlignmentAnnotation()[graphStretch];
1014 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
1017 for (AlignmentAnnotation similar : av.getAlignment()
1018 .findAnnotations(null, graphAnnotation.getCalcId(),
1019 graphAnnotation.label))
1021 similar.graphHeight = newHeight;
1027 graphAnnotation.graphHeight = newHeight;
1029 adjustPanelHeight();
1030 ap.paintAlignment(false, false);
1033 else if (dragMode == DragMode.MatrixSelect)
1036 * TODO draw a rubber band for range
1040 ap.paintAlignment(false, false);
1045 * for mouse drag left or right, delegate to
1046 * ScalePanel to adjust the column selection
1048 ap.getScalePanel().mouseDragged(evt);
1057 public void matrixSelectRange(MouseEvent evt)
1060 * get geometry of drag
1062 int fromY = Math.min(firstDragY, evt.getY());
1063 int toY = Math.max(firstDragY, evt.getY());
1064 int fromX = Math.min(firstDragX, evt.getX());
1065 int toX = Math.max(firstDragX, evt.getX());
1067 int deltaY = toY - fromY;
1068 int deltaX = toX - fromX;
1070 int[] rowIndex = getRowIndexAndOffset(fromY,
1071 av.getAlignment().getAlignmentAnnotation());
1072 int[] toRowIndex = getRowIndexAndOffset(toY,
1073 av.getAlignment().getAlignmentAnnotation());
1075 if (rowIndex == null || toRowIndex == null)
1077 jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1080 if (rowIndex[0] != toRowIndex[0])
1083 .trace("Drag went to another row. needs to be clipped");
1086 // rectangular selection on matrix style annotation
1087 AlignmentAnnotation cma = av.getAlignment()
1088 .getAlignmentAnnotation()[rowIndex[0]];
1090 int lastX = getColumnForXPos(fromX);
1091 int currentX = getColumnForXPos(toX);
1092 int fromXc = Math.min(lastX, currentX);
1093 int toXc = Math.max(lastX, currentX);
1094 ContactListI forFromX = av.getContactList(cma, fromXc);
1095 ContactListI forToX = av.getContactList(cma, toXc);
1097 if (forFromX != null && forToX != null)
1099 // FIXME will need two ContactGeometry objects when handling contact
1100 // matrices with differing numbers of rows at each
1102 ContactGeometry xcgeom = new ContactGeometry(forFromX,
1104 ContactGeometry.contactInterval lastXci = xcgeom.mapFor(rowIndex[1]);
1105 ContactGeometry.contactInterval cXci = xcgeom
1106 .mapFor(rowIndex[1] + deltaY);
1108 // mark rectangular region formed by drag
1109 jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
1110 + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
1111 + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1113 fr = Math.min(lastXci.cStart, cXci.cStart);
1114 to = Math.max(lastXci.cEnd, cXci.cEnd);
1115 int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
1116 if (mappedPos != null)
1118 jalview.bin.Console.trace("Marking " + fr + " to " + to
1119 + " mapping to sequence positions " + mappedPos[0] + " to "
1121 for (int pair = 0; pair < mappedPos.length; pair += 2)
1123 for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1125 // if (cma.sequenceRef != null)
1127 // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1128 // av.getColumnSelection().addElement(col);
1132 av.getColumnSelection().addElement(c - 1);
1136 fr = Math.min(lastX, currentX);
1137 to = Math.max(lastX, currentX);
1139 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1140 for (int c = fr; c <= to; c++)
1142 av.getColumnSelection().addElement(c);
1149 * Constructs the tooltip, and constructs and displays a status message, for
1150 * the current mouse position
1155 public void mouseMoved(MouseEvent evt)
1157 int yPos = evt.getY();
1158 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1159 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1160 int row = rowAndOffset[0];
1164 this.setToolTipText(null);
1168 int column = getColumnForXPos(evt.getX());
1170 AlignmentAnnotation ann = aa[row];
1171 if (row > -1 && ann.annotations != null
1172 && column < ann.annotations.length)
1174 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1176 setToolTipText(toolTip == null ? null
1177 : JvSwingUtils.wrapTooltip(true, toolTip));
1178 String msg = getStatusMessage(av.getAlignment(), column, ann,
1179 rowAndOffset[1], av);
1180 ap.alignFrame.setStatus(msg);
1184 this.setToolTipText(null);
1185 ap.alignFrame.setStatus(" ");
1189 private int getColumnForXPos(int x)
1191 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1192 column = Math.min(column, av.getRanges().getEndRes());
1194 if (av.hasHiddenColumns())
1196 column = av.getAlignment().getHiddenColumns()
1197 .visibleToAbsoluteColumn(column);
1203 * Answers the index in the annotations array of the visible annotation at the
1204 * given y position. This is done by adding the heights of visible annotations
1205 * until the y position has been exceeded. Answers -1 if no annotations are
1206 * visible, or the y position is below all annotations.
1212 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1218 return getRowIndexAndOffset(yPos, aa)[0];
1221 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1223 int[] res = new int[2];
1231 int height = 0, lheight = 0;
1232 for (int i = 0; i < aa.length; i++)
1237 height += aa[i].height;
1244 res[1] = yPos - lheight;
1252 * Answers a tooltip for the annotation at the current mouse position, not
1253 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1259 * @param rowAndOffset
1261 static String buildToolTip(AlignmentAnnotation ann, int column,
1262 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1265 String tooltip = null;
1266 if (ann.graphGroup > -1)
1268 StringBuilder tip = new StringBuilder(32);
1269 boolean first = true;
1270 for (int i = 0; i < anns.length; i++)
1272 if (anns[i].graphGroup == ann.graphGroup
1273 && anns[i].annotations[column] != null)
1280 tip.append(anns[i].label);
1281 String description = anns[i].annotations[column].description;
1282 if (description != null && description.length() > 0)
1284 tip.append(" ").append(description);
1288 tooltip = first ? null : tip.toString();
1290 else if (column < ann.annotations.length
1291 && ann.annotations[column] != null)
1293 tooltip = ann.annotations[column].description;
1295 // TODO abstract tooltip generator so different implementations can be built
1296 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1298 if (rowAndOffset >= ann.graphHeight)
1302 ContactListI clist = av.getContactList(ann, column);
1305 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1306 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1307 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1308 StringBuilder tooltipb = new StringBuilder();
1309 tooltipb.append("Contact from ").append(clist.getPosition())
1310 .append(", [").append(ci.cStart).append(" - ")
1311 .append(ci.cEnd).append("]").append("<br/>Mean:");
1312 Format.appendPercentage(tooltipb, (float) cr.getMean(), 2);
1313 tooltip = tooltipb.toString();
1314 int col = ann.sequenceRef.findPosition(column);
1315 int[][] highlightPos;
1316 int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1317 if (mappedPos != null)
1319 highlightPos = new int[1 + mappedPos.length][2];
1320 highlightPos[0] = new int[] { col, col };
1321 for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1323 highlightPos[h][0] = ann.sequenceRef
1324 .findPosition(mappedPos[p] - 1);
1325 highlightPos[h][1] = ann.sequenceRef
1326 .findPosition(mappedPos[p + 1] - 1);
1331 highlightPos = new int[][] { new int[] { col, col } };
1333 ap.getStructureSelectionManager()
1334 .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1341 * Constructs and returns the status bar message
1346 * @param rowAndOffset
1348 static String getStatusMessage(AlignmentI al, int column,
1349 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1352 * show alignment column and annotation description if any
1354 StringBuilder text = new StringBuilder(32);
1355 text.append(MessageManager.getString("label.column")).append(" ")
1356 .append(column + 1);
1358 if (column < ann.annotations.length && ann.annotations[column] != null)
1360 String description = ann.annotations[column].description;
1361 if (description != null && description.trim().length() > 0)
1363 text.append(" ").append(description);
1368 * if the annotation is sequence-specific, show the sequence number
1369 * in the alignment, and (if not a gap) the residue and position
1371 SequenceI seqref = ann.sequenceRef;
1374 int seqIndex = al.findIndex(seqref);
1377 text.append(", ").append(MessageManager.getString("label.sequence"))
1378 .append(" ").append(seqIndex + 1);
1379 char residue = seqref.getCharAt(column);
1380 if (!Comparison.isGap(residue))
1384 if (al.isNucleotide())
1386 name = ResidueProperties.nucleotideName
1387 .get(String.valueOf(residue));
1388 text.append(" Nucleotide: ")
1389 .append(name != null ? name : residue);
1393 name = 'X' == residue ? "X"
1394 : ('*' == residue ? "STOP"
1395 : ResidueProperties.aa2Triplet
1396 .get(String.valueOf(residue)));
1397 text.append(" Residue: ").append(name != null ? name : residue);
1399 int residuePos = seqref.findPosition(column);
1400 text.append(" (").append(residuePos).append(")");
1405 return text.toString();
1415 public void mouseClicked(MouseEvent evt)
1417 // if (activeRow != -1)
1419 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1420 // AlignmentAnnotation anot = aa[activeRow];
1424 // TODO mouseClicked-content and drawCursor are quite experimental!
1425 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1428 int pady = av.getCharHeight() / 5;
1430 graphics.setColor(Color.black);
1431 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1433 if (av.validCharWidth)
1435 graphics.setColor(Color.white);
1437 char s = seq.getCharAt(res);
1439 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1440 graphics.drawString(String.valueOf(s), charOffset + x1,
1441 (y1 + av.getCharHeight()) - pady);
1446 private volatile boolean imageFresh = false;
1448 private Rectangle visibleRect = new Rectangle(),
1449 clipBounds = new Rectangle();
1458 public void paintComponent(Graphics g)
1461 // BH: note that this method is generally recommended to
1462 // call super.paintComponent(g). Otherwise, the children of this
1463 // component will not be rendered. That is not needed here
1464 // because AnnotationPanel does not have any children. It is
1465 // just a JPanel contained in a JViewPort.
1467 computeVisibleRect(visibleRect);
1469 g.setColor(Color.white);
1470 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1474 // BH 2018 optimizing generation of new Rectangle().
1476 || (visibleRect.width != (clipBounds = g
1477 .getClipBounds(clipBounds)).width)
1478 || (visibleRect.height != clipBounds.height))
1481 g.drawImage(image, 0, 0, this);
1486 updateFadedImageWidth();
1492 if (image == null || imgWidth != image.getWidth(this)
1493 || image.getHeight(this) != getHeight())
1495 boolean tried = false;
1497 while (image == null && !tried)
1501 image = new BufferedImage(imgWidth,
1502 ap.getAnnotationPanel().getHeight(),
1503 BufferedImage.TYPE_INT_RGB);
1505 } catch (IllegalArgumentException exc)
1507 jalview.bin.Console.errPrintln(
1508 "Serious issue with viewport geometry imgWidth requested was "
1511 } catch (OutOfMemoryError oom)
1516 } catch (Exception x)
1521 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1527 gg = (Graphics2D) image.getGraphics();
1531 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1532 RenderingHints.VALUE_ANTIALIAS_ON);
1535 gg.setFont(av.getFont());
1536 fm = gg.getFontMetrics();
1537 gg.setColor(Color.white);
1538 gg.fillRect(0, 0, imgWidth, image.getHeight());
1543 gg = (Graphics2D) image.getGraphics();
1547 drawComponent(gg, av.getRanges().getStartRes(),
1548 av.getRanges().getEndRes() + 1);
1551 g.drawImage(image, 0, 0, this);
1554 public void updateFadedImageWidth()
1556 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1557 + 1) * av.getCharWidth();
1562 * set true to enable redraw timing debug output on stderr
1564 private final boolean debugRedraw = false;
1567 * non-Thread safe repaint
1570 * repaint with horizontal shift in alignment
1572 public void fastPaint(int horizontal)
1574 if ((horizontal == 0) || image == null
1575 || av.getAlignment().getAlignmentAnnotation() == null
1576 || av.getAlignment().getAlignmentAnnotation().length < 1
1577 || av.isCalcInProgress())
1583 int sr = av.getRanges().getStartRes();
1584 int er = av.getRanges().getEndRes() + 1;
1587 Graphics2D gg = (Graphics2D) image.getGraphics();
1589 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1591 // scroll is less than imgWidth away so can re-use buffered graphics
1592 gg.copyArea(0, 0, imgWidth, getHeight(),
1593 -horizontal * av.getCharWidth(), 0);
1595 if (horizontal > 0) // scrollbar pulled right, image to the left
1597 transX = (er - sr - horizontal) * av.getCharWidth();
1598 sr = er - horizontal;
1600 else if (horizontal < 0)
1602 er = sr - horizontal;
1605 gg.translate(transX, 0);
1607 drawComponent(gg, sr, er);
1609 gg.translate(-transX, 0);
1615 // Call repaint on alignment panel so that repaints from other alignment
1616 // panel components can be aggregated. Otherwise performance of the overview
1617 // window and others may be adversely affected.
1618 av.getAlignPanel().repaint();
1621 private volatile boolean lastImageGood = false;
1633 public void drawComponent(Graphics g, int startRes, int endRes)
1635 BufferedImage oldFaded = fadedImage;
1636 if (av.isCalcInProgress())
1640 lastImageGood = false;
1643 // We'll keep a record of the old image,
1644 // and draw a faded image until the calculation
1647 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1648 || fadedImage.getHeight() != image.getHeight()))
1650 // jalview.bin.Console.errPrintln("redraw faded image
1651 // ("+(fadedImage==null ?
1652 // "null image" : "") + " lastGood="+lastImageGood+")");
1653 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1654 BufferedImage.TYPE_INT_RGB);
1656 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1658 fadedG.setColor(Color.white);
1659 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1661 fadedG.setComposite(
1662 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1663 fadedG.drawImage(image, 0, 0, this);
1666 // make sure we don't overwrite the last good faded image until all
1667 // calculations have finished
1668 lastImageGood = false;
1673 if (fadedImage != null)
1675 oldFaded = fadedImage;
1680 g.setColor(Color.white);
1681 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1683 g.setFont(av.getFont());
1686 fm = g.getFontMetrics();
1689 if ((av.getAlignment().getAlignmentAnnotation() == null)
1690 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1692 g.setColor(Color.white);
1693 g.fillRect(0, 0, getWidth(), getHeight());
1694 g.setColor(Color.black);
1695 if (av.validCharWidth)
1697 g.drawString(MessageManager
1698 .getString("label.alignment_has_no_annotations"), 20, 15);
1703 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1705 if (!lastImageGood && fadedImage == null)
1707 fadedImage = oldFaded;
1709 if (dragMode == DragMode.MatrixSelect)
1711 g.setColor(Color.yellow);
1712 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1713 Math.min(firstDragY, mouseDragLastY),
1714 Math.max(firstDragX, mouseDragLastX)
1715 - Math.min(firstDragX, mouseDragLastX),
1716 Math.max(firstDragY, mouseDragLastY)
1717 - Math.min(firstDragY, mouseDragLastY));
1723 public FontMetrics getFontMetrics()
1729 public Image getFadedImage()
1735 public int getFadedImageWidth()
1737 updateFadedImageWidth();
1741 private int[] bounds = new int[2];
1744 public int[] getVisibleVRange()
1746 if (ap != null && ap.getAlabels() != null)
1748 int sOffset = -ap.getAlabels().getScrollOffset();
1749 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1750 bounds[0] = sOffset;
1751 bounds[1] = visHeight;
1761 * Try to ensure any references held are nulled
1763 public void dispose()
1773 * I created the renderer so I will dispose of it
1775 if (renderer != null)
1782 public void propertyChange(PropertyChangeEvent evt)
1784 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1785 // Both scrolling and resizing change viewport ranges: scrolling changes
1786 // both start and end points, but resize only changes end values.
1787 // Here we only want to fastpaint on a scroll, with resize using a normal
1788 // paint, so scroll events are identified as changes to the horizontal or
1789 // vertical start value.
1790 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1792 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1794 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1796 fastPaint(((int[]) evt.getNewValue())[0]
1797 - ((int[]) evt.getOldValue())[0]);
1799 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1806 * computes the visible height of the annotation panel
1808 * @param adjustPanelHeight
1809 * - when false, just adjust existing height according to other
1811 * @param annotationHeight
1812 * @return height to use for the ScrollerPreferredVisibleSize
1814 public int adjustForAlignFrame(boolean adjustPanelHeight,
1815 int annotationHeight)
1818 * Estimate available height in the AlignFrame for alignment +
1819 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1820 * hscroll, status bar, insets.
1822 int stuff = (ap.getViewName() != null ? 30 : 0)
1823 + (Platform.isAMacAndNotJS() ? 120 : 140);
1824 int availableHeight = ap.alignFrame.getHeight() - stuff;
1825 int rowHeight = av.getCharHeight();
1827 if (adjustPanelHeight)
1829 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1832 * If not enough vertical space, maximize annotation height while keeping
1833 * at least two rows of alignment visible
1835 if (annotationHeight + alignmentHeight > availableHeight)
1837 annotationHeight = Math.min(annotationHeight,
1838 availableHeight - 2 * rowHeight);
1843 // maintain same window layout whilst updating sliders
1844 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1845 availableHeight - 2 * rowHeight);
1847 return annotationHeight;