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 click
609 ap.getScalePanel().mousePressed(evt);
614 * checks whether the annotation row under the mouse click evt's handles the
618 * @return false if evt was not handled
620 boolean matrix_clicked(MouseEvent evt)
622 int[] rowIndex = getRowIndexAndOffset(evt.getY(),
623 av.getAlignment().getAlignmentAnnotation());
624 int yOffset = rowIndex[1];
626 if (rowIndex == null)
628 System.err.println("IMPLEMENTATION ERROR: matrix click out of range.");
630 AlignmentAnnotation clicked = av.getAlignment()
631 .getAlignmentAnnotation()[rowIndex[0]];
632 if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
637 // TODO - use existing threshold to select related sections of matrix
638 GraphLine thr = clicked.getThreshold();
640 int currentX = getColumnForXPos(evt.getX());
641 ContactListI forCurrentX = av.getContactList(clicked, currentX);
642 if (forCurrentX != null)
644 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
645 clicked.graphHeight);
646 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset,
649 * start and end range corresponding to the row range under the mouse at
653 fr = Math.min(cXci.cStart, cXci.cEnd);
654 to = Math.max(cXci.cStart, cXci.cEnd);
656 if (evt.isControlDown())
658 ContactMatrixI matrix = av.getContactMatrix(clicked);
662 // simplest approach is to select all group containing column
663 if (matrix.hasGroups())
665 SequenceI rseq = clicked.sequenceRef;
666 BitSet grp = matrix.getGroupsFor(currentX);
667 for (int c = fr; c <= to; c++)
669 BitSet additionalGrp = matrix.getGroupsFor(c);
670 grp.or(additionalGrp);
672 HiddenColumns hc = av.getAlignment().getHiddenColumns();
673 for (int p = grp.nextSetBit(0); p >= 0; p = grp
676 int offp = (rseq != null)
677 ? rseq.findIndex(rseq.getStart() - 1 + p)
680 if (!av.hasHiddenColumns() || hc.isVisible(offp))
682 av.getColumnSelection().addElement(offp);
686 // possible alternative for interactive selection - threshold
687 // gives 'ceiling' for forming a cluster
688 // when a row+column is selected, farthest common ancestor less
689 // than thr is used to compute cluster
695 // select corresponding range in segment under mouse
697 for (int c = fr; c <= to; c++)
699 av.getColumnSelection().addElement(c);
701 av.getColumnSelection().addElement(currentX);
704 // and also select everything lower than the max range adjacent
706 if (PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
709 ContactRange cr = forCurrentX.getRangeFor(fr, to);
711 // TODO: could use GraphLine instead of arbitrary picking
712 // TODO: could report mean/median/variance for partitions
713 // (contiguous selected vs unselected regions and inter-contig
715 // controls feathering - what other elements in row/column
717 double thresh = cr.getMean() + (cr.getMax() - cr.getMean()) * .15;
720 cval = forCurrentX.getContactAt(c);
721 if (// cr.getMin() <= cval &&
724 av.getColumnSelection().addElement(c--);
732 while (c < forCurrentX.getContactHeight())
734 cval = forCurrentX.getContactAt(c);
735 if (// cr.getMin() <= cval &&
738 av.getColumnSelection().addElement(c++);
748 ap.paintAlignment(false, false);
749 PaintRefresher.Refresh(ap, av.getSequenceSetId());
754 * Construct and display a context menu at the right-click position
759 void showPopupMenu(final int y, int x)
761 if (av.getColumnSelection() == null
762 || av.getColumnSelection().isEmpty())
767 JPopupMenu pop = new JPopupMenu(
768 MessageManager.getString("label.structure_type"));
771 * Just display the needed structure options
773 if (av.getAlignment().isNucleotide())
775 item = new JMenuItem(STEM);
776 item.addActionListener(this);
781 item = new JMenuItem(HELIX);
782 item.addActionListener(this);
784 item = new JMenuItem(SHEET);
785 item.addActionListener(this);
788 item = new JMenuItem(LABEL);
789 item.addActionListener(this);
791 item = new JMenuItem(COLOUR);
792 item.addActionListener(this);
794 item = new JMenuItem(REMOVE);
795 item.addActionListener(this);
797 pop.show(this, x, y);
801 * Action on mouse up is to clear mouse drag data and call mouseReleased on
802 * ScalePanel, to deal with defining the selection group (if any) defined by
808 public void mouseReleased(MouseEvent evt)
810 if (dragMode == DragMode.MatrixSelect)
812 matrixSelectRange(evt);
819 mouseDragging = false;
820 if (dragMode == DragMode.Resize)
822 ap.adjustAnnotationHeight();
824 dragMode = DragMode.Undefined;
825 if (!matrix_clicked(evt))
827 ap.getScalePanel().mouseReleased(evt);
831 * isPopupTrigger is set in mouseReleased on Windows
832 * (in mousePressed on Mac)
834 if (evt.isPopupTrigger() && activeRow != -1)
836 showPopupMenu(evt.getY(), evt.getX());
848 public void mouseEntered(MouseEvent evt)
850 this.mouseDragging = false;
851 ap.getScalePanel().mouseEntered(evt);
855 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
856 * with column selection on a mouse drag
861 public void mouseExited(MouseEvent evt)
863 ap.getScalePanel().mouseExited(evt);
867 * Action on starting or continuing a mouse drag. There are two possible
870 * <li>drag up or down on a graphed annotation increases or decreases the
871 * height of the graph</li>
872 * <li>dragging left or right selects the columns dragged across</li>
874 * A drag on a graph annotation is treated as column selection if it starts
875 * with more horizontal than vertical movement, and as resize if it starts
876 * with more vertical than horizontal movement. Once started, the drag does
882 public void mouseDragged(MouseEvent evt)
885 * if dragMode is Undefined:
886 * - set to Select if dx > dy
887 * - set to Resize if dy > dx
888 * - do nothing if dx == dy
890 final int x = evt.getX();
891 final int y = evt.getY();
892 if (dragMode == DragMode.Undefined)
894 int dx = Math.abs(x - mouseDragLastX);
895 int dy = Math.abs(y - mouseDragLastY);
896 if (graphStretch == -1 || dx > dy)
899 * mostly horizontal drag, or not a graph annotation
901 dragMode = DragMode.Select;
906 * mostly vertical drag
908 dragMode = DragMode.Resize;
909 notJustOne = evt.isShiftDown();
912 * but could also be a matrix drag
914 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
915 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
918 * dragging in a matrix
920 dragMode = DragMode.MatrixSelect;
921 firstDragX = mouseDragLastX;
922 firstDragY = mouseDragLastY;
927 if (dragMode == DragMode.Undefined)
931 * drag is diagonal - defer deciding whether to
932 * treat as up/down or left/right
939 if (dragMode == DragMode.Resize)
942 * resize graph annotation if mouse was dragged up or down
944 int deltaY = mouseDragLastY - evt.getY();
947 AlignmentAnnotation graphAnnotation = av.getAlignment()
948 .getAlignmentAnnotation()[graphStretch];
949 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
952 for (AlignmentAnnotation similar : av.getAlignment()
953 .findAnnotations(null, graphAnnotation.getCalcId(),
954 graphAnnotation.label))
956 similar.graphHeight = newHeight;
962 graphAnnotation.graphHeight = newHeight;
965 ap.paintAlignment(false, false);
968 else if (dragMode == DragMode.MatrixSelect)
971 * TODO draw a rubber band for range
975 ap.paintAlignment(false, false);
980 * for mouse drag left or right, delegate to
981 * ScalePanel to adjust the column selection
983 ap.getScalePanel().mouseDragged(evt);
992 public void matrixSelectRange(MouseEvent evt)
995 * get geometry of drag
997 int fromY = Math.min(firstDragY, evt.getY());
998 int toY = Math.max(firstDragY, evt.getY());
999 int fromX = Math.min(firstDragX, evt.getX());
1000 int toX = Math.max(firstDragX, evt.getX());
1002 int deltaY = toY - fromY;
1003 int deltaX = toX - fromX;
1005 int[] rowIndex = getRowIndexAndOffset(fromY,
1006 av.getAlignment().getAlignmentAnnotation());
1007 int[] toRowIndex = getRowIndexAndOffset(toY,
1008 av.getAlignment().getAlignmentAnnotation());
1010 if (rowIndex == null || toRowIndex == null)
1012 System.out.println("Drag out of range. needs to be clipped");
1015 if (rowIndex[0] != toRowIndex[0])
1017 System.out.println("Drag went to another row. needs to be clipped");
1020 // rectangular selection on matrix style annotation
1021 AlignmentAnnotation cma = av.getAlignment()
1022 .getAlignmentAnnotation()[rowIndex[0]];
1024 int lastX = getColumnForXPos(fromX);
1025 int currentX = getColumnForXPos(toX);
1026 int fromXc = Math.min(lastX, currentX);
1027 int toXc = Math.max(lastX, currentX);
1028 ContactListI forFromX = av.getContactList(cma, fromXc);
1029 ContactListI forToX = av.getContactList(cma, toXc);
1031 if (forFromX != null && forToX != null)
1033 ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
1035 ContactGeometry.contactInterval lastXci = lastXcgeom
1036 .mapFor(rowIndex[1], rowIndex[1] - deltaY);
1038 ContactGeometry cXcgeom = new ContactGeometry(forToX,
1040 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
1041 rowIndex[1] - deltaY);
1043 // mark rectangular region formed by drag
1044 System.err.println("Matrix Selection from last(" + fromXc + ",["
1045 + lastXci.cStart + "," + lastXci.cEnd + "]) to cur(" + toXc
1046 + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1048 fr = Math.min(lastXci.cStart, lastXci.cEnd);
1049 to = Math.max(lastXci.cStart, lastXci.cEnd);
1050 System.err.println("Marking " + fr + " to " + to);
1051 for (int c = fr; c <= to; c++)
1053 if (cma.sequenceRef != null)
1055 int col = cma.sequenceRef.findIndex(c);
1056 av.getColumnSelection().addElement(col);
1060 av.getColumnSelection().addElement(c);
1063 fr = Math.min(cXci.cStart, cXci.cEnd);
1064 to = Math.max(cXci.cStart, cXci.cEnd);
1065 System.err.println("Marking " + fr + " to " + to);
1066 for (int c = fr; c <= to; c++)
1068 if (cma.sequenceRef != null)
1070 int col = cma.sequenceRef.findIndex(c);
1071 av.getColumnSelection().addElement(col);
1075 av.getColumnSelection().addElement(c);
1078 fr = Math.min(lastX, currentX);
1079 to = Math.max(lastX, currentX);
1081 System.err.println("Marking " + fr + " to " + to);
1082 for (int c = fr; c <= to; c++)
1084 av.getColumnSelection().addElement(c);
1091 * Constructs the tooltip, and constructs and displays a status message, for
1092 * the current mouse position
1097 public void mouseMoved(MouseEvent evt)
1099 int yPos = evt.getY();
1100 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1101 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1102 int row = rowAndOffset[0];
1106 this.setToolTipText(null);
1110 int column = getColumnForXPos(evt.getX());
1112 AlignmentAnnotation ann = aa[row];
1113 if (row > -1 && ann.annotations != null
1114 && column < ann.annotations.length)
1116 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1118 setToolTipText(toolTip == null ? null
1119 : JvSwingUtils.wrapTooltip(true, toolTip));
1120 String msg = getStatusMessage(av.getAlignment(), column, ann,
1121 rowAndOffset[1], av);
1122 ap.alignFrame.setStatus(msg);
1126 this.setToolTipText(null);
1127 ap.alignFrame.setStatus(" ");
1131 private int getColumnForXPos(int x)
1133 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1134 column = Math.min(column, av.getRanges().getEndRes());
1136 if (av.hasHiddenColumns())
1138 column = av.getAlignment().getHiddenColumns()
1139 .visibleToAbsoluteColumn(column);
1145 * Answers the index in the annotations array of the visible annotation at the
1146 * given y position. This is done by adding the heights of visible annotations
1147 * until the y position has been exceeded. Answers -1 if no annotations are
1148 * visible, or the y position is below all annotations.
1154 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1160 return getRowIndexAndOffset(yPos, aa)[0];
1163 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1165 int[] res = new int[2];
1173 int height = 0, lheight = 0;
1174 for (int i = 0; i < aa.length; i++)
1179 height += aa[i].height;
1186 res[1] = height - yPos;
1194 * Answers a tooltip for the annotation at the current mouse position, not
1195 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1201 * @param rowAndOffset
1203 static String buildToolTip(AlignmentAnnotation ann, int column,
1204 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1207 String tooltip = null;
1208 if (ann.graphGroup > -1)
1210 StringBuilder tip = new StringBuilder(32);
1211 boolean first = true;
1212 for (int i = 0; i < anns.length; i++)
1214 if (anns[i].graphGroup == ann.graphGroup
1215 && anns[i].annotations[column] != null)
1222 tip.append(anns[i].label);
1223 String description = anns[i].annotations[column].description;
1224 if (description != null && description.length() > 0)
1226 tip.append(" ").append(description);
1230 tooltip = first ? null : tip.toString();
1232 else if (column < ann.annotations.length
1233 && ann.annotations[column] != null)
1235 tooltip = ann.annotations[column].description;
1237 // TODO abstract tooltip generator so different implementations can be built
1238 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1240 ContactListI clist = av.getContactList(ann, column);
1243 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1244 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1245 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1246 tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
1247 + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
1248 int col = ann.sequenceRef.findPosition(column);
1249 ap.getStructureSelectionManager()
1250 .highlightPositionsOn(ann.sequenceRef, new int[][]
1251 { new int[] { col, col },
1253 { ci.cStart, ci.cEnd } }, null);
1260 * Constructs and returns the status bar message
1265 * @param rowAndOffset
1267 static String getStatusMessage(AlignmentI al, int column,
1268 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1271 * show alignment column and annotation description if any
1273 StringBuilder text = new StringBuilder(32);
1274 text.append(MessageManager.getString("label.column")).append(" ")
1275 .append(column + 1);
1277 if (column < ann.annotations.length && ann.annotations[column] != null)
1279 String description = ann.annotations[column].description;
1280 if (description != null && description.trim().length() > 0)
1282 text.append(" ").append(description);
1287 * if the annotation is sequence-specific, show the sequence number
1288 * in the alignment, and (if not a gap) the residue and position
1290 SequenceI seqref = ann.sequenceRef;
1293 int seqIndex = al.findIndex(seqref);
1296 text.append(", ").append(MessageManager.getString("label.sequence"))
1297 .append(" ").append(seqIndex + 1);
1298 char residue = seqref.getCharAt(column);
1299 if (!Comparison.isGap(residue))
1303 if (al.isNucleotide())
1305 name = ResidueProperties.nucleotideName
1306 .get(String.valueOf(residue));
1307 text.append(" Nucleotide: ")
1308 .append(name != null ? name : residue);
1312 name = 'X' == residue ? "X"
1313 : ('*' == residue ? "STOP"
1314 : ResidueProperties.aa2Triplet
1315 .get(String.valueOf(residue)));
1316 text.append(" Residue: ").append(name != null ? name : residue);
1318 int residuePos = seqref.findPosition(column);
1319 text.append(" (").append(residuePos).append(")");
1324 return text.toString();
1334 public void mouseClicked(MouseEvent evt)
1336 // if (activeRow != -1)
1338 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1339 // AlignmentAnnotation anot = aa[activeRow];
1343 // TODO mouseClicked-content and drawCursor are quite experimental!
1344 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1347 int pady = av.getCharHeight() / 5;
1349 graphics.setColor(Color.black);
1350 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1352 if (av.validCharWidth)
1354 graphics.setColor(Color.white);
1356 char s = seq.getCharAt(res);
1358 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1359 graphics.drawString(String.valueOf(s), charOffset + x1,
1360 (y1 + av.getCharHeight()) - pady);
1365 private volatile boolean imageFresh = false;
1367 private Rectangle visibleRect = new Rectangle(),
1368 clipBounds = new Rectangle();
1377 public void paintComponent(Graphics g)
1380 // BH: note that this method is generally recommended to
1381 // call super.paintComponent(g). Otherwise, the children of this
1382 // component will not be rendered. That is not needed here
1383 // because AnnotationPanel does not have any children. It is
1384 // just a JPanel contained in a JViewPort.
1386 computeVisibleRect(visibleRect);
1388 g.setColor(Color.white);
1389 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1393 // BH 2018 optimizing generation of new Rectangle().
1395 || (visibleRect.width != (clipBounds = g
1396 .getClipBounds(clipBounds)).width)
1397 || (visibleRect.height != clipBounds.height))
1400 g.drawImage(image, 0, 0, this);
1405 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1406 + 1) * av.getCharWidth();
1412 if (image == null || imgWidth != image.getWidth(this)
1413 || image.getHeight(this) != getHeight())
1415 boolean tried = false;
1417 while (image == null && !tried)
1421 image = new BufferedImage(imgWidth,
1422 ap.getAnnotationPanel().getHeight(),
1423 BufferedImage.TYPE_INT_RGB);
1425 } catch (IllegalArgumentException exc)
1428 "Serious issue with viewport geometry imgWidth requested was "
1431 } catch (OutOfMemoryError oom)
1436 } catch (Exception x)
1441 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1447 gg = (Graphics2D) image.getGraphics();
1451 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1452 RenderingHints.VALUE_ANTIALIAS_ON);
1455 gg.setFont(av.getFont());
1456 fm = gg.getFontMetrics();
1457 gg.setColor(Color.white);
1458 gg.fillRect(0, 0, imgWidth, image.getHeight());
1463 gg = (Graphics2D) image.getGraphics();
1467 drawComponent(gg, av.getRanges().getStartRes(),
1468 av.getRanges().getEndRes() + 1);
1471 g.drawImage(image, 0, 0, this);
1475 * set true to enable redraw timing debug output on stderr
1477 private final boolean debugRedraw = false;
1480 * non-Thread safe repaint
1483 * repaint with horizontal shift in alignment
1485 public void fastPaint(int horizontal)
1487 if ((horizontal == 0) || image == null
1488 || av.getAlignment().getAlignmentAnnotation() == null
1489 || av.getAlignment().getAlignmentAnnotation().length < 1
1490 || av.isCalcInProgress())
1496 int sr = av.getRanges().getStartRes();
1497 int er = av.getRanges().getEndRes() + 1;
1500 Graphics2D gg = (Graphics2D) image.getGraphics();
1502 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1504 // scroll is less than imgWidth away so can re-use buffered graphics
1505 gg.copyArea(0, 0, imgWidth, getHeight(),
1506 -horizontal * av.getCharWidth(), 0);
1508 if (horizontal > 0) // scrollbar pulled right, image to the left
1510 transX = (er - sr - horizontal) * av.getCharWidth();
1511 sr = er - horizontal;
1513 else if (horizontal < 0)
1515 er = sr - horizontal;
1518 gg.translate(transX, 0);
1520 drawComponent(gg, sr, er);
1522 gg.translate(-transX, 0);
1528 // Call repaint on alignment panel so that repaints from other alignment
1529 // panel components can be aggregated. Otherwise performance of the overview
1530 // window and others may be adversely affected.
1531 av.getAlignPanel().repaint();
1534 private volatile boolean lastImageGood = false;
1546 public void drawComponent(Graphics g, int startRes, int endRes)
1548 BufferedImage oldFaded = fadedImage;
1549 if (av.isCalcInProgress())
1553 lastImageGood = false;
1556 // We'll keep a record of the old image,
1557 // and draw a faded image until the calculation
1560 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1561 || fadedImage.getHeight() != image.getHeight()))
1563 // System.err.println("redraw faded image ("+(fadedImage==null ?
1564 // "null image" : "") + " lastGood="+lastImageGood+")");
1565 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1566 BufferedImage.TYPE_INT_RGB);
1568 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1570 fadedG.setColor(Color.white);
1571 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1573 fadedG.setComposite(
1574 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1575 fadedG.drawImage(image, 0, 0, this);
1578 // make sure we don't overwrite the last good faded image until all
1579 // calculations have finished
1580 lastImageGood = false;
1585 if (fadedImage != null)
1587 oldFaded = fadedImage;
1592 g.setColor(Color.white);
1593 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1595 g.setFont(av.getFont());
1598 fm = g.getFontMetrics();
1601 if ((av.getAlignment().getAlignmentAnnotation() == null)
1602 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1604 g.setColor(Color.white);
1605 g.fillRect(0, 0, getWidth(), getHeight());
1606 g.setColor(Color.black);
1607 if (av.validCharWidth)
1609 g.drawString(MessageManager
1610 .getString("label.alignment_has_no_annotations"), 20, 15);
1615 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1617 if (!lastImageGood && fadedImage == null)
1619 fadedImage = oldFaded;
1621 if (dragMode == DragMode.MatrixSelect)
1623 g.setColor(Color.yellow);
1624 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1625 Math.min(firstDragY, mouseDragLastY),
1626 Math.max(firstDragX, mouseDragLastX)
1627 - Math.min(firstDragX, mouseDragLastX),
1628 Math.max(firstDragY, mouseDragLastY)
1629 - Math.min(firstDragY, mouseDragLastY));
1635 public FontMetrics getFontMetrics()
1641 public Image getFadedImage()
1647 public int getFadedImageWidth()
1652 private int[] bounds = new int[2];
1655 public int[] getVisibleVRange()
1657 if (ap != null && ap.getAlabels() != null)
1659 int sOffset = -ap.getAlabels().getScrollOffset();
1660 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1661 bounds[0] = sOffset;
1662 bounds[1] = visHeight;
1672 * Try to ensure any references held are nulled
1674 public void dispose()
1684 * I created the renderer so I will dispose of it
1686 if (renderer != null)
1693 public void propertyChange(PropertyChangeEvent evt)
1695 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1696 // Both scrolling and resizing change viewport ranges: scrolling changes
1697 // both start and end points, but resize only changes end values.
1698 // Here we only want to fastpaint on a scroll, with resize using a normal
1699 // paint, so scroll events are identified as changes to the horizontal or
1700 // vertical start value.
1701 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1703 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1705 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1707 fastPaint(((int[]) evt.getNewValue())[0]
1708 - ((int[]) evt.getOldValue())[0]);
1710 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1717 * computes the visible height of the annotation panel
1719 * @param adjustPanelHeight
1720 * - when false, just adjust existing height according to other
1722 * @param annotationHeight
1723 * @return height to use for the ScrollerPreferredVisibleSize
1725 public int adjustForAlignFrame(boolean adjustPanelHeight,
1726 int annotationHeight)
1729 * Estimate available height in the AlignFrame for alignment +
1730 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1731 * hscroll, status bar, insets.
1733 int stuff = (ap.getViewName() != null ? 30 : 0)
1734 + (Platform.isAMacAndNotJS() ? 120 : 140);
1735 int availableHeight = ap.alignFrame.getHeight() - stuff;
1736 int rowHeight = av.getCharHeight();
1738 if (adjustPanelHeight)
1740 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1743 * If not enough vertical space, maximize annotation height while keeping
1744 * at least two rows of alignment visible
1746 if (annotationHeight + alignmentHeight > availableHeight)
1748 annotationHeight = Math.min(annotationHeight,
1749 availableHeight - 2 * rowHeight);
1754 // maintain same window layout whilst updating sliders
1755 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1756 availableHeight - 2 * rowHeight);
1758 return annotationHeight;