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;
606 GraphLine thr = aa[graphStretch].getThreshold();
608 int currentX = getColumnForXPos(evt.getX());
609 ContactListI forCurrentX = av.getContactList(aa[graphStretch],
611 if (forCurrentX != null)
613 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
614 aa[graphStretch].graphHeight);
615 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset,
618 * start and end range corresponding to the row range under the
619 * mouse at column currentX
622 fr = Math.min(cXci.cStart, cXci.cEnd);
623 to = Math.max(cXci.cStart, cXci.cEnd);
625 if (evt.isControlDown())
627 ContactMatrixI matrix = av.getContactMatrix(aa[graphStretch]);
631 // simplest approach is to select all group containing column
632 if (matrix.hasGroups())
634 SequenceI rseq = aa[graphStretch].sequenceRef;
635 BitSet grp = matrix.getGroupsFor(currentX);
636 for (int c=fr;c<=to; c++)
638 BitSet additionalGrp = matrix.getGroupsFor(c);
639 grp.or(additionalGrp);
641 HiddenColumns hc = av.getAlignment().getHiddenColumns();
642 for (int p = grp.nextSetBit(0); p >= 0; p = grp
645 int offp = (rseq != null)
646 ? rseq.findIndex(rseq.getStart() - 1 + p)
649 if (!av.hasHiddenColumns() || hc.isVisible(offp))
651 av.getColumnSelection().addElement(offp);
655 // possible alternative for interactive selection - threshold
656 // gives 'ceiling' for forming a cluster
657 // when a row+column is selected, farthest common ancestor less
658 // than thr is used to compute cluster
664 // select corresponding range in segment under mouse
666 for (int c = fr; c <= to; c++)
668 av.getColumnSelection().addElement(c);
670 av.getColumnSelection().addElement(currentX);
673 // and also select everything lower than the max range adjacent
675 if (PAEContactMatrix.PAEMATRIX
676 .equals(aa[graphStretch].getCalcId()))
679 ContactRange cr = forCurrentX.getRangeFor(fr, to);
681 // TODO: could use GraphLine instead of arbitrary picking
682 // TODO: could report mean/median/variance for partitions
683 // (contiguous selected vs unselected regions and inter-contig
685 // controls feathering - what other elements in row/column
687 double thresh = cr.getMean()
688 + (cr.getMax() - cr.getMean()) * .15;
691 cval = forCurrentX.getContactAt(c);
692 if (// cr.getMin() <= cval &&
695 av.getColumnSelection().addElement(c--);
703 while (c < forCurrentX.getContactHeight())
705 cval = forCurrentX.getContactAt(c);
706 if (// cr.getMin() <= cval &&
709 av.getColumnSelection().addElement(c++);
724 ap.getScalePanel().mousePressed(evt);
729 * Construct and display a context menu at the right-click position
734 void showPopupMenu(final int y, int x)
736 if (av.getColumnSelection() == null
737 || av.getColumnSelection().isEmpty())
742 JPopupMenu pop = new JPopupMenu(
743 MessageManager.getString("label.structure_type"));
746 * Just display the needed structure options
748 if (av.getAlignment().isNucleotide())
750 item = new JMenuItem(STEM);
751 item.addActionListener(this);
756 item = new JMenuItem(HELIX);
757 item.addActionListener(this);
759 item = new JMenuItem(SHEET);
760 item.addActionListener(this);
763 item = new JMenuItem(LABEL);
764 item.addActionListener(this);
766 item = new JMenuItem(COLOUR);
767 item.addActionListener(this);
769 item = new JMenuItem(REMOVE);
770 item.addActionListener(this);
772 pop.show(this, x, y);
776 * Action on mouse up is to clear mouse drag data and call mouseReleased on
777 * ScalePanel, to deal with defining the selection group (if any) defined by
783 public void mouseReleased(MouseEvent evt)
785 if (dragMode == DragMode.MatrixSelect)
787 matrixSelectRange(evt);
794 mouseDragging = false;
795 if (dragMode == DragMode.Resize)
797 ap.adjustAnnotationHeight();
799 dragMode = DragMode.Undefined;
800 ap.getScalePanel().mouseReleased(evt);
803 * isPopupTrigger is set in mouseReleased on Windows
804 * (in mousePressed on Mac)
806 if (evt.isPopupTrigger() && activeRow != -1)
808 showPopupMenu(evt.getY(), evt.getX());
820 public void mouseEntered(MouseEvent evt)
822 this.mouseDragging = false;
823 ap.getScalePanel().mouseEntered(evt);
827 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
828 * with column selection on a mouse drag
833 public void mouseExited(MouseEvent evt)
835 ap.getScalePanel().mouseExited(evt);
839 * Action on starting or continuing a mouse drag. There are two possible
842 * <li>drag up or down on a graphed annotation increases or decreases the
843 * height of the graph</li>
844 * <li>dragging left or right selects the columns dragged across</li>
846 * A drag on a graph annotation is treated as column selection if it starts
847 * with more horizontal than vertical movement, and as resize if it starts
848 * with more vertical than horizontal movement. Once started, the drag does
854 public void mouseDragged(MouseEvent evt)
857 * if dragMode is Undefined:
858 * - set to Select if dx > dy
859 * - set to Resize if dy > dx
860 * - do nothing if dx == dy
862 final int x = evt.getX();
863 final int y = evt.getY();
864 if (dragMode == DragMode.Undefined)
866 int dx = Math.abs(x - mouseDragLastX);
867 int dy = Math.abs(y - mouseDragLastY);
868 if (graphStretch == -1 || dx > dy)
871 * mostly horizontal drag, or not a graph annotation
873 dragMode = DragMode.Select;
878 * mostly vertical drag
880 dragMode = DragMode.Resize;
881 notJustOne = evt.isShiftDown();
884 * but could also be a matrix drag
886 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
887 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
890 * dragging in a matrix
892 dragMode = DragMode.MatrixSelect;
893 firstDragX = mouseDragLastX;
894 firstDragY = mouseDragLastY;
899 if (dragMode == DragMode.Undefined)
903 * drag is diagonal - defer deciding whether to
904 * treat as up/down or left/right
911 if (dragMode == DragMode.Resize)
914 * resize graph annotation if mouse was dragged up or down
916 int deltaY = mouseDragLastY - evt.getY();
919 AlignmentAnnotation graphAnnotation = av.getAlignment()
920 .getAlignmentAnnotation()[graphStretch];
921 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
924 for (AlignmentAnnotation similar : av.getAlignment()
925 .findAnnotations(null, graphAnnotation.getCalcId(),
926 graphAnnotation.label))
928 similar.graphHeight = newHeight;
934 graphAnnotation.graphHeight = newHeight;
937 ap.paintAlignment(false, false);
940 else if (dragMode == DragMode.MatrixSelect)
943 * TODO draw a rubber band for range
947 ap.paintAlignment(false, false);
952 * for mouse drag left or right, delegate to
953 * ScalePanel to adjust the column selection
955 ap.getScalePanel().mouseDragged(evt);
964 public void matrixSelectRange(MouseEvent evt)
967 * get geometry of drag
969 int fromY = Math.min(firstDragY, evt.getY());
970 int toY = Math.max(firstDragY, evt.getY());
971 int fromX = Math.min(firstDragX, evt.getX());
972 int toX = Math.max(firstDragX, evt.getX());
974 int deltaY = toY - fromY;
975 int deltaX = toX - fromX;
977 int[] rowIndex = getRowIndexAndOffset(fromY,
978 av.getAlignment().getAlignmentAnnotation());
979 int[] toRowIndex = getRowIndexAndOffset(toY,
980 av.getAlignment().getAlignmentAnnotation());
982 if (rowIndex == null || toRowIndex == null)
984 System.out.println("Drag out of range. needs to be clipped");
987 if (rowIndex[0] != toRowIndex[0])
989 System.out.println("Drag went to another row. needs to be clipped");
992 // rectangular selection on matrix style annotation
993 AlignmentAnnotation cma = av.getAlignment()
994 .getAlignmentAnnotation()[rowIndex[0]];
996 int lastX = getColumnForXPos(fromX);
997 int currentX = getColumnForXPos(toX);
998 int fromXc = Math.min(lastX, currentX);
999 int toXc = Math.max(lastX, currentX);
1000 ContactListI forFromX = av.getContactList(cma, fromXc);
1001 ContactListI forToX = av.getContactList(cma, toXc);
1003 if (forFromX != null && forToX != null)
1005 ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
1007 ContactGeometry.contactInterval lastXci = lastXcgeom
1008 .mapFor(rowIndex[1], rowIndex[1] - deltaY);
1010 ContactGeometry cXcgeom = new ContactGeometry(forToX,
1012 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
1013 rowIndex[1] - deltaY);
1015 // mark rectangular region formed by drag
1016 System.err.println("Matrix Selection from last(" + fromXc + ",["
1017 + lastXci.cStart + "," + lastXci.cEnd + "]) to cur(" + toXc
1018 + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
1020 fr = Math.min(lastXci.cStart, lastXci.cEnd);
1021 to = Math.max(lastXci.cStart, lastXci.cEnd);
1022 System.err.println("Marking " + fr + " to " + to);
1023 for (int c = fr; c <= to; c++)
1025 if (cma.sequenceRef != null)
1027 int col = cma.sequenceRef.findIndex(c);
1028 av.getColumnSelection().addElement(col);
1032 av.getColumnSelection().addElement(c);
1035 fr = Math.min(cXci.cStart, cXci.cEnd);
1036 to = Math.max(cXci.cStart, cXci.cEnd);
1037 System.err.println("Marking " + fr + " to " + to);
1038 for (int c = fr; c <= to; c++)
1040 if (cma.sequenceRef != null)
1042 int col = cma.sequenceRef.findIndex(c);
1043 av.getColumnSelection().addElement(col);
1047 av.getColumnSelection().addElement(c);
1050 fr = Math.min(lastX, currentX);
1051 to = Math.max(lastX, currentX);
1053 System.err.println("Marking " + fr + " to " + to);
1054 for (int c = fr; c <= to; c++)
1056 av.getColumnSelection().addElement(c);
1063 * Constructs the tooltip, and constructs and displays a status message, for
1064 * the current mouse position
1069 public void mouseMoved(MouseEvent evt)
1071 int yPos = evt.getY();
1072 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1073 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1074 int row = rowAndOffset[0];
1078 this.setToolTipText(null);
1082 int column = getColumnForXPos(evt.getX());
1084 AlignmentAnnotation ann = aa[row];
1085 if (row > -1 && ann.annotations != null
1086 && column < ann.annotations.length)
1088 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1090 setToolTipText(toolTip == null ? null
1091 : JvSwingUtils.wrapTooltip(true, toolTip));
1092 String msg = getStatusMessage(av.getAlignment(), column, ann,
1093 rowAndOffset[1], av);
1094 ap.alignFrame.setStatus(msg);
1098 this.setToolTipText(null);
1099 ap.alignFrame.setStatus(" ");
1103 private int getColumnForXPos(int x)
1105 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1106 column = Math.min(column, av.getRanges().getEndRes());
1108 if (av.hasHiddenColumns())
1110 column = av.getAlignment().getHiddenColumns()
1111 .visibleToAbsoluteColumn(column);
1117 * Answers the index in the annotations array of the visible annotation at the
1118 * given y position. This is done by adding the heights of visible annotations
1119 * until the y position has been exceeded. Answers -1 if no annotations are
1120 * visible, or the y position is below all annotations.
1126 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1132 return getRowIndexAndOffset(yPos, aa)[0];
1135 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1137 int[] res = new int[2];
1145 int height = 0, lheight = 0;
1146 for (int i = 0; i < aa.length; i++)
1151 height += aa[i].height;
1158 res[1] = height - yPos;
1166 * Answers a tooltip for the annotation at the current mouse position, not
1167 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1173 * @param rowAndOffset
1175 static String buildToolTip(AlignmentAnnotation ann, int column,
1176 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1179 String tooltip = null;
1180 if (ann.graphGroup > -1)
1182 StringBuilder tip = new StringBuilder(32);
1183 boolean first = true;
1184 for (int i = 0; i < anns.length; i++)
1186 if (anns[i].graphGroup == ann.graphGroup
1187 && anns[i].annotations[column] != null)
1194 tip.append(anns[i].label);
1195 String description = anns[i].annotations[column].description;
1196 if (description != null && description.length() > 0)
1198 tip.append(" ").append(description);
1202 tooltip = first ? null : tip.toString();
1204 else if (column < ann.annotations.length
1205 && ann.annotations[column] != null)
1207 tooltip = ann.annotations[column].description;
1209 // TODO abstract tooltip generator so different implementations can be built
1210 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1212 ContactListI clist = av.getContactList(ann, column);
1215 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1216 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1217 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1218 tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
1219 + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
1220 int col = ann.sequenceRef.findPosition(column);
1221 ap.getStructureSelectionManager()
1222 .highlightPositionsOn(ann.sequenceRef, new int[][]
1223 { new int[] { col, col },
1225 { ci.cStart, ci.cEnd } }, null);
1232 * Constructs and returns the status bar message
1237 * @param rowAndOffset
1239 static String getStatusMessage(AlignmentI al, int column,
1240 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1243 * show alignment column and annotation description if any
1245 StringBuilder text = new StringBuilder(32);
1246 text.append(MessageManager.getString("label.column")).append(" ")
1247 .append(column + 1);
1249 if (column < ann.annotations.length && ann.annotations[column] != null)
1251 String description = ann.annotations[column].description;
1252 if (description != null && description.trim().length() > 0)
1254 text.append(" ").append(description);
1259 * if the annotation is sequence-specific, show the sequence number
1260 * in the alignment, and (if not a gap) the residue and position
1262 SequenceI seqref = ann.sequenceRef;
1265 int seqIndex = al.findIndex(seqref);
1268 text.append(", ").append(MessageManager.getString("label.sequence"))
1269 .append(" ").append(seqIndex + 1);
1270 char residue = seqref.getCharAt(column);
1271 if (!Comparison.isGap(residue))
1275 if (al.isNucleotide())
1277 name = ResidueProperties.nucleotideName
1278 .get(String.valueOf(residue));
1279 text.append(" Nucleotide: ")
1280 .append(name != null ? name : residue);
1284 name = 'X' == residue ? "X"
1285 : ('*' == residue ? "STOP"
1286 : ResidueProperties.aa2Triplet
1287 .get(String.valueOf(residue)));
1288 text.append(" Residue: ").append(name != null ? name : residue);
1290 int residuePos = seqref.findPosition(column);
1291 text.append(" (").append(residuePos).append(")");
1296 return text.toString();
1306 public void mouseClicked(MouseEvent evt)
1308 // if (activeRow != -1)
1310 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1311 // AlignmentAnnotation anot = aa[activeRow];
1315 // TODO mouseClicked-content and drawCursor are quite experimental!
1316 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1319 int pady = av.getCharHeight() / 5;
1321 graphics.setColor(Color.black);
1322 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1324 if (av.validCharWidth)
1326 graphics.setColor(Color.white);
1328 char s = seq.getCharAt(res);
1330 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1331 graphics.drawString(String.valueOf(s), charOffset + x1,
1332 (y1 + av.getCharHeight()) - pady);
1337 private volatile boolean imageFresh = false;
1339 private Rectangle visibleRect = new Rectangle(),
1340 clipBounds = new Rectangle();
1349 public void paintComponent(Graphics g)
1352 // BH: note that this method is generally recommended to
1353 // call super.paintComponent(g). Otherwise, the children of this
1354 // component will not be rendered. That is not needed here
1355 // because AnnotationPanel does not have any children. It is
1356 // just a JPanel contained in a JViewPort.
1358 computeVisibleRect(visibleRect);
1360 g.setColor(Color.white);
1361 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1365 // BH 2018 optimizing generation of new Rectangle().
1367 || (visibleRect.width != (clipBounds = g
1368 .getClipBounds(clipBounds)).width)
1369 || (visibleRect.height != clipBounds.height))
1372 g.drawImage(image, 0, 0, this);
1377 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1378 + 1) * av.getCharWidth();
1384 if (image == null || imgWidth != image.getWidth(this)
1385 || image.getHeight(this) != getHeight())
1387 boolean tried = false;
1389 while (image == null && !tried)
1393 image = new BufferedImage(imgWidth,
1394 ap.getAnnotationPanel().getHeight(),
1395 BufferedImage.TYPE_INT_RGB);
1397 } catch (IllegalArgumentException exc)
1400 "Serious issue with viewport geometry imgWidth requested was "
1403 } catch (OutOfMemoryError oom)
1408 } catch (Exception x)
1413 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1419 gg = (Graphics2D) image.getGraphics();
1423 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1424 RenderingHints.VALUE_ANTIALIAS_ON);
1427 gg.setFont(av.getFont());
1428 fm = gg.getFontMetrics();
1429 gg.setColor(Color.white);
1430 gg.fillRect(0, 0, imgWidth, image.getHeight());
1435 gg = (Graphics2D) image.getGraphics();
1439 drawComponent(gg, av.getRanges().getStartRes(),
1440 av.getRanges().getEndRes() + 1);
1443 g.drawImage(image, 0, 0, this);
1447 * set true to enable redraw timing debug output on stderr
1449 private final boolean debugRedraw = false;
1452 * non-Thread safe repaint
1455 * repaint with horizontal shift in alignment
1457 public void fastPaint(int horizontal)
1459 if ((horizontal == 0) || image == null
1460 || av.getAlignment().getAlignmentAnnotation() == null
1461 || av.getAlignment().getAlignmentAnnotation().length < 1
1462 || av.isCalcInProgress())
1468 int sr = av.getRanges().getStartRes();
1469 int er = av.getRanges().getEndRes() + 1;
1472 Graphics2D gg = (Graphics2D) image.getGraphics();
1474 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1476 // scroll is less than imgWidth away so can re-use buffered graphics
1477 gg.copyArea(0, 0, imgWidth, getHeight(),
1478 -horizontal * av.getCharWidth(), 0);
1480 if (horizontal > 0) // scrollbar pulled right, image to the left
1482 transX = (er - sr - horizontal) * av.getCharWidth();
1483 sr = er - horizontal;
1485 else if (horizontal < 0)
1487 er = sr - horizontal;
1490 gg.translate(transX, 0);
1492 drawComponent(gg, sr, er);
1494 gg.translate(-transX, 0);
1500 // Call repaint on alignment panel so that repaints from other alignment
1501 // panel components can be aggregated. Otherwise performance of the overview
1502 // window and others may be adversely affected.
1503 av.getAlignPanel().repaint();
1506 private volatile boolean lastImageGood = false;
1518 public void drawComponent(Graphics g, int startRes, int endRes)
1520 BufferedImage oldFaded = fadedImage;
1521 if (av.isCalcInProgress())
1525 lastImageGood = false;
1528 // We'll keep a record of the old image,
1529 // and draw a faded image until the calculation
1532 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1533 || fadedImage.getHeight() != image.getHeight()))
1535 // System.err.println("redraw faded image ("+(fadedImage==null ?
1536 // "null image" : "") + " lastGood="+lastImageGood+")");
1537 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1538 BufferedImage.TYPE_INT_RGB);
1540 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1542 fadedG.setColor(Color.white);
1543 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1545 fadedG.setComposite(
1546 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1547 fadedG.drawImage(image, 0, 0, this);
1550 // make sure we don't overwrite the last good faded image until all
1551 // calculations have finished
1552 lastImageGood = false;
1557 if (fadedImage != null)
1559 oldFaded = fadedImage;
1564 g.setColor(Color.white);
1565 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1567 g.setFont(av.getFont());
1570 fm = g.getFontMetrics();
1573 if ((av.getAlignment().getAlignmentAnnotation() == null)
1574 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1576 g.setColor(Color.white);
1577 g.fillRect(0, 0, getWidth(), getHeight());
1578 g.setColor(Color.black);
1579 if (av.validCharWidth)
1581 g.drawString(MessageManager
1582 .getString("label.alignment_has_no_annotations"), 20, 15);
1587 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1589 if (!lastImageGood && fadedImage == null)
1591 fadedImage = oldFaded;
1593 if (dragMode == DragMode.MatrixSelect)
1595 g.setColor(Color.yellow);
1596 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1597 Math.min(firstDragY, mouseDragLastY),
1598 Math.max(firstDragX, mouseDragLastX)
1599 - Math.min(firstDragX, mouseDragLastX),
1600 Math.max(firstDragY, mouseDragLastY)
1601 - Math.min(firstDragY, mouseDragLastY));
1607 public FontMetrics getFontMetrics()
1613 public Image getFadedImage()
1619 public int getFadedImageWidth()
1624 private int[] bounds = new int[2];
1627 public int[] getVisibleVRange()
1629 if (ap != null && ap.getAlabels() != null)
1631 int sOffset = -ap.getAlabels().getScrollOffset();
1632 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1633 bounds[0] = sOffset;
1634 bounds[1] = visHeight;
1644 * Try to ensure any references held are nulled
1646 public void dispose()
1656 * I created the renderer so I will dispose of it
1658 if (renderer != null)
1665 public void propertyChange(PropertyChangeEvent evt)
1667 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1668 // Both scrolling and resizing change viewport ranges: scrolling changes
1669 // both start and end points, but resize only changes end values.
1670 // Here we only want to fastpaint on a scroll, with resize using a normal
1671 // paint, so scroll events are identified as changes to the horizontal or
1672 // vertical start value.
1673 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1675 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1677 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1679 fastPaint(((int[]) evt.getNewValue())[0]
1680 - ((int[]) evt.getOldValue())[0]);
1682 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1689 * computes the visible height of the annotation panel
1691 * @param adjustPanelHeight
1692 * - when false, just adjust existing height according to other
1694 * @param annotationHeight
1695 * @return height to use for the ScrollerPreferredVisibleSize
1697 public int adjustForAlignFrame(boolean adjustPanelHeight,
1698 int annotationHeight)
1701 * Estimate available height in the AlignFrame for alignment +
1702 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1703 * hscroll, status bar, insets.
1705 int stuff = (ap.getViewName() != null ? 30 : 0)
1706 + (Platform.isAMacAndNotJS() ? 120 : 140);
1707 int availableHeight = ap.alignFrame.getHeight() - stuff;
1708 int rowHeight = av.getCharHeight();
1710 if (adjustPanelHeight)
1712 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1715 * If not enough vertical space, maximize annotation height while keeping
1716 * at least two rows of alignment visible
1718 if (annotationHeight + alignmentHeight > availableHeight)
1720 annotationHeight = Math.min(annotationHeight,
1721 availableHeight - 2 * rowHeight);
1726 // maintain same window layout whilst updating sliders
1727 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1728 availableHeight - 2 * rowHeight);
1730 return annotationHeight;