/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.gui; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; import java.util.List; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.Scrollable; import javax.swing.ToolTipManager; import jalview.api.AlignViewportI; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.Annotation; import jalview.datamodel.ColumnSelection; import jalview.datamodel.ContactListI; import jalview.datamodel.ContactMatrixI; import jalview.datamodel.ContactRange; import jalview.datamodel.GraphLine; import jalview.datamodel.HiddenColumns; import jalview.datamodel.SequenceI; import jalview.gui.JalviewColourChooser.ColourChooserListener; import jalview.renderer.AnnotationRenderer; import jalview.renderer.AwtRenderPanelI; import jalview.renderer.ContactGeometry; import jalview.schemes.ResidueProperties; import jalview.util.Comparison; import jalview.util.Format; import jalview.util.MessageManager; import jalview.util.Platform; import jalview.viewmodel.ViewportListenerI; import jalview.viewmodel.ViewportRanges; import jalview.ws.datamodel.MappableContactMatrixI; import jalview.ws.datamodel.alphafold.PAEContactMatrix; /** * AnnotationPanel displays visible portion of annotation rows below unwrapped * alignment * * @author $author$ * @version $Revision$ */ public class AnnotationPanel extends JPanel implements AwtRenderPanelI, MouseListener, MouseWheelListener, MouseMotionListener, ActionListener, AdjustmentListener, Scrollable, ViewportListenerI { enum DragMode { Select, Resize, Undefined, MatrixSelect }; String HELIX = MessageManager.getString("label.helix"); String SHEET = MessageManager.getString("label.sheet"); /** * For RNA secondary structure "stems" aka helices */ String STEM = MessageManager.getString("label.rna_helix"); String LABEL = MessageManager.getString("label.label"); String REMOVE = MessageManager.getString("label.remove_annotation"); String COLOUR = MessageManager.getString("action.colour"); public final Color HELIX_COLOUR = Color.red.darker(); public final Color SHEET_COLOUR = Color.green.darker().darker(); public final Color STEM_COLOUR = Color.blue.darker(); /** DOCUMENT ME!! */ public AlignViewport av; AlignmentPanel ap; public int activeRow = -1; public BufferedImage image; public volatile BufferedImage fadedImage; // private Graphics2D gg; public FontMetrics fm; public int imgWidth = 0; boolean fastPaint = false; // Used For mouse Dragging and resizing graphs int graphStretch = -1; int mouseDragLastX = -1; int mouseDragLastY = -1; int firstDragX = -1; int firstDragY = -1; DragMode dragMode = DragMode.Undefined; boolean mouseDragging = false; // for editing cursor int cursorX = 0; int cursorY = 0; public final AnnotationRenderer renderer; private MouseWheelListener[] _mwl; private boolean notJustOne; /** * Creates a new AnnotationPanel object. * * @param ap * DOCUMENT ME! */ public AnnotationPanel(AlignmentPanel ap) { ToolTipManager.sharedInstance().registerComponent(this); ToolTipManager.sharedInstance().setInitialDelay(0); ToolTipManager.sharedInstance().setDismissDelay(10000); this.ap = ap; av = ap.av; this.setLayout(null); addMouseListener(this); addMouseMotionListener(this); ap.annotationScroller.getVerticalScrollBar() .addAdjustmentListener(this); // save any wheel listeners on the scroller, so we can propagate scroll // events to them. _mwl = ap.annotationScroller.getMouseWheelListeners(); // and then set our own listener to consume all mousewheel events ap.annotationScroller.addMouseWheelListener(this); renderer = new AnnotationRenderer(); av.getRanges().addPropertyChangeListener(this); } public AnnotationPanel(AlignViewport av) { this.av = av; renderer = new AnnotationRenderer(); } @Override public void mouseWheelMoved(MouseWheelEvent e) { if (e.isShiftDown()) { e.consume(); double wheelRotation = e.getPreciseWheelRotation(); if (wheelRotation > 0) { av.getRanges().scrollRight(true); } else if (wheelRotation < 0) { av.getRanges().scrollRight(false); } } else { // TODO: find the correct way to let the event bubble up to // ap.annotationScroller for (MouseWheelListener mwl : _mwl) { if (mwl != null) { mwl.mouseWheelMoved(e); } if (e.isConsumed()) { break; } } } } @Override public Dimension getPreferredScrollableViewportSize() { Dimension ps = getPreferredSize(); return new Dimension(ps.width, adjustForAlignFrame(false, ps.height)); } @Override public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { return 30; } @Override public boolean getScrollableTracksViewportHeight() { return false; } @Override public boolean getScrollableTracksViewportWidth() { return true; } @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { return 30; } /* * (non-Javadoc) * * @see * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event * .AdjustmentEvent) */ @Override public void adjustmentValueChanged(AdjustmentEvent evt) { // update annotation label display ap.getAlabels().setScrollOffset(-evt.getValue()); } /** * Calculates the height of the annotation displayed in the annotation panel. * Callers should normally call the ap.adjustAnnotationHeight method to ensure * all annotation associated components are updated correctly. * */ public int adjustPanelHeight() { int height = av.calcPanelHeight(); this.setPreferredSize(new Dimension(1, height)); if (ap != null) { // revalidate only when the alignment panel is fully constructed ap.validate(); } return height; } /** * DOCUMENT ME! * * @param evt * DOCUMENT ME! */ @Override public void actionPerformed(ActionEvent evt) { AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation(); if (aa == null) { return; } Annotation[] anot = aa[activeRow].annotations; if (anot.length < av.getColumnSelection().getMax()) { Annotation[] temp = new Annotation[av.getColumnSelection().getMax() + 2]; System.arraycopy(anot, 0, temp, 0, anot.length); anot = temp; aa[activeRow].annotations = anot; } String action = evt.getActionCommand(); if (action.equals(REMOVE)) { for (int index : av.getColumnSelection().getSelected()) { if (av.getAlignment().getHiddenColumns().isVisible(index)) { anot[index] = null; } } } else if (action.equals(LABEL)) { String exMesg = collectAnnotVals(anot, LABEL); String label = JvOptionPane.showInputDialog( MessageManager.getString("label.enter_label"), exMesg); if (label == null) { return; } if ((label.length() > 0) && !aa[activeRow].hasText) { aa[activeRow].hasText = true; } for (int index : av.getColumnSelection().getSelected()) { if (!av.getAlignment().getHiddenColumns().isVisible(index)) { continue; } if (anot[index] == null) { anot[index] = new Annotation(label, "", ' ', 0); } else { anot[index].displayCharacter = label; } } } else if (action.equals(COLOUR)) { final Annotation[] fAnot = anot; String title = MessageManager .getString("label.select_foreground_colour"); ColourChooserListener listener = new ColourChooserListener() { @Override public void colourSelected(Color c) { HiddenColumns hiddenColumns = av.getAlignment() .getHiddenColumns(); for (int index : av.getColumnSelection().getSelected()) { if (hiddenColumns.isVisible(index)) { if (fAnot[index] == null) { fAnot[index] = new Annotation("", "", ' ', 0); } fAnot[index].colour = c; } } }; }; JalviewColourChooser.showColourChooser(this, title, Color.black, listener); } else // HELIX, SHEET or STEM { char type = 0; String symbol = "\u03B1"; // alpha if (action.equals(HELIX)) { type = 'H'; } else if (action.equals(SHEET)) { type = 'E'; symbol = "\u03B2"; // beta } // Added by LML to color stems else if (action.equals(STEM)) { type = 'S'; int column = av.getColumnSelection().getSelectedRanges().get(0)[0]; symbol = aa[activeRow].getDefaultRnaHelixSymbol(column); } if (!aa[activeRow].hasIcons) { aa[activeRow].hasIcons = true; } String label = JvOptionPane.showInputDialog(MessageManager .getString("label.enter_label_for_the_structure"), symbol); if (label == null) { return; } if ((label.length() > 0) && !aa[activeRow].hasText) { aa[activeRow].hasText = true; if (action.equals(STEM)) { aa[activeRow].showAllColLabels = true; } } for (int index : av.getColumnSelection().getSelected()) { if (!av.getAlignment().getHiddenColumns().isVisible(index)) { continue; } if (anot[index] == null) { anot[index] = new Annotation(label, "", type, 0); } anot[index].secondaryStructure = type != 'S' ? type : label.length() == 0 ? ' ' : label.charAt(0); anot[index].displayCharacter = label; } } av.getAlignment().validateAnnotation(aa[activeRow]); ap.alignmentChanged(); ap.alignFrame.setMenusForViewport(); adjustPanelHeight(); repaint(); return; } /** * Returns any existing annotation concatenated as a string. For each * annotation, takes the description, if any, else the secondary structure * character (if type is HELIX, SHEET or STEM), else the display character (if * type is LABEL). * * @param anots * @param type * @return */ private String collectAnnotVals(Annotation[] anots, String type) { // TODO is this method wanted? why? 'last' is not used StringBuilder collatedInput = new StringBuilder(64); String last = ""; ColumnSelection viscols = av.getColumnSelection(); HiddenColumns hidden = av.getAlignment().getHiddenColumns(); /* * the selection list (read-only view) is in selection order, not * column order; make a copy so we can sort it */ List selected = new ArrayList<>(viscols.getSelected()); Collections.sort(selected); for (int index : selected) { // always check for current display state - just in case if (!hidden.isVisible(index)) { continue; } String tlabel = null; if (anots[index] != null) { // LML added stem code if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM) || type.equals(LABEL)) { tlabel = anots[index].description; if (tlabel == null || tlabel.length() < 1) { if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)) { tlabel = "" + anots[index].secondaryStructure; } else { tlabel = "" + anots[index].displayCharacter; } } } if (tlabel != null && !tlabel.equals(last)) { if (last.length() > 0) { collatedInput.append(" "); } collatedInput.append(tlabel); } } } return collatedInput.toString(); } /** * Action on right mouse pressed on Mac is to show a pop-up menu for the * annotation. Action on left mouse pressed is to find which annotation is * pressed and mark the start of a column selection or graph resize operation. * * @param evt */ @Override public void mousePressed(MouseEvent evt) { AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation(); if (aa == null) { return; } mouseDragLastX = evt.getX(); mouseDragLastY = evt.getY(); /* * add visible annotation heights until we reach the y * position, to find which annotation it is in */ int height = 0; activeRow = -1; int yOffset = 0; // todo could reuse getRowIndexAndOffset ? final int y = evt.getY(); for (int i = 0; i < aa.length; i++) { if (aa[i].visible) { height += aa[i].height; } if (y < height) { if (aa[i].editable) { activeRow = i; } else if (aa[i].graph != 0) { /* * we have clicked on a resizable graph annotation */ graphStretch = i; yOffset = height - y; } break; } } /* * isPopupTrigger fires in mousePressed on Mac, * not until mouseRelease on Windows */ if (evt.isPopupTrigger() && activeRow != -1) { showPopupMenu(y, evt.getX()); return; } if (graphStretch != -1) { if (aa[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP) { // data in row has position on y as well as x axis if (evt.isAltDown() || evt.isAltGraphDown()) { dragMode = DragMode.MatrixSelect; firstDragX = mouseDragLastX; firstDragY = mouseDragLastY; } } } else { // no row (or row that can be adjusted) was pressed. Simulate a ruler // click ap.getScalePanel().mousePressed(evt); } } /** * checks whether the annotation row under the mouse click evt's handles the * event * * @param evt * @return false if evt was not handled */ boolean matrix_clicked(MouseEvent evt) { int[] rowIndex = getRowIndexAndOffset(evt.getY(), av.getAlignment().getAlignmentAnnotation()); if (rowIndex == null) { jalview.bin.Console .error("IMPLEMENTATION ERROR: matrix click out of range."); return false; } int yOffset = rowIndex[1]; AlignmentAnnotation[] allAnnotation = av.getAlignment() .getAlignmentAnnotation(); if (allAnnotation == null || rowIndex[0] < 0 || rowIndex[0] >= allAnnotation.length) { return false; } AlignmentAnnotation clicked = av.getAlignment() .getAlignmentAnnotation()[rowIndex[0]]; if (clicked.graph != AlignmentAnnotation.CONTACT_MAP) { return false; } // TODO - use existing threshold to select related sections of matrix GraphLine thr = clicked.getThreshold(); int currentX = getColumnForXPos(evt.getX()); ContactListI forCurrentX = av.getContactList(clicked, currentX); if (forCurrentX != null) { ContactGeometry cXcgeom = new ContactGeometry(forCurrentX, clicked.graphHeight); ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset); if (cXci != null) { /** * start and end range corresponding to the row range under the mouse at * column currentX */ int fr, to; fr = Math.min(cXci.cStart, cXci.cEnd); to = Math.max(cXci.cStart, cXci.cEnd); // double click selects the whole group if (evt.getClickCount() == 2) { ContactMatrixI matrix = av.getContactMatrix(clicked); if (matrix != null) { // simplest approach is to select all group containing column if (matrix.hasGroups()) { SequenceI rseq = clicked.sequenceRef; BitSet grp = new BitSet(); grp.or(matrix.getGroupsFor(forCurrentX.getPosition())); // TODO: cXci needs to be mapped to real groups for (int c = fr; c <= to; c++) { BitSet additionalGrp = matrix.getGroupsFor(c); grp.or(additionalGrp); } HiddenColumns hc = av.getAlignment().getHiddenColumns(); ColumnSelection cs = av.getColumnSelection(); for (int p = grp.nextSetBit(0); p >= 0; p = grp .nextSetBit(p + 1)) { if (matrix instanceof MappableContactMatrixI) { // find the end of this run of set bits int nextp = grp.nextClearBit(p) - 1; int[] pos = ((MappableContactMatrixI) matrix) .getMappedPositionsFor(rseq, p, nextp); p = nextp; if (pos != null) { for (int pos_p = pos[0]; pos_p <= pos[1]; pos_p++) { int col = rseq.findIndex(pos_p) - 1; if (col >= 0 && (!av.hasHiddenColumns() || hc.isVisible(col))) { cs.addElement(col); } } } } else { int offp = (rseq != null) ? rseq.findIndex(rseq.getStart() - 1 + p) : p; if (!av.hasHiddenColumns() || hc.isVisible(offp)) { cs.addElement(offp); } } } } // possible alternative for interactive selection - threshold // gives 'ceiling' for forming a cluster // when a row+column is selected, farthest common ancestor less // than thr is used to compute cluster } } else { // select corresponding range in segment under mouse { int[] rng = forCurrentX.getMappedPositionsFor(fr, to); if (rng != null) { av.getColumnSelection().addRangeOfElements(rng, true); } av.getColumnSelection().addElement(currentX); } // PAE SPECIFIC // and also select everything lower than the max range adjacent // (kind of works) if (evt.isControlDown() && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId())) { int c = fr; ContactRange cr = forCurrentX.getRangeFor(fr, to); double cval; // TODO: could use GraphLine instead of arbitrary picking // TODO: could report mean/median/variance for partitions // (contiguous selected vs unselected regions and inter-contig // regions) // controls feathering - what other elements in row/column // should we select double thresh = cr.getMean() + (cr.getMax() - cr.getMean()) * .15; while (c >= 0) { cval = forCurrentX.getContactAt(c); if (// cr.getMin() <= cval && cval <= thresh) { int[] cols = forCurrentX.getMappedPositionsFor(c, c); if (cols != null) { av.getColumnSelection().addRangeOfElements(cols, true); } else { break; } } c--; } c = to; while (c < forCurrentX.getContactHeight()) { cval = forCurrentX.getContactAt(c); if (// cr.getMin() <= cval && cval <= thresh) { int[] cols = forCurrentX.getMappedPositionsFor(c, c); if (cols != null) { av.getColumnSelection().addRangeOfElements(cols, true); } } else { break; } c++; } } } } } ap.paintAlignment(false, false); PaintRefresher.Refresh(ap, av.getSequenceSetId()); av.sendSelection(); return true; } /** * Construct and display a context menu at the right-click position * * @param y * @param x */ void showPopupMenu(final int y, int x) { if (av.getColumnSelection() == null || av.getColumnSelection().isEmpty()) { return; } JPopupMenu pop = new JPopupMenu( MessageManager.getString("label.structure_type")); JMenuItem item; /* * Just display the needed structure options */ if (av.getAlignment().isNucleotide()) { item = new JMenuItem(STEM); item.addActionListener(this); pop.add(item); } else { item = new JMenuItem(HELIX); item.addActionListener(this); pop.add(item); item = new JMenuItem(SHEET); item.addActionListener(this); pop.add(item); } item = new JMenuItem(LABEL); item.addActionListener(this); pop.add(item); item = new JMenuItem(COLOUR); item.addActionListener(this); pop.add(item); item = new JMenuItem(REMOVE); item.addActionListener(this); pop.add(item); pop.show(this, x, y); } /** * Action on mouse up is to clear mouse drag data and call mouseReleased on * ScalePanel, to deal with defining the selection group (if any) defined by * the mouse drag * * @param evt */ @Override public void mouseReleased(MouseEvent evt) { if (dragMode == DragMode.MatrixSelect) { matrixSelectRange(evt); } graphStretch = -1; mouseDragLastX = -1; mouseDragLastY = -1; firstDragX = -1; firstDragY = -1; mouseDragging = false; if (dragMode == DragMode.Resize) { ap.adjustAnnotationHeight(); } dragMode = DragMode.Undefined; if (!matrix_clicked(evt)) { ap.getScalePanel().mouseReleased(evt); } /* * isPopupTrigger is set in mouseReleased on Windows * (in mousePressed on Mac) */ if (evt.isPopupTrigger() && activeRow != -1) { showPopupMenu(evt.getY(), evt.getX()); } } /** * DOCUMENT ME! * * @param evt * DOCUMENT ME! */ @Override public void mouseEntered(MouseEvent evt) { this.mouseDragging = false; ap.getScalePanel().mouseEntered(evt); } /** * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling * with column selection on a mouse drag * * @param evt */ @Override public void mouseExited(MouseEvent evt) { ap.getScalePanel().mouseExited(evt); } /** * Action on starting or continuing a mouse drag. There are two possible * actions: * * A drag on a graph annotation is treated as column selection if it starts * with more horizontal than vertical movement, and as resize if it starts * with more vertical than horizontal movement. Once started, the drag does * not change mode. * * @param evt */ @Override public void mouseDragged(MouseEvent evt) { /* * if dragMode is Undefined: * - set to Select if dx > dy * - set to Resize if dy > dx * - do nothing if dx == dy */ final int x = evt.getX(); final int y = evt.getY(); if (dragMode == DragMode.Undefined) { int dx = Math.abs(x - mouseDragLastX); int dy = Math.abs(y - mouseDragLastY); if (graphStretch == -1 || dx > dy) { /* * mostly horizontal drag, or not a graph annotation */ dragMode = DragMode.Select; } else if (dy > dx) { /* * mostly vertical drag */ dragMode = DragMode.Resize; notJustOne = evt.isShiftDown(); /* * but could also be a matrix drag */ if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment() .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP)) { /* * dragging in a matrix */ dragMode = DragMode.MatrixSelect; firstDragX = mouseDragLastX; firstDragY = mouseDragLastY; } } } if (dragMode == DragMode.Undefined) { /* * drag is diagonal - defer deciding whether to * treat as up/down or left/right */ return; } try { if (dragMode == DragMode.Resize) { /* * resize graph annotation if mouse was dragged up or down */ int deltaY = mouseDragLastY - evt.getY(); if (deltaY != 0) { AlignmentAnnotation graphAnnotation = av.getAlignment() .getAlignmentAnnotation()[graphStretch]; int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY); if (notJustOne) { for (AlignmentAnnotation similar : av.getAlignment() .findAnnotations(null, graphAnnotation.getCalcId(), graphAnnotation.label)) { similar.graphHeight = newHeight; } } else { graphAnnotation.graphHeight = newHeight; } adjustPanelHeight(); ap.paintAlignment(false, false); } } else if (dragMode == DragMode.MatrixSelect) { /* * TODO draw a rubber band for range */ mouseDragLastX = x; mouseDragLastY = y; ap.paintAlignment(false, false); } else { /* * for mouse drag left or right, delegate to * ScalePanel to adjust the column selection */ ap.getScalePanel().mouseDragged(evt); } } finally { mouseDragLastX = x; mouseDragLastY = y; } } public void matrixSelectRange(MouseEvent evt) { /* * get geometry of drag */ int fromY = Math.min(firstDragY, evt.getY()); int toY = Math.max(firstDragY, evt.getY()); int fromX = Math.min(firstDragX, evt.getX()); int toX = Math.max(firstDragX, evt.getX()); int deltaY = toY - fromY; int deltaX = toX - fromX; int[] rowIndex = getRowIndexAndOffset(fromY, av.getAlignment().getAlignmentAnnotation()); int[] toRowIndex = getRowIndexAndOffset(toY, av.getAlignment().getAlignmentAnnotation()); if (rowIndex == null || toRowIndex == null) { jalview.bin.Console.trace("Drag out of range. needs to be clipped"); } if (rowIndex[0] != toRowIndex[0]) { jalview.bin.Console .trace("Drag went to another row. needs to be clipped"); } // rectangular selection on matrix style annotation AlignmentAnnotation cma = av.getAlignment() .getAlignmentAnnotation()[rowIndex[0]]; int lastX = getColumnForXPos(fromX); int currentX = getColumnForXPos(toX); int fromXc = Math.min(lastX, currentX); int toXc = Math.max(lastX, currentX); ContactListI forFromX = av.getContactList(cma, fromXc); ContactListI forToX = av.getContactList(cma, toXc); if (forFromX != null && forToX != null) { // FIXME will need two ContactGeometry objects when handling contact // matrices with differing numbers of rows at each // column ContactGeometry xcgeom = new ContactGeometry(forFromX, cma.graphHeight); ContactGeometry.contactInterval lastXci = xcgeom.mapFor(rowIndex[1]); ContactGeometry.contactInterval cXci = xcgeom .mapFor(rowIndex[1] + deltaY); // mark rectangular region formed by drag jalview.bin.Console.trace("Matrix Selection from last(" + fromXc + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur(" + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])"); int fr, to; fr = Math.min(lastXci.cStart, cXci.cStart); to = Math.max(lastXci.cEnd, cXci.cEnd); int[] mappedPos = forFromX.getMappedPositionsFor(fr, to); if (mappedPos != null) { jalview.bin.Console.trace("Marking " + fr + " to " + to + " mapping to sequence positions " + mappedPos[0] + " to " + mappedPos[1]); for (int pair = 0; pair < mappedPos.length; pair += 2) { for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++) // { // if (cma.sequenceRef != null) // { // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c); // av.getColumnSelection().addElement(col); // } // else { av.getColumnSelection().addElement(c - 1); } } } fr = Math.min(lastX, currentX); to = Math.max(lastX, currentX); jalview.bin.Console.trace("Marking " + fr + " to " + to); for (int c = fr; c <= to; c++) { av.getColumnSelection().addElement(c); } } } /** * Constructs the tooltip, and constructs and displays a status message, for * the current mouse position * * @param evt */ @Override public void mouseMoved(MouseEvent evt) { int yPos = evt.getY(); AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation(); int rowAndOffset[] = getRowIndexAndOffset(yPos, aa); int row = rowAndOffset[0]; if (row == -1) { this.setToolTipText(null); return; } int column = getColumnForXPos(evt.getX()); AlignmentAnnotation ann = aa[row]; if (row > -1 && ann.annotations != null && column < ann.annotations.length) { String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av, ap); setToolTipText(toolTip == null ? null : JvSwingUtils.wrapTooltip(true, toolTip)); String msg = getStatusMessage(av.getAlignment(), column, ann, rowAndOffset[1], av); ap.alignFrame.setStatus(msg); } else { this.setToolTipText(null); ap.alignFrame.setStatus(" "); } } private int getColumnForXPos(int x) { int column = (x / av.getCharWidth()) + av.getRanges().getStartRes(); column = Math.min(column, av.getRanges().getEndRes()); if (av.hasHiddenColumns()) { column = av.getAlignment().getHiddenColumns() .visibleToAbsoluteColumn(column); } return column; } /** * Answers the index in the annotations array of the visible annotation at the * given y position. This is done by adding the heights of visible annotations * until the y position has been exceeded. Answers -1 if no annotations are * visible, or the y position is below all annotations. * * @param yPos * @param aa * @return */ static int getRowIndex(int yPos, AlignmentAnnotation[] aa) { if (aa == null) { return -1; } return getRowIndexAndOffset(yPos, aa)[0]; } static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa) { int[] res = new int[2]; res[0] = -1; res[1] = 0; if (aa == null) { return res; } int row = -1; int height = 0, lheight = 0; for (int i = 0; i < aa.length; i++) { if (aa[i].visible) { lheight = height; height += aa[i].height; } if (height > yPos) { row = i; res[0] = row; res[1] = yPos - lheight; break; } } return res; } /** * Answers a tooltip for the annotation at the current mouse position, not * wrapped in <html> tags (apply if wanted). Answers null if there is no * tooltip to show. * * @param ann * @param column * @param anns * @param rowAndOffset */ static String buildToolTip(AlignmentAnnotation ann, int column, AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av, AlignmentPanel ap) { String tooltip = null; if (ann.graphGroup > -1) { StringBuilder tip = new StringBuilder(32); boolean first = true; for (int i = 0; i < anns.length; i++) { if (anns[i].graphGroup == ann.graphGroup && anns[i].annotations[column] != null) { if (!first) { tip.append("
"); } first = false; tip.append(anns[i].label); String description = anns[i].annotations[column].description; if (description != null && description.length() > 0) { tip.append(" ").append(description); } } } tooltip = first ? null : tip.toString(); } else if (column < ann.annotations.length && ann.annotations[column] != null) { tooltip = ann.annotations[column].description; } // TODO abstract tooltip generator so different implementations can be built if (ann.graph == AlignmentAnnotation.CONTACT_MAP) { if (rowAndOffset >= ann.graphHeight) { return null; } ContactListI clist = av.getContactList(ann, column); if (clist != null) { ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight); ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset); ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd); StringBuilder tooltipb = new StringBuilder(); tooltipb.append("Contact from ").append(clist.getPosition()) .append(", [").append(ci.cStart).append(" - ") .append(ci.cEnd).append("]").append("
Mean:"); Format.appendPercentage(tooltipb, (float) cr.getMean(), 2); tooltip = tooltipb.toString(); int col = ann.sequenceRef.findPosition(column); int[][] highlightPos; int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd); if (mappedPos != null) { highlightPos = new int[1 + mappedPos.length][2]; highlightPos[0] = new int[] { col, col }; for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2) { highlightPos[h][0] = ann.sequenceRef .findPosition(mappedPos[p] - 1); highlightPos[h][1] = ann.sequenceRef .findPosition(mappedPos[p + 1] - 1); } } else { highlightPos = new int[][] { new int[] { col, col } }; } ap.getStructureSelectionManager() .highlightPositionsOn(ann.sequenceRef, highlightPos, null); } } return tooltip; } /** * Constructs and returns the status bar message * * @param al * @param column * @param ann * @param rowAndOffset */ static String getStatusMessage(AlignmentI al, int column, AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av) { /* * show alignment column and annotation description if any */ StringBuilder text = new StringBuilder(32); text.append(MessageManager.getString("label.column")).append(" ") .append(column + 1); if (column < ann.annotations.length && ann.annotations[column] != null) { String description = ann.annotations[column].description; if (description != null && description.trim().length() > 0) { text.append(" ").append(description); } } /* * if the annotation is sequence-specific, show the sequence number * in the alignment, and (if not a gap) the residue and position */ SequenceI seqref = ann.sequenceRef; if (seqref != null) { int seqIndex = al.findIndex(seqref); if (seqIndex != -1) { text.append(", ").append(MessageManager.getString("label.sequence")) .append(" ").append(seqIndex + 1); char residue = seqref.getCharAt(column); if (!Comparison.isGap(residue)) { text.append(" "); String name; if (al.isNucleotide()) { name = ResidueProperties.nucleotideName .get(String.valueOf(residue)); text.append(" Nucleotide: ") .append(name != null ? name : residue); } else { name = 'X' == residue ? "X" : ('*' == residue ? "STOP" : ResidueProperties.aa2Triplet .get(String.valueOf(residue))); text.append(" Residue: ").append(name != null ? name : residue); } int residuePos = seqref.findPosition(column); text.append(" (").append(residuePos).append(")"); } } } return text.toString(); } /** * DOCUMENT ME! * * @param evt * DOCUMENT ME! */ @Override public void mouseClicked(MouseEvent evt) { // if (activeRow != -1) // { // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation(); // AlignmentAnnotation anot = aa[activeRow]; // } } // TODO mouseClicked-content and drawCursor are quite experimental! public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1, int y1) { int pady = av.getCharHeight() / 5; int charOffset = 0; graphics.setColor(Color.black); graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight()); if (av.validCharWidth) { graphics.setColor(Color.white); char s = seq.getCharAt(res); charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2; graphics.drawString(String.valueOf(s), charOffset + x1, (y1 + av.getCharHeight()) - pady); } } private volatile boolean imageFresh = false; private Rectangle visibleRect = new Rectangle(), clipBounds = new Rectangle(); /** * DOCUMENT ME! * * @param g * DOCUMENT ME! */ @Override public void paintComponent(Graphics g) { // BH: note that this method is generally recommended to // call super.paintComponent(g). Otherwise, the children of this // component will not be rendered. That is not needed here // because AnnotationPanel does not have any children. It is // just a JPanel contained in a JViewPort. computeVisibleRect(visibleRect); g.setColor(Color.white); g.fillRect(0, 0, visibleRect.width, visibleRect.height); if (image != null) { // BH 2018 optimizing generation of new Rectangle(). if (fastPaint || (visibleRect.width != (clipBounds = g .getClipBounds(clipBounds)).width) || (visibleRect.height != clipBounds.height)) { g.drawImage(image, 0, 0, this); fastPaint = false; return; } } updateFadedImageWidth(); if (imgWidth < 1) { return; } Graphics2D gg; if (image == null || imgWidth != image.getWidth(this) || image.getHeight(this) != getHeight()) { boolean tried = false; image = null; while (image == null && !tried) { try { image = new BufferedImage(imgWidth, ap.getAnnotationPanel().getHeight(), BufferedImage.TYPE_INT_RGB); tried = true; } catch (IllegalArgumentException exc) { jalview.bin.Console.errPrintln( "Serious issue with viewport geometry imgWidth requested was " + imgWidth); return; } catch (OutOfMemoryError oom) { try { System.gc(); } catch (Exception x) { } ; new OOMWarning( "Couldn't allocate memory to redraw screen. Please restart Jalview", oom); return; } } gg = (Graphics2D) image.getGraphics(); if (av.antiAlias) { gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } gg.setFont(av.getFont()); fm = gg.getFontMetrics(); gg.setColor(Color.white); gg.fillRect(0, 0, imgWidth, image.getHeight()); imageFresh = true; } else { gg = (Graphics2D) image.getGraphics(); } drawComponent(gg, av.getRanges().getStartRes(), av.getRanges().getEndRes() + 1); gg.dispose(); imageFresh = false; g.drawImage(image, 0, 0, this); } public void updateFadedImageWidth() { imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes() + 1) * av.getCharWidth(); } /** * set true to enable redraw timing debug output on stderr */ private final boolean debugRedraw = false; /** * non-Thread safe repaint * * @param horizontal * repaint with horizontal shift in alignment */ public void fastPaint(int horizontal) { if ((horizontal == 0) || image == null || av.getAlignment().getAlignmentAnnotation() == null || av.getAlignment().getAlignmentAnnotation().length < 1 || av.isCalcInProgress()) { repaint(); return; } int sr = av.getRanges().getStartRes(); int er = av.getRanges().getEndRes() + 1; int transX = 0; Graphics2D gg = (Graphics2D) image.getGraphics(); if (imgWidth > Math.abs(horizontal * av.getCharWidth())) { // scroll is less than imgWidth away so can re-use buffered graphics gg.copyArea(0, 0, imgWidth, getHeight(), -horizontal * av.getCharWidth(), 0); if (horizontal > 0) // scrollbar pulled right, image to the left { transX = (er - sr - horizontal) * av.getCharWidth(); sr = er - horizontal; } else if (horizontal < 0) { er = sr - horizontal; } } gg.translate(transX, 0); drawComponent(gg, sr, er); gg.translate(-transX, 0); gg.dispose(); fastPaint = true; // Call repaint on alignment panel so that repaints from other alignment // panel components can be aggregated. Otherwise performance of the overview // window and others may be adversely affected. av.getAlignPanel().repaint(); } private volatile boolean lastImageGood = false; /** * DOCUMENT ME! * * @param g * DOCUMENT ME! * @param startRes * DOCUMENT ME! * @param endRes * DOCUMENT ME! */ public void drawComponent(Graphics g, int startRes, int endRes) { BufferedImage oldFaded = fadedImage; if (av.isCalcInProgress()) { if (image == null) { lastImageGood = false; return; } // We'll keep a record of the old image, // and draw a faded image until the calculation // has completed if (lastImageGood && (fadedImage == null || fadedImage.getWidth() != imgWidth || fadedImage.getHeight() != image.getHeight())) { // jalview.bin.Console.errPrintln("redraw faded image // ("+(fadedImage==null ? // "null image" : "") + " lastGood="+lastImageGood+")"); fadedImage = new BufferedImage(imgWidth, image.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics(); fadedG.setColor(Color.white); fadedG.fillRect(0, 0, imgWidth, image.getHeight()); fadedG.setComposite( AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f)); fadedG.drawImage(image, 0, 0, this); } // make sure we don't overwrite the last good faded image until all // calculations have finished lastImageGood = false; } else { if (fadedImage != null) { oldFaded = fadedImage; } fadedImage = null; } g.setColor(Color.white); g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight()); g.setFont(av.getFont()); if (fm == null) { fm = g.getFontMetrics(); } if ((av.getAlignment().getAlignmentAnnotation() == null) || (av.getAlignment().getAlignmentAnnotation().length < 1)) { g.setColor(Color.white); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(Color.black); if (av.validCharWidth) { g.drawString(MessageManager .getString("label.alignment_has_no_annotations"), 20, 15); } return; } lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes, endRes); if (!lastImageGood && fadedImage == null) { fadedImage = oldFaded; } if (dragMode == DragMode.MatrixSelect) { g.setColor(Color.yellow); g.drawRect(Math.min(firstDragX, mouseDragLastX), Math.min(firstDragY, mouseDragLastY), Math.max(firstDragX, mouseDragLastX) - Math.min(firstDragX, mouseDragLastX), Math.max(firstDragY, mouseDragLastY) - Math.min(firstDragY, mouseDragLastY)); } } @Override public FontMetrics getFontMetrics() { return fm; } @Override public Image getFadedImage() { return fadedImage; } @Override public int getFadedImageWidth() { updateFadedImageWidth(); return imgWidth; } private int[] bounds = new int[2]; @Override public int[] getVisibleVRange() { if (ap != null && ap.getAlabels() != null) { int sOffset = -ap.getAlabels().getScrollOffset(); int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight(); bounds[0] = sOffset; bounds[1] = visHeight; return bounds; } else { return null; } } /** * Try to ensure any references held are nulled */ public void dispose() { av = null; ap = null; image = null; fadedImage = null; // gg = null; _mwl = null; /* * I created the renderer so I will dispose of it */ if (renderer != null) { renderer.dispose(); } } @Override public void propertyChange(PropertyChangeEvent evt) { // Respond to viewport range changes (e.g. alignment panel was scrolled) // Both scrolling and resizing change viewport ranges: scrolling changes // both start and end points, but resize only changes end values. // Here we only want to fastpaint on a scroll, with resize using a normal // paint, so scroll events are identified as changes to the horizontal or // vertical start value. if (evt.getPropertyName().equals(ViewportRanges.STARTRES)) { fastPaint((int) evt.getNewValue() - (int) evt.getOldValue()); } else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ)) { fastPaint(((int[]) evt.getNewValue())[0] - ((int[]) evt.getOldValue())[0]); } else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT)) { repaint(); } } /** * computes the visible height of the annotation panel * * @param adjustPanelHeight * - when false, just adjust existing height according to other * windows * @param annotationHeight * @return height to use for the ScrollerPreferredVisibleSize */ public int adjustForAlignFrame(boolean adjustPanelHeight, int annotationHeight) { /* * Estimate available height in the AlignFrame for alignment + * annotations. Deduct an estimate for title bar, menu bar, scale panel, * hscroll, status bar, insets. */ int stuff = (ap.getViewName() != null ? 30 : 0) + (Platform.isAMacAndNotJS() ? 120 : 140); int availableHeight = ap.alignFrame.getHeight() - stuff; int rowHeight = av.getCharHeight(); if (adjustPanelHeight) { int alignmentHeight = rowHeight * av.getAlignment().getHeight(); /* * If not enough vertical space, maximize annotation height while keeping * at least two rows of alignment visible */ if (annotationHeight + alignmentHeight > availableHeight) { annotationHeight = Math.min(annotationHeight, availableHeight - 2 * rowHeight); } } else { // maintain same window layout whilst updating sliders annotationHeight = Math.min(ap.annotationScroller.getSize().height, availableHeight - 2 * rowHeight); } return annotationHeight; } }