//----------------------------------------------------------------------------// // // // Z o o m // // // //----------------------------------------------------------------------------// // <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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.util.HashSet; import java.util.Set; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; /** * Class {@code Zoom} encapsulates a zoom ratio, which is typically * the ratio between display values (such as the size of the display of * an entity) and model values (such as the size of the entity itself). * * For example, a Zoom with ratio set to a 2.0 value would double the display * of a given entity. * * <p>Since this class is meant to be used when handling display tasks, it * also provides utility methods to go back and forth between display and * model values, for simple items such as primitive data (double), Point, * Dimension and Rectangle. * * <p>It handles an internal collection of change listeners, that are * entities registered to be notified whenever a new ratio value is set. * * <p>A new value is programmatically set by calling the {@link #setRatio} * method. The newly defined ratio value is then pushed to all registered * observers. * * <p>A {@link LogSlider} can be connected to this zoom entity, to provide * UI for both output and input. * * @author Hervé Bitteur */ public class Zoom { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(Zoom.class); // To assign a unique Id private static int globalId; //~ Instance fields -------------------------------------------------------- /** Unique event, created lazily */ protected ChangeEvent changeEvent = null; /** Potential logarithmic slider to drive this zoom */ protected LogSlider slider; /** Collection of event listeners */ protected Set<ChangeListener> listeners = new HashSet<>(); /** Current ratio value */ protected double ratio; // Unique Id (to ease debugging) private int id = ++globalId; //~ Constructors ----------------------------------------------------------- //------// // Zoom // //------// /** * Create a zoom entity, with a default ratio value of 1. */ public Zoom () { this(1); } //------// // Zoom // //------// /** * Create a zoom entity, with the provided initial ratio value. * * @param ratio the initial ratio value */ public Zoom (double ratio) { logger.debug("Zoom created with ratio {}", ratio); setRatio(ratio); } //------// // Zoom // //------// /** * Create a zoom entity, with the provided initial ratio value. and a * related slider * * @param slider the related slider * @param ratio the initial ratio value */ public Zoom (LogSlider slider, double ratio) { logger.debug("Zoom created" + " slider={} ratio={}", slider, ratio); setSlider(slider); setRatio(ratio); } //~ Methods ---------------------------------------------------------------- //-------------------// // addChangeListener // //-------------------// /** * Register a change listener, to be notified when the zoom value is * changed * * @param listener the listener to be notified */ public void addChangeListener (ChangeListener listener) { listeners.add(listener); logger.debug("addChangeListener {} -> {}", listener, listeners.size()); } //------------------// // fireStateChanged // //------------------// /** * In charge of forwarding the change notification to all registered * listeners */ public void fireStateChanged () { for (ChangeListener listener : listeners) { if (changeEvent == null) { changeEvent = new ChangeEvent(this); } logger.debug("{} Firing {}", this, listener); listener.stateChanged(changeEvent); } } //----------// // getRatio // //----------// /** * Return the current zoom ratio. Ratio is defined as display / source. * * @return the ratio */ public double getRatio () { return ratio; } //----------------------// // removeChangeListener // //----------------------// /** * Unregister a change listener * * @param listener the listener to remove * @return true if actually removed */ public boolean removeChangeListener (ChangeListener listener) { return listeners.remove(listener); } //-------// // scale // //-------// /** * Scale all the elements of provided point * * @param pt the point to be scaled */ public void scale (Point pt) { pt.x = scaled(pt.x); pt.y = scaled(pt.y); } //-------// // scale // //-------// /** * Scale all the elements of provided dimension * * @param dim the dimension to be scaled */ public void scale (Dimension dim) { dim.width = scaled(dim.width); dim.height = scaled(dim.height); } //-------// // scale // //-------// /** * Scale all the elements of provided rectangle * * @param rect the rectangle to be scaled */ public void scale (Rectangle rect) { rect.x = scaled(rect.x); rect.y = scaled(rect.y); rect.width = scaled(rect.width); rect.height = scaled(rect.height); } //--------// // scaled // //--------// /** * Coordinate computation, Source -> Display * * @param val a source value * * @return the (scaled) display value */ public int scaled (double val) { return (int) Math.rint(val * ratio); } //--------// // scaled // //--------// /** * Coordinate computation, Source -> Display * * @param dim source dimension * * @return the corresponding (scaled) dimension */ public Dimension scaled (Dimension dim) { Dimension d = new Dimension(dim); scale(d); return d; } //--------// // scaled // //--------// /** * Coordinate computation, Source -> Display * * @param rect source rectangle * * @return the corresponding (scaled) rectangle */ public Rectangle scaled (Rectangle rect) { Rectangle r = new Rectangle(rect); scale(r); return r; } //----------// // setRatio // //----------// /** * Change the display zoom ratio. Nota, if the zoom is coupled with a * slider, this slider has the final word concerning the precise zoom * value, since the slider uses integer (or fractional) values. * * @param ratio the new ratio */ public void setRatio (double ratio) { logger.debug("setRatio ratio={}", ratio); // Propagate to slider (useful to keep slider in sync when ratio is // set programmatically) if (slider != null) { slider.setDoubleValue(ratio); } else { forceRatio(ratio); } } //-----------// // setSlider // //-----------// /** * Define a related logarithmic slider, as a UI to adjust the zoom * value * * @param slider the related slider UI */ public void setSlider (final LogSlider slider) { this.slider = slider; logger.debug("setSlider"); if (slider != null) { slider.setDoubleValue(ratio); slider.addChangeListener( new ChangeListener() { @Override public void stateChanged (ChangeEvent e) { // Forward the new zoom ratio if (constants.continuousSliderReading.getValue() || !slider.getValueIsAdjusting()) { double newRatio = slider.getDoubleValue(); logger.debug("Slider firing zoom newRatio={}", newRatio); // Stop condition to avoid endless loop between // slider and zoom if (Math.abs(newRatio - ratio) > .001) { forceRatio(newRatio); } } } }); } } //----------// // toString // //----------// /** * Report a quick description * * @return the zoom information */ @Override public String toString () { return "{Zoom#" + id + " listeners=" + listeners.size() + " ratio=" + ratio + "}"; } //-------------// // truncScaled // //-------------// /** * Coordinate computation, Source -> Display, but with a truncation * rather than rounding. * * @param val a source value * @return the (scaled) display value */ public int truncScaled (double val) { return (int) (val * ratio); } //---------------// // truncUnscaled // //---------------// /** * Coordinate computation, Display -> Source, but with a truncation * rather than rounding. * * @param val a display value * @return the corresponding (unscaled) source coordinate */ public int truncUnscaled (double val) { return (int) (val / ratio); } //---------// // unscale // //---------// /** * Unscale a point * * @param pt the point to unscale */ public void unscale (Point pt) { pt.x = unscaled(pt.x); pt.y = unscaled(pt.y); } //----------// // unscaled // //----------// /** * Coordinate computation Display -> Source * * @param val a display value * @return the corresponding (unscaled) source coordinate */ public int unscaled (double val) { return (int) Math.rint(val / ratio); } //------------// // forceRatio // //------------// /** * Impose and propagate the new ratio * * @param ratio the new ratio */ private void forceRatio (double ratio) { logger.debug("forceRatio ratio={}", ratio); this.ratio = ratio; // Propagate to listeners fireStateChanged(); } //~ Inner Classes ---------------------------------------------------------- //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Constant.Boolean continuousSliderReading = new Constant.Boolean( true, "Should we allow continuous reading of the zoom slider"); } }