//----------------------------------------------------------------------------// // // // R u b b e r // // // //----------------------------------------------------------------------------// // <editor-fold defaultstate="collapsed" desc="hdr"> // // Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. // // This software is released under the GNU General Public License. // // Goto http://kenai.com/projects/audiveris to report bugs or suggestions. // //----------------------------------------------------------------------------// // </editor-fold> package omr.ui.view; import omr.constant.Constant; import omr.constant.ConstantSet; import static omr.selection.MouseMovement.*; import omr.ui.Colors; import static omr.ui.util.UIPredicates.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.BasicStroke; import java.awt.Container; import java.awt.Cursor; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.Stroke; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.util.concurrent.atomic.AtomicInteger; import javax.swing.JComponent; import javax.swing.SwingUtilities; import javax.swing.event.MouseInputAdapter; /** * Class {@code Rubber} keeps track of nothing more than a rectangle, * to define an area of interest. * * The rectangle can be degenerated to a simple point, when both its width and * height are zero. Moreover, the display can be moved or resized * (see the precise triggers below). * * <p> The rubber data is rendered as a 'rubber', so the name, using a * rectangle, reflecting the dragged position of the mouse. * * <p> Rubber data is meant to be modified by the user when he presses * and/or drags the mouse. But it can also be modified programmatically, * thanks to the {@link #resetOrigin} and {@link #resetRectangle} methods. * * <p> Basic mouse handling is provided in the following way : <ul> * * <li> Define the point of interest. Default trigger is to click with the * <b>Left</b> button. </li> * * <li> Define the rectangle of interest. Default trigger is to keep * <b>Shift</b> pressed when mouse is moved. </li> * * <li> Zoom the display to the area delimited by the rubber. Default * trigger is <b>Shift + Control</b> when mouse is released. </li> * * <li> Drag the component itself. Default trigger is when both <b>Left + * Right</b> buttons are dragged. </li> </ul> * * <p/> * Note: Actual triggers are defined by protected predicate methods * that can be redefined in a subclass. * <p/> * * <p> Mouse Events are handled in the following way: <ul> * * <li> <b>Low-level events</b> originate from a JComponent, where the * Rubber is registered as a MouseListener and a MouseMotionListener. The * component can be linked by the Rubber constructor, or later by using the * {@link #connectComponent} method. Rubber is then called on its * <i>mouseDragged, mousePressed, mouseReleased</i> methods. * * <li> <b>High-level events</b>, as computed by Rubber from low-level mouse * events, are forwarded to a connected {@link MouseMonitor} if any, which is * then called on its <i>pointSelected, pointAdded, contextSelected, * rectangleSelected, rectangleZoomed</i> methods. Generally, this * MouseMonitor is the originating JComponent, but this is not mandatory. * </ul> * * <p> The Rubber can be linked to a {@link Zoom} to cope with display * factor of the related component, but this is not mandatory: If no zoom * is connected, a display factor of 1.0 is assumed. * * @author Hervé Bitteur */ public class Rubber extends MouseInputAdapter { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(Rubber.class); private static AtomicInteger globalId = new AtomicInteger(0); /** To handle zoom through mouse wheel */ private static final double base = 2; private static final double intervals = 5; private static final double factor = Math.pow(base, 1d / intervals); //~ Instance fields -------------------------------------------------------- /** View from which the rubber will receive physical mouse events */ protected JComponent component; /** The controller to be notified about mouse actions */ protected MouseMonitor mouseMonitor; /** Related zoom if any */ protected Zoom zoom; // The raw (zoomed) rubber rectangle, with x & y as the original point // where mouse was pressed, with possibly negative width & height, and // may be going past the component borders private Rectangle rawRect; // The normalized unzoomed rubber rectangle, inside the component, with // x & y at the top left and positive width & height private Rectangle rect; // To ease debugging private final int id; //~ Constructors ----------------------------------------------------------- //--------// // Rubber // //--------// /** * Create a rubber, with no predefined parameter (zoom, component) * which are meant to be provided later. * * @see #setZoom */ public Rubber () { id = globalId.addAndGet(1); } //--------// // Rubber // //--------// /** * Create a rubber, with a linked zoom, the related component being * linked later * * @param zoom the related zoom */ public Rubber (Zoom zoom) { id = globalId.addAndGet(1); setZoom(zoom); } //--------// // Rubber // //--------// /** * Create a rubber linked to a component, with a related display zoom. * * @param component the related component * @param zoom the zoom entity to handle the display zoom */ public Rubber (JComponent component, Zoom zoom) { id = globalId.addAndGet(1); connectComponent(component); setZoom(zoom); } //~ Methods ---------------------------------------------------------------- //------------------// // connectComponent // //------------------// /** * Actually register the rubber as the mouse listener for the provided * component. * * @param component the related component */ public void connectComponent (JComponent component) { // Clean up if needed disconnectComponent(this.component); // Remember the related component (to get visible rect, etc ...) this.component = component; // To be notified of mouse clicks component.removeMouseListener(this); // No multiple notifications component.addMouseListener(this); // To be notified of mouse mouvements component.removeMouseMotionListener(this); // No multiple notifs component.addMouseMotionListener(this); // To be notified of mouse wheel mouvements component.removeMouseWheelListener(this); // No multiple notifs component.addMouseWheelListener(this); } //---------------------// // disconnectComponent // //---------------------// /** * Disconnect the provided component * * @param component the component to disconnect */ public void disconnectComponent (JComponent component) { if (component != null) { component.removeMouseListener(this); component.removeMouseMotionListener(this); component.removeMouseWheelListener(this); } } //-----------// // getCenter // //-----------// /** * Return the center of the model rectangle as defined by the * rubber. This is a dezoomed point, should the component have a * related zoom. * * @return the model center point */ public Point getCenter () { Point pt = null; if (rect != null) { pt = new Point( rect.x + (rect.width / 2), rect.y + (rect.height / 2)); } return pt; } //--------------// // getRectangle // //--------------// /** * Return the model rectangle defined by the rubber. This is a * dezoomed rectangle, should the component have a related zoom. * * @return the model rectangle */ public Rectangle getRectangle () { return rect; } //--------------// // mouseDragged // //--------------// /** * Called when the mouse is dragged. * * @param e the mouse event */ @Override public void mouseDragged (MouseEvent e) { if (mouseMonitor == null) { return; } setCursor(e); if (isDragWanted(e)) { final Rectangle vr = component.getVisibleRect(); vr.setBounds( (vr.x + rawRect.x) - e.getX(), (vr.y + rawRect.y) - e.getY(), vr.width, vr.height); SwingUtilities.invokeLater( new Runnable() { @Override public void run () { component.scrollRectToVisible(vr); } }); } else if (isRubberWanted(e)) { updateSize(e); mouseMonitor.rectangleSelected(rect, DRAGGING); } else { // Behavior equivalent to simple selection reset(e); if (isAdditionWanted(e)) { if (isContextWanted(e)) { mouseMonitor.contextAdded(getCenter(), DRAGGING); } else { mouseMonitor.pointAdded(getCenter(), DRAGGING); } } else { if (isContextWanted(e)) { mouseMonitor.contextSelected(getCenter(), DRAGGING); } else { mouseMonitor.pointSelected(getCenter(), DRAGGING); } } } } //--------------// // mousePressed // //--------------// /** * Called when the mouse is pressed. * * @param e the mouse event */ @Override public void mousePressed (MouseEvent e) { reset(e); if (mouseMonitor == null) { return; } setCursor(e); if (!isDragWanted(e)) { if (isAdditionWanted(e)) { if (isContextWanted(e)) { mouseMonitor.contextAdded(getCenter(), PRESSING); } else { mouseMonitor.pointAdded(getCenter(), PRESSING); } } else { if (isContextWanted(e)) { mouseMonitor.contextSelected(getCenter(), PRESSING); } else { mouseMonitor.pointSelected(getCenter(), PRESSING); } } } } //---------------// // mouseReleased // //---------------// /** * Called when the mouse is released. * * @param e the mouse event */ @Override public void mouseReleased (MouseEvent e) { if (mouseMonitor == null) { return; } if (isRezoomWanted(e)) { updateSize(e); mouseMonitor.rectangleZoomed(rect, RELEASING); } else if (isDragWanted(e)) { Rectangle vr = component.getVisibleRect(); rawRect.setBounds( vr.x + (vr.width / 2), vr.y + (vr.height / 2), 0, 0); normalize(); } else if (isAdditionWanted(e)) { if (isContextWanted(e)) { mouseMonitor.contextAdded(getCenter(), RELEASING); } else { mouseMonitor.pointAdded(getCenter(), RELEASING); } } else if (rect != null) { if (isContextWanted(e)) { mouseMonitor.contextSelected(getCenter(), RELEASING); } else { if ((rect.width != 0) && (rect.height != 0)) { updateSize(e); mouseMonitor.rectangleSelected(rect, RELEASING); } else { mouseMonitor.pointSelected(getCenter(), RELEASING); } } } e.getComponent() .setCursor(Cursor.getDefaultCursor()); } //-----------------// // mouseWheelMoved // //-----------------// /** * Called when the mouse wheel is moved. * If CTRL key is down, modify current zoom ratio accordingly, otherwise * forward the wheel event to proper container (JScrollPane usually). * * @param e the mouse wheel event */ @Override public void mouseWheelMoved (MouseWheelEvent e) { // CTRL is down? if (e.isControlDown()) { double ratio = zoom.getRatio(); if (e.getWheelRotation() > 0) { ratio /= factor; } else { ratio *= factor; } zoom.setRatio(ratio); } else { // Forward event to some container of the component? Container container = component.getParent(); while (container != null) { if (container instanceof JComponent) { JComponent comp = (JComponent) container; MouseWheelListener[] listeners = comp.getMouseWheelListeners(); if (listeners.length > 0) { for (MouseWheelListener listener : listeners) { listener.mouseWheelMoved(e); } return; } } container = container.getParent(); } } } //--------// // render // //--------// /** * Render the rubber rectangle. This should be called late, typically * when everything else has already been painted. Note that this needs an * unscaled graphics, since we want to draw the rubber lines (vertical * and horizontal) perfectly on top of image pixels. * * @param unscaledGraphics the graphic context (not transformed!) */ public void render (Graphics unscaledGraphics) { if (rect != null) { Graphics2D g = (Graphics2D) unscaledGraphics.create(); Rectangle r = new Rectangle(rect); if (zoom != null) { zoom.scale(r); } // Is this is a true rectangle ? if ((r.width != 0) || (r.height != 0)) { g.setColor(Colors.RUBBER_RECT); g.drawRect(r.x, r.y, r.width, r.height); } // Draw horizontal & vertical rules (point or rectangle) g.setColor(Colors.RUBBER_RULE); int x = scaled(rect.x + (rect.width / 2)); int y = scaled(rect.y + (rect.height / 2)); float pixelSize = scaled(1); if (pixelSize < 1) { pixelSize = 1f; } else { int halfPen = (int) Math.rint(pixelSize / 2); x += halfPen; y += halfPen; } Stroke s = new BasicStroke(pixelSize); g.setStroke(s); if (constants.displayCross.getValue()) { // Draw just a small cross int legLength = constants.crossLegLength.getValue(); g.drawLine(x, y - legLength, x, y + legLength); // Vertical g.drawLine(x - legLength, y, x + legLength, y); // Horizontal } else { // Draw full vertical & horizontal lines Rectangle bounds = component.getBounds(null); g.drawLine(x, 0, x, bounds.height); // Vertical g.drawLine(0, y, bounds.width, y); // Horizontal } g.dispose(); } } //-------------// // resetOrigin // //-------------// /** * Reset the rubber information, programmatically. * * @param x the new center abscissa * @param y the new center ordinate */ public void resetOrigin (int x, int y) { if (rect == null) { rect = new Rectangle(x, y, 0, 0); } else { rect.setBounds(x, y, 0, 0); } if (component != null) { component.repaint(); } } //----------------// // resetRectangle // //----------------// /** * Reset the rubber information, programmatically. * * @param newRect the new rectangle, which can be null */ public void resetRectangle (Rectangle newRect) { if (newRect != null) { if (rect == null) { rect = new Rectangle(newRect); } else { rect.setBounds(newRect); } } else { rect = null; } // if (component != null) { // component.repaint(); // } } //-----------------// // setMouseMonitor // //-----------------// /** * Define the interface of callback to be notified of mouse events * * @param mouseMonitor the entity to be notified */ public void setMouseMonitor (MouseMonitor mouseMonitor) { this.mouseMonitor = mouseMonitor; } //---------// // setZoom // //---------// /** * Allows to specify that a zoom is attached to the displayed * component, and thus the reported rectangle or center must be * dezoomed on the fly. * * @param zoom the component related zoom */ public void setZoom (Zoom zoom) { this.zoom = zoom; } //----------// // toString // //----------// @Override public String toString () { return "{Rubber #" + id + " " + rect + "}"; } //-- private access --------------------------------------------------- //-----------// // normalize // //-----------// private void normalize () { if (rect == null) { rect = new Rectangle( unscaled(rawRect.x), unscaled(rawRect.y), unscaled(rawRect.width), unscaled(rawRect.height)); } else { rect.setBounds( unscaled(rawRect.x), unscaled(rawRect.y), unscaled(rawRect.width), unscaled(rawRect.height)); } // The x & y are the original coordinates when mouse began // But width & height may be negative // Make the width and height positive, if necessary. if (rect.width < 0) { rect.width = -rect.width; rect.x = rect.x - rect.width + 1; if (rect.x < 0) { rect.width += rect.x; rect.x = 0; } } if (rect.height < 0) { rect.height = -rect.height; rect.y = rect.y - rect.height + 1; if (rect.y < 0) { rect.height += rect.y; rect.y = 0; } } // The origin must stay within the component final int compWidth = unscaled(component.getWidth()); final int compHeight = unscaled(component.getHeight()); if (rect.x < 0) { rect.x = 0; } else if (rect.x > compWidth) { rect.x = compWidth; } if (rect.y < 0) { rect.y = 0; } else if (rect.y > compHeight) { rect.y = compHeight; } // The rectangle shouldn't extend past the drawing area. if ((rect.x + rect.width) > compWidth) { rect.width = compWidth - rect.x; } if ((rect.y + rect.height) > compHeight) { rect.height = compHeight - rect.y; } } //-------// // reset // //-------// private void reset (MouseEvent e) { if (rawRect == null) { rawRect = new Rectangle(e.getX(), e.getY(), 0, 0); } else { rawRect.setBounds(e.getX(), e.getY(), 0, 0); } normalize(); resetOrigin(rect.x, rect.y); } //--------// // scaled // //--------// private int scaled (int val) { if (zoom != null) { return zoom.scaled(val); } else { return val; } } //-----------// // setCursor // //-----------// private void setCursor (MouseEvent e) { if (isDragWanted(e)) { e.getComponent() .setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); } else if (isAdditionWanted(e)) { e.getComponent() .setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); } else if (isContextWanted(e)) { e.getComponent() .setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } else if (isRubberWanted(e)) { e.getComponent() .setCursor(Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR)); } else if (isRezoomWanted(e)) { e.getComponent() .setCursor(Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR)); } else { e.getComponent() .setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } } //----------// // unscaled // //----------// private int unscaled (int val) { if (zoom != null) { return zoom.unscaled(val); } else { return val; } } //------------// // updateSize // //------------// private void updateSize (MouseEvent e) { // Update width and height of rawRect rawRect.setSize(e.getX() - rawRect.x, e.getY() - rawRect.y); normalize(); // Repaint the component (with the resized rectangle) component.repaint(); } //~ Inner Classes ---------------------------------------------------------- //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Constant.Boolean displayCross = new Constant.Boolean( true, "Should we display just a cross for rubber (or whole lines)"); Constant.Integer crossLegLength = new Constant.Integer( "Pixels", 100, "Length for each leg of the rubber cross"); } }