001/* A manipulator for resizable icons.
002
003 Copyright (c) 2003-2016 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.vergil.toolbox;
029
030import java.awt.geom.Rectangle2D;
031
032import diva.canvas.CanvasUtilities;
033import diva.canvas.Figure;
034import diva.canvas.FigureDecorator;
035import diva.canvas.event.LayerEvent;
036import diva.canvas.interactor.BoundsGeometry;
037import diva.canvas.interactor.BoundsManipulator;
038import diva.canvas.interactor.DragInteractor;
039import diva.canvas.interactor.GrabHandle;
040import ptolemy.data.BooleanToken;
041import ptolemy.data.DoubleToken;
042import ptolemy.data.Token;
043import ptolemy.data.expr.Parameter;
044import ptolemy.kernel.util.Attribute;
045import ptolemy.kernel.util.IllegalActionException;
046import ptolemy.kernel.util.InternalErrorException;
047import ptolemy.kernel.util.Locatable;
048import ptolemy.kernel.util.NamedObj;
049import ptolemy.moml.MoMLChangeRequest;
050
051///////////////////////////////////////////////////////////////////
052//// AttributeBoundsManipulator
053
054/**
055 This is a bounds manipulator supporting resizable icons.
056 It records the new size when the mouse is released, and supports
057 snap to grid.
058
059 @author Edward A. Lee
060 @version $Id$
061 @since Ptolemy II 4.0
062 @Pt.ProposedRating Red (eal)
063 @Pt.AcceptedRating Red (johnr)
064 */
065public class AttributeBoundsManipulator extends BoundsManipulator {
066    /** Construct a new bounds manipulator.
067     *  @param container The container of the icon to be manipulated.
068     */
069    public AttributeBoundsManipulator(NamedObj container) {
070        super();
071        _container = container;
072
073        // To get resizing to snap to grid, use a custom resizer,
074        // rather than the one provided by the base class.
075        _resizer = new Resizer();
076        setHandleInteractor(_resizer);
077    }
078
079    ///////////////////////////////////////////////////////////////////
080    ////                         public methods                    ////
081
082    /** Make a persistent record of the new size by issuing a change request.
083     *  @param e The mouse event.
084     */
085    @Override
086    public void mouseReleased(LayerEvent e) {
087        Figure child = getChild();
088
089        // FIXME: Diva has a bug where this method is called on the
090        // prototype rather than the instance that has a child.
091        // We work around this by getting access to the instance.
092        if (child == null && _instanceDecorator != null) {
093            child = _instanceDecorator.getChild();
094        }
095
096        if (child != null) {
097            // NOTE: Calling getBounds() on the child itself yields an
098            // inaccurate bounds, for some reason. Use getShape().
099            Rectangle2D bounds = child.getShape().getBounds2D();
100
101            double resolution = _resizer.getSnapResolution();
102            // Check to see whether the size has changed by more than the
103            // snap resolution.
104            if (_boundsOnMousePressed != null
105                    && Math.abs(bounds.getWidth()
106                            - _boundsOnMousePressed.getWidth()) < resolution
107                    && Math.abs(bounds.getHeight()
108                            - _boundsOnMousePressed.getHeight()) < resolution) {
109                // Change is not big enough. Return.
110                return;
111            }
112
113            // Use a MoMLChangeRequest here so that the resize can be
114            // undone and so that a repaint occurs.
115            Attribute widthParameter = _container.getAttribute("width");
116            Attribute heightParameter = _container.getAttribute("height");
117            Attribute locationParameter = _container.getAttribute("_location");
118
119            // Proceed only if the container has these parameters.
120            if (widthParameter instanceof Parameter
121                    && heightParameter instanceof Parameter) {
122                // Snap the new width and height to the grid (not the parameter values!).
123                // The reason is that it is the new width and height, not the parameter
124                // values, are what is visible on the screen.
125                double[] snappedWidthHeight = _resizer
126                        .constrain(bounds.getWidth(), bounds.getHeight());
127
128                // The new width and height should be proportional to the original
129                // ones. This is because the width and height parameters of the
130                // attribute are not necessarily the same as the bounds of the
131                // figure. An extreme example of this is the ArcAttribute,
132                // where the width and height parameters specify the width
133                // and height of the base ellipse used to draw the arc.
134                // Provide default values in case something goes wrong.
135                double newWidth = snappedWidthHeight[0];
136                double newHeight = snappedWidthHeight[1];
137
138                try {
139                    Token previousWidth = ((Parameter) widthParameter)
140                            .getToken();
141                    if (previousWidth instanceof DoubleToken
142                            && _boundsOnMousePressed != null) {
143                        newWidth = snappedWidthHeight[0]
144                                / _boundsOnMousePressed.getWidth()
145                                * ((DoubleToken) previousWidth).doubleValue();
146                    }
147                } catch (IllegalActionException e1) {
148                    // This should not occur.
149                    e1.printStackTrace();
150                }
151
152                try {
153                    Token previousHeight = ((Parameter) heightParameter)
154                            .getToken();
155                    if (previousHeight instanceof DoubleToken
156                            && _boundsOnMousePressed != null) {
157                        newHeight = snappedWidthHeight[1]
158                                / _boundsOnMousePressed.getHeight()
159                                * ((DoubleToken) previousHeight).doubleValue();
160                    }
161                } catch (IllegalActionException e1) {
162                    // This should not occur.
163                    e1.printStackTrace();
164                }
165
166                // Create the MoML command to change the width and height.
167                StringBuffer command = new StringBuffer(
168                        "<group><property name =\"width\" value=\"");
169                command.append(newWidth);
170                command.append("\"/><property name =\"height\" value=\"");
171                command.append(newHeight);
172                command.append("\"/>");
173
174                // Location may be the upper left corner. Hence,
175                // location needs to change too if dragged left or up.
176                if (locationParameter instanceof Locatable) {
177
178                    double[] previousLocation = ((Locatable) locationParameter)
179                            .getLocation();
180
181                    // Use these defaults if for some reason _boundsOnMousePressed == null
182                    // (which should not occur).
183                    Rectangle2D childBounds = child.getBounds();
184                    double newX = childBounds.getX();
185                    double newY = childBounds.getY();
186
187                    if (_boundsOnMousePressed != null) {
188                        // Snap the new X and Y to the grid (not the new location!).
189                        // The reason is that it is the new X and Y, not the location,
190                        // the is visible on the screen. The location could be the
191                        // center of the object, or off center anywhere.
192                        double[] snappedXY = _resizer.constrain(bounds.getX(),
193                                bounds.getY());
194
195                        // If the previous location does not match X and Y of
196                        // _boundsOnMousePressed, then the figure location is not
197                        // the upper left corner. In this case, we need to scale
198                        // displacement according to the following formulas
199                        // (this is a tricky geometry problem!).
200                        newX = snappedXY[0] + snappedWidthHeight[0]
201                                / _boundsOnMousePressed.getWidth()
202                                * (previousLocation[0]
203                                        - _boundsOnMousePressed.getX());
204                        newY = snappedXY[1] + snappedWidthHeight[1]
205                                / _boundsOnMousePressed.getHeight()
206                                * (previousLocation[1]
207                                        - _boundsOnMousePressed.getY());
208                    } else {
209                        // This is legacy code. Should never be invoked.
210                        // If the figure is centered, have to use the center
211                        // instead.
212                        try {
213                            Attribute centered = _container
214                                    .getAttribute("centered", Parameter.class);
215
216                            if (centered != null) {
217                                boolean isCentered = ((BooleanToken) ((Parameter) centered)
218                                        .getToken()).booleanValue();
219
220                                if (isCentered) {
221                                    newX = childBounds.getCenterX();
222                                    newY = childBounds.getCenterY();
223                                }
224                            }
225                        } catch (IllegalActionException ex) {
226                            // Something went wrong. Use default.
227                        }
228                    }
229
230                    command.append("<property name = \"_location\" value=\"");
231
232                    command.append(newX);
233                    command.append(", ");
234                    command.append(newY);
235                    command.append("\"/>");
236                }
237
238                command.append("</group>");
239
240                MoMLChangeRequest request = new MoMLChangeRequest(this,
241                        _container, command.toString());
242                _container.requestChange(request);
243            }
244        } else {
245            throw new InternalErrorException(
246                    "No child figure for the manipulator!");
247        }
248    }
249
250    /** Make a record of the size before resizing.
251     *  @param e The mouse event.
252     */
253    @Override
254    public void mousePressed(LayerEvent e) {
255        Figure child = getChild();
256
257        // FIXME: Diva has a bug where this method is called on the
258        // prototype rather than the instance that has a child.
259        // We work around this by getting access to the instance.
260        if (child == null && _instanceDecorator != null) {
261            child = _instanceDecorator.getChild();
262        }
263
264        if (child != null) {
265            // NOTE: Calling getBounds() on the child itself yields an
266            // inaccurate bounds, for some reason.
267            // Weirdly, to get the size right, we need to use this.
268            // But to get the location right, we need the other!
269            _boundsOnMousePressed = child.getShape().getBounds2D();
270        } else {
271            // Make sure we don't use some previous bogus value.
272            _boundsOnMousePressed = null;
273        }
274    }
275
276    /** Create a new instance of this manipulator. The new
277     *  instance will have the same grab handle, and interactor
278     *  for grab-handles.  This is typically called on the prototype
279     *  to yield a decorator that gets displayed while the object
280     *  is selected.
281     */
282    @Override
283    public FigureDecorator newInstance(Figure f) {
284        BoundsManipulator m = new AttributeBoundsManipulator(_container);
285        m.setGrabHandleFactory(this.getGrabHandleFactory());
286        m.setHandleInteractor(this.getHandleInteractor());
287        m.setDragInteractor(getDragInteractor());
288
289        // FIXME: There is a bug in Diva where mouseReleased()
290        // is called on the prototype that is used to create this
291        // new instance, not on the new instance.  So we make
292        // a record of the new instance to get access to it in
293        // mouseReleased().
294        _instanceDecorator = m;
295
296        return m;
297    }
298
299    /** Set the snap resolution.
300     *  @param resolution The snap resolution.
301     */
302    public void setSnapResolution(double resolution) {
303        _resizer.setSnapResolution(resolution);
304    }
305
306    ///////////////////////////////////////////////////////////////////
307    ////                         private members                   ////
308
309    // Bounds of the child figure upon the mouse being pressed.
310    private Rectangle2D _boundsOnMousePressed;
311
312    // FIXME: Instance used to work around Diva bug.
313    private FigureDecorator _instanceDecorator;
314
315    // Container of the icon to be manipulated.
316    private NamedObj _container;
317
318    // The local instance of the resizer.
319    private Resizer _resizer;
320
321    ///////////////////////////////////////////////////////////////////
322    ////                         inner classes                     ////
323
324    /** An interactor class that changes the bounds of the child
325     * figure and triggers a repaint.
326     */
327    private class Resizer extends DragInteractor {
328        /** Create a new resizer.
329         */
330        public Resizer() {
331            _snapConstraint = new SnapConstraint();
332            appendConstraint(_snapConstraint);
333        }
334
335        /** Modify the specified point to snap to grid using the local
336         *  resolution.
337         *  @param x The x dimension of the point to modify.
338         *  @param y The y dimension of the point to modify.
339         *  @return The constrained point.
340         */
341        public double[] constrain(double x, double y) {
342            return _snapConstraint.constrain(x, y);
343        }
344
345        /** Get the snap resolution.
346         *  @return The snap resolution.
347         */
348        public double getSnapResolution() {
349            return _snapConstraint.getResolution();
350        }
351
352        /** Override the base class to notify the enclosing BoundsInteractor.
353         *  @param e The mouse event.
354         */
355        @Override
356        public void mousePressed(LayerEvent e) {
357            super.mousePressed(e);
358            AttributeBoundsManipulator.this.mousePressed(e);
359        }
360
361        /** Override the base class to notify the enclosing BoundsInteractor.
362         *  @param e The mouse event.
363         */
364        @Override
365        public void mouseReleased(LayerEvent e) {
366            super.mouseReleased(e);
367            AttributeBoundsManipulator.this.mouseReleased(e);
368        }
369
370        /** Set the snap resolution.
371         *  @param resolution The snap resolution.
372         */
373        public void setSnapResolution(double resolution) {
374            _snapConstraint.setResolution(resolution);
375        }
376
377        /** Translate the grab-handle.
378         */
379        @Override
380        public void translate(LayerEvent e, double x, double y) {
381            // Snap to grid.
382            double[] snapped = _snapConstraint.constrain(x, y);
383
384            // Translate the grab-handle, resizing the geometry
385            GrabHandle g = (GrabHandle) e.getFigureSource();
386            g.translate(snapped[0], snapped[1]);
387
388            // Transform the child.
389            BoundsManipulator parent = (BoundsManipulator) g.getParent();
390            BoundsGeometry geometry = parent.getGeometry();
391
392            parent.getChild().transform(CanvasUtilities.computeTransform(
393                    parent.getChild().getBounds(), geometry.getBounds()));
394        }
395
396        private SnapConstraint _snapConstraint;
397    }
398}