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.Collections;
45 import java.util.List;
47 import javax.swing.JMenuItem;
48 import javax.swing.JPanel;
49 import javax.swing.JPopupMenu;
50 import javax.swing.Scrollable;
51 import javax.swing.ToolTipManager;
53 import jalview.api.AlignViewportI;
54 import jalview.datamodel.AlignmentAnnotation;
55 import jalview.datamodel.AlignmentI;
56 import jalview.datamodel.Annotation;
57 import jalview.datamodel.ColumnSelection;
58 import jalview.datamodel.ContactListI;
59 import jalview.datamodel.ContactRange;
60 import jalview.datamodel.GraphLine;
61 import jalview.datamodel.HiddenColumns;
62 import jalview.datamodel.SequenceI;
63 import jalview.gui.JalviewColourChooser.ColourChooserListener;
64 import jalview.renderer.AnnotationRenderer;
65 import jalview.renderer.AwtRenderPanelI;
66 import jalview.renderer.ContactGeometry;
67 import jalview.schemes.ResidueProperties;
68 import jalview.util.Comparison;
69 import jalview.util.MessageManager;
70 import jalview.util.Platform;
71 import jalview.viewmodel.ViewportListenerI;
72 import jalview.viewmodel.ViewportRanges;
73 import jalview.ws.datamodel.alphafold.PAEContactMatrix;
76 * AnnotationPanel displays visible portion of annotation rows below unwrapped
82 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
83 MouseListener, MouseWheelListener, MouseMotionListener,
84 ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
88 Select, Resize, Undefined, MatrixSelect
91 String HELIX = MessageManager.getString("label.helix");
93 String SHEET = MessageManager.getString("label.sheet");
96 * For RNA secondary structure "stems" aka helices
98 String STEM = MessageManager.getString("label.rna_helix");
100 String LABEL = MessageManager.getString("label.label");
102 String REMOVE = MessageManager.getString("label.remove_annotation");
104 String COLOUR = MessageManager.getString("action.colour");
106 public final Color HELIX_COLOUR = Color.red.darker();
108 public final Color SHEET_COLOUR = Color.green.darker().darker();
110 public final Color STEM_COLOUR = Color.blue.darker();
113 public AlignViewport av;
117 public int activeRow = -1;
119 public BufferedImage image;
121 public volatile BufferedImage fadedImage;
123 // private Graphics2D gg;
125 public FontMetrics fm;
127 public int imgWidth = 0;
129 boolean fastPaint = false;
131 // Used For mouse Dragging and resizing graphs
132 int graphStretch = -1;
134 int mouseDragLastX = -1;
136 int mouseDragLastY = -1;
142 DragMode dragMode = DragMode.Undefined;
144 boolean mouseDragging = false;
146 // for editing cursor
151 public final AnnotationRenderer renderer;
153 private MouseWheelListener[] _mwl;
155 private boolean notJustOne;
158 * Creates a new AnnotationPanel object.
163 public AnnotationPanel(AlignmentPanel ap)
165 ToolTipManager.sharedInstance().registerComponent(this);
166 ToolTipManager.sharedInstance().setInitialDelay(0);
167 ToolTipManager.sharedInstance().setDismissDelay(10000);
170 this.setLayout(null);
171 addMouseListener(this);
172 addMouseMotionListener(this);
173 ap.annotationScroller.getVerticalScrollBar()
174 .addAdjustmentListener(this);
175 // save any wheel listeners on the scroller, so we can propagate scroll
177 _mwl = ap.annotationScroller.getMouseWheelListeners();
178 // and then set our own listener to consume all mousewheel events
179 ap.annotationScroller.addMouseWheelListener(this);
180 renderer = new AnnotationRenderer();
182 av.getRanges().addPropertyChangeListener(this);
185 public AnnotationPanel(AlignViewport av)
188 renderer = new AnnotationRenderer();
192 public void mouseWheelMoved(MouseWheelEvent e)
197 double wheelRotation = e.getPreciseWheelRotation();
198 if (wheelRotation > 0)
200 av.getRanges().scrollRight(true);
202 else if (wheelRotation < 0)
204 av.getRanges().scrollRight(false);
209 // TODO: find the correct way to let the event bubble up to
210 // ap.annotationScroller
211 for (MouseWheelListener mwl : _mwl)
215 mwl.mouseWheelMoved(e);
226 public Dimension getPreferredScrollableViewportSize()
228 Dimension ps = getPreferredSize();
229 return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
233 public int getScrollableBlockIncrement(Rectangle visibleRect,
234 int orientation, int direction)
240 public boolean getScrollableTracksViewportHeight()
246 public boolean getScrollableTracksViewportWidth()
252 public int getScrollableUnitIncrement(Rectangle visibleRect,
253 int orientation, int direction)
262 * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
266 public void adjustmentValueChanged(AdjustmentEvent evt)
268 // update annotation label display
269 ap.getAlabels().setScrollOffset(-evt.getValue());
273 * Calculates the height of the annotation displayed in the annotation panel.
274 * Callers should normally call the ap.adjustAnnotationHeight method to ensure
275 * all annotation associated components are updated correctly.
278 public int adjustPanelHeight()
280 int height = av.calcPanelHeight();
281 this.setPreferredSize(new Dimension(1, height));
284 // revalidate only when the alignment panel is fully constructed
298 public void actionPerformed(ActionEvent evt)
300 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
305 Annotation[] anot = aa[activeRow].annotations;
307 if (anot.length < av.getColumnSelection().getMax())
309 Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
311 System.arraycopy(anot, 0, temp, 0, anot.length);
313 aa[activeRow].annotations = anot;
316 String action = evt.getActionCommand();
317 if (action.equals(REMOVE))
319 for (int index : av.getColumnSelection().getSelected())
321 if (av.getAlignment().getHiddenColumns().isVisible(index))
327 else if (action.equals(LABEL))
329 String exMesg = collectAnnotVals(anot, LABEL);
330 String label = JvOptionPane.showInputDialog(
331 MessageManager.getString("label.enter_label"), exMesg);
338 if ((label.length() > 0) && !aa[activeRow].hasText)
340 aa[activeRow].hasText = true;
343 for (int index : av.getColumnSelection().getSelected())
345 if (!av.getAlignment().getHiddenColumns().isVisible(index))
350 if (anot[index] == null)
352 anot[index] = new Annotation(label, "", ' ', 0);
356 anot[index].displayCharacter = label;
360 else if (action.equals(COLOUR))
362 final Annotation[] fAnot = anot;
363 String title = MessageManager
364 .getString("label.select_foreground_colour");
365 ColourChooserListener listener = new ColourChooserListener()
368 public void colourSelected(Color c)
370 HiddenColumns hiddenColumns = av.getAlignment()
372 for (int index : av.getColumnSelection().getSelected())
374 if (hiddenColumns.isVisible(index))
376 if (fAnot[index] == null)
378 fAnot[index] = new Annotation("", "", ' ', 0);
380 fAnot[index].colour = c;
385 JalviewColourChooser.showColourChooser(this, title, Color.black,
389 // HELIX, SHEET or STEM
392 String symbol = "\u03B1"; // alpha
394 if (action.equals(HELIX))
398 else if (action.equals(SHEET))
401 symbol = "\u03B2"; // beta
404 // Added by LML to color stems
405 else if (action.equals(STEM))
408 int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
409 symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
412 if (!aa[activeRow].hasIcons)
414 aa[activeRow].hasIcons = true;
417 String label = JvOptionPane.showInputDialog(MessageManager
418 .getString("label.enter_label_for_the_structure"), symbol);
425 if ((label.length() > 0) && !aa[activeRow].hasText)
427 aa[activeRow].hasText = true;
428 if (action.equals(STEM))
430 aa[activeRow].showAllColLabels = true;
433 for (int index : av.getColumnSelection().getSelected())
435 if (!av.getAlignment().getHiddenColumns().isVisible(index))
440 if (anot[index] == null)
442 anot[index] = new Annotation(label, "", type, 0);
445 anot[index].secondaryStructure = type != 'S' ? type
446 : label.length() == 0 ? ' ' : label.charAt(0);
447 anot[index].displayCharacter = label;
452 av.getAlignment().validateAnnotation(aa[activeRow]);
453 ap.alignmentChanged();
454 ap.alignFrame.setMenusForViewport();
462 * Returns any existing annotation concatenated as a string. For each
463 * annotation, takes the description, if any, else the secondary structure
464 * character (if type is HELIX, SHEET or STEM), else the display character (if
471 private String collectAnnotVals(Annotation[] anots, String type)
473 // TODO is this method wanted? why? 'last' is not used
475 StringBuilder collatedInput = new StringBuilder(64);
477 ColumnSelection viscols = av.getColumnSelection();
478 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
481 * the selection list (read-only view) is in selection order, not
482 * column order; make a copy so we can sort it
484 List<Integer> selected = new ArrayList<>(viscols.getSelected());
485 Collections.sort(selected);
486 for (int index : selected)
488 // always check for current display state - just in case
489 if (!hidden.isVisible(index))
493 String tlabel = null;
494 if (anots[index] != null)
495 { // LML added stem code
496 if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
497 || type.equals(LABEL))
499 tlabel = anots[index].description;
500 if (tlabel == null || tlabel.length() < 1)
502 if (type.equals(HELIX) || type.equals(SHEET)
503 || type.equals(STEM))
505 tlabel = "" + anots[index].secondaryStructure;
509 tlabel = "" + anots[index].displayCharacter;
513 if (tlabel != null && !tlabel.equals(last))
515 if (last.length() > 0)
517 collatedInput.append(" ");
519 collatedInput.append(tlabel);
523 return collatedInput.toString();
527 * Action on right mouse pressed on Mac is to show a pop-up menu for the
528 * annotation. Action on left mouse pressed is to find which annotation is
529 * pressed and mark the start of a column selection or graph resize operation.
534 public void mousePressed(MouseEvent evt)
537 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
542 mouseDragLastX = evt.getX();
543 mouseDragLastY = evt.getY();
546 * add visible annotation heights until we reach the y
547 * position, to find which annotation it is in
552 // todo could reuse getRowIndexAndOffset ?
553 final int y = evt.getY();
555 for (int i = 0; i < aa.length; i++)
559 height += aa[i].height;
568 else if (aa[i].graph != 0)
571 * we have clicked on a resizable graph annotation
574 yOffset = height - y;
581 * isPopupTrigger fires in mousePressed on Mac,
582 * not until mouseRelease on Windows
584 if (evt.isPopupTrigger() && activeRow != -1)
586 showPopupMenu(y, evt.getX());
590 if (graphStretch != -1)
593 if (aa[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP)
595 // data in row has position on y as well as x axis
596 if (evt.isAltDown() || evt.isAltGraphDown())
598 dragMode = DragMode.MatrixSelect;
599 firstDragX = mouseDragLastX;
600 firstDragY = mouseDragLastY;
604 GraphLine thr = aa[graphStretch].getThreshold();
605 int currentX = getColumnForXPos(evt.getX());
606 ContactListI forCurrentX = av.getContactList(aa[graphStretch],
608 if (forCurrentX != null)
610 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
611 aa[graphStretch].graphHeight);
612 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset,
615 fr = Math.min(cXci.cStart, cXci.cEnd);
616 to = Math.max(cXci.cStart, cXci.cEnd);
617 // select corresponding range in segment under mouse
619 for (int c = fr; c <= to; c++)
621 av.getColumnSelection().addElement(c);
623 av.getColumnSelection().addElement(currentX);
626 // and also select everything lower than the max range adjacent
628 if (PAEContactMatrix.PAEMATRIX.equals(aa[graphStretch].getCalcId()))
631 ContactRange cr = forCurrentX.getRangeFor(fr, to);
635 cval = forCurrentX.getContactAt(c);
636 if (// cr.getMin() <= cval &&
639 av.getColumnSelection().addElement(c--);
647 while (c < forCurrentX.getContactHeight())
649 cval = forCurrentX.getContactAt(c);
650 if (// cr.getMin() <= cval &&
653 av.getColumnSelection().addElement(c++);
667 ap.getScalePanel().mousePressed(evt);
672 * Construct and display a context menu at the right-click position
677 void showPopupMenu(final int y, int x)
679 if (av.getColumnSelection() == null
680 || av.getColumnSelection().isEmpty())
685 JPopupMenu pop = new JPopupMenu(
686 MessageManager.getString("label.structure_type"));
689 * Just display the needed structure options
691 if (av.getAlignment().isNucleotide())
693 item = new JMenuItem(STEM);
694 item.addActionListener(this);
699 item = new JMenuItem(HELIX);
700 item.addActionListener(this);
702 item = new JMenuItem(SHEET);
703 item.addActionListener(this);
706 item = new JMenuItem(LABEL);
707 item.addActionListener(this);
709 item = new JMenuItem(COLOUR);
710 item.addActionListener(this);
712 item = new JMenuItem(REMOVE);
713 item.addActionListener(this);
715 pop.show(this, x, y);
719 * Action on mouse up is to clear mouse drag data and call mouseReleased on
720 * ScalePanel, to deal with defining the selection group (if any) defined by
726 public void mouseReleased(MouseEvent evt)
728 if (dragMode == DragMode.MatrixSelect)
730 matrixSelectRange(evt);
737 mouseDragging = false;
738 if (dragMode == DragMode.Resize)
740 ap.adjustAnnotationHeight();
742 dragMode = DragMode.Undefined;
743 ap.getScalePanel().mouseReleased(evt);
746 * isPopupTrigger is set in mouseReleased on Windows
747 * (in mousePressed on Mac)
749 if (evt.isPopupTrigger() && activeRow != -1)
751 showPopupMenu(evt.getY(), evt.getX());
763 public void mouseEntered(MouseEvent evt)
765 this.mouseDragging = false;
766 ap.getScalePanel().mouseEntered(evt);
770 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
771 * with column selection on a mouse drag
776 public void mouseExited(MouseEvent evt)
778 ap.getScalePanel().mouseExited(evt);
782 * Action on starting or continuing a mouse drag. There are two possible
785 * <li>drag up or down on a graphed annotation increases or decreases the
786 * height of the graph</li>
787 * <li>dragging left or right selects the columns dragged across</li>
789 * A drag on a graph annotation is treated as column selection if it starts
790 * with more horizontal than vertical movement, and as resize if it starts
791 * with more vertical than horizontal movement. Once started, the drag does
797 public void mouseDragged(MouseEvent evt)
800 * if dragMode is Undefined:
801 * - set to Select if dx > dy
802 * - set to Resize if dy > dx
803 * - do nothing if dx == dy
805 final int x = evt.getX();
806 final int y = evt.getY();
807 if (dragMode == DragMode.Undefined)
809 int dx = Math.abs(x - mouseDragLastX);
810 int dy = Math.abs(y - mouseDragLastY);
811 if (graphStretch == -1 || dx > dy)
814 * mostly horizontal drag, or not a graph annotation
816 dragMode = DragMode.Select;
821 * mostly vertical drag
823 dragMode = DragMode.Resize;
824 notJustOne = evt.isShiftDown();
827 * but could also be a matrix drag
829 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
830 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
833 * dragging in a matrix
835 dragMode = DragMode.MatrixSelect;
836 firstDragX = mouseDragLastX;
837 firstDragY = mouseDragLastY;
842 if (dragMode == DragMode.Undefined)
846 * drag is diagonal - defer deciding whether to
847 * treat as up/down or left/right
854 if (dragMode == DragMode.Resize)
857 * resize graph annotation if mouse was dragged up or down
859 int deltaY = mouseDragLastY - evt.getY();
862 AlignmentAnnotation graphAnnotation = av.getAlignment()
863 .getAlignmentAnnotation()[graphStretch];
864 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
867 for (AlignmentAnnotation similar : av.getAlignment()
868 .findAnnotations(null, graphAnnotation.getCalcId(),
869 graphAnnotation.label))
871 similar.graphHeight = newHeight;
877 graphAnnotation.graphHeight = newHeight;
880 ap.paintAlignment(false, false);
883 else if (dragMode == DragMode.MatrixSelect)
886 * TODO draw a rubber band for range
890 ap.paintAlignment(false, false);
895 * for mouse drag left or right, delegate to
896 * ScalePanel to adjust the column selection
898 ap.getScalePanel().mouseDragged(evt);
907 public void matrixSelectRange(MouseEvent evt)
910 * get geometry of drag
912 int fromY = Math.min(firstDragY, evt.getY());
913 int toY = Math.max(firstDragY, evt.getY());
914 int fromX = Math.min(firstDragX, evt.getX());
915 int toX = Math.max(firstDragX, evt.getX());
917 int deltaY = toY - fromY;
918 int deltaX = toX - fromX;
920 int[] rowIndex = getRowIndexAndOffset(fromY,
921 av.getAlignment().getAlignmentAnnotation());
922 int[] toRowIndex = getRowIndexAndOffset(toY,
923 av.getAlignment().getAlignmentAnnotation());
925 if (rowIndex == null || toRowIndex == null)
927 System.out.println("Drag out of range. needs to be clipped");
930 if (rowIndex[0] != toRowIndex[0])
932 System.out.println("Drag went to another row. needs to be clipped");
935 // rectangular selection on matrix style annotation
936 AlignmentAnnotation cma = av.getAlignment()
937 .getAlignmentAnnotation()[rowIndex[0]];
939 int lastX = getColumnForXPos(fromX);
940 int currentX = getColumnForXPos(toX);
941 int fromXc = Math.min(lastX, currentX);
942 int toXc = Math.max(lastX, currentX);
943 ContactListI forFromX = av.getContactList(cma, fromXc);
944 ContactListI forToX = av.getContactList(cma, toXc);
946 if (forFromX != null && forToX != null)
948 ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
950 ContactGeometry.contactInterval lastXci = lastXcgeom
951 .mapFor(rowIndex[1], rowIndex[1] - deltaY);
953 ContactGeometry cXcgeom = new ContactGeometry(forToX,
955 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
956 rowIndex[1] - deltaY);
958 // mark rectangular region formed by drag
959 System.err.println("Matrix Selection from last(" + fromXc + ",["
960 + lastXci.cStart + "," + lastXci.cEnd + "]) to cur(" + toXc
961 + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
963 fr = Math.min(lastXci.cStart, lastXci.cEnd);
964 to = Math.max(lastXci.cStart, lastXci.cEnd);
965 System.err.println("Marking " + fr + " to " + to);
966 for (int c = fr; c <= to; c++)
968 if (cma.sequenceRef != null)
970 int col = cma.sequenceRef.findIndex(c);
971 av.getColumnSelection().addElement(col);
975 av.getColumnSelection().addElement(c);
978 fr = Math.min(cXci.cStart, cXci.cEnd);
979 to = Math.max(cXci.cStart, cXci.cEnd);
980 System.err.println("Marking " + fr + " to " + to);
981 for (int c = fr; c <= to; c++)
983 if (cma.sequenceRef != null)
985 int col = cma.sequenceRef.findIndex(c);
986 av.getColumnSelection().addElement(col);
990 av.getColumnSelection().addElement(c);
993 fr = Math.min(lastX, currentX);
994 to = Math.max(lastX, currentX);
996 System.err.println("Marking " + fr + " to " + to);
997 for (int c = fr; c <= to; c++)
999 av.getColumnSelection().addElement(c);
1006 * Constructs the tooltip, and constructs and displays a status message, for
1007 * the current mouse position
1012 public void mouseMoved(MouseEvent evt)
1014 int yPos = evt.getY();
1015 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1016 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1017 int row = rowAndOffset[0];
1021 this.setToolTipText(null);
1025 int column = getColumnForXPos(evt.getX());
1027 AlignmentAnnotation ann = aa[row];
1028 if (row > -1 && ann.annotations != null
1029 && column < ann.annotations.length)
1031 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1033 setToolTipText(toolTip == null ? null
1034 : JvSwingUtils.wrapTooltip(true, toolTip));
1035 String msg = getStatusMessage(av.getAlignment(), column, ann,
1036 rowAndOffset[1], av);
1037 ap.alignFrame.setStatus(msg);
1041 this.setToolTipText(null);
1042 ap.alignFrame.setStatus(" ");
1046 private int getColumnForXPos(int x)
1048 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1049 column = Math.min(column, av.getRanges().getEndRes());
1051 if (av.hasHiddenColumns())
1053 column = av.getAlignment().getHiddenColumns()
1054 .visibleToAbsoluteColumn(column);
1060 * Answers the index in the annotations array of the visible annotation at the
1061 * given y position. This is done by adding the heights of visible annotations
1062 * until the y position has been exceeded. Answers -1 if no annotations are
1063 * visible, or the y position is below all annotations.
1069 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1075 return getRowIndexAndOffset(yPos, aa)[0];
1078 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1080 int[] res = new int[2];
1088 int height = 0, lheight = 0;
1089 for (int i = 0; i < aa.length; i++)
1094 height += aa[i].height;
1101 res[1] = height - yPos;
1109 * Answers a tooltip for the annotation at the current mouse position, not
1110 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1116 * @param rowAndOffset
1118 static String buildToolTip(AlignmentAnnotation ann, int column,
1119 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1122 String tooltip = null;
1123 if (ann.graphGroup > -1)
1125 StringBuilder tip = new StringBuilder(32);
1126 boolean first = true;
1127 for (int i = 0; i < anns.length; i++)
1129 if (anns[i].graphGroup == ann.graphGroup
1130 && anns[i].annotations[column] != null)
1137 tip.append(anns[i].label);
1138 String description = anns[i].annotations[column].description;
1139 if (description != null && description.length() > 0)
1141 tip.append(" ").append(description);
1145 tooltip = first ? null : tip.toString();
1147 else if (column < ann.annotations.length
1148 && ann.annotations[column] != null)
1150 tooltip = ann.annotations[column].description;
1152 // TODO abstract tooltip generator so different implementations can be built
1153 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1155 ContactListI clist = av.getContactList(ann, column);
1158 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1159 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1160 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1161 tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
1162 + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
1163 int col = ann.sequenceRef.findPosition(column);
1164 ap.getStructureSelectionManager()
1165 .highlightPositionsOn(ann.sequenceRef, new int[][]
1166 { new int[] { col, col },
1168 { ci.cStart, ci.cEnd } }, null);
1175 * Constructs and returns the status bar message
1180 * @param rowAndOffset
1182 static String getStatusMessage(AlignmentI al, int column,
1183 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1186 * show alignment column and annotation description if any
1188 StringBuilder text = new StringBuilder(32);
1189 text.append(MessageManager.getString("label.column")).append(" ")
1190 .append(column + 1);
1192 if (column < ann.annotations.length && ann.annotations[column] != null)
1194 String description = ann.annotations[column].description;
1195 if (description != null && description.trim().length() > 0)
1197 text.append(" ").append(description);
1202 * if the annotation is sequence-specific, show the sequence number
1203 * in the alignment, and (if not a gap) the residue and position
1205 SequenceI seqref = ann.sequenceRef;
1208 int seqIndex = al.findIndex(seqref);
1211 text.append(", ").append(MessageManager.getString("label.sequence"))
1212 .append(" ").append(seqIndex + 1);
1213 char residue = seqref.getCharAt(column);
1214 if (!Comparison.isGap(residue))
1218 if (al.isNucleotide())
1220 name = ResidueProperties.nucleotideName
1221 .get(String.valueOf(residue));
1222 text.append(" Nucleotide: ")
1223 .append(name != null ? name : residue);
1227 name = 'X' == residue ? "X"
1228 : ('*' == residue ? "STOP"
1229 : ResidueProperties.aa2Triplet
1230 .get(String.valueOf(residue)));
1231 text.append(" Residue: ").append(name != null ? name : residue);
1233 int residuePos = seqref.findPosition(column);
1234 text.append(" (").append(residuePos).append(")");
1239 return text.toString();
1249 public void mouseClicked(MouseEvent evt)
1251 // if (activeRow != -1)
1253 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1254 // AlignmentAnnotation anot = aa[activeRow];
1258 // TODO mouseClicked-content and drawCursor are quite experimental!
1259 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1262 int pady = av.getCharHeight() / 5;
1264 graphics.setColor(Color.black);
1265 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1267 if (av.validCharWidth)
1269 graphics.setColor(Color.white);
1271 char s = seq.getCharAt(res);
1273 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1274 graphics.drawString(String.valueOf(s), charOffset + x1,
1275 (y1 + av.getCharHeight()) - pady);
1280 private volatile boolean imageFresh = false;
1282 private Rectangle visibleRect = new Rectangle(),
1283 clipBounds = new Rectangle();
1292 public void paintComponent(Graphics g)
1295 // BH: note that this method is generally recommended to
1296 // call super.paintComponent(g). Otherwise, the children of this
1297 // component will not be rendered. That is not needed here
1298 // because AnnotationPanel does not have any children. It is
1299 // just a JPanel contained in a JViewPort.
1301 computeVisibleRect(visibleRect);
1303 g.setColor(Color.white);
1304 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1308 // BH 2018 optimizing generation of new Rectangle().
1310 || (visibleRect.width != (clipBounds = g
1311 .getClipBounds(clipBounds)).width)
1312 || (visibleRect.height != clipBounds.height))
1315 g.drawImage(image, 0, 0, this);
1320 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1321 + 1) * av.getCharWidth();
1327 if (image == null || imgWidth != image.getWidth(this)
1328 || image.getHeight(this) != getHeight())
1330 boolean tried = false;
1332 while (image == null && !tried)
1336 image = new BufferedImage(imgWidth,
1337 ap.getAnnotationPanel().getHeight(),
1338 BufferedImage.TYPE_INT_RGB);
1340 } catch (IllegalArgumentException exc)
1343 "Serious issue with viewport geometry imgWidth requested was "
1346 } catch (OutOfMemoryError oom)
1351 } catch (Exception x)
1356 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1362 gg = (Graphics2D) image.getGraphics();
1366 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1367 RenderingHints.VALUE_ANTIALIAS_ON);
1370 gg.setFont(av.getFont());
1371 fm = gg.getFontMetrics();
1372 gg.setColor(Color.white);
1373 gg.fillRect(0, 0, imgWidth, image.getHeight());
1378 gg = (Graphics2D) image.getGraphics();
1382 drawComponent(gg, av.getRanges().getStartRes(),
1383 av.getRanges().getEndRes() + 1);
1386 g.drawImage(image, 0, 0, this);
1390 * set true to enable redraw timing debug output on stderr
1392 private final boolean debugRedraw = false;
1395 * non-Thread safe repaint
1398 * repaint with horizontal shift in alignment
1400 public void fastPaint(int horizontal)
1402 if ((horizontal == 0) || image == null
1403 || av.getAlignment().getAlignmentAnnotation() == null
1404 || av.getAlignment().getAlignmentAnnotation().length < 1
1405 || av.isCalcInProgress())
1411 int sr = av.getRanges().getStartRes();
1412 int er = av.getRanges().getEndRes() + 1;
1415 Graphics2D gg = (Graphics2D) image.getGraphics();
1417 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1419 // scroll is less than imgWidth away so can re-use buffered graphics
1420 gg.copyArea(0, 0, imgWidth, getHeight(),
1421 -horizontal * av.getCharWidth(), 0);
1423 if (horizontal > 0) // scrollbar pulled right, image to the left
1425 transX = (er - sr - horizontal) * av.getCharWidth();
1426 sr = er - horizontal;
1428 else if (horizontal < 0)
1430 er = sr - horizontal;
1433 gg.translate(transX, 0);
1435 drawComponent(gg, sr, er);
1437 gg.translate(-transX, 0);
1443 // Call repaint on alignment panel so that repaints from other alignment
1444 // panel components can be aggregated. Otherwise performance of the overview
1445 // window and others may be adversely affected.
1446 av.getAlignPanel().repaint();
1449 private volatile boolean lastImageGood = false;
1461 public void drawComponent(Graphics g, int startRes, int endRes)
1463 BufferedImage oldFaded = fadedImage;
1464 if (av.isCalcInProgress())
1468 lastImageGood = false;
1471 // We'll keep a record of the old image,
1472 // and draw a faded image until the calculation
1475 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1476 || fadedImage.getHeight() != image.getHeight()))
1478 // System.err.println("redraw faded image ("+(fadedImage==null ?
1479 // "null image" : "") + " lastGood="+lastImageGood+")");
1480 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1481 BufferedImage.TYPE_INT_RGB);
1483 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1485 fadedG.setColor(Color.white);
1486 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1488 fadedG.setComposite(
1489 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1490 fadedG.drawImage(image, 0, 0, this);
1493 // make sure we don't overwrite the last good faded image until all
1494 // calculations have finished
1495 lastImageGood = false;
1500 if (fadedImage != null)
1502 oldFaded = fadedImage;
1507 g.setColor(Color.white);
1508 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1510 g.setFont(av.getFont());
1513 fm = g.getFontMetrics();
1516 if ((av.getAlignment().getAlignmentAnnotation() == null)
1517 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1519 g.setColor(Color.white);
1520 g.fillRect(0, 0, getWidth(), getHeight());
1521 g.setColor(Color.black);
1522 if (av.validCharWidth)
1524 g.drawString(MessageManager
1525 .getString("label.alignment_has_no_annotations"), 20, 15);
1530 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1532 if (!lastImageGood && fadedImage == null)
1534 fadedImage = oldFaded;
1536 if (dragMode == DragMode.MatrixSelect)
1538 g.setColor(Color.yellow);
1539 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1540 Math.min(firstDragY, mouseDragLastY),
1541 Math.max(firstDragX, mouseDragLastX)
1542 - Math.min(firstDragX, mouseDragLastX),
1543 Math.max(firstDragY, mouseDragLastY)
1544 - Math.min(firstDragY, mouseDragLastY));
1550 public FontMetrics getFontMetrics()
1556 public Image getFadedImage()
1562 public int getFadedImageWidth()
1567 private int[] bounds = new int[2];
1570 public int[] getVisibleVRange()
1572 if (ap != null && ap.getAlabels() != null)
1574 int sOffset = -ap.getAlabels().getScrollOffset();
1575 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1576 bounds[0] = sOffset;
1577 bounds[1] = visHeight;
1587 * Try to ensure any references held are nulled
1589 public void dispose()
1599 * I created the renderer so I will dispose of it
1601 if (renderer != null)
1608 public void propertyChange(PropertyChangeEvent evt)
1610 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1611 // Both scrolling and resizing change viewport ranges: scrolling changes
1612 // both start and end points, but resize only changes end values.
1613 // Here we only want to fastpaint on a scroll, with resize using a normal
1614 // paint, so scroll events are identified as changes to the horizontal or
1615 // vertical start value.
1616 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1618 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1620 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1622 fastPaint(((int[]) evt.getNewValue())[0]
1623 - ((int[]) evt.getOldValue())[0]);
1625 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1632 * computes the visible height of the annotation panel
1634 * @param adjustPanelHeight
1635 * - when false, just adjust existing height according to other
1637 * @param annotationHeight
1638 * @return height to use for the ScrollerPreferredVisibleSize
1640 public int adjustForAlignFrame(boolean adjustPanelHeight,
1641 int annotationHeight)
1644 * Estimate available height in the AlignFrame for alignment +
1645 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1646 * hscroll, status bar, insets.
1648 int stuff = (ap.getViewName() != null ? 30 : 0)
1649 + (Platform.isAMacAndNotJS() ? 120 : 140);
1650 int availableHeight = ap.alignFrame.getHeight() - stuff;
1651 int rowHeight = av.getCharHeight();
1653 if (adjustPanelHeight)
1655 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1658 * If not enough vertical space, maximize annotation height while keeping
1659 * at least two rows of alignment visible
1661 if (annotationHeight + alignmentHeight > availableHeight)
1663 annotationHeight = Math.min(annotationHeight,
1664 availableHeight - 2 * rowHeight);
1669 // maintain same window layout whilst updating sliders
1670 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1671 availableHeight - 2 * rowHeight);
1673 return annotationHeight;