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}