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 // possible alternative for interactive selection - threshold gives 'ceiling' for forming a cluster
609 // when a row+column is selected, farthest common ancestor less than thr is used to compute cluster
610 int currentX = getColumnForXPos(evt.getX());
611 ContactMatrixI matrix = av.getContactMatrix(aa[graphStretch]);
614 if (matrix.hasGroups())
616 SequenceI rseq = aa[graphStretch].sequenceRef;
617 BitSet grp = matrix.getGroupsFor(currentX);
618 ColumnSelection cs = av.getColumnSelection();
619 HiddenColumns hc = av.getAlignment().getHiddenColumns();
620 for (int p=grp.nextSetBit(0); p>=0; p = grp.nextSetBit(p+1))
622 int offp = (rseq!=null) ? rseq.findIndex(rseq.getStart()-1+p) : p;
624 if (!av.hasHiddenColumns() || hc.isVisible(offp))
626 av.getColumnSelection().addElement(offp);
631 ContactListI forCurrentX = av.getContactList(aa[graphStretch],
633 if (forCurrentX != null)
635 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
636 aa[graphStretch].graphHeight);
637 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset,
640 fr = Math.min(cXci.cStart, cXci.cEnd);
641 to = Math.max(cXci.cStart, cXci.cEnd);
642 // select corresponding range in segment under mouse
644 for (int c = fr; c <= to; c++)
646 av.getColumnSelection().addElement(c);
648 av.getColumnSelection().addElement(currentX);
651 // and also select everything lower than the max range adjacent
653 if (PAEContactMatrix.PAEMATRIX.equals(aa[graphStretch].getCalcId()))
656 ContactRange cr = forCurrentX.getRangeFor(fr, to);
658 // TODO: could use GraphLine instead of arbitrary picking
659 // TODO: could report mean/median/variance for partitions (contiguous selected vs unselected regions and inter-contig regions)
660 // controls feathering - what other elements in row/column should we select
661 double thresh=cr.getMean()+(cr.getMax()-cr.getMean())*.15;
664 cval = forCurrentX.getContactAt(c);
665 if (// cr.getMin() <= cval &&
668 av.getColumnSelection().addElement(c--);
676 while (c < forCurrentX.getContactHeight())
678 cval = forCurrentX.getContactAt(c);
679 if (// cr.getMin() <= cval &&
682 av.getColumnSelection().addElement(c++);
697 ap.getScalePanel().mousePressed(evt);
702 * Construct and display a context menu at the right-click position
707 void showPopupMenu(final int y, int x)
709 if (av.getColumnSelection() == null
710 || av.getColumnSelection().isEmpty())
715 JPopupMenu pop = new JPopupMenu(
716 MessageManager.getString("label.structure_type"));
719 * Just display the needed structure options
721 if (av.getAlignment().isNucleotide())
723 item = new JMenuItem(STEM);
724 item.addActionListener(this);
729 item = new JMenuItem(HELIX);
730 item.addActionListener(this);
732 item = new JMenuItem(SHEET);
733 item.addActionListener(this);
736 item = new JMenuItem(LABEL);
737 item.addActionListener(this);
739 item = new JMenuItem(COLOUR);
740 item.addActionListener(this);
742 item = new JMenuItem(REMOVE);
743 item.addActionListener(this);
745 pop.show(this, x, y);
749 * Action on mouse up is to clear mouse drag data and call mouseReleased on
750 * ScalePanel, to deal with defining the selection group (if any) defined by
756 public void mouseReleased(MouseEvent evt)
758 if (dragMode == DragMode.MatrixSelect)
760 matrixSelectRange(evt);
767 mouseDragging = false;
768 if (dragMode == DragMode.Resize)
770 ap.adjustAnnotationHeight();
772 dragMode = DragMode.Undefined;
773 ap.getScalePanel().mouseReleased(evt);
776 * isPopupTrigger is set in mouseReleased on Windows
777 * (in mousePressed on Mac)
779 if (evt.isPopupTrigger() && activeRow != -1)
781 showPopupMenu(evt.getY(), evt.getX());
793 public void mouseEntered(MouseEvent evt)
795 this.mouseDragging = false;
796 ap.getScalePanel().mouseEntered(evt);
800 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
801 * with column selection on a mouse drag
806 public void mouseExited(MouseEvent evt)
808 ap.getScalePanel().mouseExited(evt);
812 * Action on starting or continuing a mouse drag. There are two possible
815 * <li>drag up or down on a graphed annotation increases or decreases the
816 * height of the graph</li>
817 * <li>dragging left or right selects the columns dragged across</li>
819 * A drag on a graph annotation is treated as column selection if it starts
820 * with more horizontal than vertical movement, and as resize if it starts
821 * with more vertical than horizontal movement. Once started, the drag does
827 public void mouseDragged(MouseEvent evt)
830 * if dragMode is Undefined:
831 * - set to Select if dx > dy
832 * - set to Resize if dy > dx
833 * - do nothing if dx == dy
835 final int x = evt.getX();
836 final int y = evt.getY();
837 if (dragMode == DragMode.Undefined)
839 int dx = Math.abs(x - mouseDragLastX);
840 int dy = Math.abs(y - mouseDragLastY);
841 if (graphStretch == -1 || dx > dy)
844 * mostly horizontal drag, or not a graph annotation
846 dragMode = DragMode.Select;
851 * mostly vertical drag
853 dragMode = DragMode.Resize;
854 notJustOne = evt.isShiftDown();
857 * but could also be a matrix drag
859 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
860 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
863 * dragging in a matrix
865 dragMode = DragMode.MatrixSelect;
866 firstDragX = mouseDragLastX;
867 firstDragY = mouseDragLastY;
872 if (dragMode == DragMode.Undefined)
876 * drag is diagonal - defer deciding whether to
877 * treat as up/down or left/right
884 if (dragMode == DragMode.Resize)
887 * resize graph annotation if mouse was dragged up or down
889 int deltaY = mouseDragLastY - evt.getY();
892 AlignmentAnnotation graphAnnotation = av.getAlignment()
893 .getAlignmentAnnotation()[graphStretch];
894 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
897 for (AlignmentAnnotation similar : av.getAlignment()
898 .findAnnotations(null, graphAnnotation.getCalcId(),
899 graphAnnotation.label))
901 similar.graphHeight = newHeight;
907 graphAnnotation.graphHeight = newHeight;
910 ap.paintAlignment(false, false);
913 else if (dragMode == DragMode.MatrixSelect)
916 * TODO draw a rubber band for range
920 ap.paintAlignment(false, false);
925 * for mouse drag left or right, delegate to
926 * ScalePanel to adjust the column selection
928 ap.getScalePanel().mouseDragged(evt);
937 public void matrixSelectRange(MouseEvent evt)
940 * get geometry of drag
942 int fromY = Math.min(firstDragY, evt.getY());
943 int toY = Math.max(firstDragY, evt.getY());
944 int fromX = Math.min(firstDragX, evt.getX());
945 int toX = Math.max(firstDragX, evt.getX());
947 int deltaY = toY - fromY;
948 int deltaX = toX - fromX;
950 int[] rowIndex = getRowIndexAndOffset(fromY,
951 av.getAlignment().getAlignmentAnnotation());
952 int[] toRowIndex = getRowIndexAndOffset(toY,
953 av.getAlignment().getAlignmentAnnotation());
955 if (rowIndex == null || toRowIndex == null)
957 System.out.println("Drag out of range. needs to be clipped");
960 if (rowIndex[0] != toRowIndex[0])
962 System.out.println("Drag went to another row. needs to be clipped");
965 // rectangular selection on matrix style annotation
966 AlignmentAnnotation cma = av.getAlignment()
967 .getAlignmentAnnotation()[rowIndex[0]];
969 int lastX = getColumnForXPos(fromX);
970 int currentX = getColumnForXPos(toX);
971 int fromXc = Math.min(lastX, currentX);
972 int toXc = Math.max(lastX, currentX);
973 ContactListI forFromX = av.getContactList(cma, fromXc);
974 ContactListI forToX = av.getContactList(cma, toXc);
976 if (forFromX != null && forToX != null)
978 ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
980 ContactGeometry.contactInterval lastXci = lastXcgeom
981 .mapFor(rowIndex[1], rowIndex[1] - deltaY);
983 ContactGeometry cXcgeom = new ContactGeometry(forToX,
985 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
986 rowIndex[1] - deltaY);
988 // mark rectangular region formed by drag
989 System.err.println("Matrix Selection from last(" + fromXc + ",["
990 + lastXci.cStart + "," + lastXci.cEnd + "]) to cur(" + toXc
991 + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
993 fr = Math.min(lastXci.cStart, lastXci.cEnd);
994 to = Math.max(lastXci.cStart, lastXci.cEnd);
995 System.err.println("Marking " + fr + " to " + to);
996 for (int c = fr; c <= to; c++)
998 if (cma.sequenceRef != null)
1000 int col = cma.sequenceRef.findIndex(c);
1001 av.getColumnSelection().addElement(col);
1005 av.getColumnSelection().addElement(c);
1008 fr = Math.min(cXci.cStart, cXci.cEnd);
1009 to = Math.max(cXci.cStart, cXci.cEnd);
1010 System.err.println("Marking " + fr + " to " + to);
1011 for (int c = fr; c <= to; c++)
1013 if (cma.sequenceRef != null)
1015 int col = cma.sequenceRef.findIndex(c);
1016 av.getColumnSelection().addElement(col);
1020 av.getColumnSelection().addElement(c);
1023 fr = Math.min(lastX, currentX);
1024 to = Math.max(lastX, currentX);
1026 System.err.println("Marking " + fr + " to " + to);
1027 for (int c = fr; c <= to; c++)
1029 av.getColumnSelection().addElement(c);
1036 * Constructs the tooltip, and constructs and displays a status message, for
1037 * the current mouse position
1042 public void mouseMoved(MouseEvent evt)
1044 int yPos = evt.getY();
1045 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1046 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1047 int row = rowAndOffset[0];
1051 this.setToolTipText(null);
1055 int column = getColumnForXPos(evt.getX());
1057 AlignmentAnnotation ann = aa[row];
1058 if (row > -1 && ann.annotations != null
1059 && column < ann.annotations.length)
1061 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1063 setToolTipText(toolTip == null ? null
1064 : JvSwingUtils.wrapTooltip(true, toolTip));
1065 String msg = getStatusMessage(av.getAlignment(), column, ann,
1066 rowAndOffset[1], av);
1067 ap.alignFrame.setStatus(msg);
1071 this.setToolTipText(null);
1072 ap.alignFrame.setStatus(" ");
1076 private int getColumnForXPos(int x)
1078 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1079 column = Math.min(column, av.getRanges().getEndRes());
1081 if (av.hasHiddenColumns())
1083 column = av.getAlignment().getHiddenColumns()
1084 .visibleToAbsoluteColumn(column);
1090 * Answers the index in the annotations array of the visible annotation at the
1091 * given y position. This is done by adding the heights of visible annotations
1092 * until the y position has been exceeded. Answers -1 if no annotations are
1093 * visible, or the y position is below all annotations.
1099 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1105 return getRowIndexAndOffset(yPos, aa)[0];
1108 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1110 int[] res = new int[2];
1118 int height = 0, lheight = 0;
1119 for (int i = 0; i < aa.length; i++)
1124 height += aa[i].height;
1131 res[1] = height - yPos;
1139 * Answers a tooltip for the annotation at the current mouse position, not
1140 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1146 * @param rowAndOffset
1148 static String buildToolTip(AlignmentAnnotation ann, int column,
1149 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1152 String tooltip = null;
1153 if (ann.graphGroup > -1)
1155 StringBuilder tip = new StringBuilder(32);
1156 boolean first = true;
1157 for (int i = 0; i < anns.length; i++)
1159 if (anns[i].graphGroup == ann.graphGroup
1160 && anns[i].annotations[column] != null)
1167 tip.append(anns[i].label);
1168 String description = anns[i].annotations[column].description;
1169 if (description != null && description.length() > 0)
1171 tip.append(" ").append(description);
1175 tooltip = first ? null : tip.toString();
1177 else if (column < ann.annotations.length
1178 && ann.annotations[column] != null)
1180 tooltip = ann.annotations[column].description;
1182 // TODO abstract tooltip generator so different implementations can be built
1183 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1185 ContactListI clist = av.getContactList(ann, column);
1188 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1189 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1190 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1191 tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
1192 + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
1193 int col = ann.sequenceRef.findPosition(column);
1194 ap.getStructureSelectionManager()
1195 .highlightPositionsOn(ann.sequenceRef, new int[][]
1196 { new int[] { col, col },
1198 { ci.cStart, ci.cEnd } }, null);
1205 * Constructs and returns the status bar message
1210 * @param rowAndOffset
1212 static String getStatusMessage(AlignmentI al, int column,
1213 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1216 * show alignment column and annotation description if any
1218 StringBuilder text = new StringBuilder(32);
1219 text.append(MessageManager.getString("label.column")).append(" ")
1220 .append(column + 1);
1222 if (column < ann.annotations.length && ann.annotations[column] != null)
1224 String description = ann.annotations[column].description;
1225 if (description != null && description.trim().length() > 0)
1227 text.append(" ").append(description);
1232 * if the annotation is sequence-specific, show the sequence number
1233 * in the alignment, and (if not a gap) the residue and position
1235 SequenceI seqref = ann.sequenceRef;
1238 int seqIndex = al.findIndex(seqref);
1241 text.append(", ").append(MessageManager.getString("label.sequence"))
1242 .append(" ").append(seqIndex + 1);
1243 char residue = seqref.getCharAt(column);
1244 if (!Comparison.isGap(residue))
1248 if (al.isNucleotide())
1250 name = ResidueProperties.nucleotideName
1251 .get(String.valueOf(residue));
1252 text.append(" Nucleotide: ")
1253 .append(name != null ? name : residue);
1257 name = 'X' == residue ? "X"
1258 : ('*' == residue ? "STOP"
1259 : ResidueProperties.aa2Triplet
1260 .get(String.valueOf(residue)));
1261 text.append(" Residue: ").append(name != null ? name : residue);
1263 int residuePos = seqref.findPosition(column);
1264 text.append(" (").append(residuePos).append(")");
1269 return text.toString();
1279 public void mouseClicked(MouseEvent evt)
1281 // if (activeRow != -1)
1283 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1284 // AlignmentAnnotation anot = aa[activeRow];
1288 // TODO mouseClicked-content and drawCursor are quite experimental!
1289 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1292 int pady = av.getCharHeight() / 5;
1294 graphics.setColor(Color.black);
1295 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1297 if (av.validCharWidth)
1299 graphics.setColor(Color.white);
1301 char s = seq.getCharAt(res);
1303 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1304 graphics.drawString(String.valueOf(s), charOffset + x1,
1305 (y1 + av.getCharHeight()) - pady);
1310 private volatile boolean imageFresh = false;
1312 private Rectangle visibleRect = new Rectangle(),
1313 clipBounds = new Rectangle();
1322 public void paintComponent(Graphics g)
1325 // BH: note that this method is generally recommended to
1326 // call super.paintComponent(g). Otherwise, the children of this
1327 // component will not be rendered. That is not needed here
1328 // because AnnotationPanel does not have any children. It is
1329 // just a JPanel contained in a JViewPort.
1331 computeVisibleRect(visibleRect);
1333 g.setColor(Color.white);
1334 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1338 // BH 2018 optimizing generation of new Rectangle().
1340 || (visibleRect.width != (clipBounds = g
1341 .getClipBounds(clipBounds)).width)
1342 || (visibleRect.height != clipBounds.height))
1345 g.drawImage(image, 0, 0, this);
1350 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1351 + 1) * av.getCharWidth();
1357 if (image == null || imgWidth != image.getWidth(this)
1358 || image.getHeight(this) != getHeight())
1360 boolean tried = false;
1362 while (image == null && !tried)
1366 image = new BufferedImage(imgWidth,
1367 ap.getAnnotationPanel().getHeight(),
1368 BufferedImage.TYPE_INT_RGB);
1370 } catch (IllegalArgumentException exc)
1373 "Serious issue with viewport geometry imgWidth requested was "
1376 } catch (OutOfMemoryError oom)
1381 } catch (Exception x)
1386 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1392 gg = (Graphics2D) image.getGraphics();
1396 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1397 RenderingHints.VALUE_ANTIALIAS_ON);
1400 gg.setFont(av.getFont());
1401 fm = gg.getFontMetrics();
1402 gg.setColor(Color.white);
1403 gg.fillRect(0, 0, imgWidth, image.getHeight());
1408 gg = (Graphics2D) image.getGraphics();
1412 drawComponent(gg, av.getRanges().getStartRes(),
1413 av.getRanges().getEndRes() + 1);
1416 g.drawImage(image, 0, 0, this);
1420 * set true to enable redraw timing debug output on stderr
1422 private final boolean debugRedraw = false;
1425 * non-Thread safe repaint
1428 * repaint with horizontal shift in alignment
1430 public void fastPaint(int horizontal)
1432 if ((horizontal == 0) || image == null
1433 || av.getAlignment().getAlignmentAnnotation() == null
1434 || av.getAlignment().getAlignmentAnnotation().length < 1
1435 || av.isCalcInProgress())
1441 int sr = av.getRanges().getStartRes();
1442 int er = av.getRanges().getEndRes() + 1;
1445 Graphics2D gg = (Graphics2D) image.getGraphics();
1447 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1449 // scroll is less than imgWidth away so can re-use buffered graphics
1450 gg.copyArea(0, 0, imgWidth, getHeight(),
1451 -horizontal * av.getCharWidth(), 0);
1453 if (horizontal > 0) // scrollbar pulled right, image to the left
1455 transX = (er - sr - horizontal) * av.getCharWidth();
1456 sr = er - horizontal;
1458 else if (horizontal < 0)
1460 er = sr - horizontal;
1463 gg.translate(transX, 0);
1465 drawComponent(gg, sr, er);
1467 gg.translate(-transX, 0);
1473 // Call repaint on alignment panel so that repaints from other alignment
1474 // panel components can be aggregated. Otherwise performance of the overview
1475 // window and others may be adversely affected.
1476 av.getAlignPanel().repaint();
1479 private volatile boolean lastImageGood = false;
1491 public void drawComponent(Graphics g, int startRes, int endRes)
1493 BufferedImage oldFaded = fadedImage;
1494 if (av.isCalcInProgress())
1498 lastImageGood = false;
1501 // We'll keep a record of the old image,
1502 // and draw a faded image until the calculation
1505 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1506 || fadedImage.getHeight() != image.getHeight()))
1508 // System.err.println("redraw faded image ("+(fadedImage==null ?
1509 // "null image" : "") + " lastGood="+lastImageGood+")");
1510 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1511 BufferedImage.TYPE_INT_RGB);
1513 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1515 fadedG.setColor(Color.white);
1516 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1518 fadedG.setComposite(
1519 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1520 fadedG.drawImage(image, 0, 0, this);
1523 // make sure we don't overwrite the last good faded image until all
1524 // calculations have finished
1525 lastImageGood = false;
1530 if (fadedImage != null)
1532 oldFaded = fadedImage;
1537 g.setColor(Color.white);
1538 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1540 g.setFont(av.getFont());
1543 fm = g.getFontMetrics();
1546 if ((av.getAlignment().getAlignmentAnnotation() == null)
1547 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1549 g.setColor(Color.white);
1550 g.fillRect(0, 0, getWidth(), getHeight());
1551 g.setColor(Color.black);
1552 if (av.validCharWidth)
1554 g.drawString(MessageManager
1555 .getString("label.alignment_has_no_annotations"), 20, 15);
1560 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1562 if (!lastImageGood && fadedImage == null)
1564 fadedImage = oldFaded;
1566 if (dragMode == DragMode.MatrixSelect)
1568 g.setColor(Color.yellow);
1569 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1570 Math.min(firstDragY, mouseDragLastY),
1571 Math.max(firstDragX, mouseDragLastX)
1572 - Math.min(firstDragX, mouseDragLastX),
1573 Math.max(firstDragY, mouseDragLastY)
1574 - Math.min(firstDragY, mouseDragLastY));
1580 public FontMetrics getFontMetrics()
1586 public Image getFadedImage()
1592 public int getFadedImageWidth()
1597 private int[] bounds = new int[2];
1600 public int[] getVisibleVRange()
1602 if (ap != null && ap.getAlabels() != null)
1604 int sOffset = -ap.getAlabels().getScrollOffset();
1605 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1606 bounds[0] = sOffset;
1607 bounds[1] = visHeight;
1617 * Try to ensure any references held are nulled
1619 public void dispose()
1629 * I created the renderer so I will dispose of it
1631 if (renderer != null)
1638 public void propertyChange(PropertyChangeEvent evt)
1640 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1641 // Both scrolling and resizing change viewport ranges: scrolling changes
1642 // both start and end points, but resize only changes end values.
1643 // Here we only want to fastpaint on a scroll, with resize using a normal
1644 // paint, so scroll events are identified as changes to the horizontal or
1645 // vertical start value.
1646 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1648 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1650 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1652 fastPaint(((int[]) evt.getNewValue())[0]
1653 - ((int[]) evt.getOldValue())[0]);
1655 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1662 * computes the visible height of the annotation panel
1664 * @param adjustPanelHeight
1665 * - when false, just adjust existing height according to other
1667 * @param annotationHeight
1668 * @return height to use for the ScrollerPreferredVisibleSize
1670 public int adjustForAlignFrame(boolean adjustPanelHeight,
1671 int annotationHeight)
1674 * Estimate available height in the AlignFrame for alignment +
1675 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1676 * hscroll, status bar, insets.
1678 int stuff = (ap.getViewName() != null ? 30 : 0)
1679 + (Platform.isAMacAndNotJS() ? 120 : 140);
1680 int availableHeight = ap.alignFrame.getHeight() - stuff;
1681 int rowHeight = av.getCharHeight();
1683 if (adjustPanelHeight)
1685 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1688 * If not enough vertical space, maximize annotation height while keeping
1689 * at least two rows of alignment visible
1691 if (annotationHeight + alignmentHeight > availableHeight)
1693 annotationHeight = Math.min(annotationHeight,
1694 availableHeight - 2 * rowHeight);
1699 // maintain same window layout whilst updating sliders
1700 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1701 availableHeight - 2 * rowHeight);
1703 return annotationHeight;