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.alphafold.PAEContactMatrix;
78 * AnnotationPanel displays visible portion of annotation rows below unwrapped
84 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
85 MouseListener, MouseWheelListener, MouseMotionListener,
86 ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
90 Select, Resize, Undefined, MatrixSelect
93 String HELIX = MessageManager.getString("label.helix");
95 String SHEET = MessageManager.getString("label.sheet");
98 * For RNA secondary structure "stems" aka helices
100 String STEM = MessageManager.getString("label.rna_helix");
102 String LABEL = MessageManager.getString("label.label");
104 String REMOVE = MessageManager.getString("label.remove_annotation");
106 String COLOUR = MessageManager.getString("action.colour");
108 public final Color HELIX_COLOUR = Color.red.darker();
110 public final Color SHEET_COLOUR = Color.green.darker().darker();
112 public final Color STEM_COLOUR = Color.blue.darker();
115 public AlignViewport av;
119 public int activeRow = -1;
121 public BufferedImage image;
123 public volatile BufferedImage fadedImage;
125 // private Graphics2D gg;
127 public FontMetrics fm;
129 public int imgWidth = 0;
131 boolean fastPaint = false;
133 // Used For mouse Dragging and resizing graphs
134 int graphStretch = -1;
136 int mouseDragLastX = -1;
138 int mouseDragLastY = -1;
144 DragMode dragMode = DragMode.Undefined;
146 boolean mouseDragging = false;
148 // for editing cursor
153 public final AnnotationRenderer renderer;
155 private MouseWheelListener[] _mwl;
157 private boolean notJustOne;
160 * Creates a new AnnotationPanel object.
165 public AnnotationPanel(AlignmentPanel ap)
167 ToolTipManager.sharedInstance().registerComponent(this);
168 ToolTipManager.sharedInstance().setInitialDelay(0);
169 ToolTipManager.sharedInstance().setDismissDelay(10000);
172 this.setLayout(null);
173 addMouseListener(this);
174 addMouseMotionListener(this);
175 ap.annotationScroller.getVerticalScrollBar()
176 .addAdjustmentListener(this);
177 // save any wheel listeners on the scroller, so we can propagate scroll
179 _mwl = ap.annotationScroller.getMouseWheelListeners();
180 // and then set our own listener to consume all mousewheel events
181 ap.annotationScroller.addMouseWheelListener(this);
182 renderer = new AnnotationRenderer();
184 av.getRanges().addPropertyChangeListener(this);
187 public AnnotationPanel(AlignViewport av)
190 renderer = new AnnotationRenderer();
194 public void mouseWheelMoved(MouseWheelEvent e)
199 double wheelRotation = e.getPreciseWheelRotation();
200 if (wheelRotation > 0)
202 av.getRanges().scrollRight(true);
204 else if (wheelRotation < 0)
206 av.getRanges().scrollRight(false);
211 // TODO: find the correct way to let the event bubble up to
212 // ap.annotationScroller
213 for (MouseWheelListener mwl : _mwl)
217 mwl.mouseWheelMoved(e);
228 public Dimension getPreferredScrollableViewportSize()
230 Dimension ps = getPreferredSize();
231 return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
235 public int getScrollableBlockIncrement(Rectangle visibleRect,
236 int orientation, int direction)
242 public boolean getScrollableTracksViewportHeight()
248 public boolean getScrollableTracksViewportWidth()
254 public int getScrollableUnitIncrement(Rectangle visibleRect,
255 int orientation, int direction)
264 * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
268 public void adjustmentValueChanged(AdjustmentEvent evt)
270 // update annotation label display
271 ap.getAlabels().setScrollOffset(-evt.getValue());
275 * Calculates the height of the annotation displayed in the annotation panel.
276 * Callers should normally call the ap.adjustAnnotationHeight method to ensure
277 * all annotation associated components are updated correctly.
280 public int adjustPanelHeight()
282 int height = av.calcPanelHeight();
283 this.setPreferredSize(new Dimension(1, height));
286 // revalidate only when the alignment panel is fully constructed
300 public void actionPerformed(ActionEvent evt)
302 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
307 Annotation[] anot = aa[activeRow].annotations;
309 if (anot.length < av.getColumnSelection().getMax())
311 Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
313 System.arraycopy(anot, 0, temp, 0, anot.length);
315 aa[activeRow].annotations = anot;
318 String action = evt.getActionCommand();
319 if (action.equals(REMOVE))
321 for (int index : av.getColumnSelection().getSelected())
323 if (av.getAlignment().getHiddenColumns().isVisible(index))
329 else if (action.equals(LABEL))
331 String exMesg = collectAnnotVals(anot, LABEL);
332 String label = JvOptionPane.showInputDialog(
333 MessageManager.getString("label.enter_label"), exMesg);
340 if ((label.length() > 0) && !aa[activeRow].hasText)
342 aa[activeRow].hasText = true;
345 for (int index : av.getColumnSelection().getSelected())
347 if (!av.getAlignment().getHiddenColumns().isVisible(index))
352 if (anot[index] == null)
354 anot[index] = new Annotation(label, "", ' ', 0);
358 anot[index].displayCharacter = label;
362 else if (action.equals(COLOUR))
364 final Annotation[] fAnot = anot;
365 String title = MessageManager
366 .getString("label.select_foreground_colour");
367 ColourChooserListener listener = new ColourChooserListener()
370 public void colourSelected(Color c)
372 HiddenColumns hiddenColumns = av.getAlignment()
374 for (int index : av.getColumnSelection().getSelected())
376 if (hiddenColumns.isVisible(index))
378 if (fAnot[index] == null)
380 fAnot[index] = new Annotation("", "", ' ', 0);
382 fAnot[index].colour = c;
387 JalviewColourChooser.showColourChooser(this, title, Color.black,
391 // HELIX, SHEET or STEM
394 String symbol = "\u03B1"; // alpha
396 if (action.equals(HELIX))
400 else if (action.equals(SHEET))
403 symbol = "\u03B2"; // beta
406 // Added by LML to color stems
407 else if (action.equals(STEM))
410 int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
411 symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
414 if (!aa[activeRow].hasIcons)
416 aa[activeRow].hasIcons = true;
419 String label = JvOptionPane.showInputDialog(MessageManager
420 .getString("label.enter_label_for_the_structure"), symbol);
427 if ((label.length() > 0) && !aa[activeRow].hasText)
429 aa[activeRow].hasText = true;
430 if (action.equals(STEM))
432 aa[activeRow].showAllColLabels = true;
435 for (int index : av.getColumnSelection().getSelected())
437 if (!av.getAlignment().getHiddenColumns().isVisible(index))
442 if (anot[index] == null)
444 anot[index] = new Annotation(label, "", type, 0);
447 anot[index].secondaryStructure = type != 'S' ? type
448 : label.length() == 0 ? ' ' : label.charAt(0);
449 anot[index].displayCharacter = label;
454 av.getAlignment().validateAnnotation(aa[activeRow]);
455 ap.alignmentChanged();
456 ap.alignFrame.setMenusForViewport();
464 * Returns any existing annotation concatenated as a string. For each
465 * annotation, takes the description, if any, else the secondary structure
466 * character (if type is HELIX, SHEET or STEM), else the display character (if
473 private String collectAnnotVals(Annotation[] anots, String type)
475 // TODO is this method wanted? why? 'last' is not used
477 StringBuilder collatedInput = new StringBuilder(64);
479 ColumnSelection viscols = av.getColumnSelection();
480 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
483 * the selection list (read-only view) is in selection order, not
484 * column order; make a copy so we can sort it
486 List<Integer> selected = new ArrayList<>(viscols.getSelected());
487 Collections.sort(selected);
488 for (int index : selected)
490 // always check for current display state - just in case
491 if (!hidden.isVisible(index))
495 String tlabel = null;
496 if (anots[index] != null)
497 { // LML added stem code
498 if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
499 || type.equals(LABEL))
501 tlabel = anots[index].description;
502 if (tlabel == null || tlabel.length() < 1)
504 if (type.equals(HELIX) || type.equals(SHEET)
505 || type.equals(STEM))
507 tlabel = "" + anots[index].secondaryStructure;
511 tlabel = "" + anots[index].displayCharacter;
515 if (tlabel != null && !tlabel.equals(last))
517 if (last.length() > 0)
519 collatedInput.append(" ");
521 collatedInput.append(tlabel);
525 return collatedInput.toString();
529 * Action on right mouse pressed on Mac is to show a pop-up menu for the
530 * annotation. Action on left mouse pressed is to find which annotation is
531 * pressed and mark the start of a column selection or graph resize operation.
536 public void mousePressed(MouseEvent evt)
539 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
544 mouseDragLastX = evt.getX();
545 mouseDragLastY = evt.getY();
548 * add visible annotation heights until we reach the y
549 * position, to find which annotation it is in
554 // todo could reuse getRowIndexAndOffset ?
555 final int y = evt.getY();
557 for (int i = 0; i < aa.length; i++)
561 height += aa[i].height;
570 else if (aa[i].graph != 0)
573 * we have clicked on a resizable graph annotation
576 yOffset = height - y;
583 * isPopupTrigger fires in mousePressed on Mac,
584 * not until mouseRelease on Windows
586 if (evt.isPopupTrigger() && activeRow != -1)
588 showPopupMenu(y, evt.getX());
592 if (graphStretch != -1)
595 if (aa[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP)
597 // data in row has position on y as well as x axis
598 if (evt.isAltDown() || evt.isAltGraphDown())
600 dragMode = DragMode.MatrixSelect;
601 firstDragX = mouseDragLastX;
602 firstDragY = mouseDragLastY;
608 // no row (or row that can be adjusted) was pressed. Simulate a ruler
610 ap.getScalePanel().mousePressed(evt);
615 * checks whether the annotation row under the mouse click evt's handles the
619 * @return false if evt was not handled
621 boolean matrix_clicked(MouseEvent evt)
623 int[] rowIndex = getRowIndexAndOffset(evt.getY(),
624 av.getAlignment().getAlignmentAnnotation());
625 if (rowIndex == null)
628 .error("IMPLEMENTATION ERROR: matrix click out of range.");
631 int yOffset = rowIndex[1];
633 AlignmentAnnotation clicked = av.getAlignment()
634 .getAlignmentAnnotation()[rowIndex[0]];
635 if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
640 // TODO - use existing threshold to select related sections of matrix
641 GraphLine thr = clicked.getThreshold();
643 int currentX = getColumnForXPos(evt.getX());
644 ContactListI forCurrentX = av.getContactList(clicked, currentX);
645 if (forCurrentX != null)
647 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
648 clicked.graphHeight);
649 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset,
652 * start and end range corresponding to the row range under the mouse at
656 fr = Math.min(cXci.cStart, cXci.cEnd);
657 to = Math.max(cXci.cStart, cXci.cEnd);
659 if (evt.isControlDown())
661 ContactMatrixI matrix = av.getContactMatrix(clicked);
665 // simplest approach is to select all group containing column
666 if (matrix.hasGroups())
668 SequenceI rseq = clicked.sequenceRef;
669 BitSet grp = matrix.getGroupsFor(currentX);
670 for (int c = fr; c <= to; c++)
672 BitSet additionalGrp = matrix.getGroupsFor(c);
673 grp.or(additionalGrp);
675 HiddenColumns hc = av.getAlignment().getHiddenColumns();
676 for (int p = grp.nextSetBit(0); p >= 0; p = grp
679 int offp = (rseq != null)
680 ? rseq.findIndex(rseq.getStart() - 1 + p)
683 if (!av.hasHiddenColumns() || hc.isVisible(offp))
685 av.getColumnSelection().addElement(offp);
689 // possible alternative for interactive selection - threshold
690 // gives 'ceiling' for forming a cluster
691 // when a row+column is selected, farthest common ancestor less
692 // than thr is used to compute cluster
698 // select corresponding range in segment under mouse
700 for (int c = fr; c <= to; c++)
702 av.getColumnSelection().addElement(c);
704 av.getColumnSelection().addElement(currentX);
707 // and also select everything lower than the max range adjacent
709 if (PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
712 ContactRange cr = forCurrentX.getRangeFor(fr, to);
714 // TODO: could use GraphLine instead of arbitrary picking
715 // TODO: could report mean/median/variance for partitions
716 // (contiguous selected vs unselected regions and inter-contig
718 // controls feathering - what other elements in row/column
720 double thresh = cr.getMean() + (cr.getMax() - cr.getMean()) * .15;
723 cval = forCurrentX.getContactAt(c);
724 if (// cr.getMin() <= cval &&
727 av.getColumnSelection().addElement(c--);
735 while (c < forCurrentX.getContactHeight())
737 cval = forCurrentX.getContactAt(c);
738 if (// cr.getMin() <= cval &&
741 av.getColumnSelection().addElement(c++);
751 ap.paintAlignment(false, false);
752 PaintRefresher.Refresh(ap, av.getSequenceSetId());
758 * Construct and display a context menu at the right-click position
763 void showPopupMenu(final int y, int x)
765 if (av.getColumnSelection() == null
766 || av.getColumnSelection().isEmpty())
771 JPopupMenu pop = new JPopupMenu(
772 MessageManager.getString("label.structure_type"));
775 * Just display the needed structure options
777 if (av.getAlignment().isNucleotide())
779 item = new JMenuItem(STEM);
780 item.addActionListener(this);
785 item = new JMenuItem(HELIX);
786 item.addActionListener(this);
788 item = new JMenuItem(SHEET);
789 item.addActionListener(this);
792 item = new JMenuItem(LABEL);
793 item.addActionListener(this);
795 item = new JMenuItem(COLOUR);
796 item.addActionListener(this);
798 item = new JMenuItem(REMOVE);
799 item.addActionListener(this);
801 pop.show(this, x, y);
805 * Action on mouse up is to clear mouse drag data and call mouseReleased on
806 * ScalePanel, to deal with defining the selection group (if any) defined by
812 public void mouseReleased(MouseEvent evt)
814 if (dragMode == DragMode.MatrixSelect)
816 matrixSelectRange(evt);
823 mouseDragging = false;
824 if (dragMode == DragMode.Resize)
826 ap.adjustAnnotationHeight();
828 dragMode = DragMode.Undefined;
829 if (!matrix_clicked(evt))
831 ap.getScalePanel().mouseReleased(evt);
835 * isPopupTrigger is set in mouseReleased on Windows
836 * (in mousePressed on Mac)
838 if (evt.isPopupTrigger() && activeRow != -1)
840 showPopupMenu(evt.getY(), evt.getX());
852 public void mouseEntered(MouseEvent evt)
854 this.mouseDragging = false;
855 ap.getScalePanel().mouseEntered(evt);
859 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
860 * with column selection on a mouse drag
865 public void mouseExited(MouseEvent evt)
867 ap.getScalePanel().mouseExited(evt);
871 * Action on starting or continuing a mouse drag. There are two possible
874 * <li>drag up or down on a graphed annotation increases or decreases the
875 * height of the graph</li>
876 * <li>dragging left or right selects the columns dragged across</li>
878 * A drag on a graph annotation is treated as column selection if it starts
879 * with more horizontal than vertical movement, and as resize if it starts
880 * with more vertical than horizontal movement. Once started, the drag does
886 public void mouseDragged(MouseEvent evt)
889 * if dragMode is Undefined:
890 * - set to Select if dx > dy
891 * - set to Resize if dy > dx
892 * - do nothing if dx == dy
894 final int x = evt.getX();
895 final int y = evt.getY();
896 if (dragMode == DragMode.Undefined)
898 int dx = Math.abs(x - mouseDragLastX);
899 int dy = Math.abs(y - mouseDragLastY);
900 if (graphStretch == -1 || dx > dy)
903 * mostly horizontal drag, or not a graph annotation
905 dragMode = DragMode.Select;
910 * mostly vertical drag
912 dragMode = DragMode.Resize;
913 notJustOne = evt.isShiftDown();
916 * but could also be a matrix drag
918 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
919 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
922 * dragging in a matrix
924 dragMode = DragMode.MatrixSelect;
925 firstDragX = mouseDragLastX;
926 firstDragY = mouseDragLastY;
931 if (dragMode == DragMode.Undefined)
935 * drag is diagonal - defer deciding whether to
936 * treat as up/down or left/right
943 if (dragMode == DragMode.Resize)
946 * resize graph annotation if mouse was dragged up or down
948 int deltaY = mouseDragLastY - evt.getY();
951 AlignmentAnnotation graphAnnotation = av.getAlignment()
952 .getAlignmentAnnotation()[graphStretch];
953 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
956 for (AlignmentAnnotation similar : av.getAlignment()
957 .findAnnotations(null, graphAnnotation.getCalcId(),
958 graphAnnotation.label))
960 similar.graphHeight = newHeight;
966 graphAnnotation.graphHeight = newHeight;
969 ap.paintAlignment(false, false);
972 else if (dragMode == DragMode.MatrixSelect)
975 * TODO draw a rubber band for range
979 ap.paintAlignment(false, false);
984 * for mouse drag left or right, delegate to
985 * ScalePanel to adjust the column selection
987 ap.getScalePanel().mouseDragged(evt);
996 public void matrixSelectRange(MouseEvent evt)
999 * get geometry of drag
1001 int fromY = Math.min(firstDragY, evt.getY());
1002 int toY = Math.max(firstDragY, evt.getY());
1003 int fromX = Math.min(firstDragX, evt.getX());
1004 int toX = Math.max(firstDragX, evt.getX());
1006 int deltaY = toY - fromY;
1007 int deltaX = toX - fromX;
1009 int[] rowIndex = getRowIndexAndOffset(fromY,
1010 av.getAlignment().getAlignmentAnnotation());
1011 int[] toRowIndex = getRowIndexAndOffset(toY,
1012 av.getAlignment().getAlignmentAnnotation());
1014 if (rowIndex == null || toRowIndex == null)
1016 jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1019 if (rowIndex[0] != toRowIndex[0])
1022 .trace("Drag went to another row. needs to be clipped");
1025 // rectangular selection on matrix style annotation
1026 AlignmentAnnotation cma = av.getAlignment()
1027 .getAlignmentAnnotation()[rowIndex[0]];
1029 int lastX = getColumnForXPos(fromX);
1030 int currentX = getColumnForXPos(toX);
1031 int fromXc = Math.min(lastX, currentX);
1032 int toXc = Math.max(lastX, currentX);
1033 ContactListI forFromX = av.getContactList(cma, fromXc);
1034 ContactListI forToX = av.getContactList(cma, toXc);
1036 if (forFromX != null && forToX != null)
1038 ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
1040 ContactGeometry.contactInterval lastXci = lastXcgeom
1041 .mapFor(rowIndex[1], rowIndex[1] - deltaY);
1043 ContactGeometry cXcgeom = new ContactGeometry(forToX,
1045 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
1046 rowIndex[1] - deltaY);
1048 // mark rectangular region formed by drag
1049 jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
1050 + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
1051 + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1053 fr = Math.min(lastXci.cStart, lastXci.cEnd);
1054 to = Math.max(lastXci.cStart, lastXci.cEnd);
1055 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1056 for (int c = fr; c <= to; c++)
1058 if (cma.sequenceRef != null)
1060 int col = cma.sequenceRef.findIndex(c);
1061 av.getColumnSelection().addElement(col);
1065 av.getColumnSelection().addElement(c);
1068 fr = Math.min(cXci.cStart, cXci.cEnd);
1069 to = Math.max(cXci.cStart, cXci.cEnd);
1070 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1071 for (int c = fr; c <= to; c++)
1073 if (cma.sequenceRef != null)
1075 int col = cma.sequenceRef.findIndex(c);
1076 av.getColumnSelection().addElement(col);
1080 av.getColumnSelection().addElement(c);
1083 fr = Math.min(lastX, currentX);
1084 to = Math.max(lastX, currentX);
1086 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1087 for (int c = fr; c <= to; c++)
1089 av.getColumnSelection().addElement(c);
1096 * Constructs the tooltip, and constructs and displays a status message, for
1097 * the current mouse position
1102 public void mouseMoved(MouseEvent evt)
1104 int yPos = evt.getY();
1105 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1106 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1107 int row = rowAndOffset[0];
1111 this.setToolTipText(null);
1115 int column = getColumnForXPos(evt.getX());
1117 AlignmentAnnotation ann = aa[row];
1118 if (row > -1 && ann.annotations != null
1119 && column < ann.annotations.length)
1121 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1123 setToolTipText(toolTip == null ? null
1124 : JvSwingUtils.wrapTooltip(true, toolTip));
1125 String msg = getStatusMessage(av.getAlignment(), column, ann,
1126 rowAndOffset[1], av);
1127 ap.alignFrame.setStatus(msg);
1131 this.setToolTipText(null);
1132 ap.alignFrame.setStatus(" ");
1136 private int getColumnForXPos(int x)
1138 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1139 column = Math.min(column, av.getRanges().getEndRes());
1141 if (av.hasHiddenColumns())
1143 column = av.getAlignment().getHiddenColumns()
1144 .visibleToAbsoluteColumn(column);
1150 * Answers the index in the annotations array of the visible annotation at the
1151 * given y position. This is done by adding the heights of visible annotations
1152 * until the y position has been exceeded. Answers -1 if no annotations are
1153 * visible, or the y position is below all annotations.
1159 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1165 return getRowIndexAndOffset(yPos, aa)[0];
1168 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1170 int[] res = new int[2];
1178 int height = 0, lheight = 0;
1179 for (int i = 0; i < aa.length; i++)
1184 height += aa[i].height;
1191 res[1] = height - yPos;
1199 * Answers a tooltip for the annotation at the current mouse position, not
1200 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1206 * @param rowAndOffset
1208 static String buildToolTip(AlignmentAnnotation ann, int column,
1209 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1212 String tooltip = null;
1213 if (ann.graphGroup > -1)
1215 StringBuilder tip = new StringBuilder(32);
1216 boolean first = true;
1217 for (int i = 0; i < anns.length; i++)
1219 if (anns[i].graphGroup == ann.graphGroup
1220 && anns[i].annotations[column] != null)
1227 tip.append(anns[i].label);
1228 String description = anns[i].annotations[column].description;
1229 if (description != null && description.length() > 0)
1231 tip.append(" ").append(description);
1235 tooltip = first ? null : tip.toString();
1237 else if (column < ann.annotations.length
1238 && ann.annotations[column] != null)
1240 tooltip = ann.annotations[column].description;
1242 // TODO abstract tooltip generator so different implementations can be built
1243 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1245 ContactListI clist = av.getContactList(ann, column);
1248 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1249 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1250 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1251 tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
1252 + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
1253 int col = ann.sequenceRef.findPosition(column);
1254 ap.getStructureSelectionManager()
1255 .highlightPositionsOn(ann.sequenceRef, new int[][]
1256 { new int[] { col, col },
1258 { ci.cStart, ci.cEnd } }, null);
1265 * Constructs and returns the status bar message
1270 * @param rowAndOffset
1272 static String getStatusMessage(AlignmentI al, int column,
1273 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1276 * show alignment column and annotation description if any
1278 StringBuilder text = new StringBuilder(32);
1279 text.append(MessageManager.getString("label.column")).append(" ")
1280 .append(column + 1);
1282 if (column < ann.annotations.length && ann.annotations[column] != null)
1284 String description = ann.annotations[column].description;
1285 if (description != null && description.trim().length() > 0)
1287 text.append(" ").append(description);
1292 * if the annotation is sequence-specific, show the sequence number
1293 * in the alignment, and (if not a gap) the residue and position
1295 SequenceI seqref = ann.sequenceRef;
1298 int seqIndex = al.findIndex(seqref);
1301 text.append(", ").append(MessageManager.getString("label.sequence"))
1302 .append(" ").append(seqIndex + 1);
1303 char residue = seqref.getCharAt(column);
1304 if (!Comparison.isGap(residue))
1308 if (al.isNucleotide())
1310 name = ResidueProperties.nucleotideName
1311 .get(String.valueOf(residue));
1312 text.append(" Nucleotide: ")
1313 .append(name != null ? name : residue);
1317 name = 'X' == residue ? "X"
1318 : ('*' == residue ? "STOP"
1319 : ResidueProperties.aa2Triplet
1320 .get(String.valueOf(residue)));
1321 text.append(" Residue: ").append(name != null ? name : residue);
1323 int residuePos = seqref.findPosition(column);
1324 text.append(" (").append(residuePos).append(")");
1329 return text.toString();
1339 public void mouseClicked(MouseEvent evt)
1341 // if (activeRow != -1)
1343 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1344 // AlignmentAnnotation anot = aa[activeRow];
1348 // TODO mouseClicked-content and drawCursor are quite experimental!
1349 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1352 int pady = av.getCharHeight() / 5;
1354 graphics.setColor(Color.black);
1355 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1357 if (av.validCharWidth)
1359 graphics.setColor(Color.white);
1361 char s = seq.getCharAt(res);
1363 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1364 graphics.drawString(String.valueOf(s), charOffset + x1,
1365 (y1 + av.getCharHeight()) - pady);
1370 private volatile boolean imageFresh = false;
1372 private Rectangle visibleRect = new Rectangle(),
1373 clipBounds = new Rectangle();
1382 public void paintComponent(Graphics g)
1385 // BH: note that this method is generally recommended to
1386 // call super.paintComponent(g). Otherwise, the children of this
1387 // component will not be rendered. That is not needed here
1388 // because AnnotationPanel does not have any children. It is
1389 // just a JPanel contained in a JViewPort.
1391 computeVisibleRect(visibleRect);
1393 g.setColor(Color.white);
1394 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1398 // BH 2018 optimizing generation of new Rectangle().
1400 || (visibleRect.width != (clipBounds = g
1401 .getClipBounds(clipBounds)).width)
1402 || (visibleRect.height != clipBounds.height))
1405 g.drawImage(image, 0, 0, this);
1410 updateFadedImageWidth();
1416 if (image == null || imgWidth != image.getWidth(this)
1417 || image.getHeight(this) != getHeight())
1419 boolean tried = false;
1421 while (image == null && !tried)
1425 image = new BufferedImage(imgWidth,
1426 ap.getAnnotationPanel().getHeight(),
1427 BufferedImage.TYPE_INT_RGB);
1429 } catch (IllegalArgumentException exc)
1432 "Serious issue with viewport geometry imgWidth requested was "
1435 } catch (OutOfMemoryError oom)
1440 } catch (Exception x)
1445 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1451 gg = (Graphics2D) image.getGraphics();
1455 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1456 RenderingHints.VALUE_ANTIALIAS_ON);
1459 gg.setFont(av.getFont());
1460 fm = gg.getFontMetrics();
1461 gg.setColor(Color.white);
1462 gg.fillRect(0, 0, imgWidth, image.getHeight());
1467 gg = (Graphics2D) image.getGraphics();
1471 drawComponent(gg, av.getRanges().getStartRes(),
1472 av.getRanges().getEndRes() + 1);
1475 g.drawImage(image, 0, 0, this);
1478 public void updateFadedImageWidth()
1480 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1481 + 1) * av.getCharWidth();
1486 * set true to enable redraw timing debug output on stderr
1488 private final boolean debugRedraw = false;
1491 * non-Thread safe repaint
1494 * repaint with horizontal shift in alignment
1496 public void fastPaint(int horizontal)
1498 if ((horizontal == 0) || image == null
1499 || av.getAlignment().getAlignmentAnnotation() == null
1500 || av.getAlignment().getAlignmentAnnotation().length < 1
1501 || av.isCalcInProgress())
1507 int sr = av.getRanges().getStartRes();
1508 int er = av.getRanges().getEndRes() + 1;
1511 Graphics2D gg = (Graphics2D) image.getGraphics();
1513 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1515 // scroll is less than imgWidth away so can re-use buffered graphics
1516 gg.copyArea(0, 0, imgWidth, getHeight(),
1517 -horizontal * av.getCharWidth(), 0);
1519 if (horizontal > 0) // scrollbar pulled right, image to the left
1521 transX = (er - sr - horizontal) * av.getCharWidth();
1522 sr = er - horizontal;
1524 else if (horizontal < 0)
1526 er = sr - horizontal;
1529 gg.translate(transX, 0);
1531 drawComponent(gg, sr, er);
1533 gg.translate(-transX, 0);
1539 // Call repaint on alignment panel so that repaints from other alignment
1540 // panel components can be aggregated. Otherwise performance of the overview
1541 // window and others may be adversely affected.
1542 av.getAlignPanel().repaint();
1545 private volatile boolean lastImageGood = false;
1557 public void drawComponent(Graphics g, int startRes, int endRes)
1559 BufferedImage oldFaded = fadedImage;
1560 if (av.isCalcInProgress())
1564 lastImageGood = false;
1567 // We'll keep a record of the old image,
1568 // and draw a faded image until the calculation
1571 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1572 || fadedImage.getHeight() != image.getHeight()))
1574 // System.err.println("redraw faded image ("+(fadedImage==null ?
1575 // "null image" : "") + " lastGood="+lastImageGood+")");
1576 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1577 BufferedImage.TYPE_INT_RGB);
1579 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1581 fadedG.setColor(Color.white);
1582 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1584 fadedG.setComposite(
1585 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1586 fadedG.drawImage(image, 0, 0, this);
1589 // make sure we don't overwrite the last good faded image until all
1590 // calculations have finished
1591 lastImageGood = false;
1596 if (fadedImage != null)
1598 oldFaded = fadedImage;
1603 g.setColor(Color.white);
1604 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1606 g.setFont(av.getFont());
1609 fm = g.getFontMetrics();
1612 if ((av.getAlignment().getAlignmentAnnotation() == null)
1613 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1615 g.setColor(Color.white);
1616 g.fillRect(0, 0, getWidth(), getHeight());
1617 g.setColor(Color.black);
1618 if (av.validCharWidth)
1620 g.drawString(MessageManager
1621 .getString("label.alignment_has_no_annotations"), 20, 15);
1626 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1628 if (!lastImageGood && fadedImage == null)
1630 fadedImage = oldFaded;
1632 if (dragMode == DragMode.MatrixSelect)
1634 g.setColor(Color.yellow);
1635 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1636 Math.min(firstDragY, mouseDragLastY),
1637 Math.max(firstDragX, mouseDragLastX)
1638 - Math.min(firstDragX, mouseDragLastX),
1639 Math.max(firstDragY, mouseDragLastY)
1640 - Math.min(firstDragY, mouseDragLastY));
1646 public FontMetrics getFontMetrics()
1652 public Image getFadedImage()
1658 public int getFadedImageWidth()
1660 updateFadedImageWidth();
1664 private int[] bounds = new int[2];
1667 public int[] getVisibleVRange()
1669 if (ap != null && ap.getAlabels() != null)
1671 int sOffset = -ap.getAlabels().getScrollOffset();
1672 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1673 bounds[0] = sOffset;
1674 bounds[1] = visHeight;
1684 * Try to ensure any references held are nulled
1686 public void dispose()
1696 * I created the renderer so I will dispose of it
1698 if (renderer != null)
1705 public void propertyChange(PropertyChangeEvent evt)
1707 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1708 // Both scrolling and resizing change viewport ranges: scrolling changes
1709 // both start and end points, but resize only changes end values.
1710 // Here we only want to fastpaint on a scroll, with resize using a normal
1711 // paint, so scroll events are identified as changes to the horizontal or
1712 // vertical start value.
1713 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1715 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1717 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1719 fastPaint(((int[]) evt.getNewValue())[0]
1720 - ((int[]) evt.getOldValue())[0]);
1722 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1729 * computes the visible height of the annotation panel
1731 * @param adjustPanelHeight
1732 * - when false, just adjust existing height according to other
1734 * @param annotationHeight
1735 * @return height to use for the ScrollerPreferredVisibleSize
1737 public int adjustForAlignFrame(boolean adjustPanelHeight,
1738 int annotationHeight)
1741 * Estimate available height in the AlignFrame for alignment +
1742 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1743 * hscroll, status bar, insets.
1745 int stuff = (ap.getViewName() != null ? 30 : 0)
1746 + (Platform.isAMacAndNotJS() ? 120 : 140);
1747 int availableHeight = ap.alignFrame.getHeight() - stuff;
1748 int rowHeight = av.getCharHeight();
1750 if (adjustPanelHeight)
1752 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1755 * If not enough vertical space, maximize annotation height while keeping
1756 * at least two rows of alignment visible
1758 if (annotationHeight + alignmentHeight > availableHeight)
1760 annotationHeight = Math.min(annotationHeight,
1761 availableHeight - 2 * rowHeight);
1766 // maintain same window layout whilst updating sliders
1767 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1768 availableHeight - 2 * rowHeight);
1770 return annotationHeight;