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 || rowIndex[0]>=allAnnotation.length)
640 AlignmentAnnotation clicked = av.getAlignment()
641 .getAlignmentAnnotation()[rowIndex[0]];
642 if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
647 // TODO - use existing threshold to select related sections of matrix
648 GraphLine thr = clicked.getThreshold();
650 int currentX = getColumnForXPos(evt.getX());
651 ContactListI forCurrentX = av.getContactList(clicked, currentX);
652 if (forCurrentX != null)
654 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
655 clicked.graphHeight);
656 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset);
660 * start and end range corresponding to the row range under the mouse at
664 fr = Math.min(cXci.cStart, cXci.cEnd);
665 to = Math.max(cXci.cStart, cXci.cEnd);
667 // double click selects the whole group
668 if (evt.getClickCount() == 2)
670 ContactMatrixI matrix = av.getContactMatrix(clicked);
674 // simplest approach is to select all group containing column
675 if (matrix.hasGroups())
677 SequenceI rseq = clicked.sequenceRef;
678 BitSet grp = new BitSet();
679 grp.or(matrix.getGroupsFor(currentX));
680 // TODO: cXci needs to be mapped to real groups
681 for (int c = fr; c <= to; c++)
683 BitSet additionalGrp = matrix.getGroupsFor(c);
684 grp.or(additionalGrp);
687 HiddenColumns hc = av.getAlignment().getHiddenColumns();
688 ColumnSelection cs = av.getColumnSelection();
690 for (int p = grp.nextSetBit(0); p >= 0; p = grp
693 if (matrix instanceof MappableContactMatrixI)
695 // find the end of this run of set bits
696 int nextp = grp.nextClearBit(p) - 1;
697 int[] pos = ((MappableContactMatrixI) matrix)
698 .getMappedPositionsFor(rseq, p, nextp);
703 for (int pos_p = pos[0]; pos_p <= pos[1]; pos_p++)
705 int col = rseq.findIndex(pos_p) - 1;
706 if (col >= 0 && (!av.hasHiddenColumns()
707 || hc.isVisible(col)))
716 int offp = (rseq != null)
717 ? rseq.findIndex(rseq.getStart() - 1 + p)
720 if (!av.hasHiddenColumns() || hc.isVisible(offp))
727 // possible alternative for interactive selection - threshold
728 // gives 'ceiling' for forming a cluster
729 // when a row+column is selected, farthest common ancestor less
730 // than thr is used to compute cluster
736 // select corresponding range in segment under mouse
738 int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
741 av.getColumnSelection().addRangeOfElements(rng, true);
743 av.getColumnSelection().addElement(currentX);
746 // and also select everything lower than the max range adjacent
748 if (evt.isControlDown()
749 && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
752 ContactRange cr = forCurrentX.getRangeFor(fr, to);
754 // TODO: could use GraphLine instead of arbitrary picking
755 // TODO: could report mean/median/variance for partitions
756 // (contiguous selected vs unselected regions and inter-contig
758 // controls feathering - what other elements in row/column
760 double thresh = cr.getMean()
761 + (cr.getMax() - cr.getMean()) * .15;
764 cval = forCurrentX.getContactAt(c);
765 if (// cr.getMin() <= cval &&
768 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
771 av.getColumnSelection().addRangeOfElements(cols, true);
781 while (c < forCurrentX.getContactHeight())
783 cval = forCurrentX.getContactAt(c);
784 if (// cr.getMin() <= cval &&
787 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
790 av.getColumnSelection().addRangeOfElements(cols, true);
805 ap.paintAlignment(false, false);
806 PaintRefresher.Refresh(ap, av.getSequenceSetId());
812 * Construct and display a context menu at the right-click position
817 void showPopupMenu(final int y, int x)
819 if (av.getColumnSelection() == null
820 || av.getColumnSelection().isEmpty())
825 JPopupMenu pop = new JPopupMenu(
826 MessageManager.getString("label.structure_type"));
829 * Just display the needed structure options
831 if (av.getAlignment().isNucleotide())
833 item = new JMenuItem(STEM);
834 item.addActionListener(this);
839 item = new JMenuItem(HELIX);
840 item.addActionListener(this);
842 item = new JMenuItem(SHEET);
843 item.addActionListener(this);
846 item = new JMenuItem(LABEL);
847 item.addActionListener(this);
849 item = new JMenuItem(COLOUR);
850 item.addActionListener(this);
852 item = new JMenuItem(REMOVE);
853 item.addActionListener(this);
855 pop.show(this, x, y);
859 * Action on mouse up is to clear mouse drag data and call mouseReleased on
860 * ScalePanel, to deal with defining the selection group (if any) defined by
866 public void mouseReleased(MouseEvent evt)
868 if (dragMode == DragMode.MatrixSelect)
870 matrixSelectRange(evt);
877 mouseDragging = false;
878 if (dragMode == DragMode.Resize)
880 ap.adjustAnnotationHeight();
882 dragMode = DragMode.Undefined;
883 if (!matrix_clicked(evt))
885 ap.getScalePanel().mouseReleased(evt);
889 * isPopupTrigger is set in mouseReleased on Windows
890 * (in mousePressed on Mac)
892 if (evt.isPopupTrigger() && activeRow != -1)
894 showPopupMenu(evt.getY(), evt.getX());
906 public void mouseEntered(MouseEvent evt)
908 this.mouseDragging = false;
909 ap.getScalePanel().mouseEntered(evt);
913 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
914 * with column selection on a mouse drag
919 public void mouseExited(MouseEvent evt)
921 ap.getScalePanel().mouseExited(evt);
925 * Action on starting or continuing a mouse drag. There are two possible
928 * <li>drag up or down on a graphed annotation increases or decreases the
929 * height of the graph</li>
930 * <li>dragging left or right selects the columns dragged across</li>
932 * A drag on a graph annotation is treated as column selection if it starts
933 * with more horizontal than vertical movement, and as resize if it starts
934 * with more vertical than horizontal movement. Once started, the drag does
940 public void mouseDragged(MouseEvent evt)
943 * if dragMode is Undefined:
944 * - set to Select if dx > dy
945 * - set to Resize if dy > dx
946 * - do nothing if dx == dy
948 final int x = evt.getX();
949 final int y = evt.getY();
950 if (dragMode == DragMode.Undefined)
952 int dx = Math.abs(x - mouseDragLastX);
953 int dy = Math.abs(y - mouseDragLastY);
954 if (graphStretch == -1 || dx > dy)
957 * mostly horizontal drag, or not a graph annotation
959 dragMode = DragMode.Select;
964 * mostly vertical drag
966 dragMode = DragMode.Resize;
967 notJustOne = evt.isShiftDown();
970 * but could also be a matrix drag
972 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
973 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
976 * dragging in a matrix
978 dragMode = DragMode.MatrixSelect;
979 firstDragX = mouseDragLastX;
980 firstDragY = mouseDragLastY;
985 if (dragMode == DragMode.Undefined)
989 * drag is diagonal - defer deciding whether to
990 * treat as up/down or left/right
997 if (dragMode == DragMode.Resize)
1000 * resize graph annotation if mouse was dragged up or down
1002 int deltaY = mouseDragLastY - evt.getY();
1005 AlignmentAnnotation graphAnnotation = av.getAlignment()
1006 .getAlignmentAnnotation()[graphStretch];
1007 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
1010 for (AlignmentAnnotation similar : av.getAlignment()
1011 .findAnnotations(null, graphAnnotation.getCalcId(),
1012 graphAnnotation.label))
1014 similar.graphHeight = newHeight;
1020 graphAnnotation.graphHeight = newHeight;
1022 adjustPanelHeight();
1023 ap.paintAlignment(false, false);
1026 else if (dragMode == DragMode.MatrixSelect)
1029 * TODO draw a rubber band for range
1033 ap.paintAlignment(false, false);
1038 * for mouse drag left or right, delegate to
1039 * ScalePanel to adjust the column selection
1041 ap.getScalePanel().mouseDragged(evt);
1050 public void matrixSelectRange(MouseEvent evt)
1053 * get geometry of drag
1055 int fromY = Math.min(firstDragY, evt.getY());
1056 int toY = Math.max(firstDragY, evt.getY());
1057 int fromX = Math.min(firstDragX, evt.getX());
1058 int toX = Math.max(firstDragX, evt.getX());
1060 int deltaY = toY - fromY;
1061 int deltaX = toX - fromX;
1063 int[] rowIndex = getRowIndexAndOffset(fromY,
1064 av.getAlignment().getAlignmentAnnotation());
1065 int[] toRowIndex = getRowIndexAndOffset(toY,
1066 av.getAlignment().getAlignmentAnnotation());
1068 if (rowIndex == null || toRowIndex == null)
1070 jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1073 if (rowIndex[0] != toRowIndex[0])
1076 .trace("Drag went to another row. needs to be clipped");
1079 // rectangular selection on matrix style annotation
1080 AlignmentAnnotation cma = av.getAlignment()
1081 .getAlignmentAnnotation()[rowIndex[0]];
1083 int lastX = getColumnForXPos(fromX);
1084 int currentX = getColumnForXPos(toX);
1085 int fromXc = Math.min(lastX, currentX);
1086 int toXc = Math.max(lastX, currentX);
1087 ContactListI forFromX = av.getContactList(cma, fromXc);
1088 ContactListI forToX = av.getContactList(cma, toXc);
1090 if (forFromX != null && forToX != null)
1092 // FIXME will need two ContactGeometry objects when handling contact matrices with differing numbers of rows at each
1094 ContactGeometry xcgeom = new ContactGeometry(forFromX,
1096 ContactGeometry.contactInterval lastXci = xcgeom
1097 .mapFor(rowIndex[1]);
1098 ContactGeometry.contactInterval cXci = xcgeom.mapFor(rowIndex[1] + deltaY);
1100 // mark rectangular region formed by drag
1101 jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
1102 + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
1103 + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1105 fr = Math.min(lastXci.cStart, cXci.cStart);
1106 to = Math.max(lastXci.cEnd, cXci.cEnd);
1107 int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
1108 if (mappedPos != null)
1110 jalview.bin.Console.trace("Marking " + fr + " to " + to
1111 + " mapping to sequence positions " + mappedPos[0] + " to "
1113 for (int pair = 0; pair < mappedPos.length; pair += 2)
1115 for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1117 // if (cma.sequenceRef != null)
1119 // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1120 // av.getColumnSelection().addElement(col);
1124 av.getColumnSelection().addElement(c-1);
1128 fr = Math.min(lastX, currentX);
1129 to = Math.max(lastX, currentX);
1131 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1132 for (int c = fr; c <= to; c++)
1134 av.getColumnSelection().addElement(c);
1141 * Constructs the tooltip, and constructs and displays a status message, for
1142 * the current mouse position
1147 public void mouseMoved(MouseEvent evt)
1149 int yPos = evt.getY();
1150 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1151 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1152 int row = rowAndOffset[0];
1156 this.setToolTipText(null);
1160 int column = getColumnForXPos(evt.getX());
1162 AlignmentAnnotation ann = aa[row];
1163 if (row > -1 && ann.annotations != null
1164 && column < ann.annotations.length)
1166 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1168 setToolTipText(toolTip == null ? null
1169 : JvSwingUtils.wrapTooltip(true, toolTip));
1170 String msg = getStatusMessage(av.getAlignment(), column, ann,
1171 rowAndOffset[1], av);
1172 ap.alignFrame.setStatus(msg);
1176 this.setToolTipText(null);
1177 ap.alignFrame.setStatus(" ");
1181 private int getColumnForXPos(int x)
1183 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1184 column = Math.min(column, av.getRanges().getEndRes());
1186 if (av.hasHiddenColumns())
1188 column = av.getAlignment().getHiddenColumns()
1189 .visibleToAbsoluteColumn(column);
1195 * Answers the index in the annotations array of the visible annotation at the
1196 * given y position. This is done by adding the heights of visible annotations
1197 * until the y position has been exceeded. Answers -1 if no annotations are
1198 * visible, or the y position is below all annotations.
1204 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1210 return getRowIndexAndOffset(yPos, aa)[0];
1213 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1215 int[] res = new int[2];
1223 int height = 0, lheight = 0;
1224 for (int i = 0; i < aa.length; i++)
1229 height += aa[i].height;
1236 res[1] = yPos-lheight;
1244 * Answers a tooltip for the annotation at the current mouse position, not
1245 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1251 * @param rowAndOffset
1253 static String buildToolTip(AlignmentAnnotation ann, int column,
1254 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1257 String tooltip = null;
1258 if (ann.graphGroup > -1)
1260 StringBuilder tip = new StringBuilder(32);
1261 boolean first = true;
1262 for (int i = 0; i < anns.length; i++)
1264 if (anns[i].graphGroup == ann.graphGroup
1265 && anns[i].annotations[column] != null)
1272 tip.append(anns[i].label);
1273 String description = anns[i].annotations[column].description;
1274 if (description != null && description.length() > 0)
1276 tip.append(" ").append(description);
1280 tooltip = first ? null : tip.toString();
1282 else if (column < ann.annotations.length
1283 && ann.annotations[column] != null)
1285 tooltip = ann.annotations[column].description;
1287 // TODO abstract tooltip generator so different implementations can be built
1288 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1290 if (rowAndOffset>=ann.graphHeight)
1294 ContactListI clist = av.getContactList(ann, column);
1297 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1298 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1299 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1300 StringBuilder tooltipb = new StringBuilder();
1301 tooltipb.append("Contact from ")
1302 .append(clist.getPosition()).append(", [").append(ci.cStart).append(" - ").append(ci.cEnd).append("]").append("<br/>Mean:");
1303 Format.appendPercentage(tooltipb, (float)cr.getMean(),2);
1304 tooltip = tooltipb.toString();
1305 int col = ann.sequenceRef.findPosition(column);
1306 int[][] highlightPos;
1307 int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1308 if (mappedPos != null)
1310 highlightPos = new int[1 + mappedPos.length][2];
1311 highlightPos[0] = new int[] { col, col };
1312 for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1314 highlightPos[h][0] = ann.sequenceRef
1315 .findPosition(mappedPos[p] - 1);
1316 highlightPos[h][1] = ann.sequenceRef
1317 .findPosition(mappedPos[p + 1] - 1);
1322 highlightPos = new int[][] { new int[] { col, col } };
1324 ap.getStructureSelectionManager()
1325 .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1332 * Constructs and returns the status bar message
1337 * @param rowAndOffset
1339 static String getStatusMessage(AlignmentI al, int column,
1340 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1343 * show alignment column and annotation description if any
1345 StringBuilder text = new StringBuilder(32);
1346 text.append(MessageManager.getString("label.column")).append(" ")
1347 .append(column + 1);
1349 if (column < ann.annotations.length && ann.annotations[column] != null)
1351 String description = ann.annotations[column].description;
1352 if (description != null && description.trim().length() > 0)
1354 text.append(" ").append(description);
1359 * if the annotation is sequence-specific, show the sequence number
1360 * in the alignment, and (if not a gap) the residue and position
1362 SequenceI seqref = ann.sequenceRef;
1365 int seqIndex = al.findIndex(seqref);
1368 text.append(", ").append(MessageManager.getString("label.sequence"))
1369 .append(" ").append(seqIndex + 1);
1370 char residue = seqref.getCharAt(column);
1371 if (!Comparison.isGap(residue))
1375 if (al.isNucleotide())
1377 name = ResidueProperties.nucleotideName
1378 .get(String.valueOf(residue));
1379 text.append(" Nucleotide: ")
1380 .append(name != null ? name : residue);
1384 name = 'X' == residue ? "X"
1385 : ('*' == residue ? "STOP"
1386 : ResidueProperties.aa2Triplet
1387 .get(String.valueOf(residue)));
1388 text.append(" Residue: ").append(name != null ? name : residue);
1390 int residuePos = seqref.findPosition(column);
1391 text.append(" (").append(residuePos).append(")");
1396 return text.toString();
1406 public void mouseClicked(MouseEvent evt)
1408 // if (activeRow != -1)
1410 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1411 // AlignmentAnnotation anot = aa[activeRow];
1415 // TODO mouseClicked-content and drawCursor are quite experimental!
1416 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1419 int pady = av.getCharHeight() / 5;
1421 graphics.setColor(Color.black);
1422 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1424 if (av.validCharWidth)
1426 graphics.setColor(Color.white);
1428 char s = seq.getCharAt(res);
1430 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1431 graphics.drawString(String.valueOf(s), charOffset + x1,
1432 (y1 + av.getCharHeight()) - pady);
1437 private volatile boolean imageFresh = false;
1439 private Rectangle visibleRect = new Rectangle(),
1440 clipBounds = new Rectangle();
1449 public void paintComponent(Graphics g)
1452 // BH: note that this method is generally recommended to
1453 // call super.paintComponent(g). Otherwise, the children of this
1454 // component will not be rendered. That is not needed here
1455 // because AnnotationPanel does not have any children. It is
1456 // just a JPanel contained in a JViewPort.
1458 computeVisibleRect(visibleRect);
1460 g.setColor(Color.white);
1461 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1465 // BH 2018 optimizing generation of new Rectangle().
1467 || (visibleRect.width != (clipBounds = g
1468 .getClipBounds(clipBounds)).width)
1469 || (visibleRect.height != clipBounds.height))
1472 g.drawImage(image, 0, 0, this);
1477 updateFadedImageWidth();
1483 if (image == null || imgWidth != image.getWidth(this)
1484 || image.getHeight(this) != getHeight())
1486 boolean tried = false;
1488 while (image == null && !tried)
1492 image = new BufferedImage(imgWidth,
1493 ap.getAnnotationPanel().getHeight(),
1494 BufferedImage.TYPE_INT_RGB);
1496 } catch (IllegalArgumentException exc)
1498 jalview.bin.Console.errPrintln(
1499 "Serious issue with viewport geometry imgWidth requested was "
1502 } catch (OutOfMemoryError oom)
1507 } catch (Exception x)
1512 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1518 gg = (Graphics2D) image.getGraphics();
1522 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1523 RenderingHints.VALUE_ANTIALIAS_ON);
1526 gg.setFont(av.getFont());
1527 fm = gg.getFontMetrics();
1528 gg.setColor(Color.white);
1529 gg.fillRect(0, 0, imgWidth, image.getHeight());
1534 gg = (Graphics2D) image.getGraphics();
1538 drawComponent(gg, av.getRanges().getStartRes(),
1539 av.getRanges().getEndRes() + 1);
1542 g.drawImage(image, 0, 0, this);
1545 public void updateFadedImageWidth()
1547 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1548 + 1) * av.getCharWidth();
1553 * set true to enable redraw timing debug output on stderr
1555 private final boolean debugRedraw = false;
1558 * non-Thread safe repaint
1561 * repaint with horizontal shift in alignment
1563 public void fastPaint(int horizontal)
1565 if ((horizontal == 0) || image == null
1566 || av.getAlignment().getAlignmentAnnotation() == null
1567 || av.getAlignment().getAlignmentAnnotation().length < 1
1568 || av.isCalcInProgress())
1574 int sr = av.getRanges().getStartRes();
1575 int er = av.getRanges().getEndRes() + 1;
1578 Graphics2D gg = (Graphics2D) image.getGraphics();
1580 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1582 // scroll is less than imgWidth away so can re-use buffered graphics
1583 gg.copyArea(0, 0, imgWidth, getHeight(),
1584 -horizontal * av.getCharWidth(), 0);
1586 if (horizontal > 0) // scrollbar pulled right, image to the left
1588 transX = (er - sr - horizontal) * av.getCharWidth();
1589 sr = er - horizontal;
1591 else if (horizontal < 0)
1593 er = sr - horizontal;
1596 gg.translate(transX, 0);
1598 drawComponent(gg, sr, er);
1600 gg.translate(-transX, 0);
1606 // Call repaint on alignment panel so that repaints from other alignment
1607 // panel components can be aggregated. Otherwise performance of the overview
1608 // window and others may be adversely affected.
1609 av.getAlignPanel().repaint();
1612 private volatile boolean lastImageGood = false;
1624 public void drawComponent(Graphics g, int startRes, int endRes)
1626 BufferedImage oldFaded = fadedImage;
1627 if (av.isCalcInProgress())
1631 lastImageGood = false;
1634 // We'll keep a record of the old image,
1635 // and draw a faded image until the calculation
1638 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1639 || fadedImage.getHeight() != image.getHeight()))
1641 // jalview.bin.Console.errPrintln("redraw faded image ("+(fadedImage==null ?
1642 // "null image" : "") + " lastGood="+lastImageGood+")");
1643 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1644 BufferedImage.TYPE_INT_RGB);
1646 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1648 fadedG.setColor(Color.white);
1649 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1651 fadedG.setComposite(
1652 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1653 fadedG.drawImage(image, 0, 0, this);
1656 // make sure we don't overwrite the last good faded image until all
1657 // calculations have finished
1658 lastImageGood = false;
1663 if (fadedImage != null)
1665 oldFaded = fadedImage;
1670 g.setColor(Color.white);
1671 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1673 g.setFont(av.getFont());
1676 fm = g.getFontMetrics();
1679 if ((av.getAlignment().getAlignmentAnnotation() == null)
1680 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1682 g.setColor(Color.white);
1683 g.fillRect(0, 0, getWidth(), getHeight());
1684 g.setColor(Color.black);
1685 if (av.validCharWidth)
1687 g.drawString(MessageManager
1688 .getString("label.alignment_has_no_annotations"), 20, 15);
1693 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1695 if (!lastImageGood && fadedImage == null)
1697 fadedImage = oldFaded;
1699 if (dragMode == DragMode.MatrixSelect)
1701 g.setColor(Color.yellow);
1702 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1703 Math.min(firstDragY, mouseDragLastY),
1704 Math.max(firstDragX, mouseDragLastX)
1705 - Math.min(firstDragX, mouseDragLastX),
1706 Math.max(firstDragY, mouseDragLastY)
1707 - Math.min(firstDragY, mouseDragLastY));
1713 public FontMetrics getFontMetrics()
1719 public Image getFadedImage()
1725 public int getFadedImageWidth()
1727 updateFadedImageWidth();
1731 private int[] bounds = new int[2];
1734 public int[] getVisibleVRange()
1736 if (ap != null && ap.getAlabels() != null)
1738 int sOffset = -ap.getAlabels().getScrollOffset();
1739 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1740 bounds[0] = sOffset;
1741 bounds[1] = visHeight;
1751 * Try to ensure any references held are nulled
1753 public void dispose()
1763 * I created the renderer so I will dispose of it
1765 if (renderer != null)
1772 public void propertyChange(PropertyChangeEvent evt)
1774 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1775 // Both scrolling and resizing change viewport ranges: scrolling changes
1776 // both start and end points, but resize only changes end values.
1777 // Here we only want to fastpaint on a scroll, with resize using a normal
1778 // paint, so scroll events are identified as changes to the horizontal or
1779 // vertical start value.
1780 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1782 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1784 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1786 fastPaint(((int[]) evt.getNewValue())[0]
1787 - ((int[]) evt.getOldValue())[0]);
1789 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1796 * computes the visible height of the annotation panel
1798 * @param adjustPanelHeight
1799 * - when false, just adjust existing height according to other
1801 * @param annotationHeight
1802 * @return height to use for the ScrollerPreferredVisibleSize
1804 public int adjustForAlignFrame(boolean adjustPanelHeight,
1805 int annotationHeight)
1808 * Estimate available height in the AlignFrame for alignment +
1809 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1810 * hscroll, status bar, insets.
1812 int stuff = (ap.getViewName() != null ? 30 : 0)
1813 + (Platform.isAMacAndNotJS() ? 120 : 140);
1814 int availableHeight = ap.alignFrame.getHeight() - stuff;
1815 int rowHeight = av.getCharHeight();
1817 if (adjustPanelHeight)
1819 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1822 * If not enough vertical space, maximize annotation height while keeping
1823 * at least two rows of alignment visible
1825 if (annotationHeight + alignmentHeight > availableHeight)
1827 annotationHeight = Math.min(annotationHeight,
1828 availableHeight - 2 * rowHeight);
1833 // maintain same window layout whilst updating sliders
1834 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1835 availableHeight - 2 * rowHeight);
1837 return annotationHeight;