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.MessageManager;
72 import jalview.util.Platform;
73 import jalview.viewmodel.ViewportListenerI;
74 import jalview.viewmodel.ViewportRanges;
75 import jalview.ws.datamodel.MappableContactMatrixI;
76 import jalview.ws.datamodel.alphafold.PAEContactMatrix;
79 * AnnotationPanel displays visible portion of annotation rows below unwrapped
85 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
86 MouseListener, MouseWheelListener, MouseMotionListener,
87 ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
91 Select, Resize, Undefined, MatrixSelect
94 String HELIX = MessageManager.getString("label.helix");
96 String SHEET = MessageManager.getString("label.sheet");
99 * For RNA secondary structure "stems" aka helices
101 String STEM = MessageManager.getString("label.rna_helix");
103 String LABEL = MessageManager.getString("label.label");
105 String REMOVE = MessageManager.getString("label.remove_annotation");
107 String COLOUR = MessageManager.getString("action.colour");
109 public final Color HELIX_COLOUR = Color.red.darker();
111 public final Color SHEET_COLOUR = Color.green.darker().darker();
113 public final Color STEM_COLOUR = Color.blue.darker();
116 public AlignViewport av;
120 public int activeRow = -1;
122 public BufferedImage image;
124 public volatile BufferedImage fadedImage;
126 // private Graphics2D gg;
128 public FontMetrics fm;
130 public int imgWidth = 0;
132 boolean fastPaint = false;
134 // Used For mouse Dragging and resizing graphs
135 int graphStretch = -1;
137 int mouseDragLastX = -1;
139 int mouseDragLastY = -1;
145 DragMode dragMode = DragMode.Undefined;
147 boolean mouseDragging = false;
149 // for editing cursor
154 public final AnnotationRenderer renderer;
156 private MouseWheelListener[] _mwl;
158 private boolean notJustOne;
161 * Creates a new AnnotationPanel object.
166 public AnnotationPanel(AlignmentPanel ap)
168 ToolTipManager.sharedInstance().registerComponent(this);
169 ToolTipManager.sharedInstance().setInitialDelay(0);
170 ToolTipManager.sharedInstance().setDismissDelay(10000);
173 this.setLayout(null);
174 addMouseListener(this);
175 addMouseMotionListener(this);
176 ap.annotationScroller.getVerticalScrollBar()
177 .addAdjustmentListener(this);
178 // save any wheel listeners on the scroller, so we can propagate scroll
180 _mwl = ap.annotationScroller.getMouseWheelListeners();
181 // and then set our own listener to consume all mousewheel events
182 ap.annotationScroller.addMouseWheelListener(this);
183 renderer = new AnnotationRenderer();
185 av.getRanges().addPropertyChangeListener(this);
188 public AnnotationPanel(AlignViewport av)
191 renderer = new AnnotationRenderer();
195 public void mouseWheelMoved(MouseWheelEvent e)
200 double wheelRotation = e.getPreciseWheelRotation();
201 if (wheelRotation > 0)
203 av.getRanges().scrollRight(true);
205 else if (wheelRotation < 0)
207 av.getRanges().scrollRight(false);
212 // TODO: find the correct way to let the event bubble up to
213 // ap.annotationScroller
214 for (MouseWheelListener mwl : _mwl)
218 mwl.mouseWheelMoved(e);
229 public Dimension getPreferredScrollableViewportSize()
231 Dimension ps = getPreferredSize();
232 return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
236 public int getScrollableBlockIncrement(Rectangle visibleRect,
237 int orientation, int direction)
243 public boolean getScrollableTracksViewportHeight()
249 public boolean getScrollableTracksViewportWidth()
255 public int getScrollableUnitIncrement(Rectangle visibleRect,
256 int orientation, int direction)
265 * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
269 public void adjustmentValueChanged(AdjustmentEvent evt)
271 // update annotation label display
272 ap.getAlabels().setScrollOffset(-evt.getValue());
276 * Calculates the height of the annotation displayed in the annotation panel.
277 * Callers should normally call the ap.adjustAnnotationHeight method to ensure
278 * all annotation associated components are updated correctly.
281 public int adjustPanelHeight()
283 int height = av.calcPanelHeight();
284 this.setPreferredSize(new Dimension(1, height));
287 // revalidate only when the alignment panel is fully constructed
301 public void actionPerformed(ActionEvent evt)
303 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
308 Annotation[] anot = aa[activeRow].annotations;
310 if (anot.length < av.getColumnSelection().getMax())
312 Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
314 System.arraycopy(anot, 0, temp, 0, anot.length);
316 aa[activeRow].annotations = anot;
319 String action = evt.getActionCommand();
320 if (action.equals(REMOVE))
322 for (int index : av.getColumnSelection().getSelected())
324 if (av.getAlignment().getHiddenColumns().isVisible(index))
330 else if (action.equals(LABEL))
332 String exMesg = collectAnnotVals(anot, LABEL);
333 String label = JvOptionPane.showInputDialog(
334 MessageManager.getString("label.enter_label"), exMesg);
341 if ((label.length() > 0) && !aa[activeRow].hasText)
343 aa[activeRow].hasText = true;
346 for (int index : av.getColumnSelection().getSelected())
348 if (!av.getAlignment().getHiddenColumns().isVisible(index))
353 if (anot[index] == null)
355 anot[index] = new Annotation(label, "", ' ', 0);
359 anot[index].displayCharacter = label;
363 else if (action.equals(COLOUR))
365 final Annotation[] fAnot = anot;
366 String title = MessageManager
367 .getString("label.select_foreground_colour");
368 ColourChooserListener listener = new ColourChooserListener()
371 public void colourSelected(Color c)
373 HiddenColumns hiddenColumns = av.getAlignment()
375 for (int index : av.getColumnSelection().getSelected())
377 if (hiddenColumns.isVisible(index))
379 if (fAnot[index] == null)
381 fAnot[index] = new Annotation("", "", ' ', 0);
383 fAnot[index].colour = c;
388 JalviewColourChooser.showColourChooser(this, title, Color.black,
392 // HELIX, SHEET or STEM
395 String symbol = "\u03B1"; // alpha
397 if (action.equals(HELIX))
401 else if (action.equals(SHEET))
404 symbol = "\u03B2"; // beta
407 // Added by LML to color stems
408 else if (action.equals(STEM))
411 int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
412 symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
415 if (!aa[activeRow].hasIcons)
417 aa[activeRow].hasIcons = true;
420 String label = JvOptionPane.showInputDialog(MessageManager
421 .getString("label.enter_label_for_the_structure"), symbol);
428 if ((label.length() > 0) && !aa[activeRow].hasText)
430 aa[activeRow].hasText = true;
431 if (action.equals(STEM))
433 aa[activeRow].showAllColLabels = true;
436 for (int index : av.getColumnSelection().getSelected())
438 if (!av.getAlignment().getHiddenColumns().isVisible(index))
443 if (anot[index] == null)
445 anot[index] = new Annotation(label, "", type, 0);
448 anot[index].secondaryStructure = type != 'S' ? type
449 : label.length() == 0 ? ' ' : label.charAt(0);
450 anot[index].displayCharacter = label;
455 av.getAlignment().validateAnnotation(aa[activeRow]);
456 ap.alignmentChanged();
457 ap.alignFrame.setMenusForViewport();
465 * Returns any existing annotation concatenated as a string. For each
466 * annotation, takes the description, if any, else the secondary structure
467 * character (if type is HELIX, SHEET or STEM), else the display character (if
474 private String collectAnnotVals(Annotation[] anots, String type)
476 // TODO is this method wanted? why? 'last' is not used
478 StringBuilder collatedInput = new StringBuilder(64);
480 ColumnSelection viscols = av.getColumnSelection();
481 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
484 * the selection list (read-only view) is in selection order, not
485 * column order; make a copy so we can sort it
487 List<Integer> selected = new ArrayList<>(viscols.getSelected());
488 Collections.sort(selected);
489 for (int index : selected)
491 // always check for current display state - just in case
492 if (!hidden.isVisible(index))
496 String tlabel = null;
497 if (anots[index] != null)
498 { // LML added stem code
499 if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
500 || type.equals(LABEL))
502 tlabel = anots[index].description;
503 if (tlabel == null || tlabel.length() < 1)
505 if (type.equals(HELIX) || type.equals(SHEET)
506 || type.equals(STEM))
508 tlabel = "" + anots[index].secondaryStructure;
512 tlabel = "" + anots[index].displayCharacter;
516 if (tlabel != null && !tlabel.equals(last))
518 if (last.length() > 0)
520 collatedInput.append(" ");
522 collatedInput.append(tlabel);
526 return collatedInput.toString();
530 * Action on right mouse pressed on Mac is to show a pop-up menu for the
531 * annotation. Action on left mouse pressed is to find which annotation is
532 * pressed and mark the start of a column selection or graph resize operation.
537 public void mousePressed(MouseEvent evt)
540 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
545 mouseDragLastX = evt.getX();
546 mouseDragLastY = evt.getY();
549 * add visible annotation heights until we reach the y
550 * position, to find which annotation it is in
555 // todo could reuse getRowIndexAndOffset ?
556 final int y = evt.getY();
558 for (int i = 0; i < aa.length; i++)
562 height += aa[i].height;
571 else if (aa[i].graph != 0)
574 * we have clicked on a resizable graph annotation
577 yOffset = height - y;
584 * isPopupTrigger fires in mousePressed on Mac,
585 * not until mouseRelease on Windows
587 if (evt.isPopupTrigger() && activeRow != -1)
589 showPopupMenu(y, evt.getX());
593 if (graphStretch != -1)
596 if (aa[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP)
598 // data in row has position on y as well as x axis
599 if (evt.isAltDown() || evt.isAltGraphDown())
601 dragMode = DragMode.MatrixSelect;
602 firstDragX = mouseDragLastX;
603 firstDragY = mouseDragLastY;
609 // no row (or row that can be adjusted) was pressed. Simulate a ruler
611 ap.getScalePanel().mousePressed(evt);
616 * checks whether the annotation row under the mouse click evt's handles the
620 * @return false if evt was not handled
622 boolean matrix_clicked(MouseEvent evt)
624 int[] rowIndex = getRowIndexAndOffset(evt.getY(),
625 av.getAlignment().getAlignmentAnnotation());
626 if (rowIndex == null)
629 .error("IMPLEMENTATION ERROR: matrix click out of range.");
632 int yOffset = rowIndex[1];
634 AlignmentAnnotation clicked = av.getAlignment()
635 .getAlignmentAnnotation()[rowIndex[0]];
636 if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
641 // TODO - use existing threshold to select related sections of matrix
642 GraphLine thr = clicked.getThreshold();
644 int currentX = getColumnForXPos(evt.getX());
645 ContactListI forCurrentX = av.getContactList(clicked, currentX);
646 if (forCurrentX != null)
648 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
649 clicked.graphHeight);
650 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset,
653 * start and end range corresponding to the row range under the mouse at
657 fr = Math.min(cXci.cStart, cXci.cEnd);
658 to = Math.max(cXci.cStart, cXci.cEnd);
660 // double click selects the whole group
661 if (evt.getClickCount() == 2)
663 ContactMatrixI matrix = av.getContactMatrix(clicked);
667 // simplest approach is to select all group containing column
668 if (matrix.hasGroups())
670 SequenceI rseq = clicked.sequenceRef;
671 BitSet grp = new BitSet();
672 grp.or(matrix.getGroupsFor(currentX));
673 // TODO: cXci needs to be mapped to real groups
674 for (int c = fr; c <= to; c++)
676 BitSet additionalGrp = matrix.getGroupsFor(c);
677 grp.or(additionalGrp);
680 HiddenColumns hc = av.getAlignment().getHiddenColumns();
681 ColumnSelection cs = av.getColumnSelection();
683 for (int p=grp.nextSetBit(0); p >= 0; p = grp
686 if (matrix instanceof MappableContactMatrixI)
688 // find the end of this run of set bits
689 int nextp = grp.nextClearBit(p)-1;
690 int[] pos = ((MappableContactMatrixI)matrix).getMappedPositionsFor(rseq, p,nextp);
695 for (int pos_p = pos[0];pos_p<=pos[1];pos_p++)
697 int col = rseq.findIndex(pos_p)-1;
698 if (col>=0 && (!av.hasHiddenColumns() || hc.isVisible(col)))
705 int offp = (rseq != null)
706 ? rseq.findIndex(rseq.getStart() - 1 + p)
709 if (!av.hasHiddenColumns() || hc.isVisible(offp))
716 // possible alternative for interactive selection - threshold
717 // gives 'ceiling' for forming a cluster
718 // when a row+column is selected, farthest common ancestor less
719 // than thr is used to compute cluster
725 // select corresponding range in segment under mouse
727 int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
730 av.getColumnSelection().addRangeOfElements(rng, true);
732 av.getColumnSelection().addElement(currentX);
735 // and also select everything lower than the max range adjacent
737 if (evt.isControlDown()
738 && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
741 ContactRange cr = forCurrentX.getRangeFor(fr, to);
743 // TODO: could use GraphLine instead of arbitrary picking
744 // TODO: could report mean/median/variance for partitions
745 // (contiguous selected vs unselected regions and inter-contig
747 // controls feathering - what other elements in row/column
749 double thresh = cr.getMean() + (cr.getMax() - cr.getMean()) * .15;
752 cval = forCurrentX.getContactAt(c);
753 if (// cr.getMin() <= cval &&
756 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
759 av.getColumnSelection().addRangeOfElements(cols, true);
769 while (c < forCurrentX.getContactHeight())
771 cval = forCurrentX.getContactAt(c);
772 if (// cr.getMin() <= cval &&
775 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
778 av.getColumnSelection().addRangeOfElements(cols, true);
791 ap.paintAlignment(false, false);
792 PaintRefresher.Refresh(ap, av.getSequenceSetId());
798 * Construct and display a context menu at the right-click position
803 void showPopupMenu(final int y, int x)
805 if (av.getColumnSelection() == null
806 || av.getColumnSelection().isEmpty())
811 JPopupMenu pop = new JPopupMenu(
812 MessageManager.getString("label.structure_type"));
815 * Just display the needed structure options
817 if (av.getAlignment().isNucleotide())
819 item = new JMenuItem(STEM);
820 item.addActionListener(this);
825 item = new JMenuItem(HELIX);
826 item.addActionListener(this);
828 item = new JMenuItem(SHEET);
829 item.addActionListener(this);
832 item = new JMenuItem(LABEL);
833 item.addActionListener(this);
835 item = new JMenuItem(COLOUR);
836 item.addActionListener(this);
838 item = new JMenuItem(REMOVE);
839 item.addActionListener(this);
841 pop.show(this, x, y);
845 * Action on mouse up is to clear mouse drag data and call mouseReleased on
846 * ScalePanel, to deal with defining the selection group (if any) defined by
852 public void mouseReleased(MouseEvent evt)
854 if (dragMode == DragMode.MatrixSelect)
856 matrixSelectRange(evt);
863 mouseDragging = false;
864 if (dragMode == DragMode.Resize)
866 ap.adjustAnnotationHeight();
868 dragMode = DragMode.Undefined;
869 if (!matrix_clicked(evt))
871 ap.getScalePanel().mouseReleased(evt);
875 * isPopupTrigger is set in mouseReleased on Windows
876 * (in mousePressed on Mac)
878 if (evt.isPopupTrigger() && activeRow != -1)
880 showPopupMenu(evt.getY(), evt.getX());
892 public void mouseEntered(MouseEvent evt)
894 this.mouseDragging = false;
895 ap.getScalePanel().mouseEntered(evt);
899 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
900 * with column selection on a mouse drag
905 public void mouseExited(MouseEvent evt)
907 ap.getScalePanel().mouseExited(evt);
911 * Action on starting or continuing a mouse drag. There are two possible
914 * <li>drag up or down on a graphed annotation increases or decreases the
915 * height of the graph</li>
916 * <li>dragging left or right selects the columns dragged across</li>
918 * A drag on a graph annotation is treated as column selection if it starts
919 * with more horizontal than vertical movement, and as resize if it starts
920 * with more vertical than horizontal movement. Once started, the drag does
926 public void mouseDragged(MouseEvent evt)
929 * if dragMode is Undefined:
930 * - set to Select if dx > dy
931 * - set to Resize if dy > dx
932 * - do nothing if dx == dy
934 final int x = evt.getX();
935 final int y = evt.getY();
936 if (dragMode == DragMode.Undefined)
938 int dx = Math.abs(x - mouseDragLastX);
939 int dy = Math.abs(y - mouseDragLastY);
940 if (graphStretch == -1 || dx > dy)
943 * mostly horizontal drag, or not a graph annotation
945 dragMode = DragMode.Select;
950 * mostly vertical drag
952 dragMode = DragMode.Resize;
953 notJustOne = evt.isShiftDown();
956 * but could also be a matrix drag
958 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
959 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
962 * dragging in a matrix
964 dragMode = DragMode.MatrixSelect;
965 firstDragX = mouseDragLastX;
966 firstDragY = mouseDragLastY;
971 if (dragMode == DragMode.Undefined)
975 * drag is diagonal - defer deciding whether to
976 * treat as up/down or left/right
983 if (dragMode == DragMode.Resize)
986 * resize graph annotation if mouse was dragged up or down
988 int deltaY = mouseDragLastY - evt.getY();
991 AlignmentAnnotation graphAnnotation = av.getAlignment()
992 .getAlignmentAnnotation()[graphStretch];
993 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
996 for (AlignmentAnnotation similar : av.getAlignment()
997 .findAnnotations(null, graphAnnotation.getCalcId(),
998 graphAnnotation.label))
1000 similar.graphHeight = newHeight;
1006 graphAnnotation.graphHeight = newHeight;
1008 adjustPanelHeight();
1009 ap.paintAlignment(false, false);
1012 else if (dragMode == DragMode.MatrixSelect)
1015 * TODO draw a rubber band for range
1019 ap.paintAlignment(false, false);
1024 * for mouse drag left or right, delegate to
1025 * ScalePanel to adjust the column selection
1027 ap.getScalePanel().mouseDragged(evt);
1036 public void matrixSelectRange(MouseEvent evt)
1039 * get geometry of drag
1041 int fromY = Math.min(firstDragY, evt.getY());
1042 int toY = Math.max(firstDragY, evt.getY());
1043 int fromX = Math.min(firstDragX, evt.getX());
1044 int toX = Math.max(firstDragX, evt.getX());
1046 int deltaY = toY - fromY;
1047 int deltaX = toX - fromX;
1049 int[] rowIndex = getRowIndexAndOffset(fromY,
1050 av.getAlignment().getAlignmentAnnotation());
1051 int[] toRowIndex = getRowIndexAndOffset(toY,
1052 av.getAlignment().getAlignmentAnnotation());
1054 if (rowIndex == null || toRowIndex == null)
1056 jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1059 if (rowIndex[0] != toRowIndex[0])
1062 .trace("Drag went to another row. needs to be clipped");
1065 // rectangular selection on matrix style annotation
1066 AlignmentAnnotation cma = av.getAlignment()
1067 .getAlignmentAnnotation()[rowIndex[0]];
1069 int lastX = getColumnForXPos(fromX);
1070 int currentX = getColumnForXPos(toX);
1071 int fromXc = Math.min(lastX, currentX);
1072 int toXc = Math.max(lastX, currentX);
1073 ContactListI forFromX = av.getContactList(cma, fromXc);
1074 ContactListI forToX = av.getContactList(cma, toXc);
1076 if (forFromX != null && forToX != null)
1078 ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
1080 ContactGeometry.contactInterval lastXci = lastXcgeom
1081 .mapFor(rowIndex[1], rowIndex[1] + deltaY);
1083 ContactGeometry cXcgeom = new ContactGeometry(forToX,
1085 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
1086 rowIndex[1] + deltaY);
1088 // mark rectangular region formed by drag
1089 jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
1090 + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
1091 + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1093 fr = Math.min(lastXci.cStart, lastXci.cEnd);
1094 to = Math.max(lastXci.cStart, lastXci.cEnd);
1095 int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
1096 if (mappedPos != null)
1098 jalview.bin.Console.trace("Marking " + fr + " to " + to
1099 + " mapping to sequence positions " + mappedPos[0] + " to "
1101 for (int pair = 0; pair < mappedPos.length; pair += 2)
1103 for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1105 // if (cma.sequenceRef != null)
1107 // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1108 // av.getColumnSelection().addElement(col);
1112 av.getColumnSelection().addElement(c);
1116 // and again for most recent corner of drag
1117 fr = Math.min(cXci.cStart, cXci.cEnd);
1118 to = Math.max(cXci.cStart, cXci.cEnd);
1119 mappedPos = forFromX.getMappedPositionsFor(fr, to);
1120 if (mappedPos != null)
1122 for (int pair = 0; pair < mappedPos.length; pair += 2)
1124 jalview.bin.Console.trace("Marking " + fr + " to " + to
1125 + " mapping to sequence positions " + mappedPos[pair]
1126 + " to " + mappedPos[pair + 1]);
1127 for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1129 // if (cma.sequenceRef != null)
1132 // cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1133 // av.getColumnSelection().addElement(col);
1137 av.getColumnSelection().addElement(c);
1142 fr = Math.min(lastX, currentX);
1143 to = Math.max(lastX, currentX);
1145 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1146 for (int c = fr; c <= to; c++)
1148 av.getColumnSelection().addElement(c);
1155 * Constructs the tooltip, and constructs and displays a status message, for
1156 * the current mouse position
1161 public void mouseMoved(MouseEvent evt)
1163 int yPos = evt.getY();
1164 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1165 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1166 int row = rowAndOffset[0];
1170 this.setToolTipText(null);
1174 int column = getColumnForXPos(evt.getX());
1176 AlignmentAnnotation ann = aa[row];
1177 if (row > -1 && ann.annotations != null
1178 && column < ann.annotations.length)
1180 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1182 setToolTipText(toolTip == null ? null
1183 : JvSwingUtils.wrapTooltip(true, toolTip));
1184 String msg = getStatusMessage(av.getAlignment(), column, ann,
1185 rowAndOffset[1], av);
1186 ap.alignFrame.setStatus(msg);
1190 this.setToolTipText(null);
1191 ap.alignFrame.setStatus(" ");
1195 private int getColumnForXPos(int x)
1197 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1198 column = Math.min(column, av.getRanges().getEndRes());
1200 if (av.hasHiddenColumns())
1202 column = av.getAlignment().getHiddenColumns()
1203 .visibleToAbsoluteColumn(column);
1209 * Answers the index in the annotations array of the visible annotation at the
1210 * given y position. This is done by adding the heights of visible annotations
1211 * until the y position has been exceeded. Answers -1 if no annotations are
1212 * visible, or the y position is below all annotations.
1218 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1224 return getRowIndexAndOffset(yPos, aa)[0];
1227 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1229 int[] res = new int[2];
1237 int height = 0, lheight = 0;
1238 for (int i = 0; i < aa.length; i++)
1243 height += aa[i].height;
1250 res[1] = yPos-lheight;
1258 * Answers a tooltip for the annotation at the current mouse position, not
1259 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1265 * @param rowAndOffset
1267 static String buildToolTip(AlignmentAnnotation ann, int column,
1268 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1271 String tooltip = null;
1272 if (ann.graphGroup > -1)
1274 StringBuilder tip = new StringBuilder(32);
1275 boolean first = true;
1276 for (int i = 0; i < anns.length; i++)
1278 if (anns[i].graphGroup == ann.graphGroup
1279 && anns[i].annotations[column] != null)
1286 tip.append(anns[i].label);
1287 String description = anns[i].annotations[column].description;
1288 if (description != null && description.length() > 0)
1290 tip.append(" ").append(description);
1294 tooltip = first ? null : tip.toString();
1296 else if (column < ann.annotations.length
1297 && ann.annotations[column] != null)
1299 tooltip = ann.annotations[column].description;
1301 // TODO abstract tooltip generator so different implementations can be built
1302 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1304 if (rowAndOffset>=ann.graphHeight)
1308 ContactListI clist = av.getContactList(ann, column);
1311 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1312 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1313 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1314 tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
1315 + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
1317 int col = ann.sequenceRef.findPosition(column);
1318 int[][] highlightPos;
1319 int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1320 if (mappedPos != null)
1322 highlightPos = new int[1 + mappedPos.length][2];
1323 highlightPos[0] = new int[] { col, col };
1324 for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1326 highlightPos[h][0] = ann.sequenceRef
1327 .findPosition(mappedPos[p] - 1);
1328 highlightPos[h][1] = ann.sequenceRef
1329 .findPosition(mappedPos[p + 1] - 1);
1334 highlightPos = new int[][] { new int[] { col, col } };
1336 ap.getStructureSelectionManager()
1337 .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1344 * Constructs and returns the status bar message
1349 * @param rowAndOffset
1351 static String getStatusMessage(AlignmentI al, int column,
1352 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1355 * show alignment column and annotation description if any
1357 StringBuilder text = new StringBuilder(32);
1358 text.append(MessageManager.getString("label.column")).append(" ")
1359 .append(column + 1);
1361 if (column < ann.annotations.length && ann.annotations[column] != null)
1363 String description = ann.annotations[column].description;
1364 if (description != null && description.trim().length() > 0)
1366 text.append(" ").append(description);
1371 * if the annotation is sequence-specific, show the sequence number
1372 * in the alignment, and (if not a gap) the residue and position
1374 SequenceI seqref = ann.sequenceRef;
1377 int seqIndex = al.findIndex(seqref);
1380 text.append(", ").append(MessageManager.getString("label.sequence"))
1381 .append(" ").append(seqIndex + 1);
1382 char residue = seqref.getCharAt(column);
1383 if (!Comparison.isGap(residue))
1387 if (al.isNucleotide())
1389 name = ResidueProperties.nucleotideName
1390 .get(String.valueOf(residue));
1391 text.append(" Nucleotide: ")
1392 .append(name != null ? name : residue);
1396 name = 'X' == residue ? "X"
1397 : ('*' == residue ? "STOP"
1398 : ResidueProperties.aa2Triplet
1399 .get(String.valueOf(residue)));
1400 text.append(" Residue: ").append(name != null ? name : residue);
1402 int residuePos = seqref.findPosition(column);
1403 text.append(" (").append(residuePos).append(")");
1408 return text.toString();
1418 public void mouseClicked(MouseEvent evt)
1420 // if (activeRow != -1)
1422 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1423 // AlignmentAnnotation anot = aa[activeRow];
1427 // TODO mouseClicked-content and drawCursor are quite experimental!
1428 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1431 int pady = av.getCharHeight() / 5;
1433 graphics.setColor(Color.black);
1434 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1436 if (av.validCharWidth)
1438 graphics.setColor(Color.white);
1440 char s = seq.getCharAt(res);
1442 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1443 graphics.drawString(String.valueOf(s), charOffset + x1,
1444 (y1 + av.getCharHeight()) - pady);
1449 private volatile boolean imageFresh = false;
1451 private Rectangle visibleRect = new Rectangle(),
1452 clipBounds = new Rectangle();
1461 public void paintComponent(Graphics g)
1464 // BH: note that this method is generally recommended to
1465 // call super.paintComponent(g). Otherwise, the children of this
1466 // component will not be rendered. That is not needed here
1467 // because AnnotationPanel does not have any children. It is
1468 // just a JPanel contained in a JViewPort.
1470 computeVisibleRect(visibleRect);
1472 g.setColor(Color.white);
1473 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1477 // BH 2018 optimizing generation of new Rectangle().
1479 || (visibleRect.width != (clipBounds = g
1480 .getClipBounds(clipBounds)).width)
1481 || (visibleRect.height != clipBounds.height))
1484 g.drawImage(image, 0, 0, this);
1489 updateFadedImageWidth();
1495 if (image == null || imgWidth != image.getWidth(this)
1496 || image.getHeight(this) != getHeight())
1498 boolean tried = false;
1500 while (image == null && !tried)
1504 image = new BufferedImage(imgWidth,
1505 ap.getAnnotationPanel().getHeight(),
1506 BufferedImage.TYPE_INT_RGB);
1508 } catch (IllegalArgumentException exc)
1511 "Serious issue with viewport geometry imgWidth requested was "
1514 } catch (OutOfMemoryError oom)
1519 } catch (Exception x)
1524 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1530 gg = (Graphics2D) image.getGraphics();
1534 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1535 RenderingHints.VALUE_ANTIALIAS_ON);
1538 gg.setFont(av.getFont());
1539 fm = gg.getFontMetrics();
1540 gg.setColor(Color.white);
1541 gg.fillRect(0, 0, imgWidth, image.getHeight());
1546 gg = (Graphics2D) image.getGraphics();
1550 drawComponent(gg, av.getRanges().getStartRes(),
1551 av.getRanges().getEndRes() + 1);
1554 g.drawImage(image, 0, 0, this);
1557 public void updateFadedImageWidth()
1559 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1560 + 1) * av.getCharWidth();
1565 * set true to enable redraw timing debug output on stderr
1567 private final boolean debugRedraw = false;
1570 * non-Thread safe repaint
1573 * repaint with horizontal shift in alignment
1575 public void fastPaint(int horizontal)
1577 if ((horizontal == 0) || image == null
1578 || av.getAlignment().getAlignmentAnnotation() == null
1579 || av.getAlignment().getAlignmentAnnotation().length < 1
1580 || av.isCalcInProgress())
1586 int sr = av.getRanges().getStartRes();
1587 int er = av.getRanges().getEndRes() + 1;
1590 Graphics2D gg = (Graphics2D) image.getGraphics();
1592 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1594 // scroll is less than imgWidth away so can re-use buffered graphics
1595 gg.copyArea(0, 0, imgWidth, getHeight(),
1596 -horizontal * av.getCharWidth(), 0);
1598 if (horizontal > 0) // scrollbar pulled right, image to the left
1600 transX = (er - sr - horizontal) * av.getCharWidth();
1601 sr = er - horizontal;
1603 else if (horizontal < 0)
1605 er = sr - horizontal;
1608 gg.translate(transX, 0);
1610 drawComponent(gg, sr, er);
1612 gg.translate(-transX, 0);
1618 // Call repaint on alignment panel so that repaints from other alignment
1619 // panel components can be aggregated. Otherwise performance of the overview
1620 // window and others may be adversely affected.
1621 av.getAlignPanel().repaint();
1624 private volatile boolean lastImageGood = false;
1636 public void drawComponent(Graphics g, int startRes, int endRes)
1638 BufferedImage oldFaded = fadedImage;
1639 if (av.isCalcInProgress())
1643 lastImageGood = false;
1646 // We'll keep a record of the old image,
1647 // and draw a faded image until the calculation
1650 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1651 || fadedImage.getHeight() != image.getHeight()))
1653 // System.err.println("redraw faded image ("+(fadedImage==null ?
1654 // "null image" : "") + " lastGood="+lastImageGood+")");
1655 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1656 BufferedImage.TYPE_INT_RGB);
1658 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1660 fadedG.setColor(Color.white);
1661 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1663 fadedG.setComposite(
1664 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1665 fadedG.drawImage(image, 0, 0, this);
1668 // make sure we don't overwrite the last good faded image until all
1669 // calculations have finished
1670 lastImageGood = false;
1675 if (fadedImage != null)
1677 oldFaded = fadedImage;
1682 g.setColor(Color.white);
1683 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1685 g.setFont(av.getFont());
1688 fm = g.getFontMetrics();
1691 if ((av.getAlignment().getAlignmentAnnotation() == null)
1692 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1694 g.setColor(Color.white);
1695 g.fillRect(0, 0, getWidth(), getHeight());
1696 g.setColor(Color.black);
1697 if (av.validCharWidth)
1699 g.drawString(MessageManager
1700 .getString("label.alignment_has_no_annotations"), 20, 15);
1705 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1707 if (!lastImageGood && fadedImage == null)
1709 fadedImage = oldFaded;
1711 if (dragMode == DragMode.MatrixSelect)
1713 g.setColor(Color.yellow);
1714 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1715 Math.min(firstDragY, mouseDragLastY),
1716 Math.max(firstDragX, mouseDragLastX)
1717 - Math.min(firstDragX, mouseDragLastX),
1718 Math.max(firstDragY, mouseDragLastY)
1719 - Math.min(firstDragY, mouseDragLastY));
1725 public FontMetrics getFontMetrics()
1731 public Image getFadedImage()
1737 public int getFadedImageWidth()
1739 updateFadedImageWidth();
1743 private int[] bounds = new int[2];
1746 public int[] getVisibleVRange()
1748 if (ap != null && ap.getAlabels() != null)
1750 int sOffset = -ap.getAlabels().getScrollOffset();
1751 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1752 bounds[0] = sOffset;
1753 bounds[1] = visHeight;
1763 * Try to ensure any references held are nulled
1765 public void dispose()
1775 * I created the renderer so I will dispose of it
1777 if (renderer != null)
1784 public void propertyChange(PropertyChangeEvent evt)
1786 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1787 // Both scrolling and resizing change viewport ranges: scrolling changes
1788 // both start and end points, but resize only changes end values.
1789 // Here we only want to fastpaint on a scroll, with resize using a normal
1790 // paint, so scroll events are identified as changes to the horizontal or
1791 // vertical start value.
1792 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1794 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1796 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1798 fastPaint(((int[]) evt.getNewValue())[0]
1799 - ((int[]) evt.getOldValue())[0]);
1801 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1808 * computes the visible height of the annotation panel
1810 * @param adjustPanelHeight
1811 * - when false, just adjust existing height according to other
1813 * @param annotationHeight
1814 * @return height to use for the ScrollerPreferredVisibleSize
1816 public int adjustForAlignFrame(boolean adjustPanelHeight,
1817 int annotationHeight)
1820 * Estimate available height in the AlignFrame for alignment +
1821 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1822 * hscroll, status bar, insets.
1824 int stuff = (ap.getViewName() != null ? 30 : 0)
1825 + (Platform.isAMacAndNotJS() ? 120 : 140);
1826 int availableHeight = ap.alignFrame.getHeight() - stuff;
1827 int rowHeight = av.getCharHeight();
1829 if (adjustPanelHeight)
1831 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1834 * If not enough vertical space, maximize annotation height while keeping
1835 * at least two rows of alignment visible
1837 if (annotationHeight + alignmentHeight > availableHeight)
1839 annotationHeight = Math.min(annotationHeight,
1840 availableHeight - 2 * rowHeight);
1845 // maintain same window layout whilst updating sliders
1846 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1847 availableHeight - 2 * rowHeight);
1849 return annotationHeight;