/* * Jalview - A Sequence Alignment Editor and Viewer (Version 2.9.0b2) * Copyright (C) 2015 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 jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.Annotation; import jalview.datamodel.ColumnSelection; import jalview.datamodel.SequenceI; import jalview.renderer.AnnotationRenderer; import jalview.renderer.AwtRenderPanelI; import jalview.util.MessageManager; 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 javax.swing.JColorChooser; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.Scrollable; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; /** * 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 { 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; Graphics2D gg; public FontMetrics fm; public int imgWidth = 0; boolean fastPaint = false; // Used For mouse Dragging and resizing graphs int graphStretch = -1; int graphStretchY = -1; int min; // used by mouseDragged to see if user int max; // used by mouseDragged to see if user boolean mouseDragging = false; boolean MAC = false; // for editing cursor int cursorX = 0; int cursorY = 0; public final AnnotationRenderer renderer; private MouseWheelListener[] _mwl; /** * Creates a new AnnotationPanel object. * * @param ap * DOCUMENT ME! */ public AnnotationPanel(AlignmentPanel ap) { MAC = jalview.util.Platform.isAMac(); 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(); } public AnnotationPanel(AlignViewport av) { this.av = av; renderer = new AnnotationRenderer(); } @Override public void mouseWheelMoved(MouseWheelEvent e) { if (e.isShiftDown()) { e.consume(); if (e.getWheelRotation() > 0) { ap.scrollRight(true); } else { ap.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() { return getPreferredSize(); } @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; } if (evt.getActionCommand().equals(REMOVE)) { for (int i = 0; i < av.getColumnSelection().size(); i++) { anot[av.getColumnSelection().columnAt(i)] = null; } } else if (evt.getActionCommand().equals(LABEL)) { String exMesg = collectAnnotVals(anot, av.getColumnSelection(), LABEL); String label = JOptionPane.showInputDialog(this, MessageManager.getString("label.enter_label"), exMesg); if (label == null) { return; } if ((label.length() > 0) && !aa[activeRow].hasText) { aa[activeRow].hasText = true; } for (int i = 0; i < av.getColumnSelection().size(); i++) { int index = av.getColumnSelection().columnAt(i); if (!av.getColumnSelection().isVisible(index)) { continue; } if (anot[index] == null) { anot[index] = new Annotation(label, "", ' ', 0); // TODO: verify that // null exceptions // aren't raised // elsewhere. } else { anot[index].displayCharacter = label; } } } else if (evt.getActionCommand().equals(COLOUR)) { Color col = JColorChooser.showDialog(this, MessageManager.getString("label.select_foreground_colour"), Color.black); for (int i = 0; i < av.getColumnSelection().size(); i++) { int index = av.getColumnSelection().columnAt(i); if (!av.getColumnSelection().isVisible(index)) { continue; } if (anot[index] == null) { anot[index] = new Annotation("", "", ' ', 0); } anot[index].colour = col; } } else // HELIX OR SHEET { char type = 0; String symbol = "\u03B1"; if (evt.getActionCommand().equals(HELIX)) { type = 'H'; } else if (evt.getActionCommand().equals(SHEET)) { type = 'E'; symbol = "\u03B2"; } // Added by LML to color stems else if (evt.getActionCommand().equals(STEM)) { type = 'S'; symbol = "\u03C3"; } if (!aa[activeRow].hasIcons) { aa[activeRow].hasIcons = true; } String label = JOptionPane.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 (evt.getActionCommand().equals(STEM)) { aa[activeRow].showAllColLabels = true; } } for (int i = 0; i < av.getColumnSelection().size(); i++) { int index = av.getColumnSelection().columnAt(i); if (!av.getColumnSelection().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(); adjustPanelHeight(); repaint(); return; } private String collectAnnotVals(Annotation[] anot, ColumnSelection columnSelection, String label2) { String collatedInput = ""; String last = ""; ColumnSelection viscols = av.getColumnSelection(); // TODO: refactor and save av.getColumnSelection for efficiency for (int i = 0; i < columnSelection.size(); i++) { int index = columnSelection.columnAt(i); // always check for current display state - just in case if (!viscols.isVisible(index)) { continue; } String tlabel = null; if (anot[index] != null) { // LML added stem code if (label2.equals(HELIX) || label2.equals(SHEET) || label2.equals(STEM) || label2.equals(LABEL)) { tlabel = anot[index].description; if (tlabel == null || tlabel.length() < 1) { if (label2.equals(HELIX) || label2.equals(SHEET) || label2.equals(STEM)) { tlabel = "" + anot[index].secondaryStructure; } else { tlabel = "" + anot[index].displayCharacter; } } } if (tlabel != null && !tlabel.equals(last)) { if (last.length() > 0) { collatedInput += " "; } collatedInput += tlabel; } } } return collatedInput; } /** * DOCUMENT ME! * * @param evt * DOCUMENT ME! */ @Override public void mousePressed(MouseEvent evt) { AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation(); if (aa == null) { return; } int height = 0; activeRow = -1; for (int i = 0; i < aa.length; i++) { if (aa[i].visible) { height += aa[i].height; } if (evt.getY() < height) { if (aa[i].editable) { activeRow = i; } else if (aa[i].graph > 0) { // Stretch Graph graphStretch = i; graphStretchY = evt.getY(); } break; } } if (SwingUtilities.isRightMouseButton(evt) && activeRow != -1) { if (av.getColumnSelection() == null) { return; } JPopupMenu pop = new JPopupMenu( MessageManager.getString("label.structure_type")); JMenuItem item; /* * Just display the needed structure options */ if (av.getAlignment().isNucleotide() == true) { 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, evt.getX(), evt.getY()); return; } if (aa == null) { return; } ap.getScalePanel().mousePressed(evt); } /** * DOCUMENT ME! * * @param evt * DOCUMENT ME! */ @Override public void mouseReleased(MouseEvent evt) { graphStretch = -1; graphStretchY = -1; mouseDragging = false; ap.getScalePanel().mouseReleased(evt); } /** * DOCUMENT ME! * * @param evt * DOCUMENT ME! */ @Override public void mouseEntered(MouseEvent evt) { ap.getScalePanel().mouseEntered(evt); } /** * DOCUMENT ME! * * @param evt * DOCUMENT ME! */ @Override public void mouseExited(MouseEvent evt) { ap.getScalePanel().mouseExited(evt); } /** * DOCUMENT ME! * * @param evt * DOCUMENT ME! */ @Override public void mouseDragged(MouseEvent evt) { if (graphStretch > -1) { av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY - evt.getY(); if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0) { av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0; } graphStretchY = evt.getY(); adjustPanelHeight(); ap.paintAlignment(true); } else { ap.getScalePanel().mouseDragged(evt); } } /** * DOCUMENT ME! * * @param evt * DOCUMENT ME! */ @Override public void mouseMoved(MouseEvent evt) { AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation(); if (aa == null) { this.setToolTipText(null); return; } int row = -1; int height = 0; for (int i = 0; i < aa.length; i++) { if (aa[i].visible) { height += aa[i].height; } if (evt.getY() < height) { row = i; break; } } if (row == -1) { this.setToolTipText(null); return; } int res = (evt.getX() / av.getCharWidth()) + av.getStartRes(); if (av.hasHiddenColumns()) { res = av.getColumnSelection().adjustForHiddenColumns(res); } if (row > -1 && aa[row].annotations != null && res < aa[row].annotations.length) { if (aa[row].graphGroup > -1) { StringBuffer tip = new StringBuffer(""); for (int gg = 0; gg < aa.length; gg++) { if (aa[gg].graphGroup == aa[row].graphGroup && aa[gg].annotations[res] != null) { tip.append(aa[gg].label + " " + aa[gg].annotations[res].description + "
"); } } if (tip.length() != 6) { tip.setLength(tip.length() - 4); this.setToolTipText(tip.toString() + ""); } } else if (aa[row].annotations[res] != null && aa[row].annotations[res].description != null && aa[row].annotations[res].description.length() > 0) { this.setToolTipText(JvSwingUtils.wrapTooltip(true, aa[row].annotations[res].description)); } else { // clear the tooltip. this.setToolTipText(null); } if (aa[row].annotations[res] != null) { StringBuffer text = new StringBuffer("Sequence position " + (res + 1)); if (aa[row].annotations[res].description != null) { text.append(" " + aa[row].annotations[res].description); } ap.alignFrame.statusBar.setText(text.toString()); } } else { this.setToolTipText(null); } } /** * 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; /** * DOCUMENT ME! * * @param g * DOCUMENT ME! */ @Override public void paintComponent(Graphics g) { g.setColor(Color.white); g.fillRect(0, 0, getWidth(), getHeight()); if (image != null) { if (fastPaint || (getVisibleRect().width != g.getClipBounds().width) || (getVisibleRect().height != g.getClipBounds().height)) { g.drawImage(image, 0, 0, this); fastPaint = false; return; } } imgWidth = (av.endRes - av.startRes + 1) * av.getCharWidth(); if (imgWidth < 1) { return; } if (image == null || imgWidth != image.getWidth(this) || image.getHeight(this) != getHeight()) { try { image = new BufferedImage(imgWidth, ap.getAnnotationPanel() .getHeight(), BufferedImage.TYPE_INT_RGB); } 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; } drawComponent(gg, av.startRes, av.endRes + 1); imageFresh = false; g.drawImage(image, 0, 0, this); } /** * 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) || gg == null || av.getAlignment().getAlignmentAnnotation() == null || av.getAlignment().getAlignmentAnnotation().length < 1 || av.isCalcInProgress()) { repaint(); return; } long stime = System.currentTimeMillis(); gg.copyArea(0, 0, imgWidth, getHeight(), -horizontal * av.getCharWidth(), 0); long mtime = System.currentTimeMillis(); int sr = av.startRes; int er = av.endRes + 1; int transX = 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); long dtime = System.currentTimeMillis(); fastPaint = true; repaint(); long rtime = System.currentTimeMillis(); if (debugRedraw) { System.err.println("Scroll:\t" + horizontal + "\tCopyArea:\t" + (mtime - stime) + "\tDraw component:\t" + (dtime - mtime) + "\tRepaint call:\t" + (rtime - dtime)); } } 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())) { // System.err.println("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; } } @Override public FontMetrics getFontMetrics() { return fm; } @Override public Image getFadedImage() { return fadedImage; } @Override public int getFadedImageWidth() { 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; } } }