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];
634 AlignmentAnnotation[] allAnnotation = av.getAlignment()
635 .getAlignmentAnnotation();
636 if (allAnnotation == null || rowIndex[0] < 0
637 || rowIndex[0] >= allAnnotation.length)
641 AlignmentAnnotation clicked = av.getAlignment()
642 .getAlignmentAnnotation()[rowIndex[0]];
643 if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
648 // TODO - use existing threshold to select related sections of matrix
649 GraphLine thr = clicked.getThreshold();
651 int currentX = getColumnForXPos(evt.getX());
652 ContactListI forCurrentX = av.getContactList(clicked, currentX);
653 if (forCurrentX != null)
655 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
656 clicked.graphHeight);
657 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset);
661 * start and end range corresponding to the row range under the mouse at
665 fr = Math.min(cXci.cStart, cXci.cEnd);
666 to = Math.max(cXci.cStart, cXci.cEnd);
668 // double click selects the whole group
669 if (evt.getClickCount() == 2)
671 ContactMatrixI matrix = av.getContactMatrix(clicked);
675 // simplest approach is to select all group containing column
676 if (matrix.hasGroups())
678 SequenceI rseq = clicked.sequenceRef;
679 BitSet grp = new BitSet();
680 grp.or(matrix.getGroupsFor(currentX));
681 // TODO: cXci needs to be mapped to real groups
682 for (int c = fr; c <= to; c++)
684 BitSet additionalGrp = matrix.getGroupsFor(c);
685 grp.or(additionalGrp);
688 HiddenColumns hc = av.getAlignment().getHiddenColumns();
689 ColumnSelection cs = av.getColumnSelection();
691 for (int p = grp.nextSetBit(0); p >= 0; p = grp
694 if (matrix instanceof MappableContactMatrixI)
696 // find the end of this run of set bits
697 int nextp = grp.nextClearBit(p) - 1;
698 int[] pos = ((MappableContactMatrixI) matrix)
699 .getMappedPositionsFor(rseq, p, nextp);
704 for (int pos_p = pos[0]; pos_p <= pos[1]; pos_p++)
706 int col = rseq.findIndex(pos_p) - 1;
707 if (col >= 0 && (!av.hasHiddenColumns()
708 || hc.isVisible(col)))
717 int offp = (rseq != null)
718 ? rseq.findIndex(rseq.getStart() - 1 + p)
721 if (!av.hasHiddenColumns() || hc.isVisible(offp))
728 // possible alternative for interactive selection - threshold
729 // gives 'ceiling' for forming a cluster
730 // when a row+column is selected, farthest common ancestor less
731 // than thr is used to compute cluster
737 // select corresponding range in segment under mouse
739 int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
742 av.getColumnSelection().addRangeOfElements(rng, true);
744 av.getColumnSelection().addElement(currentX);
747 // and also select everything lower than the max range adjacent
749 if (evt.isControlDown()
750 && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
753 ContactRange cr = forCurrentX.getRangeFor(fr, to);
755 // TODO: could use GraphLine instead of arbitrary picking
756 // TODO: could report mean/median/variance for partitions
757 // (contiguous selected vs unselected regions and inter-contig
759 // controls feathering - what other elements in row/column
761 double thresh = cr.getMean()
762 + (cr.getMax() - cr.getMean()) * .15;
765 cval = forCurrentX.getContactAt(c);
766 if (// cr.getMin() <= cval &&
769 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
772 av.getColumnSelection().addRangeOfElements(cols, true);
782 while (c < forCurrentX.getContactHeight())
784 cval = forCurrentX.getContactAt(c);
785 if (// cr.getMin() <= cval &&
788 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
791 av.getColumnSelection().addRangeOfElements(cols, true);
806 ap.paintAlignment(false, false);
807 PaintRefresher.Refresh(ap, av.getSequenceSetId());
813 * Construct and display a context menu at the right-click position
818 void showPopupMenu(final int y, int x)
820 if (av.getColumnSelection() == null
821 || av.getColumnSelection().isEmpty())
826 JPopupMenu pop = new JPopupMenu(
827 MessageManager.getString("label.structure_type"));
830 * Just display the needed structure options
832 if (av.getAlignment().isNucleotide())
834 item = new JMenuItem(STEM);
835 item.addActionListener(this);
840 item = new JMenuItem(HELIX);
841 item.addActionListener(this);
843 item = new JMenuItem(SHEET);
844 item.addActionListener(this);
847 item = new JMenuItem(LABEL);
848 item.addActionListener(this);
850 item = new JMenuItem(COLOUR);
851 item.addActionListener(this);
853 item = new JMenuItem(REMOVE);
854 item.addActionListener(this);
856 pop.show(this, x, y);
860 * Action on mouse up is to clear mouse drag data and call mouseReleased on
861 * ScalePanel, to deal with defining the selection group (if any) defined by
867 public void mouseReleased(MouseEvent evt)
869 if (dragMode == DragMode.MatrixSelect)
871 matrixSelectRange(evt);
878 mouseDragging = false;
879 if (dragMode == DragMode.Resize)
881 ap.adjustAnnotationHeight();
883 dragMode = DragMode.Undefined;
884 if (!matrix_clicked(evt))
886 ap.getScalePanel().mouseReleased(evt);
890 * isPopupTrigger is set in mouseReleased on Windows
891 * (in mousePressed on Mac)
893 if (evt.isPopupTrigger() && activeRow != -1)
895 showPopupMenu(evt.getY(), evt.getX());
907 public void mouseEntered(MouseEvent evt)
909 this.mouseDragging = false;
910 ap.getScalePanel().mouseEntered(evt);
914 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
915 * with column selection on a mouse drag
920 public void mouseExited(MouseEvent evt)
922 ap.getScalePanel().mouseExited(evt);
926 * Action on starting or continuing a mouse drag. There are two possible
929 * <li>drag up or down on a graphed annotation increases or decreases the
930 * height of the graph</li>
931 * <li>dragging left or right selects the columns dragged across</li>
933 * A drag on a graph annotation is treated as column selection if it starts
934 * with more horizontal than vertical movement, and as resize if it starts
935 * with more vertical than horizontal movement. Once started, the drag does
941 public void mouseDragged(MouseEvent evt)
944 * if dragMode is Undefined:
945 * - set to Select if dx > dy
946 * - set to Resize if dy > dx
947 * - do nothing if dx == dy
949 final int x = evt.getX();
950 final int y = evt.getY();
951 if (dragMode == DragMode.Undefined)
953 int dx = Math.abs(x - mouseDragLastX);
954 int dy = Math.abs(y - mouseDragLastY);
955 if (graphStretch == -1 || dx > dy)
958 * mostly horizontal drag, or not a graph annotation
960 dragMode = DragMode.Select;
965 * mostly vertical drag
967 dragMode = DragMode.Resize;
968 notJustOne = evt.isShiftDown();
971 * but could also be a matrix drag
973 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
974 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
977 * dragging in a matrix
979 dragMode = DragMode.MatrixSelect;
980 firstDragX = mouseDragLastX;
981 firstDragY = mouseDragLastY;
986 if (dragMode == DragMode.Undefined)
990 * drag is diagonal - defer deciding whether to
991 * treat as up/down or left/right
998 if (dragMode == DragMode.Resize)
1001 * resize graph annotation if mouse was dragged up or down
1003 int deltaY = mouseDragLastY - evt.getY();
1006 AlignmentAnnotation graphAnnotation = av.getAlignment()
1007 .getAlignmentAnnotation()[graphStretch];
1008 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
1011 for (AlignmentAnnotation similar : av.getAlignment()
1012 .findAnnotations(null, graphAnnotation.getCalcId(),
1013 graphAnnotation.label))
1015 similar.graphHeight = newHeight;
1021 graphAnnotation.graphHeight = newHeight;
1023 adjustPanelHeight();
1024 ap.paintAlignment(false, false);
1027 else if (dragMode == DragMode.MatrixSelect)
1030 * TODO draw a rubber band for range
1034 ap.paintAlignment(false, false);
1039 * for mouse drag left or right, delegate to
1040 * ScalePanel to adjust the column selection
1042 ap.getScalePanel().mouseDragged(evt);
1051 public void matrixSelectRange(MouseEvent evt)
1054 * get geometry of drag
1056 int fromY = Math.min(firstDragY, evt.getY());
1057 int toY = Math.max(firstDragY, evt.getY());
1058 int fromX = Math.min(firstDragX, evt.getX());
1059 int toX = Math.max(firstDragX, evt.getX());
1061 int deltaY = toY - fromY;
1062 int deltaX = toX - fromX;
1064 int[] rowIndex = getRowIndexAndOffset(fromY,
1065 av.getAlignment().getAlignmentAnnotation());
1066 int[] toRowIndex = getRowIndexAndOffset(toY,
1067 av.getAlignment().getAlignmentAnnotation());
1069 if (rowIndex == null || toRowIndex == null)
1071 jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1074 if (rowIndex[0] != toRowIndex[0])
1077 .trace("Drag went to another row. needs to be clipped");
1080 // rectangular selection on matrix style annotation
1081 AlignmentAnnotation cma = av.getAlignment()
1082 .getAlignmentAnnotation()[rowIndex[0]];
1084 int lastX = getColumnForXPos(fromX);
1085 int currentX = getColumnForXPos(toX);
1086 int fromXc = Math.min(lastX, currentX);
1087 int toXc = Math.max(lastX, currentX);
1088 ContactListI forFromX = av.getContactList(cma, fromXc);
1089 ContactListI forToX = av.getContactList(cma, toXc);
1091 if (forFromX != null && forToX != null)
1093 // FIXME will need two ContactGeometry objects when handling contact
1094 // matrices with differing numbers of rows at each
1096 ContactGeometry xcgeom = new ContactGeometry(forFromX,
1098 ContactGeometry.contactInterval lastXci = xcgeom.mapFor(rowIndex[1]);
1099 ContactGeometry.contactInterval cXci = xcgeom
1100 .mapFor(rowIndex[1] + deltaY);
1102 // mark rectangular region formed by drag
1103 jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
1104 + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
1105 + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1107 fr = Math.min(lastXci.cStart, cXci.cStart);
1108 to = Math.max(lastXci.cEnd, cXci.cEnd);
1109 int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
1110 if (mappedPos != null)
1112 jalview.bin.Console.trace("Marking " + fr + " to " + to
1113 + " mapping to sequence positions " + mappedPos[0] + " to "
1115 for (int pair = 0; pair < mappedPos.length; pair += 2)
1117 for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1119 // if (cma.sequenceRef != null)
1121 // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1122 // av.getColumnSelection().addElement(col);
1126 av.getColumnSelection().addElement(c - 1);
1130 fr = Math.min(lastX, currentX);
1131 to = Math.max(lastX, currentX);
1133 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1134 for (int c = fr; c <= to; c++)
1136 av.getColumnSelection().addElement(c);
1143 * Constructs the tooltip, and constructs and displays a status message, for
1144 * the current mouse position
1149 public void mouseMoved(MouseEvent evt)
1151 int yPos = evt.getY();
1152 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1153 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1154 int row = rowAndOffset[0];
1158 this.setToolTipText(null);
1162 int column = getColumnForXPos(evt.getX());
1164 AlignmentAnnotation ann = aa[row];
1165 if (row > -1 && ann.annotations != null
1166 && column < ann.annotations.length)
1168 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1170 setToolTipText(toolTip == null ? null
1171 : JvSwingUtils.wrapTooltip(true, toolTip));
1172 String msg = getStatusMessage(av.getAlignment(), column, ann,
1173 rowAndOffset[1], av);
1174 ap.alignFrame.setStatus(msg);
1178 this.setToolTipText(null);
1179 ap.alignFrame.setStatus(" ");
1183 private int getColumnForXPos(int x)
1185 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1186 column = Math.min(column, av.getRanges().getEndRes());
1188 if (av.hasHiddenColumns())
1190 column = av.getAlignment().getHiddenColumns()
1191 .visibleToAbsoluteColumn(column);
1197 * Answers the index in the annotations array of the visible annotation at the
1198 * given y position. This is done by adding the heights of visible annotations
1199 * until the y position has been exceeded. Answers -1 if no annotations are
1200 * visible, or the y position is below all annotations.
1206 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1212 return getRowIndexAndOffset(yPos, aa)[0];
1215 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1217 int[] res = new int[2];
1225 int height = 0, lheight = 0;
1226 for (int i = 0; i < aa.length; i++)
1231 height += aa[i].height;
1238 res[1] = yPos - lheight;
1246 * Answers a tooltip for the annotation at the current mouse position, not
1247 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1253 * @param rowAndOffset
1255 static String buildToolTip(AlignmentAnnotation ann, int column,
1256 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1259 String tooltip = null;
1260 if (ann.graphGroup > -1)
1262 StringBuilder tip = new StringBuilder(32);
1263 boolean first = true;
1264 for (int i = 0; i < anns.length; i++)
1266 if (anns[i].graphGroup == ann.graphGroup
1267 && anns[i].annotations[column] != null)
1274 tip.append(anns[i].label);
1275 String description = anns[i].annotations[column].description;
1276 if (description != null && description.length() > 0)
1278 tip.append(" ").append(description);
1282 tooltip = first ? null : tip.toString();
1284 else if (column < ann.annotations.length
1285 && ann.annotations[column] != null)
1287 tooltip = ann.annotations[column].description;
1289 // TODO abstract tooltip generator so different implementations can be built
1290 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1292 if (rowAndOffset >= ann.graphHeight)
1296 ContactListI clist = av.getContactList(ann, column);
1299 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1300 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1301 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1302 StringBuilder tooltipb = new StringBuilder();
1303 tooltipb.append("Contact from ").append(clist.getPosition())
1304 .append(", [").append(ci.cStart).append(" - ")
1305 .append(ci.cEnd).append("]").append("<br/>Mean:");
1306 Format.appendPercentage(tooltipb, (float) cr.getMean(), 2);
1307 tooltip = tooltipb.toString();
1308 int col = ann.sequenceRef.findPosition(column);
1309 int[][] highlightPos;
1310 int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1311 if (mappedPos != null)
1313 highlightPos = new int[1 + mappedPos.length][2];
1314 highlightPos[0] = new int[] { col, col };
1315 for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1317 highlightPos[h][0] = ann.sequenceRef
1318 .findPosition(mappedPos[p] - 1);
1319 highlightPos[h][1] = ann.sequenceRef
1320 .findPosition(mappedPos[p + 1] - 1);
1325 highlightPos = new int[][] { new int[] { col, col } };
1327 ap.getStructureSelectionManager()
1328 .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1335 * Constructs and returns the status bar message
1340 * @param rowAndOffset
1342 static String getStatusMessage(AlignmentI al, int column,
1343 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1346 * show alignment column and annotation description if any
1348 StringBuilder text = new StringBuilder(32);
1349 text.append(MessageManager.getString("label.column")).append(" ")
1350 .append(column + 1);
1352 if (column < ann.annotations.length && ann.annotations[column] != null)
1354 String description = ann.annotations[column].description;
1355 if (description != null && description.trim().length() > 0)
1357 text.append(" ").append(description);
1362 * if the annotation is sequence-specific, show the sequence number
1363 * in the alignment, and (if not a gap) the residue and position
1365 SequenceI seqref = ann.sequenceRef;
1368 int seqIndex = al.findIndex(seqref);
1371 text.append(", ").append(MessageManager.getString("label.sequence"))
1372 .append(" ").append(seqIndex + 1);
1373 char residue = seqref.getCharAt(column);
1374 if (!Comparison.isGap(residue))
1378 if (al.isNucleotide())
1380 name = ResidueProperties.nucleotideName
1381 .get(String.valueOf(residue));
1382 text.append(" Nucleotide: ")
1383 .append(name != null ? name : residue);
1387 name = 'X' == residue ? "X"
1388 : ('*' == residue ? "STOP"
1389 : ResidueProperties.aa2Triplet
1390 .get(String.valueOf(residue)));
1391 text.append(" Residue: ").append(name != null ? name : residue);
1393 int residuePos = seqref.findPosition(column);
1394 text.append(" (").append(residuePos).append(")");
1399 return text.toString();
1409 public void mouseClicked(MouseEvent evt)
1411 // if (activeRow != -1)
1413 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1414 // AlignmentAnnotation anot = aa[activeRow];
1418 // TODO mouseClicked-content and drawCursor are quite experimental!
1419 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1422 int pady = av.getCharHeight() / 5;
1424 graphics.setColor(Color.black);
1425 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1427 if (av.validCharWidth)
1429 graphics.setColor(Color.white);
1431 char s = seq.getCharAt(res);
1433 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1434 graphics.drawString(String.valueOf(s), charOffset + x1,
1435 (y1 + av.getCharHeight()) - pady);
1440 private volatile boolean imageFresh = false;
1442 private Rectangle visibleRect = new Rectangle(),
1443 clipBounds = new Rectangle();
1452 public void paintComponent(Graphics g)
1455 // BH: note that this method is generally recommended to
1456 // call super.paintComponent(g). Otherwise, the children of this
1457 // component will not be rendered. That is not needed here
1458 // because AnnotationPanel does not have any children. It is
1459 // just a JPanel contained in a JViewPort.
1461 computeVisibleRect(visibleRect);
1463 g.setColor(Color.white);
1464 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1468 // BH 2018 optimizing generation of new Rectangle().
1470 || (visibleRect.width != (clipBounds = g
1471 .getClipBounds(clipBounds)).width)
1472 || (visibleRect.height != clipBounds.height))
1475 g.drawImage(image, 0, 0, this);
1480 updateFadedImageWidth();
1486 if (image == null || imgWidth != image.getWidth(this)
1487 || image.getHeight(this) != getHeight())
1489 boolean tried = false;
1491 while (image == null && !tried)
1495 image = new BufferedImage(imgWidth,
1496 ap.getAnnotationPanel().getHeight(),
1497 BufferedImage.TYPE_INT_RGB);
1499 } catch (IllegalArgumentException exc)
1501 jalview.bin.Console.errPrintln(
1502 "Serious issue with viewport geometry imgWidth requested was "
1505 } catch (OutOfMemoryError oom)
1510 } catch (Exception x)
1515 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1521 gg = (Graphics2D) image.getGraphics();
1525 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1526 RenderingHints.VALUE_ANTIALIAS_ON);
1529 gg.setFont(av.getFont());
1530 fm = gg.getFontMetrics();
1531 gg.setColor(Color.white);
1532 gg.fillRect(0, 0, imgWidth, image.getHeight());
1537 gg = (Graphics2D) image.getGraphics();
1541 drawComponent(gg, av.getRanges().getStartRes(),
1542 av.getRanges().getEndRes() + 1);
1545 g.drawImage(image, 0, 0, this);
1548 public void updateFadedImageWidth()
1550 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1551 + 1) * av.getCharWidth();
1556 * set true to enable redraw timing debug output on stderr
1558 private final boolean debugRedraw = false;
1561 * non-Thread safe repaint
1564 * repaint with horizontal shift in alignment
1566 public void fastPaint(int horizontal)
1568 if ((horizontal == 0) || image == null
1569 || av.getAlignment().getAlignmentAnnotation() == null
1570 || av.getAlignment().getAlignmentAnnotation().length < 1
1571 || av.isCalcInProgress())
1577 int sr = av.getRanges().getStartRes();
1578 int er = av.getRanges().getEndRes() + 1;
1581 Graphics2D gg = (Graphics2D) image.getGraphics();
1583 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1585 // scroll is less than imgWidth away so can re-use buffered graphics
1586 gg.copyArea(0, 0, imgWidth, getHeight(),
1587 -horizontal * av.getCharWidth(), 0);
1589 if (horizontal > 0) // scrollbar pulled right, image to the left
1591 transX = (er - sr - horizontal) * av.getCharWidth();
1592 sr = er - horizontal;
1594 else if (horizontal < 0)
1596 er = sr - horizontal;
1599 gg.translate(transX, 0);
1601 drawComponent(gg, sr, er);
1603 gg.translate(-transX, 0);
1609 // Call repaint on alignment panel so that repaints from other alignment
1610 // panel components can be aggregated. Otherwise performance of the overview
1611 // window and others may be adversely affected.
1612 av.getAlignPanel().repaint();
1615 private volatile boolean lastImageGood = false;
1627 public void drawComponent(Graphics g, int startRes, int endRes)
1629 BufferedImage oldFaded = fadedImage;
1630 if (av.isCalcInProgress())
1634 lastImageGood = false;
1637 // We'll keep a record of the old image,
1638 // and draw a faded image until the calculation
1641 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1642 || fadedImage.getHeight() != image.getHeight()))
1644 // jalview.bin.Console.errPrintln("redraw faded image
1645 // ("+(fadedImage==null ?
1646 // "null image" : "") + " lastGood="+lastImageGood+")");
1647 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1648 BufferedImage.TYPE_INT_RGB);
1650 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1652 fadedG.setColor(Color.white);
1653 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1655 fadedG.setComposite(
1656 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1657 fadedG.drawImage(image, 0, 0, this);
1660 // make sure we don't overwrite the last good faded image until all
1661 // calculations have finished
1662 lastImageGood = false;
1667 if (fadedImage != null)
1669 oldFaded = fadedImage;
1674 g.setColor(Color.white);
1675 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1677 g.setFont(av.getFont());
1680 fm = g.getFontMetrics();
1683 if ((av.getAlignment().getAlignmentAnnotation() == null)
1684 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1686 g.setColor(Color.white);
1687 g.fillRect(0, 0, getWidth(), getHeight());
1688 g.setColor(Color.black);
1689 if (av.validCharWidth)
1691 g.drawString(MessageManager
1692 .getString("label.alignment_has_no_annotations"), 20, 15);
1697 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1699 if (!lastImageGood && fadedImage == null)
1701 fadedImage = oldFaded;
1703 if (dragMode == DragMode.MatrixSelect)
1705 g.setColor(Color.yellow);
1706 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1707 Math.min(firstDragY, mouseDragLastY),
1708 Math.max(firstDragX, mouseDragLastX)
1709 - Math.min(firstDragX, mouseDragLastX),
1710 Math.max(firstDragY, mouseDragLastY)
1711 - Math.min(firstDragY, mouseDragLastY));
1717 public FontMetrics getFontMetrics()
1723 public Image getFadedImage()
1729 public int getFadedImageWidth()
1731 updateFadedImageWidth();
1735 private int[] bounds = new int[2];
1738 public int[] getVisibleVRange()
1740 if (ap != null && ap.getAlabels() != null)
1742 int sOffset = -ap.getAlabels().getScrollOffset();
1743 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1744 bounds[0] = sOffset;
1745 bounds[1] = visHeight;
1755 * Try to ensure any references held are nulled
1757 public void dispose()
1767 * I created the renderer so I will dispose of it
1769 if (renderer != null)
1776 public void propertyChange(PropertyChangeEvent evt)
1778 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1779 // Both scrolling and resizing change viewport ranges: scrolling changes
1780 // both start and end points, but resize only changes end values.
1781 // Here we only want to fastpaint on a scroll, with resize using a normal
1782 // paint, so scroll events are identified as changes to the horizontal or
1783 // vertical start value.
1784 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1786 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1788 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1790 fastPaint(((int[]) evt.getNewValue())[0]
1791 - ((int[]) evt.getOldValue())[0]);
1793 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1800 * computes the visible height of the annotation panel
1802 * @param adjustPanelHeight
1803 * - when false, just adjust existing height according to other
1805 * @param annotationHeight
1806 * @return height to use for the ScrollerPreferredVisibleSize
1808 public int adjustForAlignFrame(boolean adjustPanelHeight,
1809 int annotationHeight)
1812 * Estimate available height in the AlignFrame for alignment +
1813 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1814 * hscroll, status bar, insets.
1816 int stuff = (ap.getViewName() != null ? 30 : 0)
1817 + (Platform.isAMacAndNotJS() ? 120 : 140);
1818 int availableHeight = ap.alignFrame.getHeight() - stuff;
1819 int rowHeight = av.getCharHeight();
1821 if (adjustPanelHeight)
1823 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1826 * If not enough vertical space, maximize annotation height while keeping
1827 * at least two rows of alignment visible
1829 if (annotationHeight + alignmentHeight > availableHeight)
1831 annotationHeight = Math.min(annotationHeight,
1832 availableHeight - 2 * rowHeight);
1837 // maintain same window layout whilst updating sliders
1838 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1839 availableHeight - 2 * rowHeight);
1841 return annotationHeight;