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();
607 int currentX = getColumnForXPos(evt.getX());
608 ContactMatrixI matrix = av.getContactMatrix(aa[graphStretch]);
611 if (matrix.hasGroups())
613 SequenceI rseq = aa[graphStretch].sequenceRef;
614 BitSet grp = matrix.getGroupsFor(currentX);
615 ColumnSelection cs = av.getColumnSelection();
616 HiddenColumns hc = av.getAlignment().getHiddenColumns();
617 for (int p=grp.nextSetBit(0); p>=0; p = grp.nextSetBit(p+1))
619 int offp = (rseq!=null) ? rseq.findIndex(rseq.getStart()-1+p) : p;
621 if (!av.hasHiddenColumns() || hc.isVisible(offp))
623 av.getColumnSelection().addElement(offp);
628 ContactListI forCurrentX = av.getContactList(aa[graphStretch],
630 if (forCurrentX != null)
632 ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
633 aa[graphStretch].graphHeight);
634 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset,
637 fr = Math.min(cXci.cStart, cXci.cEnd);
638 to = Math.max(cXci.cStart, cXci.cEnd);
639 // select corresponding range in segment under mouse
641 for (int c = fr; c <= to; c++)
643 av.getColumnSelection().addElement(c);
645 av.getColumnSelection().addElement(currentX);
648 // and also select everything lower than the max range adjacent
650 if (PAEContactMatrix.PAEMATRIX.equals(aa[graphStretch].getCalcId()))
653 ContactRange cr = forCurrentX.getRangeFor(fr, to);
655 // TODO: could use GraphLine instead of arbitrary picking
656 // TODO: could report mean/median/variance for partitions (contiguous selected vs unselected regions and inter-contig regions)
657 // controls feathering - what other elements in row/column should we select
658 double thresh=cr.getMean()+(cr.getMax()-cr.getMean())*.15;
661 cval = forCurrentX.getContactAt(c);
662 if (// cr.getMin() <= cval &&
665 av.getColumnSelection().addElement(c--);
673 while (c < forCurrentX.getContactHeight())
675 cval = forCurrentX.getContactAt(c);
676 if (// cr.getMin() <= cval &&
679 av.getColumnSelection().addElement(c++);
694 ap.getScalePanel().mousePressed(evt);
699 * Construct and display a context menu at the right-click position
704 void showPopupMenu(final int y, int x)
706 if (av.getColumnSelection() == null
707 || av.getColumnSelection().isEmpty())
712 JPopupMenu pop = new JPopupMenu(
713 MessageManager.getString("label.structure_type"));
716 * Just display the needed structure options
718 if (av.getAlignment().isNucleotide())
720 item = new JMenuItem(STEM);
721 item.addActionListener(this);
726 item = new JMenuItem(HELIX);
727 item.addActionListener(this);
729 item = new JMenuItem(SHEET);
730 item.addActionListener(this);
733 item = new JMenuItem(LABEL);
734 item.addActionListener(this);
736 item = new JMenuItem(COLOUR);
737 item.addActionListener(this);
739 item = new JMenuItem(REMOVE);
740 item.addActionListener(this);
742 pop.show(this, x, y);
746 * Action on mouse up is to clear mouse drag data and call mouseReleased on
747 * ScalePanel, to deal with defining the selection group (if any) defined by
753 public void mouseReleased(MouseEvent evt)
755 if (dragMode == DragMode.MatrixSelect)
757 matrixSelectRange(evt);
764 mouseDragging = false;
765 if (dragMode == DragMode.Resize)
767 ap.adjustAnnotationHeight();
769 dragMode = DragMode.Undefined;
770 ap.getScalePanel().mouseReleased(evt);
773 * isPopupTrigger is set in mouseReleased on Windows
774 * (in mousePressed on Mac)
776 if (evt.isPopupTrigger() && activeRow != -1)
778 showPopupMenu(evt.getY(), evt.getX());
790 public void mouseEntered(MouseEvent evt)
792 this.mouseDragging = false;
793 ap.getScalePanel().mouseEntered(evt);
797 * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
798 * with column selection on a mouse drag
803 public void mouseExited(MouseEvent evt)
805 ap.getScalePanel().mouseExited(evt);
809 * Action on starting or continuing a mouse drag. There are two possible
812 * <li>drag up or down on a graphed annotation increases or decreases the
813 * height of the graph</li>
814 * <li>dragging left or right selects the columns dragged across</li>
816 * A drag on a graph annotation is treated as column selection if it starts
817 * with more horizontal than vertical movement, and as resize if it starts
818 * with more vertical than horizontal movement. Once started, the drag does
824 public void mouseDragged(MouseEvent evt)
827 * if dragMode is Undefined:
828 * - set to Select if dx > dy
829 * - set to Resize if dy > dx
830 * - do nothing if dx == dy
832 final int x = evt.getX();
833 final int y = evt.getY();
834 if (dragMode == DragMode.Undefined)
836 int dx = Math.abs(x - mouseDragLastX);
837 int dy = Math.abs(y - mouseDragLastY);
838 if (graphStretch == -1 || dx > dy)
841 * mostly horizontal drag, or not a graph annotation
843 dragMode = DragMode.Select;
848 * mostly vertical drag
850 dragMode = DragMode.Resize;
851 notJustOne = evt.isShiftDown();
854 * but could also be a matrix drag
856 if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
857 .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
860 * dragging in a matrix
862 dragMode = DragMode.MatrixSelect;
863 firstDragX = mouseDragLastX;
864 firstDragY = mouseDragLastY;
869 if (dragMode == DragMode.Undefined)
873 * drag is diagonal - defer deciding whether to
874 * treat as up/down or left/right
881 if (dragMode == DragMode.Resize)
884 * resize graph annotation if mouse was dragged up or down
886 int deltaY = mouseDragLastY - evt.getY();
889 AlignmentAnnotation graphAnnotation = av.getAlignment()
890 .getAlignmentAnnotation()[graphStretch];
891 int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
894 for (AlignmentAnnotation similar : av.getAlignment()
895 .findAnnotations(null, graphAnnotation.getCalcId(),
896 graphAnnotation.label))
898 similar.graphHeight = newHeight;
904 graphAnnotation.graphHeight = newHeight;
907 ap.paintAlignment(false, false);
910 else if (dragMode == DragMode.MatrixSelect)
913 * TODO draw a rubber band for range
917 ap.paintAlignment(false, false);
922 * for mouse drag left or right, delegate to
923 * ScalePanel to adjust the column selection
925 ap.getScalePanel().mouseDragged(evt);
934 public void matrixSelectRange(MouseEvent evt)
937 * get geometry of drag
939 int fromY = Math.min(firstDragY, evt.getY());
940 int toY = Math.max(firstDragY, evt.getY());
941 int fromX = Math.min(firstDragX, evt.getX());
942 int toX = Math.max(firstDragX, evt.getX());
944 int deltaY = toY - fromY;
945 int deltaX = toX - fromX;
947 int[] rowIndex = getRowIndexAndOffset(fromY,
948 av.getAlignment().getAlignmentAnnotation());
949 int[] toRowIndex = getRowIndexAndOffset(toY,
950 av.getAlignment().getAlignmentAnnotation());
952 if (rowIndex == null || toRowIndex == null)
954 System.out.println("Drag out of range. needs to be clipped");
957 if (rowIndex[0] != toRowIndex[0])
959 System.out.println("Drag went to another row. needs to be clipped");
962 // rectangular selection on matrix style annotation
963 AlignmentAnnotation cma = av.getAlignment()
964 .getAlignmentAnnotation()[rowIndex[0]];
966 int lastX = getColumnForXPos(fromX);
967 int currentX = getColumnForXPos(toX);
968 int fromXc = Math.min(lastX, currentX);
969 int toXc = Math.max(lastX, currentX);
970 ContactListI forFromX = av.getContactList(cma, fromXc);
971 ContactListI forToX = av.getContactList(cma, toXc);
973 if (forFromX != null && forToX != null)
975 ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
977 ContactGeometry.contactInterval lastXci = lastXcgeom
978 .mapFor(rowIndex[1], rowIndex[1] - deltaY);
980 ContactGeometry cXcgeom = new ContactGeometry(forToX,
982 ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
983 rowIndex[1] - deltaY);
985 // mark rectangular region formed by drag
986 System.err.println("Matrix Selection from last(" + fromXc + ",["
987 + lastXci.cStart + "," + lastXci.cEnd + "]) to cur(" + toXc
988 + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
990 fr = Math.min(lastXci.cStart, lastXci.cEnd);
991 to = Math.max(lastXci.cStart, lastXci.cEnd);
992 System.err.println("Marking " + fr + " to " + to);
993 for (int c = fr; c <= to; c++)
995 if (cma.sequenceRef != null)
997 int col = cma.sequenceRef.findIndex(c);
998 av.getColumnSelection().addElement(col);
1002 av.getColumnSelection().addElement(c);
1005 fr = Math.min(cXci.cStart, cXci.cEnd);
1006 to = Math.max(cXci.cStart, cXci.cEnd);
1007 System.err.println("Marking " + fr + " to " + to);
1008 for (int c = fr; c <= to; c++)
1010 if (cma.sequenceRef != null)
1012 int col = cma.sequenceRef.findIndex(c);
1013 av.getColumnSelection().addElement(col);
1017 av.getColumnSelection().addElement(c);
1020 fr = Math.min(lastX, currentX);
1021 to = Math.max(lastX, currentX);
1023 System.err.println("Marking " + fr + " to " + to);
1024 for (int c = fr; c <= to; c++)
1026 av.getColumnSelection().addElement(c);
1033 * Constructs the tooltip, and constructs and displays a status message, for
1034 * the current mouse position
1039 public void mouseMoved(MouseEvent evt)
1041 int yPos = evt.getY();
1042 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1043 int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
1044 int row = rowAndOffset[0];
1048 this.setToolTipText(null);
1052 int column = getColumnForXPos(evt.getX());
1054 AlignmentAnnotation ann = aa[row];
1055 if (row > -1 && ann.annotations != null
1056 && column < ann.annotations.length)
1058 String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
1060 setToolTipText(toolTip == null ? null
1061 : JvSwingUtils.wrapTooltip(true, toolTip));
1062 String msg = getStatusMessage(av.getAlignment(), column, ann,
1063 rowAndOffset[1], av);
1064 ap.alignFrame.setStatus(msg);
1068 this.setToolTipText(null);
1069 ap.alignFrame.setStatus(" ");
1073 private int getColumnForXPos(int x)
1075 int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
1076 column = Math.min(column, av.getRanges().getEndRes());
1078 if (av.hasHiddenColumns())
1080 column = av.getAlignment().getHiddenColumns()
1081 .visibleToAbsoluteColumn(column);
1087 * Answers the index in the annotations array of the visible annotation at the
1088 * given y position. This is done by adding the heights of visible annotations
1089 * until the y position has been exceeded. Answers -1 if no annotations are
1090 * visible, or the y position is below all annotations.
1096 static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
1102 return getRowIndexAndOffset(yPos, aa)[0];
1105 static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
1107 int[] res = new int[2];
1115 int height = 0, lheight = 0;
1116 for (int i = 0; i < aa.length; i++)
1121 height += aa[i].height;
1128 res[1] = height - yPos;
1136 * Answers a tooltip for the annotation at the current mouse position, not
1137 * wrapped in <html> tags (apply if wanted). Answers null if there is no
1143 * @param rowAndOffset
1145 static String buildToolTip(AlignmentAnnotation ann, int column,
1146 AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
1149 String tooltip = null;
1150 if (ann.graphGroup > -1)
1152 StringBuilder tip = new StringBuilder(32);
1153 boolean first = true;
1154 for (int i = 0; i < anns.length; i++)
1156 if (anns[i].graphGroup == ann.graphGroup
1157 && anns[i].annotations[column] != null)
1164 tip.append(anns[i].label);
1165 String description = anns[i].annotations[column].description;
1166 if (description != null && description.length() > 0)
1168 tip.append(" ").append(description);
1172 tooltip = first ? null : tip.toString();
1174 else if (column < ann.annotations.length
1175 && ann.annotations[column] != null)
1177 tooltip = ann.annotations[column].description;
1179 // TODO abstract tooltip generator so different implementations can be built
1180 if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
1182 ContactListI clist = av.getContactList(ann, column);
1185 ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
1186 ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
1187 ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
1188 tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
1189 + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
1190 int col = ann.sequenceRef.findPosition(column);
1191 ap.getStructureSelectionManager()
1192 .highlightPositionsOn(ann.sequenceRef, new int[][]
1193 { new int[] { col, col },
1195 { ci.cStart, ci.cEnd } }, null);
1202 * Constructs and returns the status bar message
1207 * @param rowAndOffset
1209 static String getStatusMessage(AlignmentI al, int column,
1210 AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
1213 * show alignment column and annotation description if any
1215 StringBuilder text = new StringBuilder(32);
1216 text.append(MessageManager.getString("label.column")).append(" ")
1217 .append(column + 1);
1219 if (column < ann.annotations.length && ann.annotations[column] != null)
1221 String description = ann.annotations[column].description;
1222 if (description != null && description.trim().length() > 0)
1224 text.append(" ").append(description);
1229 * if the annotation is sequence-specific, show the sequence number
1230 * in the alignment, and (if not a gap) the residue and position
1232 SequenceI seqref = ann.sequenceRef;
1235 int seqIndex = al.findIndex(seqref);
1238 text.append(", ").append(MessageManager.getString("label.sequence"))
1239 .append(" ").append(seqIndex + 1);
1240 char residue = seqref.getCharAt(column);
1241 if (!Comparison.isGap(residue))
1245 if (al.isNucleotide())
1247 name = ResidueProperties.nucleotideName
1248 .get(String.valueOf(residue));
1249 text.append(" Nucleotide: ")
1250 .append(name != null ? name : residue);
1254 name = 'X' == residue ? "X"
1255 : ('*' == residue ? "STOP"
1256 : ResidueProperties.aa2Triplet
1257 .get(String.valueOf(residue)));
1258 text.append(" Residue: ").append(name != null ? name : residue);
1260 int residuePos = seqref.findPosition(column);
1261 text.append(" (").append(residuePos).append(")");
1266 return text.toString();
1276 public void mouseClicked(MouseEvent evt)
1278 // if (activeRow != -1)
1280 // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1281 // AlignmentAnnotation anot = aa[activeRow];
1285 // TODO mouseClicked-content and drawCursor are quite experimental!
1286 public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1289 int pady = av.getCharHeight() / 5;
1291 graphics.setColor(Color.black);
1292 graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1294 if (av.validCharWidth)
1296 graphics.setColor(Color.white);
1298 char s = seq.getCharAt(res);
1300 charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1301 graphics.drawString(String.valueOf(s), charOffset + x1,
1302 (y1 + av.getCharHeight()) - pady);
1307 private volatile boolean imageFresh = false;
1309 private Rectangle visibleRect = new Rectangle(),
1310 clipBounds = new Rectangle();
1319 public void paintComponent(Graphics g)
1322 // BH: note that this method is generally recommended to
1323 // call super.paintComponent(g). Otherwise, the children of this
1324 // component will not be rendered. That is not needed here
1325 // because AnnotationPanel does not have any children. It is
1326 // just a JPanel contained in a JViewPort.
1328 computeVisibleRect(visibleRect);
1330 g.setColor(Color.white);
1331 g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1335 // BH 2018 optimizing generation of new Rectangle().
1337 || (visibleRect.width != (clipBounds = g
1338 .getClipBounds(clipBounds)).width)
1339 || (visibleRect.height != clipBounds.height))
1342 g.drawImage(image, 0, 0, this);
1347 imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1348 + 1) * av.getCharWidth();
1354 if (image == null || imgWidth != image.getWidth(this)
1355 || image.getHeight(this) != getHeight())
1357 boolean tried = false;
1359 while (image == null && !tried)
1363 image = new BufferedImage(imgWidth,
1364 ap.getAnnotationPanel().getHeight(),
1365 BufferedImage.TYPE_INT_RGB);
1367 } catch (IllegalArgumentException exc)
1370 "Serious issue with viewport geometry imgWidth requested was "
1373 } catch (OutOfMemoryError oom)
1378 } catch (Exception x)
1383 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1389 gg = (Graphics2D) image.getGraphics();
1393 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1394 RenderingHints.VALUE_ANTIALIAS_ON);
1397 gg.setFont(av.getFont());
1398 fm = gg.getFontMetrics();
1399 gg.setColor(Color.white);
1400 gg.fillRect(0, 0, imgWidth, image.getHeight());
1405 gg = (Graphics2D) image.getGraphics();
1409 drawComponent(gg, av.getRanges().getStartRes(),
1410 av.getRanges().getEndRes() + 1);
1413 g.drawImage(image, 0, 0, this);
1417 * set true to enable redraw timing debug output on stderr
1419 private final boolean debugRedraw = false;
1422 * non-Thread safe repaint
1425 * repaint with horizontal shift in alignment
1427 public void fastPaint(int horizontal)
1429 if ((horizontal == 0) || image == null
1430 || av.getAlignment().getAlignmentAnnotation() == null
1431 || av.getAlignment().getAlignmentAnnotation().length < 1
1432 || av.isCalcInProgress())
1438 int sr = av.getRanges().getStartRes();
1439 int er = av.getRanges().getEndRes() + 1;
1442 Graphics2D gg = (Graphics2D) image.getGraphics();
1444 if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1446 // scroll is less than imgWidth away so can re-use buffered graphics
1447 gg.copyArea(0, 0, imgWidth, getHeight(),
1448 -horizontal * av.getCharWidth(), 0);
1450 if (horizontal > 0) // scrollbar pulled right, image to the left
1452 transX = (er - sr - horizontal) * av.getCharWidth();
1453 sr = er - horizontal;
1455 else if (horizontal < 0)
1457 er = sr - horizontal;
1460 gg.translate(transX, 0);
1462 drawComponent(gg, sr, er);
1464 gg.translate(-transX, 0);
1470 // Call repaint on alignment panel so that repaints from other alignment
1471 // panel components can be aggregated. Otherwise performance of the overview
1472 // window and others may be adversely affected.
1473 av.getAlignPanel().repaint();
1476 private volatile boolean lastImageGood = false;
1488 public void drawComponent(Graphics g, int startRes, int endRes)
1490 BufferedImage oldFaded = fadedImage;
1491 if (av.isCalcInProgress())
1495 lastImageGood = false;
1498 // We'll keep a record of the old image,
1499 // and draw a faded image until the calculation
1502 && (fadedImage == null || fadedImage.getWidth() != imgWidth
1503 || fadedImage.getHeight() != image.getHeight()))
1505 // System.err.println("redraw faded image ("+(fadedImage==null ?
1506 // "null image" : "") + " lastGood="+lastImageGood+")");
1507 fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1508 BufferedImage.TYPE_INT_RGB);
1510 Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1512 fadedG.setColor(Color.white);
1513 fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1515 fadedG.setComposite(
1516 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1517 fadedG.drawImage(image, 0, 0, this);
1520 // make sure we don't overwrite the last good faded image until all
1521 // calculations have finished
1522 lastImageGood = false;
1527 if (fadedImage != null)
1529 oldFaded = fadedImage;
1534 g.setColor(Color.white);
1535 g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1537 g.setFont(av.getFont());
1540 fm = g.getFontMetrics();
1543 if ((av.getAlignment().getAlignmentAnnotation() == null)
1544 || (av.getAlignment().getAlignmentAnnotation().length < 1))
1546 g.setColor(Color.white);
1547 g.fillRect(0, 0, getWidth(), getHeight());
1548 g.setColor(Color.black);
1549 if (av.validCharWidth)
1551 g.drawString(MessageManager
1552 .getString("label.alignment_has_no_annotations"), 20, 15);
1557 lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1559 if (!lastImageGood && fadedImage == null)
1561 fadedImage = oldFaded;
1563 if (dragMode == DragMode.MatrixSelect)
1565 g.setColor(Color.yellow);
1566 g.drawRect(Math.min(firstDragX, mouseDragLastX),
1567 Math.min(firstDragY, mouseDragLastY),
1568 Math.max(firstDragX, mouseDragLastX)
1569 - Math.min(firstDragX, mouseDragLastX),
1570 Math.max(firstDragY, mouseDragLastY)
1571 - Math.min(firstDragY, mouseDragLastY));
1577 public FontMetrics getFontMetrics()
1583 public Image getFadedImage()
1589 public int getFadedImageWidth()
1594 private int[] bounds = new int[2];
1597 public int[] getVisibleVRange()
1599 if (ap != null && ap.getAlabels() != null)
1601 int sOffset = -ap.getAlabels().getScrollOffset();
1602 int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1603 bounds[0] = sOffset;
1604 bounds[1] = visHeight;
1614 * Try to ensure any references held are nulled
1616 public void dispose()
1626 * I created the renderer so I will dispose of it
1628 if (renderer != null)
1635 public void propertyChange(PropertyChangeEvent evt)
1637 // Respond to viewport range changes (e.g. alignment panel was scrolled)
1638 // Both scrolling and resizing change viewport ranges: scrolling changes
1639 // both start and end points, but resize only changes end values.
1640 // Here we only want to fastpaint on a scroll, with resize using a normal
1641 // paint, so scroll events are identified as changes to the horizontal or
1642 // vertical start value.
1643 if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1645 fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1647 else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1649 fastPaint(((int[]) evt.getNewValue())[0]
1650 - ((int[]) evt.getOldValue())[0]);
1652 else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1659 * computes the visible height of the annotation panel
1661 * @param adjustPanelHeight
1662 * - when false, just adjust existing height according to other
1664 * @param annotationHeight
1665 * @return height to use for the ScrollerPreferredVisibleSize
1667 public int adjustForAlignFrame(boolean adjustPanelHeight,
1668 int annotationHeight)
1671 * Estimate available height in the AlignFrame for alignment +
1672 * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1673 * hscroll, status bar, insets.
1675 int stuff = (ap.getViewName() != null ? 30 : 0)
1676 + (Platform.isAMacAndNotJS() ? 120 : 140);
1677 int availableHeight = ap.alignFrame.getHeight() - stuff;
1678 int rowHeight = av.getCharHeight();
1680 if (adjustPanelHeight)
1682 int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1685 * If not enough vertical space, maximize annotation height while keeping
1686 * at least two rows of alignment visible
1688 if (annotationHeight + alignmentHeight > availableHeight)
1690 annotationHeight = Math.min(annotationHeight,
1691 availableHeight - 2 * rowHeight);
1696 // maintain same window layout whilst updating sliders
1697 annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1698 availableHeight - 2 * rowHeight);
1700 return annotationHeight;