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 if (rowIndex == null)
627 .error("IMPLEMENTATION ERROR: matrix click out of range.");
630 int yOffset = rowIndex[1];
632 AlignmentAnnotation clicked = av.getAlignment()
633 .getAlignmentAnnotation()[rowIndex[0]];
634 if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
639 // TODO - use existing threshold to select related sections of matrix
640 GraphLine thr = clicked.getThreshold();
642 int currentX = getColumnForXPos(evt.getX());
643 ContactListI forCurrentX = av.getContactList(clicked, currentX);
644 if (forCurrentX != null)
646 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
647 clicked.graphHeight);
648 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset,
651 * start and end range corresponding to the row range under the mouse at
655 fr = Math.min(cXci.cStart, cXci.cEnd);
656 to = Math.max(cXci.cStart, cXci.cEnd);
658 if (evt.isControlDown())
660 ContactMatrixI matrix = av.getContactMatrix(clicked);
664 // simplest approach is to select all group containing column
665 if (matrix.hasGroups())
667 SequenceI rseq = clicked.sequenceRef;
668 BitSet grp = matrix.getGroupsFor(currentX);
669 for (int c = fr; c <= to; c++)
671 BitSet additionalGrp = matrix.getGroupsFor(c);
672 grp.or(additionalGrp);
674 HiddenColumns hc = av.getAlignment().getHiddenColumns();
675 for (int p = grp.nextSetBit(0); p >= 0; p = grp
678 int offp = (rseq != null)
679 ? rseq.findIndex(rseq.getStart() - 1 + p)
682 if (!av.hasHiddenColumns() || hc.isVisible(offp))
684 av.getColumnSelection().addElement(offp);
688 // possible alternative for interactive selection - threshold
689 // gives 'ceiling' for forming a cluster
690 // when a row+column is selected, farthest common ancestor less
691 // than thr is used to compute cluster
697 // select corresponding range in segment under mouse
699 for (int c = fr; c <= to; c++)
701 av.getColumnSelection().addElement(c);
703 av.getColumnSelection().addElement(currentX);
706 // and also select everything lower than the max range adjacent
708 if (PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
711 ContactRange cr = forCurrentX.getRangeFor(fr, to);
713 // TODO: could use GraphLine instead of arbitrary picking
714 // TODO: could report mean/median/variance for partitions
715 // (contiguous selected vs unselected regions and inter-contig
717 // controls feathering - what other elements in row/column
719 double thresh = cr.getMean() + (cr.getMax() - cr.getMean()) * .15;
722 cval = forCurrentX.getContactAt(c);
723 if (// cr.getMin() <= cval &&
726 av.getColumnSelection().addElement(c--);
734 while (c < forCurrentX.getContactHeight())
736 cval = forCurrentX.getContactAt(c);
737 if (// cr.getMin() <= cval &&
740 av.getColumnSelection().addElement(c++);
750 ap.paintAlignment(false, false);
751 PaintRefresher.Refresh(ap, av.getSequenceSetId());
756 * Construct and display a context menu at the right-click position
761 void showPopupMenu(final int y, int x)
763 if (av.getColumnSelection() == null
764 || av.getColumnSelection().isEmpty())
769 JPopupMenu pop = new JPopupMenu(
770 MessageManager.getString("label.structure_type"));
773 * Just display the needed structure options
775 if (av.getAlignment().isNucleotide())
777 item = new JMenuItem(STEM);
778 item.addActionListener(this);
783 item = new JMenuItem(HELIX);
784 item.addActionListener(this);
786 item = new JMenuItem(SHEET);
787 item.addActionListener(this);
790 item = new JMenuItem(LABEL);
791 item.addActionListener(this);
793 item = new JMenuItem(COLOUR);
794 item.addActionListener(this);
796 item = new JMenuItem(REMOVE);
797 item.addActionListener(this);
799 pop.show(this, x, y);
803 * Action on mouse up is to clear mouse drag data and call mouseReleased on
804 * ScalePanel, to deal with defining the selection group (if any) defined by
810 public void mouseReleased(MouseEvent evt)
812 if (dragMode == DragMode.MatrixSelect)
814 matrixSelectRange(evt);
821 mouseDragging = false;
822 if (dragMode == DragMode.Resize)
824 ap.adjustAnnotationHeight();
826 dragMode = DragMode.Undefined;
827 if (!matrix_clicked(evt))
829 ap.getScalePanel().mouseReleased(evt);
833 * isPopupTrigger is set in mouseReleased on Windows
834 * (in mousePressed on Mac)
836 if (evt.isPopupTrigger() && activeRow != -1)
838 showPopupMenu(evt.getY(), evt.getX());
850 public void mouseEntered(MouseEvent evt)
852 this.mouseDragging = false;
853 ap.getScalePanel().mouseEntered(evt);
857 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
858 * with column selection on a mouse drag
863 public void mouseExited(MouseEvent evt)
865 ap.getScalePanel().mouseExited(evt);
869 * Action on starting or continuing a mouse drag. There are two possible
872 * <li>drag up or down on a graphed annotation increases or decreases the
873 * height of the graph</li>
874 * <li>dragging left or right selects the columns dragged across</li>
876 * A drag on a graph annotation is treated as column selection if it starts
877 * with more horizontal than vertical movement, and as resize if it starts
878 * with more vertical than horizontal movement. Once started, the drag does
884 public void mouseDragged(MouseEvent evt)
887 * if dragMode is Undefined:
888 * - set to Select if dx > dy
889 * - set to Resize if dy > dx
890 * - do nothing if dx == dy
892 final int x = evt.getX();
893 final int y = evt.getY();
894 if (dragMode == DragMode.Undefined)
896 int dx = Math.abs(x - mouseDragLastX);
897 int dy = Math.abs(y - mouseDragLastY);
898 if (graphStretch == -1 || dx > dy)
901 * mostly horizontal drag, or not a graph annotation
903 dragMode = DragMode.Select;
908 * mostly vertical drag
910 dragMode = DragMode.Resize;
911 notJustOne = evt.isShiftDown();
914 * but could also be a matrix drag
916 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
917 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
920 * dragging in a matrix
922 dragMode = DragMode.MatrixSelect;
923 firstDragX = mouseDragLastX;
924 firstDragY = mouseDragLastY;
929 if (dragMode == DragMode.Undefined)
933 * drag is diagonal - defer deciding whether to
934 * treat as up/down or left/right
941 if (dragMode == DragMode.Resize)
944 * resize graph annotation if mouse was dragged up or down
946 int deltaY = mouseDragLastY - evt.getY();
949 AlignmentAnnotation graphAnnotation = av.getAlignment()
950 .getAlignmentAnnotation()[graphStretch];
951 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
954 for (AlignmentAnnotation similar : av.getAlignment()
955 .findAnnotations(null, graphAnnotation.getCalcId(),
956 graphAnnotation.label))
958 similar.graphHeight = newHeight;
964 graphAnnotation.graphHeight = newHeight;
967 ap.paintAlignment(false, false);
970 else if (dragMode == DragMode.MatrixSelect)
973 * TODO draw a rubber band for range
977 ap.paintAlignment(false, false);
982 * for mouse drag left or right, delegate to
983 * ScalePanel to adjust the column selection
985 ap.getScalePanel().mouseDragged(evt);
994 public void matrixSelectRange(MouseEvent evt)
997 * get geometry of drag
999 int fromY = Math.min(firstDragY, evt.getY());
1000 int toY = Math.max(firstDragY, evt.getY());
1001 int fromX = Math.min(firstDragX, evt.getX());
1002 int toX = Math.max(firstDragX, evt.getX());
1004 int deltaY = toY - fromY;
1005 int deltaX = toX - fromX;
1007 int[] rowIndex = getRowIndexAndOffset(fromY,
1008 av.getAlignment().getAlignmentAnnotation());
1009 int[] toRowIndex = getRowIndexAndOffset(toY,
1010 av.getAlignment().getAlignmentAnnotation());
1012 if (rowIndex == null || toRowIndex == null)
1014 jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1017 if (rowIndex[0] != toRowIndex[0])
1019 jalview.bin.Console.trace("Drag went to another row. needs to be clipped");
1022 // rectangular selection on matrix style annotation
1023 AlignmentAnnotation cma = av.getAlignment()
1024 .getAlignmentAnnotation()[rowIndex[0]];
1026 int lastX = getColumnForXPos(fromX);
1027 int currentX = getColumnForXPos(toX);
1028 int fromXc = Math.min(lastX, currentX);
1029 int toXc = Math.max(lastX, currentX);
1030 ContactListI forFromX = av.getContactList(cma, fromXc);
1031 ContactListI forToX = av.getContactList(cma, toXc);
1033 if (forFromX != null && forToX != null)
1035 ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
1037 ContactGeometry.contactInterval lastXci = lastXcgeom
1038 .mapFor(rowIndex[1], rowIndex[1] - deltaY);
1040 ContactGeometry cXcgeom = new ContactGeometry(forToX,
1042 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
1043 rowIndex[1] - deltaY);
1045 // mark rectangular region formed by drag
1046 jalview.bin.Console.trace("Matrix Selection from last(" + fromXc + ",["
1047 + lastXci.cStart + "," + lastXci.cEnd + "]) to cur(" + toXc
1048 + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1050 fr = Math.min(lastXci.cStart, lastXci.cEnd);
1051 to = Math.max(lastXci.cStart, lastXci.cEnd);
1052 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1053 for (int c = fr; c <= to; c++)
1055 if (cma.sequenceRef != null)
1057 int col = cma.sequenceRef.findIndex(c);
1058 av.getColumnSelection().addElement(col);
1062 av.getColumnSelection().addElement(c);
1065 fr = Math.min(cXci.cStart, cXci.cEnd);
1066 to = Math.max(cXci.cStart, cXci.cEnd);
1067 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1068 for (int c = fr; c <= to; c++)
1070 if (cma.sequenceRef != null)
1072 int col = cma.sequenceRef.findIndex(c);
1073 av.getColumnSelection().addElement(col);
1077 av.getColumnSelection().addElement(c);
1080 fr = Math.min(lastX, currentX);
1081 to = Math.max(lastX, currentX);
1083 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1084 for (int c = fr; c <= to; c++)
1086 av.getColumnSelection().addElement(c);
1093 * Constructs the tooltip, and constructs and displays a status message, for
1094 * the current mouse position
1099 public void mouseMoved(MouseEvent evt)
1101 int yPos = evt.getY();
1102 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1103 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1104 int row = rowAndOffset[0];
1108 this.setToolTipText(null);
1112 int column = getColumnForXPos(evt.getX());
1114 AlignmentAnnotation ann = aa[row];
1115 if (row > -1 && ann.annotations != null
1116 && column < ann.annotations.length)
1118 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1120 setToolTipText(toolTip == null ? null
1121 : JvSwingUtils.wrapTooltip(true, toolTip));
1122 String msg = getStatusMessage(av.getAlignment(), column, ann,
1123 rowAndOffset[1], av);
1124 ap.alignFrame.setStatus(msg);
1128 this.setToolTipText(null);
1129 ap.alignFrame.setStatus(" ");
1133 private int getColumnForXPos(int x)
1135 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1136 column = Math.min(column, av.getRanges().getEndRes());
1138 if (av.hasHiddenColumns())
1140 column = av.getAlignment().getHiddenColumns()
1141 .visibleToAbsoluteColumn(column);
1147 * Answers the index in the annotations array of the visible annotation at the
1148 * given y position. This is done by adding the heights of visible annotations
1149 * until the y position has been exceeded. Answers -1 if no annotations are
1150 * visible, or the y position is below all annotations.
1156 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1162 return getRowIndexAndOffset(yPos, aa)[0];
1165 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1167 int[] res = new int[2];
1175 int height = 0, lheight = 0;
1176 for (int i = 0; i < aa.length; i++)
1181 height += aa[i].height;
1188 res[1] = height - yPos;
1196 * Answers a tooltip for the annotation at the current mouse position, not
1197 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1203 * @param rowAndOffset
1205 static String buildToolTip(AlignmentAnnotation ann, int column,
1206 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1209 String tooltip = null;
1210 if (ann.graphGroup > -1)
1212 StringBuilder tip = new StringBuilder(32);
1213 boolean first = true;
1214 for (int i = 0; i < anns.length; i++)
1216 if (anns[i].graphGroup == ann.graphGroup
1217 && anns[i].annotations[column] != null)
1224 tip.append(anns[i].label);
1225 String description = anns[i].annotations[column].description;
1226 if (description != null && description.length() > 0)
1228 tip.append(" ").append(description);
1232 tooltip = first ? null : tip.toString();
1234 else if (column < ann.annotations.length
1235 && ann.annotations[column] != null)
1237 tooltip = ann.annotations[column].description;
1239 // TODO abstract tooltip generator so different implementations can be built
1240 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1242 ContactListI clist = av.getContactList(ann, column);
1245 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1246 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1247 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1248 tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
1249 + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
1250 int col = ann.sequenceRef.findPosition(column);
1251 ap.getStructureSelectionManager()
1252 .highlightPositionsOn(ann.sequenceRef, new int[][]
1253 { new int[] { col, col },
1255 { ci.cStart, ci.cEnd } }, null);
1262 * Constructs and returns the status bar message
1267 * @param rowAndOffset
1269 static String getStatusMessage(AlignmentI al, int column,
1270 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1273 * show alignment column and annotation description if any
1275 StringBuilder text = new StringBuilder(32);
1276 text.append(MessageManager.getString("label.column")).append(" ")
1277 .append(column + 1);
1279 if (column < ann.annotations.length && ann.annotations[column] != null)
1281 String description = ann.annotations[column].description;
1282 if (description != null && description.trim().length() > 0)
1284 text.append(" ").append(description);
1289 * if the annotation is sequence-specific, show the sequence number
1290 * in the alignment, and (if not a gap) the residue and position
1292 SequenceI seqref = ann.sequenceRef;
1295 int seqIndex = al.findIndex(seqref);
1298 text.append(", ").append(MessageManager.getString("label.sequence"))
1299 .append(" ").append(seqIndex + 1);
1300 char residue = seqref.getCharAt(column);
1301 if (!Comparison.isGap(residue))
1305 if (al.isNucleotide())
1307 name = ResidueProperties.nucleotideName
1308 .get(String.valueOf(residue));
1309 text.append(" Nucleotide: ")
1310 .append(name != null ? name : residue);
1314 name = 'X' == residue ? "X"
1315 : ('*' == residue ? "STOP"
1316 : ResidueProperties.aa2Triplet
1317 .get(String.valueOf(residue)));
1318 text.append(" Residue: ").append(name != null ? name : residue);
1320 int residuePos = seqref.findPosition(column);
1321 text.append(" (").append(residuePos).append(")");
1326 return text.toString();
1336 public void mouseClicked(MouseEvent evt)
1338 // if (activeRow != -1)
1340 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1341 // AlignmentAnnotation anot = aa[activeRow];
1345 // TODO mouseClicked-content and drawCursor are quite experimental!
1346 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1349 int pady = av.getCharHeight() / 5;
1351 graphics.setColor(Color.black);
1352 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1354 if (av.validCharWidth)
1356 graphics.setColor(Color.white);
1358 char s = seq.getCharAt(res);
1360 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1361 graphics.drawString(String.valueOf(s), charOffset + x1,
1362 (y1 + av.getCharHeight()) - pady);
1367 private volatile boolean imageFresh = false;
1369 private Rectangle visibleRect = new Rectangle(),
1370 clipBounds = new Rectangle();
1379 public void paintComponent(Graphics g)
1382 // BH: note that this method is generally recommended to
1383 // call super.paintComponent(g). Otherwise, the children of this
1384 // component will not be rendered. That is not needed here
1385 // because AnnotationPanel does not have any children. It is
1386 // just a JPanel contained in a JViewPort.
1388 computeVisibleRect(visibleRect);
1390 g.setColor(Color.white);
1391 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1395 // BH 2018 optimizing generation of new Rectangle().
1397 || (visibleRect.width != (clipBounds = g
1398 .getClipBounds(clipBounds)).width)
1399 || (visibleRect.height != clipBounds.height))
1402 g.drawImage(image, 0, 0, this);
1407 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1408 + 1) * av.getCharWidth();
1414 if (image == null || imgWidth != image.getWidth(this)
1415 || image.getHeight(this) != getHeight())
1417 boolean tried = false;
1419 while (image == null && !tried)
1423 image = new BufferedImage(imgWidth,
1424 ap.getAnnotationPanel().getHeight(),
1425 BufferedImage.TYPE_INT_RGB);
1427 } catch (IllegalArgumentException exc)
1430 "Serious issue with viewport geometry imgWidth requested was "
1433 } catch (OutOfMemoryError oom)
1438 } catch (Exception x)
1443 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1449 gg = (Graphics2D) image.getGraphics();
1453 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1454 RenderingHints.VALUE_ANTIALIAS_ON);
1457 gg.setFont(av.getFont());
1458 fm = gg.getFontMetrics();
1459 gg.setColor(Color.white);
1460 gg.fillRect(0, 0, imgWidth, image.getHeight());
1465 gg = (Graphics2D) image.getGraphics();
1469 drawComponent(gg, av.getRanges().getStartRes(),
1470 av.getRanges().getEndRes() + 1);
1473 g.drawImage(image, 0, 0, this);
1477 * set true to enable redraw timing debug output on stderr
1479 private final boolean debugRedraw = false;
1482 * non-Thread safe repaint
1485 * repaint with horizontal shift in alignment
1487 public void fastPaint(int horizontal)
1489 if ((horizontal == 0) || image == null
1490 || av.getAlignment().getAlignmentAnnotation() == null
1491 || av.getAlignment().getAlignmentAnnotation().length < 1
1492 || av.isCalcInProgress())
1498 int sr = av.getRanges().getStartRes();
1499 int er = av.getRanges().getEndRes() + 1;
1502 Graphics2D gg = (Graphics2D) image.getGraphics();
1504 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1506 // scroll is less than imgWidth away so can re-use buffered graphics
1507 gg.copyArea(0, 0, imgWidth, getHeight(),
1508 -horizontal * av.getCharWidth(), 0);
1510 if (horizontal > 0) // scrollbar pulled right, image to the left
1512 transX = (er - sr - horizontal) * av.getCharWidth();
1513 sr = er - horizontal;
1515 else if (horizontal < 0)
1517 er = sr - horizontal;
1520 gg.translate(transX, 0);
1522 drawComponent(gg, sr, er);
1524 gg.translate(-transX, 0);
1530 // Call repaint on alignment panel so that repaints from other alignment
1531 // panel components can be aggregated. Otherwise performance of the overview
1532 // window and others may be adversely affected.
1533 av.getAlignPanel().repaint();
1536 private volatile boolean lastImageGood = false;
1548 public void drawComponent(Graphics g, int startRes, int endRes)
1550 BufferedImage oldFaded = fadedImage;
1551 if (av.isCalcInProgress())
1555 lastImageGood = false;
1558 // We'll keep a record of the old image,
1559 // and draw a faded image until the calculation
1562 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1563 || fadedImage.getHeight() != image.getHeight()))
1565 // System.err.println("redraw faded image ("+(fadedImage==null ?
1566 // "null image" : "") + " lastGood="+lastImageGood+")");
1567 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1568 BufferedImage.TYPE_INT_RGB);
1570 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1572 fadedG.setColor(Color.white);
1573 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1575 fadedG.setComposite(
1576 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1577 fadedG.drawImage(image, 0, 0, this);
1580 // make sure we don't overwrite the last good faded image until all
1581 // calculations have finished
1582 lastImageGood = false;
1587 if (fadedImage != null)
1589 oldFaded = fadedImage;
1594 g.setColor(Color.white);
1595 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1597 g.setFont(av.getFont());
1600 fm = g.getFontMetrics();
1603 if ((av.getAlignment().getAlignmentAnnotation() == null)
1604 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1606 g.setColor(Color.white);
1607 g.fillRect(0, 0, getWidth(), getHeight());
1608 g.setColor(Color.black);
1609 if (av.validCharWidth)
1611 g.drawString(MessageManager
1612 .getString("label.alignment_has_no_annotations"), 20, 15);
1617 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1619 if (!lastImageGood && fadedImage == null)
1621 fadedImage = oldFaded;
1623 if (dragMode == DragMode.MatrixSelect)
1625 g.setColor(Color.yellow);
1626 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1627 Math.min(firstDragY, mouseDragLastY),
1628 Math.max(firstDragX, mouseDragLastX)
1629 - Math.min(firstDragX, mouseDragLastX),
1630 Math.max(firstDragY, mouseDragLastY)
1631 - Math.min(firstDragY, mouseDragLastY));
1637 public FontMetrics getFontMetrics()
1643 public Image getFadedImage()
1649 public int getFadedImageWidth()
1654 private int[] bounds = new int[2];
1657 public int[] getVisibleVRange()
1659 if (ap != null && ap.getAlabels() != null)
1661 int sOffset = -ap.getAlabels().getScrollOffset();
1662 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1663 bounds[0] = sOffset;
1664 bounds[1] = visHeight;
1674 * Try to ensure any references held are nulled
1676 public void dispose()
1686 * I created the renderer so I will dispose of it
1688 if (renderer != null)
1695 public void propertyChange(PropertyChangeEvent evt)
1697 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1698 // Both scrolling and resizing change viewport ranges: scrolling changes
1699 // both start and end points, but resize only changes end values.
1700 // Here we only want to fastpaint on a scroll, with resize using a normal
1701 // paint, so scroll events are identified as changes to the horizontal or
1702 // vertical start value.
1703 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1705 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1707 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1709 fastPaint(((int[]) evt.getNewValue())[0]
1710 - ((int[]) evt.getOldValue())[0]);
1712 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1719 * computes the visible height of the annotation panel
1721 * @param adjustPanelHeight
1722 * - when false, just adjust existing height according to other
1724 * @param annotationHeight
1725 * @return height to use for the ScrollerPreferredVisibleSize
1727 public int adjustForAlignFrame(boolean adjustPanelHeight,
1728 int annotationHeight)
1731 * Estimate available height in the AlignFrame for alignment +
1732 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1733 * hscroll, status bar, insets.
1735 int stuff = (ap.getViewName() != null ? 30 : 0)
1736 + (Platform.isAMacAndNotJS() ? 120 : 140);
1737 int availableHeight = ap.alignFrame.getHeight() - stuff;
1738 int rowHeight = av.getCharHeight();
1740 if (adjustPanelHeight)
1742 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1745 * If not enough vertical space, maximize annotation height while keeping
1746 * at least two rows of alignment visible
1748 if (annotationHeight + alignmentHeight > availableHeight)
1750 annotationHeight = Math.min(annotationHeight,
1751 availableHeight - 2 * rowHeight);
1756 // maintain same window layout whilst updating sliders
1757 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1758 availableHeight - 2 * rowHeight);
1760 return annotationHeight;