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 // double click selects the whole group
660 if (evt.getClickCount() == 2)
662 ContactMatrixI matrix = av.getContactMatrix(clicked);
666 // simplest approach is to select all group containing column
667 if (matrix.hasGroups())
669 SequenceI rseq = clicked.sequenceRef;
670 BitSet grp = matrix.getGroupsFor(currentX);
671 // TODO: cXci needs to be mapped to real groups
672 for (int c = fr; c <= to; c++)
674 BitSet additionalGrp = matrix.getGroupsFor(c);
675 grp.or(additionalGrp);
677 HiddenColumns hc = av.getAlignment().getHiddenColumns();
678 for (int p = grp.nextSetBit(0); p >= 0; p = grp
681 int offp = (rseq != null)
682 ? rseq.findIndex(rseq.getStart() - 1 + p)
685 if (!av.hasHiddenColumns() || hc.isVisible(offp))
687 av.getColumnSelection().addElement(offp);
691 // possible alternative for interactive selection - threshold
692 // gives 'ceiling' for forming a cluster
693 // when a row+column is selected, farthest common ancestor less
694 // than thr is used to compute cluster
700 // select corresponding range in segment under mouse
702 int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
705 av.getColumnSelection().addRangeOfElements(rng, true);
707 av.getColumnSelection().addElement(currentX);
710 // and also select everything lower than the max range adjacent
712 if (evt.isControlDown()
713 && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
716 ContactRange cr = forCurrentX.getRangeFor(fr, to);
718 // TODO: could use GraphLine instead of arbitrary picking
719 // TODO: could report mean/median/variance for partitions
720 // (contiguous selected vs unselected regions and inter-contig
722 // controls feathering - what other elements in row/column
724 double thresh = cr.getMean() + (cr.getMax() - cr.getMean()) * .15;
727 cval = forCurrentX.getContactAt(c);
728 if (// cr.getMin() <= cval &&
731 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
734 av.getColumnSelection().addRangeOfElements(cols, true);
744 while (c < forCurrentX.getContactHeight())
746 cval = forCurrentX.getContactAt(c);
747 if (// cr.getMin() <= cval &&
750 int[] cols = forCurrentX.getMappedPositionsFor(c, c);
753 av.getColumnSelection().addRangeOfElements(cols, true);
766 ap.paintAlignment(false, false);
767 PaintRefresher.Refresh(ap, av.getSequenceSetId());
773 * Construct and display a context menu at the right-click position
778 void showPopupMenu(final int y, int x)
780 if (av.getColumnSelection() == null
781 || av.getColumnSelection().isEmpty())
786 JPopupMenu pop = new JPopupMenu(
787 MessageManager.getString("label.structure_type"));
790 * Just display the needed structure options
792 if (av.getAlignment().isNucleotide())
794 item = new JMenuItem(STEM);
795 item.addActionListener(this);
800 item = new JMenuItem(HELIX);
801 item.addActionListener(this);
803 item = new JMenuItem(SHEET);
804 item.addActionListener(this);
807 item = new JMenuItem(LABEL);
808 item.addActionListener(this);
810 item = new JMenuItem(COLOUR);
811 item.addActionListener(this);
813 item = new JMenuItem(REMOVE);
814 item.addActionListener(this);
816 pop.show(this, x, y);
820 * Action on mouse up is to clear mouse drag data and call mouseReleased on
821 * ScalePanel, to deal with defining the selection group (if any) defined by
827 public void mouseReleased(MouseEvent evt)
829 if (dragMode == DragMode.MatrixSelect)
831 matrixSelectRange(evt);
838 mouseDragging = false;
839 if (dragMode == DragMode.Resize)
841 ap.adjustAnnotationHeight();
843 dragMode = DragMode.Undefined;
844 if (!matrix_clicked(evt))
846 ap.getScalePanel().mouseReleased(evt);
850 * isPopupTrigger is set in mouseReleased on Windows
851 * (in mousePressed on Mac)
853 if (evt.isPopupTrigger() && activeRow != -1)
855 showPopupMenu(evt.getY(), evt.getX());
867 public void mouseEntered(MouseEvent evt)
869 this.mouseDragging = false;
870 ap.getScalePanel().mouseEntered(evt);
874 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
875 * with column selection on a mouse drag
880 public void mouseExited(MouseEvent evt)
882 ap.getScalePanel().mouseExited(evt);
886 * Action on starting or continuing a mouse drag. There are two possible
889 * <li>drag up or down on a graphed annotation increases or decreases the
890 * height of the graph</li>
891 * <li>dragging left or right selects the columns dragged across</li>
893 * A drag on a graph annotation is treated as column selection if it starts
894 * with more horizontal than vertical movement, and as resize if it starts
895 * with more vertical than horizontal movement. Once started, the drag does
901 public void mouseDragged(MouseEvent evt)
904 * if dragMode is Undefined:
905 * - set to Select if dx > dy
906 * - set to Resize if dy > dx
907 * - do nothing if dx == dy
909 final int x = evt.getX();
910 final int y = evt.getY();
911 if (dragMode == DragMode.Undefined)
913 int dx = Math.abs(x - mouseDragLastX);
914 int dy = Math.abs(y - mouseDragLastY);
915 if (graphStretch == -1 || dx > dy)
918 * mostly horizontal drag, or not a graph annotation
920 dragMode = DragMode.Select;
925 * mostly vertical drag
927 dragMode = DragMode.Resize;
928 notJustOne = evt.isShiftDown();
931 * but could also be a matrix drag
933 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
934 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
937 * dragging in a matrix
939 dragMode = DragMode.MatrixSelect;
940 firstDragX = mouseDragLastX;
941 firstDragY = mouseDragLastY;
946 if (dragMode == DragMode.Undefined)
950 * drag is diagonal - defer deciding whether to
951 * treat as up/down or left/right
958 if (dragMode == DragMode.Resize)
961 * resize graph annotation if mouse was dragged up or down
963 int deltaY = mouseDragLastY - evt.getY();
966 AlignmentAnnotation graphAnnotation = av.getAlignment()
967 .getAlignmentAnnotation()[graphStretch];
968 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
971 for (AlignmentAnnotation similar : av.getAlignment()
972 .findAnnotations(null, graphAnnotation.getCalcId(),
973 graphAnnotation.label))
975 similar.graphHeight = newHeight;
981 graphAnnotation.graphHeight = newHeight;
984 ap.paintAlignment(false, false);
987 else if (dragMode == DragMode.MatrixSelect)
990 * TODO draw a rubber band for range
994 ap.paintAlignment(false, false);
999 * for mouse drag left or right, delegate to
1000 * ScalePanel to adjust the column selection
1002 ap.getScalePanel().mouseDragged(evt);
1011 public void matrixSelectRange(MouseEvent evt)
1014 * get geometry of drag
1016 int fromY = Math.min(firstDragY, evt.getY());
1017 int toY = Math.max(firstDragY, evt.getY());
1018 int fromX = Math.min(firstDragX, evt.getX());
1019 int toX = Math.max(firstDragX, evt.getX());
1021 int deltaY = toY - fromY;
1022 int deltaX = toX - fromX;
1024 int[] rowIndex = getRowIndexAndOffset(fromY,
1025 av.getAlignment().getAlignmentAnnotation());
1026 int[] toRowIndex = getRowIndexAndOffset(toY,
1027 av.getAlignment().getAlignmentAnnotation());
1029 if (rowIndex == null || toRowIndex == null)
1031 jalview.bin.Console.trace("Drag out of range. needs to be clipped");
1034 if (rowIndex[0] != toRowIndex[0])
1037 .trace("Drag went to another row. needs to be clipped");
1040 // rectangular selection on matrix style annotation
1041 AlignmentAnnotation cma = av.getAlignment()
1042 .getAlignmentAnnotation()[rowIndex[0]];
1044 int lastX = getColumnForXPos(fromX);
1045 int currentX = getColumnForXPos(toX);
1046 int fromXc = Math.min(lastX, currentX);
1047 int toXc = Math.max(lastX, currentX);
1048 ContactListI forFromX = av.getContactList(cma, fromXc);
1049 ContactListI forToX = av.getContactList(cma, toXc);
1051 if (forFromX != null && forToX != null)
1053 ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
1055 ContactGeometry.contactInterval lastXci = lastXcgeom
1056 .mapFor(rowIndex[1], rowIndex[1] - deltaY);
1058 ContactGeometry cXcgeom = new ContactGeometry(forToX,
1060 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
1061 rowIndex[1] - deltaY);
1063 // mark rectangular region formed by drag
1064 jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
1065 + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
1066 + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1068 fr = Math.min(lastXci.cStart, lastXci.cEnd);
1069 to = Math.max(lastXci.cStart, lastXci.cEnd);
1070 int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
1071 if (mappedPos != null)
1073 jalview.bin.Console.trace("Marking " + fr + " to " + to
1074 + " mapping to sequence positions " + mappedPos[0] + " to "
1076 for (int pair = 0; pair < mappedPos.length; pair += 2)
1078 for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1080 // if (cma.sequenceRef != null)
1082 // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1083 // av.getColumnSelection().addElement(col);
1087 av.getColumnSelection().addElement(c);
1091 // and again for most recent corner of drag
1092 fr = Math.min(cXci.cStart, cXci.cEnd);
1093 to = Math.max(cXci.cStart, cXci.cEnd);
1094 mappedPos = forFromX.getMappedPositionsFor(fr, to);
1095 if (mappedPos != null)
1097 for (int pair = 0; pair < mappedPos.length; pair += 2)
1099 jalview.bin.Console.trace("Marking " + fr + " to " + to
1100 + " mapping to sequence positions " + mappedPos[pair]
1101 + " to " + mappedPos[pair + 1]);
1102 for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
1104 // if (cma.sequenceRef != null)
1107 // cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
1108 // av.getColumnSelection().addElement(col);
1112 av.getColumnSelection().addElement(c);
1117 fr = Math.min(lastX, currentX);
1118 to = Math.max(lastX, currentX);
1120 jalview.bin.Console.trace("Marking " + fr + " to " + to);
1121 for (int c = fr; c <= to; c++)
1123 av.getColumnSelection().addElement(c);
1130 * Constructs the tooltip, and constructs and displays a status message, for
1131 * the current mouse position
1136 public void mouseMoved(MouseEvent evt)
1138 int yPos = evt.getY();
1139 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1140 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1141 int row = rowAndOffset[0];
1145 this.setToolTipText(null);
1149 int column = getColumnForXPos(evt.getX());
1151 AlignmentAnnotation ann = aa[row];
1152 if (row > -1 && ann.annotations != null
1153 && column < ann.annotations.length)
1155 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1157 setToolTipText(toolTip == null ? null
1158 : JvSwingUtils.wrapTooltip(true, toolTip));
1159 String msg = getStatusMessage(av.getAlignment(), column, ann,
1160 rowAndOffset[1], av);
1161 ap.alignFrame.setStatus(msg);
1165 this.setToolTipText(null);
1166 ap.alignFrame.setStatus(" ");
1170 private int getColumnForXPos(int x)
1172 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1173 column = Math.min(column, av.getRanges().getEndRes());
1175 if (av.hasHiddenColumns())
1177 column = av.getAlignment().getHiddenColumns()
1178 .visibleToAbsoluteColumn(column);
1184 * Answers the index in the annotations array of the visible annotation at the
1185 * given y position. This is done by adding the heights of visible annotations
1186 * until the y position has been exceeded. Answers -1 if no annotations are
1187 * visible, or the y position is below all annotations.
1193 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1199 return getRowIndexAndOffset(yPos, aa)[0];
1202 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1204 int[] res = new int[2];
1212 int height = 0, lheight = 0;
1213 for (int i = 0; i < aa.length; i++)
1218 height += aa[i].height;
1225 res[1] = height - yPos;
1233 * Answers a tooltip for the annotation at the current mouse position, not
1234 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1240 * @param rowAndOffset
1242 static String buildToolTip(AlignmentAnnotation ann, int column,
1243 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1246 String tooltip = null;
1247 if (ann.graphGroup > -1)
1249 StringBuilder tip = new StringBuilder(32);
1250 boolean first = true;
1251 for (int i = 0; i < anns.length; i++)
1253 if (anns[i].graphGroup == ann.graphGroup
1254 && anns[i].annotations[column] != null)
1261 tip.append(anns[i].label);
1262 String description = anns[i].annotations[column].description;
1263 if (description != null && description.length() > 0)
1265 tip.append(" ").append(description);
1269 tooltip = first ? null : tip.toString();
1271 else if (column < ann.annotations.length
1272 && ann.annotations[column] != null)
1274 tooltip = ann.annotations[column].description;
1276 // TODO abstract tooltip generator so different implementations can be built
1277 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1279 ContactListI clist = av.getContactList(ann, column);
1282 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1283 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1284 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1285 tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
1286 + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
1288 int col = ann.sequenceRef.findPosition(column);
1289 int[][] highlightPos;
1290 int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
1291 if (mappedPos != null)
1293 highlightPos = new int[1 + mappedPos.length][2];
1294 highlightPos[0] = new int[] { col, col };
1295 for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
1297 highlightPos[h][0] = ann.sequenceRef
1298 .findPosition(mappedPos[p] - 1);
1299 highlightPos[h][1] = ann.sequenceRef
1300 .findPosition(mappedPos[p + 1] - 1);
1305 highlightPos = new int[][] { new int[] { col, col } };
1307 ap.getStructureSelectionManager()
1308 .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
1315 * Constructs and returns the status bar message
1320 * @param rowAndOffset
1322 static String getStatusMessage(AlignmentI al, int column,
1323 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1326 * show alignment column and annotation description if any
1328 StringBuilder text = new StringBuilder(32);
1329 text.append(MessageManager.getString("label.column")).append(" ")
1330 .append(column + 1);
1332 if (column < ann.annotations.length && ann.annotations[column] != null)
1334 String description = ann.annotations[column].description;
1335 if (description != null && description.trim().length() > 0)
1337 text.append(" ").append(description);
1342 * if the annotation is sequence-specific, show the sequence number
1343 * in the alignment, and (if not a gap) the residue and position
1345 SequenceI seqref = ann.sequenceRef;
1348 int seqIndex = al.findIndex(seqref);
1351 text.append(", ").append(MessageManager.getString("label.sequence"))
1352 .append(" ").append(seqIndex + 1);
1353 char residue = seqref.getCharAt(column);
1354 if (!Comparison.isGap(residue))
1358 if (al.isNucleotide())
1360 name = ResidueProperties.nucleotideName
1361 .get(String.valueOf(residue));
1362 text.append(" Nucleotide: ")
1363 .append(name != null ? name : residue);
1367 name = 'X' == residue ? "X"
1368 : ('*' == residue ? "STOP"
1369 : ResidueProperties.aa2Triplet
1370 .get(String.valueOf(residue)));
1371 text.append(" Residue: ").append(name != null ? name : residue);
1373 int residuePos = seqref.findPosition(column);
1374 text.append(" (").append(residuePos).append(")");
1379 return text.toString();
1389 public void mouseClicked(MouseEvent evt)
1391 // if (activeRow != -1)
1393 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1394 // AlignmentAnnotation anot = aa[activeRow];
1398 // TODO mouseClicked-content and drawCursor are quite experimental!
1399 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1402 int pady = av.getCharHeight() / 5;
1404 graphics.setColor(Color.black);
1405 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1407 if (av.validCharWidth)
1409 graphics.setColor(Color.white);
1411 char s = seq.getCharAt(res);
1413 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1414 graphics.drawString(String.valueOf(s), charOffset + x1,
1415 (y1 + av.getCharHeight()) - pady);
1420 private volatile boolean imageFresh = false;
1422 private Rectangle visibleRect = new Rectangle(),
1423 clipBounds = new Rectangle();
1432 public void paintComponent(Graphics g)
1435 // BH: note that this method is generally recommended to
1436 // call super.paintComponent(g). Otherwise, the children of this
1437 // component will not be rendered. That is not needed here
1438 // because AnnotationPanel does not have any children. It is
1439 // just a JPanel contained in a JViewPort.
1441 computeVisibleRect(visibleRect);
1443 g.setColor(Color.white);
1444 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1448 // BH 2018 optimizing generation of new Rectangle().
1450 || (visibleRect.width != (clipBounds = g
1451 .getClipBounds(clipBounds)).width)
1452 || (visibleRect.height != clipBounds.height))
1455 g.drawImage(image, 0, 0, this);
1460 updateFadedImageWidth();
1466 if (image == null || imgWidth != image.getWidth(this)
1467 || image.getHeight(this) != getHeight())
1469 boolean tried = false;
1471 while (image == null && !tried)
1475 image = new BufferedImage(imgWidth,
1476 ap.getAnnotationPanel().getHeight(),
1477 BufferedImage.TYPE_INT_RGB);
1479 } catch (IllegalArgumentException exc)
1482 "Serious issue with viewport geometry imgWidth requested was "
1485 } catch (OutOfMemoryError oom)
1490 } catch (Exception x)
1495 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1501 gg = (Graphics2D) image.getGraphics();
1505 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1506 RenderingHints.VALUE_ANTIALIAS_ON);
1509 gg.setFont(av.getFont());
1510 fm = gg.getFontMetrics();
1511 gg.setColor(Color.white);
1512 gg.fillRect(0, 0, imgWidth, image.getHeight());
1517 gg = (Graphics2D) image.getGraphics();
1521 drawComponent(gg, av.getRanges().getStartRes(),
1522 av.getRanges().getEndRes() + 1);
1525 g.drawImage(image, 0, 0, this);
1528 public void updateFadedImageWidth()
1530 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1531 + 1) * av.getCharWidth();
1536 * set true to enable redraw timing debug output on stderr
1538 private final boolean debugRedraw = false;
1541 * non-Thread safe repaint
1544 * repaint with horizontal shift in alignment
1546 public void fastPaint(int horizontal)
1548 if ((horizontal == 0) || image == null
1549 || av.getAlignment().getAlignmentAnnotation() == null
1550 || av.getAlignment().getAlignmentAnnotation().length < 1
1551 || av.isCalcInProgress())
1557 int sr = av.getRanges().getStartRes();
1558 int er = av.getRanges().getEndRes() + 1;
1561 Graphics2D gg = (Graphics2D) image.getGraphics();
1563 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1565 // scroll is less than imgWidth away so can re-use buffered graphics
1566 gg.copyArea(0, 0, imgWidth, getHeight(),
1567 -horizontal * av.getCharWidth(), 0);
1569 if (horizontal > 0) // scrollbar pulled right, image to the left
1571 transX = (er - sr - horizontal) * av.getCharWidth();
1572 sr = er - horizontal;
1574 else if (horizontal < 0)
1576 er = sr - horizontal;
1579 gg.translate(transX, 0);
1581 drawComponent(gg, sr, er);
1583 gg.translate(-transX, 0);
1589 // Call repaint on alignment panel so that repaints from other alignment
1590 // panel components can be aggregated. Otherwise performance of the overview
1591 // window and others may be adversely affected.
1592 av.getAlignPanel().repaint();
1595 private volatile boolean lastImageGood = false;
1607 public void drawComponent(Graphics g, int startRes, int endRes)
1609 BufferedImage oldFaded = fadedImage;
1610 if (av.isCalcInProgress())
1614 lastImageGood = false;
1617 // We'll keep a record of the old image,
1618 // and draw a faded image until the calculation
1621 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1622 || fadedImage.getHeight() != image.getHeight()))
1624 // System.err.println("redraw faded image ("+(fadedImage==null ?
1625 // "null image" : "") + " lastGood="+lastImageGood+")");
1626 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1627 BufferedImage.TYPE_INT_RGB);
1629 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1631 fadedG.setColor(Color.white);
1632 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1634 fadedG.setComposite(
1635 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1636 fadedG.drawImage(image, 0, 0, this);
1639 // make sure we don't overwrite the last good faded image until all
1640 // calculations have finished
1641 lastImageGood = false;
1646 if (fadedImage != null)
1648 oldFaded = fadedImage;
1653 g.setColor(Color.white);
1654 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1656 g.setFont(av.getFont());
1659 fm = g.getFontMetrics();
1662 if ((av.getAlignment().getAlignmentAnnotation() == null)
1663 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1665 g.setColor(Color.white);
1666 g.fillRect(0, 0, getWidth(), getHeight());
1667 g.setColor(Color.black);
1668 if (av.validCharWidth)
1670 g.drawString(MessageManager
1671 .getString("label.alignment_has_no_annotations"), 20, 15);
1676 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1678 if (!lastImageGood && fadedImage == null)
1680 fadedImage = oldFaded;
1682 if (dragMode == DragMode.MatrixSelect)
1684 g.setColor(Color.yellow);
1685 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1686 Math.min(firstDragY, mouseDragLastY),
1687 Math.max(firstDragX, mouseDragLastX)
1688 - Math.min(firstDragX, mouseDragLastX),
1689 Math.max(firstDragY, mouseDragLastY)
1690 - Math.min(firstDragY, mouseDragLastY));
1696 public FontMetrics getFontMetrics()
1702 public Image getFadedImage()
1708 public int getFadedImageWidth()
1710 updateFadedImageWidth();
1714 private int[] bounds = new int[2];
1717 public int[] getVisibleVRange()
1719 if (ap != null && ap.getAlabels() != null)
1721 int sOffset = -ap.getAlabels().getScrollOffset();
1722 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1723 bounds[0] = sOffset;
1724 bounds[1] = visHeight;
1734 * Try to ensure any references held are nulled
1736 public void dispose()
1746 * I created the renderer so I will dispose of it
1748 if (renderer != null)
1755 public void propertyChange(PropertyChangeEvent evt)
1757 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1758 // Both scrolling and resizing change viewport ranges: scrolling changes
1759 // both start and end points, but resize only changes end values.
1760 // Here we only want to fastpaint on a scroll, with resize using a normal
1761 // paint, so scroll events are identified as changes to the horizontal or
1762 // vertical start value.
1763 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1765 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1767 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1769 fastPaint(((int[]) evt.getNewValue())[0]
1770 - ((int[]) evt.getOldValue())[0]);
1772 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1779 * computes the visible height of the annotation panel
1781 * @param adjustPanelHeight
1782 * - when false, just adjust existing height according to other
1784 * @param annotationHeight
1785 * @return height to use for the ScrollerPreferredVisibleSize
1787 public int adjustForAlignFrame(boolean adjustPanelHeight,
1788 int annotationHeight)
1791 * Estimate available height in the AlignFrame for alignment +
1792 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1793 * hscroll, status bar, insets.
1795 int stuff = (ap.getViewName() != null ? 30 : 0)
1796 + (Platform.isAMacAndNotJS() ? 120 : 140);
1797 int availableHeight = ap.alignFrame.getHeight() - stuff;
1798 int rowHeight = av.getCharHeight();
1800 if (adjustPanelHeight)
1802 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1805 * If not enough vertical space, maximize annotation height while keeping
1806 * at least two rows of alignment visible
1808 if (annotationHeight + alignmentHeight > availableHeight)
1810 annotationHeight = Math.min(annotationHeight,
1811 availableHeight - 2 * rowHeight);
1816 // maintain same window layout whilst updating sliders
1817 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1818 availableHeight - 2 * rowHeight);
1820 return annotationHeight;