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,
654 * start and end range corresponding to the row range under the mouse at
658 fr = Math.min(cXci.cStart, cXci.cEnd);
659 to = Math.max(cXci.cStart, cXci.cEnd);
661 // double click selects the whole group
662 if (evt.getClickCount() == 2)
664 ContactMatrixI matrix = av.getContactMatrix(clicked);
668 // simplest approach is to select all group containing column
669 if (matrix.hasGroups())
671 SequenceI rseq = clicked.sequenceRef;
672 BitSet grp = new BitSet();
673 grp.or(matrix.getGroupsFor(currentX));
674 // TODO: cXci needs to be mapped to real groups
675 for (int c = fr; c <= to; c++)
677 BitSet additionalGrp = matrix.getGroupsFor(c);
678 grp.or(additionalGrp);
681 HiddenColumns hc = av.getAlignment().getHiddenColumns();
682 ColumnSelection cs = av.getColumnSelection();
684 for (int p=grp.nextSetBit(0); p >= 0; p = grp
687 if (matrix instanceof MappableContactMatrixI)
689 // find the end of this run of set bits
690 int nextp = grp.nextClearBit(p)-1;
691 int[] pos = ((MappableContactMatrixI)matrix).getMappedPositionsFor(rseq, p,nextp);
696 for (int pos_p = pos[0];pos_p<=pos[1];pos_p++)
698 int col = rseq.findIndex(pos_p)-1;
699 if (col>=0 && (!av.hasHiddenColumns() || hc.isVisible(col)))
706 int offp = (rseq != null)
707 ? rseq.findIndex(rseq.getStart() - 1 + p)
710 if (!av.hasHiddenColumns() || hc.isVisible(offp))
717 // possible alternative for interactive selection - threshold
718 // gives 'ceiling' for forming a cluster
719 // when a row+column is selected, farthest common ancestor less
720 // than thr is used to compute cluster
726 // select corresponding range in segment under mouse
728 int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
731 av.getColumnSelection().addRangeOfElements(rng, true);
733 av.getColumnSelection().addElement(currentX);
736 // and also select everything lower than the max range adjacent
738 if (evt.isControlDown()
739 && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
742 ContactRange cr = forCurrentX.getRangeFor(fr, to);
744 // TODO: could use GraphLine instead of arbitrary picking
745 // TODO: could report mean/median/variance for partitions
746 // (contiguous selected vs unselected regions and inter-contig
748 // controls feathering - what other elements in row/column
750 double thresh = cr.getMean() + (cr.getMax() - cr.getMean()) * .15;
753 cval = forCurrentX.getContactAt(c);
754 if (// cr.getMin() <= cval &&
757 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
760 av.getColumnSelection().addRangeOfElements(cols, true);
770 while (c < forCurrentX.getContactHeight())
772 cval = forCurrentX.getContactAt(c);
773 if (// cr.getMin() <= cval &&
776 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
779 av.getColumnSelection().addRangeOfElements(cols, true);
792 ap.paintAlignment(false, false);
793 PaintRefresher.Refresh(ap, av.getSequenceSetId());
799 * Construct and display a context menu at the right-click position
804 void showPopupMenu(final int y, int x)
806 if (av.getColumnSelection() == null
807 || av.getColumnSelection().isEmpty())
812 JPopupMenu pop = new JPopupMenu(
813 MessageManager.getString("label.structure_type"));
816 * Just display the needed structure options
818 if (av.getAlignment().isNucleotide())
820 item = new JMenuItem(STEM);
821 item.addActionListener(this);
826 item = new JMenuItem(HELIX);
827 item.addActionListener(this);
829 item = new JMenuItem(SHEET);
830 item.addActionListener(this);
833 item = new JMenuItem(LABEL);
834 item.addActionListener(this);
836 item = new JMenuItem(COLOUR);
837 item.addActionListener(this);
839 item = new JMenuItem(REMOVE);
840 item.addActionListener(this);
842 pop.show(this, x, y);
846 * Action on mouse up is to clear mouse drag data and call mouseReleased on
847 * ScalePanel, to deal with defining the selection group (if any) defined by
853 public void mouseReleased(MouseEvent evt)
855 if (dragMode == DragMode.MatrixSelect)
857 matrixSelectRange(evt);
864 mouseDragging = false;
865 if (dragMode == DragMode.Resize)
867 ap.adjustAnnotationHeight();
869 dragMode = DragMode.Undefined;
870 if (!matrix_clicked(evt))
872 ap.getScalePanel().mouseReleased(evt);
876 * isPopupTrigger is set in mouseReleased on Windows
877 * (in mousePressed on Mac)
879 if (evt.isPopupTrigger() && activeRow != -1)
881 showPopupMenu(evt.getY(), evt.getX());
893 public void mouseEntered(MouseEvent evt)
895 this.mouseDragging = false;
896 ap.getScalePanel().mouseEntered(evt);
900 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
901 * with column selection on a mouse drag
906 public void mouseExited(MouseEvent evt)
908 ap.getScalePanel().mouseExited(evt);
912 * Action on starting or continuing a mouse drag. There are two possible
915 * <li>drag up or down on a graphed annotation increases or decreases the
916 * height of the graph</li>
917 * <li>dragging left or right selects the columns dragged across</li>
919 * A drag on a graph annotation is treated as column selection if it starts
920 * with more horizontal than vertical movement, and as resize if it starts
921 * with more vertical than horizontal movement. Once started, the drag does
927 public void mouseDragged(MouseEvent evt)
930 * if dragMode is Undefined:
931 * - set to Select if dx > dy
932 * - set to Resize if dy > dx
933 * - do nothing if dx == dy
935 final int x = evt.getX();
936 final int y = evt.getY();
937 if (dragMode == DragMode.Undefined)
939 int dx = Math.abs(x - mouseDragLastX);
940 int dy = Math.abs(y - mouseDragLastY);
941 if (graphStretch == -1 || dx > dy)
944 * mostly horizontal drag, or not a graph annotation
946 dragMode = DragMode.Select;
951 * mostly vertical drag
953 dragMode = DragMode.Resize;
954 notJustOne = evt.isShiftDown();
957 * but could also be a matrix drag
959 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
960 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
963 * dragging in a matrix
965 dragMode = DragMode.MatrixSelect;
966 firstDragX = mouseDragLastX;
967 firstDragY = mouseDragLastY;
972 if (dragMode == DragMode.Undefined)
976 * drag is diagonal - defer deciding whether to
977 * treat as up/down or left/right
984 if (dragMode == DragMode.Resize)
987 * resize graph annotation if mouse was dragged up or down
989 int deltaY = mouseDragLastY - evt.getY();
992 AlignmentAnnotation graphAnnotation = av.getAlignment()
993 .getAlignmentAnnotation()[graphStretch];
994 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
997 for (AlignmentAnnotation similar : av.getAlignment()
998 .findAnnotations(null, graphAnnotation.getCalcId(),
999 graphAnnotation.label))
1001 similar.graphHeight = newHeight;
1007 graphAnnotation.graphHeight = newHeight;
1009 adjustPanelHeight();
1010 ap.paintAlignment(false, false);
1013 else if (dragMode == DragMode.MatrixSelect)
1016 * TODO draw a rubber band for range
1020 ap.paintAlignment(false, false);
1025 * for mouse drag left or right, delegate to
1026 * ScalePanel to adjust the column selection
1028 ap.getScalePanel().mouseDragged(evt);
1037 public void matrixSelectRange(MouseEvent evt)
1040 * get geometry of drag
1042 int fromY = Math.min(firstDragY, evt.getY());
1043 int toY = Math.max(firstDragY, evt.getY());
1044 int fromX = Math.min(firstDragX, evt.getX());
1045 int toX = Math.max(firstDragX, evt.getX());
1047 int deltaY = toY - fromY;
1048 int deltaX = toX - fromX;
1050 int[] rowIndex = getRowIndexAndOffset(fromY,
1051 av.getAlignment().getAlignmentAnnotation());
1052 int[] toRowIndex = getRowIndexAndOffset(toY,
1053 av.getAlignment().getAlignmentAnnotation());
1055 if (rowIndex == null || toRowIndex == null)
1057 jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1060 if (rowIndex[0] != toRowIndex[0])
1063 .trace("Drag went to another row. needs to be clipped");
1066 // rectangular selection on matrix style annotation
1067 AlignmentAnnotation cma = av.getAlignment()
1068 .getAlignmentAnnotation()[rowIndex[0]];
1070 int lastX = getColumnForXPos(fromX);
1071 int currentX = getColumnForXPos(toX);
1072 int fromXc = Math.min(lastX, currentX);
1073 int toXc = Math.max(lastX, currentX);
1074 ContactListI forFromX = av.getContactList(cma, fromXc);
1075 ContactListI forToX = av.getContactList(cma, toXc);
1077 if (forFromX != null && forToX != null)
1079 ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
1081 ContactGeometry.contactInterval lastXci = lastXcgeom
1082 .mapFor(rowIndex[1], rowIndex[1] + deltaY);
1084 ContactGeometry cXcgeom = new ContactGeometry(forToX,
1086 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
1087 rowIndex[1] + deltaY);
1089 // mark rectangular region formed by drag
1090 jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
1091 + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
1092 + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1094 fr = Math.min(lastXci.cStart, lastXci.cEnd);
1095 to = Math.max(lastXci.cStart, lastXci.cEnd);
1096 int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
1097 if (mappedPos != null)
1099 jalview.bin.Console.trace("Marking " + fr + " to " + to
1100 + " mapping to sequence positions " + mappedPos[0] + " to "
1102 for (int pair = 0; pair < mappedPos.length; pair += 2)
1104 for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1106 // if (cma.sequenceRef != null)
1108 // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1109 // av.getColumnSelection().addElement(col);
1113 av.getColumnSelection().addElement(c);
1117 // and again for most recent corner of drag
1118 fr = Math.min(cXci.cStart, cXci.cEnd);
1119 to = Math.max(cXci.cStart, cXci.cEnd);
1120 mappedPos = forFromX.getMappedPositionsFor(fr, to);
1121 if (mappedPos != null)
1123 for (int pair = 0; pair < mappedPos.length; pair += 2)
1125 jalview.bin.Console.trace("Marking " + fr + " to " + to
1126 + " mapping to sequence positions " + mappedPos[pair]
1127 + " to " + mappedPos[pair + 1]);
1128 for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1130 // if (cma.sequenceRef != null)
1133 // cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1134 // av.getColumnSelection().addElement(col);
1138 av.getColumnSelection().addElement(c);
1143 fr = Math.min(lastX, currentX);
1144 to = Math.max(lastX, currentX);
1146 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1147 for (int c = fr; c <= to; c++)
1149 av.getColumnSelection().addElement(c);
1156 * Constructs the tooltip, and constructs and displays a status message, for
1157 * the current mouse position
1162 public void mouseMoved(MouseEvent evt)
1164 int yPos = evt.getY();
1165 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1166 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1167 int row = rowAndOffset[0];
1171 this.setToolTipText(null);
1175 int column = getColumnForXPos(evt.getX());
1177 AlignmentAnnotation ann = aa[row];
1178 if (row > -1 && ann.annotations != null
1179 && column < ann.annotations.length)
1181 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1183 setToolTipText(toolTip == null ? null
1184 : JvSwingUtils.wrapTooltip(true, toolTip));
1185 String msg = getStatusMessage(av.getAlignment(), column, ann,
1186 rowAndOffset[1], av);
1187 ap.alignFrame.setStatus(msg);
1191 this.setToolTipText(null);
1192 ap.alignFrame.setStatus(" ");
1196 private int getColumnForXPos(int x)
1198 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1199 column = Math.min(column, av.getRanges().getEndRes());
1201 if (av.hasHiddenColumns())
1203 column = av.getAlignment().getHiddenColumns()
1204 .visibleToAbsoluteColumn(column);
1210 * Answers the index in the annotations array of the visible annotation at the
1211 * given y position. This is done by adding the heights of visible annotations
1212 * until the y position has been exceeded. Answers -1 if no annotations are
1213 * visible, or the y position is below all annotations.
1219 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1225 return getRowIndexAndOffset(yPos, aa)[0];
1228 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1230 int[] res = new int[2];
1238 int height = 0, lheight = 0;
1239 for (int i = 0; i < aa.length; i++)
1244 height += aa[i].height;
1251 res[1] = yPos-lheight;
1259 * Answers a tooltip for the annotation at the current mouse position, not
1260 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1266 * @param rowAndOffset
1268 static String buildToolTip(AlignmentAnnotation ann, int column,
1269 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1272 String tooltip = null;
1273 if (ann.graphGroup > -1)
1275 StringBuilder tip = new StringBuilder(32);
1276 boolean first = true;
1277 for (int i = 0; i < anns.length; i++)
1279 if (anns[i].graphGroup == ann.graphGroup
1280 && anns[i].annotations[column] != null)
1287 tip.append(anns[i].label);
1288 String description = anns[i].annotations[column].description;
1289 if (description != null && description.length() > 0)
1291 tip.append(" ").append(description);
1295 tooltip = first ? null : tip.toString();
1297 else if (column < ann.annotations.length
1298 && ann.annotations[column] != null)
1300 tooltip = ann.annotations[column].description;
1302 // TODO abstract tooltip generator so different implementations can be built
1303 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1305 if (rowAndOffset>=ann.graphHeight)
1309 ContactListI clist = av.getContactList(ann, column);
1312 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1313 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1314 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1315 StringBuilder tooltipb = new StringBuilder();
1316 tooltipb.append("Contact from ")
1317 .append(clist.getPosition()).append(", [").append(ci.cStart).append(" - ").append(ci.cEnd).append("]").append("<br/>Mean:");
1318 Format.appendPercentage(tooltipb, (float)cr.getMean(),2);
1319 tooltip = tooltipb.toString();
1320 int col = ann.sequenceRef.findPosition(column);
1321 int[][] highlightPos;
1322 int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1323 if (mappedPos != null)
1325 highlightPos = new int[1 + mappedPos.length][2];
1326 highlightPos[0] = new int[] { col, col };
1327 for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1329 highlightPos[h][0] = ann.sequenceRef
1330 .findPosition(mappedPos[p] - 1);
1331 highlightPos[h][1] = ann.sequenceRef
1332 .findPosition(mappedPos[p + 1] - 1);
1337 highlightPos = new int[][] { new int[] { col, col } };
1339 ap.getStructureSelectionManager()
1340 .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1347 * Constructs and returns the status bar message
1352 * @param rowAndOffset
1354 static String getStatusMessage(AlignmentI al, int column,
1355 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1358 * show alignment column and annotation description if any
1360 StringBuilder text = new StringBuilder(32);
1361 text.append(MessageManager.getString("label.column")).append(" ")
1362 .append(column + 1);
1364 if (column < ann.annotations.length && ann.annotations[column] != null)
1366 String description = ann.annotations[column].description;
1367 if (description != null && description.trim().length() > 0)
1369 text.append(" ").append(description);
1374 * if the annotation is sequence-specific, show the sequence number
1375 * in the alignment, and (if not a gap) the residue and position
1377 SequenceI seqref = ann.sequenceRef;
1380 int seqIndex = al.findIndex(seqref);
1383 text.append(", ").append(MessageManager.getString("label.sequence"))
1384 .append(" ").append(seqIndex + 1);
1385 char residue = seqref.getCharAt(column);
1386 if (!Comparison.isGap(residue))
1390 if (al.isNucleotide())
1392 name = ResidueProperties.nucleotideName
1393 .get(String.valueOf(residue));
1394 text.append(" Nucleotide: ")
1395 .append(name != null ? name : residue);
1399 name = 'X' == residue ? "X"
1400 : ('*' == residue ? "STOP"
1401 : ResidueProperties.aa2Triplet
1402 .get(String.valueOf(residue)));
1403 text.append(" Residue: ").append(name != null ? name : residue);
1405 int residuePos = seqref.findPosition(column);
1406 text.append(" (").append(residuePos).append(")");
1411 return text.toString();
1421 public void mouseClicked(MouseEvent evt)
1423 // if (activeRow != -1)
1425 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1426 // AlignmentAnnotation anot = aa[activeRow];
1430 // TODO mouseClicked-content and drawCursor are quite experimental!
1431 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1434 int pady = av.getCharHeight() / 5;
1436 graphics.setColor(Color.black);
1437 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1439 if (av.validCharWidth)
1441 graphics.setColor(Color.white);
1443 char s = seq.getCharAt(res);
1445 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1446 graphics.drawString(String.valueOf(s), charOffset + x1,
1447 (y1 + av.getCharHeight()) - pady);
1452 private volatile boolean imageFresh = false;
1454 private Rectangle visibleRect = new Rectangle(),
1455 clipBounds = new Rectangle();
1464 public void paintComponent(Graphics g)
1467 // BH: note that this method is generally recommended to
1468 // call super.paintComponent(g). Otherwise, the children of this
1469 // component will not be rendered. That is not needed here
1470 // because AnnotationPanel does not have any children. It is
1471 // just a JPanel contained in a JViewPort.
1473 computeVisibleRect(visibleRect);
1475 g.setColor(Color.white);
1476 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1480 // BH 2018 optimizing generation of new Rectangle().
1482 || (visibleRect.width != (clipBounds = g
1483 .getClipBounds(clipBounds)).width)
1484 || (visibleRect.height != clipBounds.height))
1487 g.drawImage(image, 0, 0, this);
1492 updateFadedImageWidth();
1498 if (image == null || imgWidth != image.getWidth(this)
1499 || image.getHeight(this) != getHeight())
1501 boolean tried = false;
1503 while (image == null && !tried)
1507 image = new BufferedImage(imgWidth,
1508 ap.getAnnotationPanel().getHeight(),
1509 BufferedImage.TYPE_INT_RGB);
1511 } catch (IllegalArgumentException exc)
1513 jalview.bin.Console.errPrintln(
1514 "Serious issue with viewport geometry imgWidth requested was "
1517 } catch (OutOfMemoryError oom)
1522 } catch (Exception x)
1527 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1533 gg = (Graphics2D) image.getGraphics();
1537 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1538 RenderingHints.VALUE_ANTIALIAS_ON);
1541 gg.setFont(av.getFont());
1542 fm = gg.getFontMetrics();
1543 gg.setColor(Color.white);
1544 gg.fillRect(0, 0, imgWidth, image.getHeight());
1549 gg = (Graphics2D) image.getGraphics();
1553 drawComponent(gg, av.getRanges().getStartRes(),
1554 av.getRanges().getEndRes() + 1);
1557 g.drawImage(image, 0, 0, this);
1560 public void updateFadedImageWidth()
1562 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1563 + 1) * av.getCharWidth();
1568 * set true to enable redraw timing debug output on stderr
1570 private final boolean debugRedraw = false;
1573 * non-Thread safe repaint
1576 * repaint with horizontal shift in alignment
1578 public void fastPaint(int horizontal)
1580 if ((horizontal == 0) || image == null
1581 || av.getAlignment().getAlignmentAnnotation() == null
1582 || av.getAlignment().getAlignmentAnnotation().length < 1
1583 || av.isCalcInProgress())
1589 int sr = av.getRanges().getStartRes();
1590 int er = av.getRanges().getEndRes() + 1;
1593 Graphics2D gg = (Graphics2D) image.getGraphics();
1595 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1597 // scroll is less than imgWidth away so can re-use buffered graphics
1598 gg.copyArea(0, 0, imgWidth, getHeight(),
1599 -horizontal * av.getCharWidth(), 0);
1601 if (horizontal > 0) // scrollbar pulled right, image to the left
1603 transX = (er - sr - horizontal) * av.getCharWidth();
1604 sr = er - horizontal;
1606 else if (horizontal < 0)
1608 er = sr - horizontal;
1611 gg.translate(transX, 0);
1613 drawComponent(gg, sr, er);
1615 gg.translate(-transX, 0);
1621 // Call repaint on alignment panel so that repaints from other alignment
1622 // panel components can be aggregated. Otherwise performance of the overview
1623 // window and others may be adversely affected.
1624 av.getAlignPanel().repaint();
1627 private volatile boolean lastImageGood = false;
1639 public void drawComponent(Graphics g, int startRes, int endRes)
1641 BufferedImage oldFaded = fadedImage;
1642 if (av.isCalcInProgress())
1646 lastImageGood = false;
1649 // We'll keep a record of the old image,
1650 // and draw a faded image until the calculation
1653 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1654 || fadedImage.getHeight() != image.getHeight()))
1656 // jalview.bin.Console.errPrintln("redraw faded image ("+(fadedImage==null ?
1657 // "null image" : "") + " lastGood="+lastImageGood+")");
1658 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1659 BufferedImage.TYPE_INT_RGB);
1661 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1663 fadedG.setColor(Color.white);
1664 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1666 fadedG.setComposite(
1667 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1668 fadedG.drawImage(image, 0, 0, this);
1671 // make sure we don't overwrite the last good faded image until all
1672 // calculations have finished
1673 lastImageGood = false;
1678 if (fadedImage != null)
1680 oldFaded = fadedImage;
1685 g.setColor(Color.white);
1686 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1688 g.setFont(av.getFont());
1691 fm = g.getFontMetrics();
1694 if ((av.getAlignment().getAlignmentAnnotation() == null)
1695 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1697 g.setColor(Color.white);
1698 g.fillRect(0, 0, getWidth(), getHeight());
1699 g.setColor(Color.black);
1700 if (av.validCharWidth)
1702 g.drawString(MessageManager
1703 .getString("label.alignment_has_no_annotations"), 20, 15);
1708 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1710 if (!lastImageGood && fadedImage == null)
1712 fadedImage = oldFaded;
1714 if (dragMode == DragMode.MatrixSelect)
1716 g.setColor(Color.yellow);
1717 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1718 Math.min(firstDragY, mouseDragLastY),
1719 Math.max(firstDragX, mouseDragLastX)
1720 - Math.min(firstDragX, mouseDragLastX),
1721 Math.max(firstDragY, mouseDragLastY)
1722 - Math.min(firstDragY, mouseDragLastY));
1728 public FontMetrics getFontMetrics()
1734 public Image getFadedImage()
1740 public int getFadedImageWidth()
1742 updateFadedImageWidth();
1746 private int[] bounds = new int[2];
1749 public int[] getVisibleVRange()
1751 if (ap != null && ap.getAlabels() != null)
1753 int sOffset = -ap.getAlabels().getScrollOffset();
1754 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1755 bounds[0] = sOffset;
1756 bounds[1] = visHeight;
1766 * Try to ensure any references held are nulled
1768 public void dispose()
1778 * I created the renderer so I will dispose of it
1780 if (renderer != null)
1787 public void propertyChange(PropertyChangeEvent evt)
1789 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1790 // Both scrolling and resizing change viewport ranges: scrolling changes
1791 // both start and end points, but resize only changes end values.
1792 // Here we only want to fastpaint on a scroll, with resize using a normal
1793 // paint, so scroll events are identified as changes to the horizontal or
1794 // vertical start value.
1795 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1797 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1799 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1801 fastPaint(((int[]) evt.getNewValue())[0]
1802 - ((int[]) evt.getOldValue())[0]);
1804 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1811 * computes the visible height of the annotation panel
1813 * @param adjustPanelHeight
1814 * - when false, just adjust existing height according to other
1816 * @param annotationHeight
1817 * @return height to use for the ScrollerPreferredVisibleSize
1819 public int adjustForAlignFrame(boolean adjustPanelHeight,
1820 int annotationHeight)
1823 * Estimate available height in the AlignFrame for alignment +
1824 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1825 * hscroll, status bar, insets.
1827 int stuff = (ap.getViewName() != null ? 30 : 0)
1828 + (Platform.isAMacAndNotJS() ? 120 : 140);
1829 int availableHeight = ap.alignFrame.getHeight() - stuff;
1830 int rowHeight = av.getCharHeight();
1832 if (adjustPanelHeight)
1834 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1837 * If not enough vertical space, maximize annotation height while keeping
1838 * at least two rows of alignment visible
1840 if (annotationHeight + alignmentHeight > availableHeight)
1842 annotationHeight = Math.min(annotationHeight,
1843 availableHeight - 2 * rowHeight);
1848 // maintain same window layout whilst updating sliders
1849 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1850 availableHeight - 2 * rowHeight);
1852 return annotationHeight;