//----------------------------------------------------------------------------// // // // R u b b e r P a n e l // // // //----------------------------------------------------------------------------// // <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.ConstantSet; import omr.selection.LocationEvent; import omr.selection.MouseMovement; import omr.selection.SelectionHint; import static omr.selection.SelectionHint.*; import omr.selection.SelectionService; import omr.selection.UserEvent; import omr.ui.PixelCount; import omr.util.ClassUtil; import org.bushe.swing.event.EventSubscriber; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.util.ConcurrentModificationException; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; /** * Class {@code RubberPanel} is a combination of two linked entities: * a {@link Zoom} and a {@link Rubber}. * * <p>Its <i>paintComponent</i> method is declared final to ensure that the * rendering is done in proper sequence, with the rubber rectangle rendered at * the end on top of any other stuff. Any specific rendering required by a * subclass is performed by overriding the {@link #render} method. * * <p>The Zoom instance and the Rubber instance can be provided separately, * after this RubberPanel has been constructed. This is meant for cases * where the same Zoom and Rubber instances are shared by several views, as in * the {@link omr.sheet.ui.SheetAssembly} example. * * <p>When using this class, we have to provide our own Zoom instance, either at * contruction time by using the proper constructor or later by using the {@link * #setZoom} method. The class then registers itself as an observer of the * Zoom instance, to be notified when the zoom ratio is modified. * * @author Hervé Bitteur */ public class RubberPanel extends JPanel implements ChangeListener, MouseMonitor, EventSubscriber<UserEvent> { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger( RubberPanel.class); //~ Instance fields -------------------------------------------------------- /** Current display zoom, if any */ protected Zoom zoom; /** Rubber band mouse handling, if any */ protected Rubber rubber; /** Model size (independent of display zoom) */ protected Dimension modelSize; /** Location Service if any (for Location event) */ protected SelectionService locationService; //~ Constructors ----------------------------------------------------------- //-------------// // RubberPanel // //-------------// /** * Create a bare RubberPanel, assuming zoom and rubber will be * assigned later. */ public RubberPanel () { logger.debug("new RubberPanel"); } //-------------// // RubberPanel // //-------------// /** * Create a RubberPanel, with the specified Rubber to interact via the * mouse, and a specified Zoom instance * * @param zoom related display zoom * @param rubber the rubber instance to be linked to this panel */ public RubberPanel (Zoom zoom, Rubber rubber) { setZoom(zoom); setRubber(rubber); logger.debug("new RubberPanel zoom={} rubber={}", zoom, rubber); } //~ Methods ---------------------------------------------------------------- //-----------// // setRubber // //-----------// /** * Allows to provide the rubber instance, only after this RubberPanel * has been built. This can be used to solve circular elaboration problems. * * @param rubber the rubber instance to be used */ public final void setRubber (Rubber rubber) { this.rubber = rubber; rubber.setZoom(zoom); rubber.connectComponent(this); rubber.setMouseMonitor(this); } //---------// // setZoom // //---------// /** * Assign a zoom to this panel * * @param zoom the zoom assigned */ public final void setZoom (final Zoom zoom) { // Clean up if needed unsetZoom(this.zoom); this.zoom = zoom; if (zoom != null) { // Add a listener on this zoom zoom.addChangeListener(this); } } //--------------// // contextAdded // //--------------// @Override public void contextAdded (Point pt, MouseMovement movement) { // Nothing by default } //-----------------// // contextSelected // //-----------------// @Override public void contextSelected (Point pt, MouseMovement movement) { // Nothing by default } //--------------// // getModelSize // //--------------// /** * Report the size of the model object, that is the unscaled size. * * @return the original size */ public Dimension getModelSize () { if (modelSize != null) { return new Dimension(modelSize); } else { return null; } } //----------------// // getPanelCenter // //----------------// /** * Retrieve the current center of the display, and report its * corresponding model location. * * @return the unscaled coordinates of the panel center */ public Point getPanelCenter () { Rectangle vr = getVisibleRect(); Point pt = new Point( zoom.unscaled(vr.x + (vr.width / 2)), zoom.unscaled(vr.y + (vr.height / 2))); logger.debug("getPanelCenter={}", pt); return pt; } //----------------------// // getSelectedRectangle // //----------------------// /** * Report the rectangle currently selected, or null * * @return the absolute rectangle selected */ public Rectangle getSelectedRectangle () { if (locationService == null) { logger.error("No locationService for {}", this); return null; } LocationEvent locationEvent = (LocationEvent) locationService.getLastEvent( LocationEvent.class); return (locationEvent != null) ? locationEvent.getData() : null; } //---------// // getZoom // //---------// /** * Return the current zoom * * @return the used zoom */ public Zoom getZoom () { return zoom; } //---------// // onEvent // //---------// /** * Notification of a location selection (pixel or score) * * @param event the location event */ @Override public void onEvent (UserEvent event) { try { // Ignore RELEASING if (event.movement == MouseMovement.RELEASING) { return; } logger.debug("{} onEvent {}", getClass().getName(), event); if (event instanceof LocationEvent) { // Location => move view focus on this location w/ markers LocationEvent locationEvent = (LocationEvent) event; showFocusLocation(locationEvent.getData(), false); } } catch (Exception ex) { logger.warn(getClass().getName() + " onEvent error", ex); } } //------------// // pointAdded // //------------// @Override public void pointAdded (Point pt, MouseMovement movement) { setFocusLocation(new Rectangle(pt), movement, LOCATION_ADD); } //---------------// // pointSelected // //---------------// @Override public void pointSelected (Point pt, MouseMovement movement) { setFocusLocation(new Rectangle(pt), movement, LOCATION_INIT); } //---------// // publish // //---------// public void publish (LocationEvent locationEvent) { locationService.publish(locationEvent); } //-------------------// // rectangleSelected // //-------------------// @Override public void rectangleSelected (Rectangle rect, MouseMovement movement) { setFocusLocation(rect, movement, LOCATION_INIT); } //-----------------// // rectangleZoomed // //-----------------// @Override public void rectangleZoomed (final Rectangle rect, MouseMovement movement) { logger.debug("{} rectangleZoomed {}", getClass().getName(), rect); if (rect != null) { // First focus on center of the specified rectangle setFocusLocation(rect, movement, LOCATION_INIT); showFocusLocation(rect, true); // Then, adjust zoom ratio to fit the rectangle size SwingUtilities.invokeLater( new Runnable() { @Override public void run () { Rectangle vr = getVisibleRect(); double zoomX = (double) vr.width / (double) rect.width; double zoomY = (double) vr.height / (double) rect.height; zoom.setRatio(Math.min(zoomX, zoomY)); } }); } } //--------------------// // setLocationService // //--------------------// /** * Allow to inject a dependency on a location service. * This location is used * for two purposes: <ol> * * <li>First, this panel is a producer of location information. The * location can be modified both programmatically (by calling method * {@link #setFocusLocation}) and interactively by mouse event * (pointSelected or rectangleSelected which in turn call * setFocusLocation) .</li> * * <li>Second, this panel is a consumer of location information, since * it makes the selected location visible in the display, through the * method {@link #showFocusLocation}.</li> </ol> * * <p><b>Nota</b>: Setting the location selection does not * automatically register this view on the selection object. If * such registering is needed, it must be done manually through method * {@link #subscribe}. (TODO: Question: Why?) * * @param locationService the proper location service to be updated */ public void setLocationService (SelectionService locationService) { if ((this.locationService != null) && (this.locationService != locationService)) { this.locationService.unsubscribe(LocationEvent.class, this); } this.locationService = locationService; } //--------------// // setModelSize // //--------------// /** * Assign the size of the model object, that is the unscaled size. * * @param modelSize the model size to use */ public void setModelSize (Dimension modelSize) { this.modelSize = new Dimension(modelSize); } //-------------------// // showFocusLocation // //-------------------// /** * Update the display, so that the location rectangle gets visible. * * <b>NOTA</b>: Subclasses that override this method should call this * super implementation or the display will not be updated by default. * * @param rect the location information * @param centered true to center the display on rect center */ public void showFocusLocation (final Rectangle rect, final boolean centered) { if (zoom == null) { return; // For degenerated cases (no real view) } if (getModelSize() == null) { return; } setPreferredSize(zoom.scaled(getModelSize())); revalidate(); repaint(); if (rect != null) { rubber.resetRectangle(rect); // Check whether the rectangle is fully visible, // if not, scroll so as to make (most of) it visible Rectangle scaledRect = zoom.scaled(rect); Point center = new Point( scaledRect.x + (scaledRect.width / 2), scaledRect.y + (scaledRect.height / 2)); if (centered) { Rectangle vr = getVisibleRect(); scaledRect = new Rectangle( center.x - (vr.width / 2), center.y - (vr.height / 2), vr.width, vr.height); } else { int margin = constants.focusMargin.getValue(); if (margin == 0) { scaledRect.grow(1, 1); // Workaround } else { scaledRect.grow(margin, margin); } } scrollRectToVisible(scaledRect); } } //--------------// // stateChanged // //--------------// /** * Entry called when the ratio of the related zoom has changed * * @param e the zoom event */ @Override public void stateChanged (ChangeEvent e) { // Force a redisplay? // if (isShowing()) { showFocusLocation(getSelectedRectangle(), true); // } } //-----------// // subscribe // //-----------// /** * Subscribe to the (previously injected) location service (either Sheet or * Score location, depending on the context) */ public void subscribe () { logger.debug("Subscribe {} {}", getClass().getSimpleName(), getName()); // Subscribe to location events if (locationService != null) { locationService.subscribeStrongly(LocationEvent.class, this); } } //----------// // toString // //----------// @Override public String toString () { return ClassUtil.nameOf(this); } //-------------// // unsetRubber // //-------------// /** * Cut the connection between this view and the rubber * * @param rubber the rubber to disconnect */ public void unsetRubber (Rubber rubber) { rubber.setMouseMonitor(null); } //-----------// // unsetZoom // //-----------// /** * Deassign the zoom, unregistering this component as a zoom listener * * @param zoom the zoom to unregister from * @return true if actually disconnected */ public boolean unsetZoom (Zoom zoom) { if (zoom != null) { return zoom.removeChangeListener(this); } else { return false; } } //-------------// // unsubscribe // //-------------// /** * Unsubscribe from the related location service */ public void unsubscribe () { logger.debug( "Unsubscribe {} {}", getClass().getSimpleName(), getName()); // Unsubscribe to location events if (locationService != null) { locationService.unsubscribe(LocationEvent.class, this); } } //----------------// // paintComponent // //----------------// /** * Final method, called by Swing. If something has to be changed in the * rendering of the model, override the render method instead. * * @param initialGraphics the graphic context */ @Override protected final void paintComponent (Graphics initialGraphics) { // Paint background first super.paintComponent(initialGraphics); // Adjust graphics context to desired zoom ratio if (zoom != null) { Graphics2D g = (Graphics2D) initialGraphics.create(); g.scale(zoom.getRatio(), zoom.getRatio()); try { // Then, drawing specific to the view (to be provided in subclass) render(g); } catch (ConcurrentModificationException ex) { // It's hard to avoid concurrent modifs since the GUI may need to // repaint a view, while some processing is taking place ... ///logger.warn("RubberPanel paintComponent failed", ex); repaint(); // To trigger another painting later ... } catch (Throwable ex) { logger.warn("RubberPanel paintComponent ", ex); } finally { // Finally the rubber, now that everything else has been drawn if (rubber != null) { rubber.render(initialGraphics); } g.dispose(); } } } //--------// // render // //--------// /** * This is just a place holder, the real rendering must be provided by a * subclass to actually render the object displayed, since the rubber is * automatically rendered after this one. * * @param g the graphic context */ protected void render (Graphics2D g) { // Empty by default } //------------------// // setFocusLocation // //------------------// /** * Modifies the location information. This method simply posts the * location information on the proper Service object, provided that * such object has been previously injected (by means of the method * {@link #setLocationService}. * * @param rect the location information * @param movement the button movement * @param hint the related selection hint */ protected void setFocusLocation (Rectangle rect, MouseMovement movement, SelectionHint hint) { logger.debug("setFocusLocation rect={} hint={}", rect, hint); // Publish the new user-selected location if (locationService != null) { locationService.publish( new LocationEvent(this, hint, movement, new Rectangle(rect))); } } //~ Inner Classes ---------------------------------------------------------- //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- PixelCount focusMargin = new PixelCount( 20, "Margin visible around a focus"); } }