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] = height - yPos;
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 ContactListI clist = av.getContactList(ann, column);
1307 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1308 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1309 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1310 tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
1311 + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
1313 int col = ann.sequenceRef.findPosition(column);
1314 int[][] highlightPos;
1315 int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1316 if (mappedPos != null)
1318 highlightPos = new int[1 + mappedPos.length][2];
1319 highlightPos[0] = new int[] { col, col };
1320 for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1322 highlightPos[h][0] = ann.sequenceRef
1323 .findPosition(mappedPos[p] - 1);
1324 highlightPos[h][1] = ann.sequenceRef
1325 .findPosition(mappedPos[p + 1] - 1);
1330 highlightPos = new int[][] { new int[] { col, col } };
1332 ap.getStructureSelectionManager()
1333 .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1340 * Constructs and returns the status bar message
1345 * @param rowAndOffset
1347 static String getStatusMessage(AlignmentI al, int column,
1348 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1351 * show alignment column and annotation description if any
1353 StringBuilder text = new StringBuilder(32);
1354 text.append(MessageManager.getString("label.column")).append(" ")
1355 .append(column + 1);
1357 if (column < ann.annotations.length && ann.annotations[column] != null)
1359 String description = ann.annotations[column].description;
1360 if (description != null && description.trim().length() > 0)
1362 text.append(" ").append(description);
1367 * if the annotation is sequence-specific, show the sequence number
1368 * in the alignment, and (if not a gap) the residue and position
1370 SequenceI seqref = ann.sequenceRef;
1373 int seqIndex = al.findIndex(seqref);
1376 text.append(", ").append(MessageManager.getString("label.sequence"))
1377 .append(" ").append(seqIndex + 1);
1378 char residue = seqref.getCharAt(column);
1379 if (!Comparison.isGap(residue))
1383 if (al.isNucleotide())
1385 name = ResidueProperties.nucleotideName
1386 .get(String.valueOf(residue));
1387 text.append(" Nucleotide: ")
1388 .append(name != null ? name : residue);
1392 name = 'X' == residue ? "X"
1393 : ('*' == residue ? "STOP"
1394 : ResidueProperties.aa2Triplet
1395 .get(String.valueOf(residue)));
1396 text.append(" Residue: ").append(name != null ? name : residue);
1398 int residuePos = seqref.findPosition(column);
1399 text.append(" (").append(residuePos).append(")");
1404 return text.toString();
1414 public void mouseClicked(MouseEvent evt)
1416 // if (activeRow != -1)
1418 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1419 // AlignmentAnnotation anot = aa[activeRow];
1423 // TODO mouseClicked-content and drawCursor are quite experimental!
1424 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1427 int pady = av.getCharHeight() / 5;
1429 graphics.setColor(Color.black);
1430 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1432 if (av.validCharWidth)
1434 graphics.setColor(Color.white);
1436 char s = seq.getCharAt(res);
1438 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1439 graphics.drawString(String.valueOf(s), charOffset + x1,
1440 (y1 + av.getCharHeight()) - pady);
1445 private volatile boolean imageFresh = false;
1447 private Rectangle visibleRect = new Rectangle(),
1448 clipBounds = new Rectangle();
1457 public void paintComponent(Graphics g)
1460 // BH: note that this method is generally recommended to
1461 // call super.paintComponent(g). Otherwise, the children of this
1462 // component will not be rendered. That is not needed here
1463 // because AnnotationPanel does not have any children. It is
1464 // just a JPanel contained in a JViewPort.
1466 computeVisibleRect(visibleRect);
1468 g.setColor(Color.white);
1469 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1473 // BH 2018 optimizing generation of new Rectangle().
1475 || (visibleRect.width != (clipBounds = g
1476 .getClipBounds(clipBounds)).width)
1477 || (visibleRect.height != clipBounds.height))
1480 g.drawImage(image, 0, 0, this);
1485 updateFadedImageWidth();
1491 if (image == null || imgWidth != image.getWidth(this)
1492 || image.getHeight(this) != getHeight())
1494 boolean tried = false;
1496 while (image == null && !tried)
1500 image = new BufferedImage(imgWidth,
1501 ap.getAnnotationPanel().getHeight(),
1502 BufferedImage.TYPE_INT_RGB);
1504 } catch (IllegalArgumentException exc)
1507 "Serious issue with viewport geometry imgWidth requested was "
1510 } catch (OutOfMemoryError oom)
1515 } catch (Exception x)
1520 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1526 gg = (Graphics2D) image.getGraphics();
1530 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1531 RenderingHints.VALUE_ANTIALIAS_ON);
1534 gg.setFont(av.getFont());
1535 fm = gg.getFontMetrics();
1536 gg.setColor(Color.white);
1537 gg.fillRect(0, 0, imgWidth, image.getHeight());
1542 gg = (Graphics2D) image.getGraphics();
1546 drawComponent(gg, av.getRanges().getStartRes(),
1547 av.getRanges().getEndRes() + 1);
1550 g.drawImage(image, 0, 0, this);
1553 public void updateFadedImageWidth()
1555 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1556 + 1) * av.getCharWidth();
1561 * set true to enable redraw timing debug output on stderr
1563 private final boolean debugRedraw = false;
1566 * non-Thread safe repaint
1569 * repaint with horizontal shift in alignment
1571 public void fastPaint(int horizontal)
1573 if ((horizontal == 0) || image == null
1574 || av.getAlignment().getAlignmentAnnotation() == null
1575 || av.getAlignment().getAlignmentAnnotation().length < 1
1576 || av.isCalcInProgress())
1582 int sr = av.getRanges().getStartRes();
1583 int er = av.getRanges().getEndRes() + 1;
1586 Graphics2D gg = (Graphics2D) image.getGraphics();
1588 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1590 // scroll is less than imgWidth away so can re-use buffered graphics
1591 gg.copyArea(0, 0, imgWidth, getHeight(),
1592 -horizontal * av.getCharWidth(), 0);
1594 if (horizontal > 0) // scrollbar pulled right, image to the left
1596 transX = (er - sr - horizontal) * av.getCharWidth();
1597 sr = er - horizontal;
1599 else if (horizontal < 0)
1601 er = sr - horizontal;
1604 gg.translate(transX, 0);
1606 drawComponent(gg, sr, er);
1608 gg.translate(-transX, 0);
1614 // Call repaint on alignment panel so that repaints from other alignment
1615 // panel components can be aggregated. Otherwise performance of the overview
1616 // window and others may be adversely affected.
1617 av.getAlignPanel().repaint();
1620 private volatile boolean lastImageGood = false;
1632 public void drawComponent(Graphics g, int startRes, int endRes)
1634 BufferedImage oldFaded = fadedImage;
1635 if (av.isCalcInProgress())
1639 lastImageGood = false;
1642 // We'll keep a record of the old image,
1643 // and draw a faded image until the calculation
1646 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1647 || fadedImage.getHeight() != image.getHeight()))
1649 // System.err.println("redraw faded image ("+(fadedImage==null ?
1650 // "null image" : "") + " lastGood="+lastImageGood+")");
1651 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1652 BufferedImage.TYPE_INT_RGB);
1654 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1656 fadedG.setColor(Color.white);
1657 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1659 fadedG.setComposite(
1660 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1661 fadedG.drawImage(image, 0, 0, this);
1664 // make sure we don't overwrite the last good faded image until all
1665 // calculations have finished
1666 lastImageGood = false;
1671 if (fadedImage != null)
1673 oldFaded = fadedImage;
1678 g.setColor(Color.white);
1679 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1681 g.setFont(av.getFont());
1684 fm = g.getFontMetrics();
1687 if ((av.getAlignment().getAlignmentAnnotation() == null)
1688 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1690 g.setColor(Color.white);
1691 g.fillRect(0, 0, getWidth(), getHeight());
1692 g.setColor(Color.black);
1693 if (av.validCharWidth)
1695 g.drawString(MessageManager
1696 .getString("label.alignment_has_no_annotations"), 20, 15);
1701 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1703 if (!lastImageGood && fadedImage == null)
1705 fadedImage = oldFaded;
1707 if (dragMode == DragMode.MatrixSelect)
1709 g.setColor(Color.yellow);
1710 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1711 Math.min(firstDragY, mouseDragLastY),
1712 Math.max(firstDragX, mouseDragLastX)
1713 - Math.min(firstDragX, mouseDragLastX),
1714 Math.max(firstDragY, mouseDragLastY)
1715 - Math.min(firstDragY, mouseDragLastY));
1721 public FontMetrics getFontMetrics()
1727 public Image getFadedImage()
1733 public int getFadedImageWidth()
1735 updateFadedImageWidth();
1739 private int[] bounds = new int[2];
1742 public int[] getVisibleVRange()
1744 if (ap != null && ap.getAlabels() != null)
1746 int sOffset = -ap.getAlabels().getScrollOffset();
1747 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1748 bounds[0] = sOffset;
1749 bounds[1] = visHeight;
1759 * Try to ensure any references held are nulled
1761 public void dispose()
1771 * I created the renderer so I will dispose of it
1773 if (renderer != null)
1780 public void propertyChange(PropertyChangeEvent evt)
1782 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1783 // Both scrolling and resizing change viewport ranges: scrolling changes
1784 // both start and end points, but resize only changes end values.
1785 // Here we only want to fastpaint on a scroll, with resize using a normal
1786 // paint, so scroll events are identified as changes to the horizontal or
1787 // vertical start value.
1788 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1790 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1792 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1794 fastPaint(((int[]) evt.getNewValue())[0]
1795 - ((int[]) evt.getOldValue())[0]);
1797 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1804 * computes the visible height of the annotation panel
1806 * @param adjustPanelHeight
1807 * - when false, just adjust existing height according to other
1809 * @param annotationHeight
1810 * @return height to use for the ScrollerPreferredVisibleSize
1812 public int adjustForAlignFrame(boolean adjustPanelHeight,
1813 int annotationHeight)
1816 * Estimate available height in the AlignFrame for alignment +
1817 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1818 * hscroll, status bar, insets.
1820 int stuff = (ap.getViewName() != null ? 30 : 0)
1821 + (Platform.isAMacAndNotJS() ? 120 : 140);
1822 int availableHeight = ap.alignFrame.getHeight() - stuff;
1823 int rowHeight = av.getCharHeight();
1825 if (adjustPanelHeight)
1827 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1830 * If not enough vertical space, maximize annotation height while keeping
1831 * at least two rows of alignment visible
1833 if (annotationHeight + alignmentHeight > availableHeight)
1835 annotationHeight = Math.min(annotationHeight,
1836 availableHeight - 2 * rowHeight);
1841 // maintain same window layout whilst updating sliders
1842 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1843 availableHeight - 2 * rowHeight);
1845 return annotationHeight;