001/* A drag interactor for locatable nodes 002 003 Copyright (c) 1998-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.basic; 029 030import java.awt.geom.Point2D; 031import java.util.HashSet; 032import java.util.Iterator; 033import java.util.List; 034 035import diva.canvas.Figure; 036import diva.canvas.event.LayerEvent; 037import diva.canvas.interactor.SelectionModel; 038import diva.graph.GraphPane; 039import diva.graph.NodeDragInteractor; 040import diva.util.UserObjectContainer; 041import ptolemy.kernel.Entity; 042import ptolemy.kernel.undo.UndoStackAttribute; 043import ptolemy.kernel.util.IllegalActionException; 044import ptolemy.kernel.util.Locatable; 045import ptolemy.kernel.util.Location; 046import ptolemy.kernel.util.NamedObj; 047import ptolemy.moml.MoMLChangeRequest; 048import ptolemy.moml.MoMLUndoEntry; 049import ptolemy.util.MessageHandler; 050import ptolemy.vergil.toolbox.SnapConstraint; 051 052/////////////////////////////////////////////////////////////////// 053//// LocatableNodeDragInteractor 054 055/** 056 An interaction role that drags nodes that have locatable objects 057 as semantic objects. When the node is dragged, this interactor 058 updates the location in the locatable object with the new location of the 059 figure. 060 <p> 061 The dragging of a selection is undoable, and is based on the difference 062 between the point where the mouse was pressed and where the mouse was 063 released. This information is used to create MoML to undo the move if 064 requested. 065 066 @author Steve Neuendorffer 067 @version $Id$ 068 @since Ptolemy II 2.0 069 @Pt.ProposedRating Red (eal) 070 @Pt.AcceptedRating Red (johnr) 071 */ 072public class LocatableNodeDragInteractor extends NodeDragInteractor { 073 /** Create a new interactor contained within the given controller. 074 * @param controller The controller. 075 */ 076 public LocatableNodeDragInteractor(LocatableNodeController controller) { 077 super(controller.getController()); 078 _controller = controller; 079 080 // Create a snap constraint with the default snap resolution. 081 _snapConstraint = new SnapConstraint(); 082 appendConstraint(_snapConstraint); 083 } 084 085 /////////////////////////////////////////////////////////////////// 086 //// public methods //// 087 088 /** When the mouse is pressed before dragging, store a copy of the 089 * pressed point location so that a relative move can be 090 * evaluated for undo purposes. 091 * @param e The press event. 092 */ 093 @Override 094 public void mousePressed(LayerEvent e) { 095 super.mousePressed(e); 096 _dragStart = _getConstrainedPoint(e); 097 } 098 099 /** When the mouse is released after dragging, mark the frame modified 100 * and update the panner, and generate an undo entry for the move. 101 * If no movement has occurred, then do nothing. 102 * @param e The release event. 103 */ 104 @Override 105 public void mouseReleased(LayerEvent e) { 106 107 // We should factor out the common code in this method and in 108 // transform(). 109 // Work out the transform the drag performed. 110 double[] dragEnd = _getConstrainedPoint(e); 111 double[] transform = new double[2]; 112 transform[0] = _dragStart[0] - dragEnd[0]; 113 transform[1] = _dragStart[1] - dragEnd[1]; 114 115 if (transform[0] == 0.0 && transform[1] == 0.0) { 116 return; 117 } 118 119 BasicGraphController graphController = (BasicGraphController) _controller 120 .getController(); 121 BasicGraphFrame frame = graphController.getFrame(); 122 123 SelectionModel model = graphController.getSelectionModel(); 124 AbstractBasicGraphModel graphModel = (AbstractBasicGraphModel) graphController 125 .getGraphModel(); 126 Object[] selection = model.getSelectionAsArray(); 127 Object[] userObjects = new Object[selection.length]; 128 129 // First get the user objects from the selection. 130 for (int i = 0; i < selection.length; i++) { 131 userObjects[i] = ((Figure) selection[i]).getUserObject(); 132 } 133 134 // First make a set of all the semantic objects as they may 135 // appear more than once 136 HashSet<NamedObj> namedObjSet = new HashSet<NamedObj>(); 137 138 for (Object element : selection) { 139 if (element instanceof Figure) { 140 Object userObject = ((Figure) element).getUserObject(); 141 142 if (graphModel.isEdge(userObject) 143 || graphModel.isNode(userObject)) { 144 NamedObj actual = (NamedObj) graphModel 145 .getSemanticObject(userObject); 146 147 if (actual != null) { 148 namedObjSet.add(actual); 149 } else { 150 // Special case, may need to handle by not going to 151 // MoML and which may not be undoable. 152 // FIXME: This is no way to handle it... 153 System.out.println( 154 "Object with no semantic object , class: " 155 + userObject.getClass().getName()); 156 } 157 } 158 } 159 } 160 161 // Generate the MoML to carry out move. 162 // Note that the location has already been set by the mouseMoved() 163 // call, but we need to do this so that the undo is generated and 164 // so that the change propagates. 165 // The toplevel is the container being edited. 166 final NamedObj toplevel = (NamedObj) graphModel.getRoot(); 167 // The object situated under the location where the mouse was released 168 NamedObj dropTarget = null; 169 170 StringBuffer moml = new StringBuffer(); 171 StringBuffer undoMoml = new StringBuffer(); 172 moml.append("<group>\n"); 173 undoMoml.append("<group>\n"); 174 175 for (NamedObj element : namedObjSet) { 176 List<?> locationList = element.attributeList(Locatable.class); 177 178 if (locationList.isEmpty()) { 179 // Nothing to do as there was no previous location 180 // attribute (applies to "unseen" relations) 181 continue; 182 } 183 Locatable locatable = (Locatable) locationList.get(0); 184 // Use the MoML element name in case the location is a vertex 185 String locationElementName = ((NamedObj) locatable) 186 .getElementName(); 187 String locationName = locatable.getName(); 188 // The location class, which can change if a relative location is dragged. 189 String locationClazz = locatable.getClass().getName(); 190 // The new relativeTo property of the relative location. 191 String newRelativeTo = ""; 192 String newRelativeToElementName = ""; 193 // The old relativeTo property of the relative location. 194 String oldRelativeTo = ""; 195 String oldRelativeToElementName = ""; 196 197 // If locatable is an instance of RelativeLocation, 198 // then its getLocation() method returns the absolute 199 // location, not the relative location. 200 double[] newLocation = null; 201 if (locatable instanceof RelativeLocation) { 202 RelativeLocation relativeLocation = (RelativeLocation) locatable; 203 newLocation = relativeLocation.getRelativeLocation(); 204 oldRelativeTo = relativeLocation.relativeTo.getExpression(); 205 oldRelativeToElementName = relativeLocation.relativeToElementName 206 .getExpression(); 207 } else { 208 newLocation = locatable.getLocation(); 209 } 210 211 // NOTE: we use the transform worked out for the drag to 212 // set the original MoML location. 213 // Should do this before we break or create the relative location link. 214 double[] oldLocation = new double[2]; 215 oldLocation[0] = newLocation[0] + transform[0]; 216 oldLocation[1] = newLocation[1] + transform[1]; 217 218 // RelativeLocatables can be dropped onto an object to create a 219 // link to that object. In this case the location attribute is 220 // replaced by a RelativeLocation that holds a relative offset. 221 boolean changeRelativeTo = false; 222 if (element instanceof RelativeLocatable) { 223 if (dropTarget == null) { 224 // Find the drop target if not yet done. Pass the selection 225 // as filter so that objects from the selection are not chosen. 226 dropTarget = _getObjectUnder( 227 new Point2D.Double(dragEnd[0], dragEnd[1]), 228 selection); 229 } 230 // Check to see whether the target is an Entity, and if it is, 231 // then make the position relative to that entity. Also, 232 // Do not accept relative locatables as drop target!! This could lead 233 // to a cycle in the references, and ultimately to a stack overflow 234 // when trying to compute the positions! 235 // FIXME: Could make this check weaker by checking for cycles. 236 if (dropTarget instanceof Entity 237 && !(dropTarget instanceof RelativeLocatable)) { 238 // Set the new values for the relativeTo properties. 239 newRelativeTo = dropTarget.getName(); 240 newRelativeToElementName = dropTarget.getElementName(); 241 // Change the location class! 242 // FIXME: This doesn't work with object-oriented classes!!! 243 locationClazz = RelativeLocation.class.getName(); 244 // Now the location value is relative, so take a fixed offset. 245 newLocation = new double[] { 246 RelativeLocation.INITIAL_OFFSET, 247 RelativeLocation.INITIAL_OFFSET }; 248 changeRelativeTo = true; 249 } else if (oldRelativeTo 250 .length() > 0 /* && newLocation != null*/) { 251 // We have no drop target, so check the current distance to the 252 // relativeTo object. If it exceeds a threshold, break the reference. 253 double distance = Math.sqrt(newLocation[0] * newLocation[0] 254 + newLocation[1] * newLocation[1]); 255 if (distance > RelativeLocation.BREAK_THRESHOLD) { 256 // Set the relativeTo property to the empty string. 257 changeRelativeTo = true; 258 // Request the absolute location for correct new placement. 259 newLocation = locatable.getLocation(); 260 } 261 } 262 } 263 264 // Give default values in case the previous locations value 265 // has not yet been set 266 if (newLocation == null) { 267 newLocation = new double[] { 0, 0 }; 268 } 269 270 // Create the MoML, wrapping the new location attribute 271 // in an element referring to the container 272 String containingElementName = element.getElementName(); 273 String elementToMove = "<" + containingElementName + " name=\"" 274 + element.getName() + "\" >\n"; 275 moml.append(elementToMove); 276 undoMoml.append(elementToMove); 277 278 moml.append("<" + locationElementName + " name=\"" + locationName 279 + "\" class=\"" + locationClazz + "\" value=\"[" 280 + newLocation[0] + ", " + newLocation[1] + "]\" >\n"); 281 undoMoml.append("<" + locationElementName + " name=\"" 282 + locationName + "\" value=\"[" + oldLocation[0] + ", " 283 + oldLocation[1] + "]\" >\n"); 284 285 if (changeRelativeTo) { 286 // Case 1: We have dragged onto another object. Create a reference to 287 // the drop target and store it as properties of the location. 288 // Case 2: We have dragged the locatable away from its relativeTo 289 // object. In this case delete the reference to break the link. 290 moml.append("<property name=\"relativeTo\" value=\"" 291 + newRelativeTo + "\"/>"); 292 moml.append("<property name=\"relativeToElementName\" value=\"" 293 + newRelativeToElementName + "\"/>"); 294 // The old reference must be restored upon undo. 295 undoMoml.append("<property name=\"relativeTo\" value=\"" 296 + oldRelativeTo + "\"/>"); 297 undoMoml.append( 298 "<property name=\"relativeToElementName\" value=\"" 299 + oldRelativeToElementName + "\"/>"); 300 } 301 302 moml.append("</" + locationElementName + ">\n"); 303 undoMoml.append("</" + locationElementName + ">\n"); 304 moml.append("</" + containingElementName + ">\n"); 305 undoMoml.append("</" + containingElementName + ">\n"); 306 } 307 308 moml.append("</group>\n"); 309 undoMoml.append("</group>\n"); 310 311 final String finalUndoMoML = undoMoml.toString(); 312 313 // Request the change. 314 MoMLChangeRequest request = new MoMLChangeRequest(this, toplevel, 315 moml.toString()) { 316 @Override 317 protected void _execute() throws Exception { 318 super._execute(); 319 320 // Next create and register the undo entry; 321 // The MoML by itself will not cause an undo 322 // to register because the value is not changing. 323 // Note that this must be done inside the change 324 // request because write permission on the 325 // workspace is required to push an entry 326 // on the undo stack. If this is done outside 327 // the change request, there is a race condition 328 // on the undo, and a deadlock could result if 329 // the model is running. 330 MoMLUndoEntry newEntry = new MoMLUndoEntry(toplevel, 331 finalUndoMoML); 332 UndoStackAttribute undoInfo = UndoStackAttribute 333 .getUndoInfo(toplevel); 334 undoInfo.push(newEntry); 335 } 336 }; 337 338 toplevel.requestChange(request); 339 340 if (frame != null) { 341 // NOTE: Use changeExecuted rather than directly calling 342 // setModified() so that the panner is also updated. 343 frame.changeExecuted(null); 344 } 345 } 346 347 /** Specify the snap resolution. The default snap resolution is 5.0. 348 * @param resolution The snap resolution. 349 */ 350 public void setSnapResolution(double resolution) { 351 _snapConstraint.setResolution(resolution); 352 } 353 354 /** Drag all selected nodes and move any attached edges. 355 * Update the locatable nodes with the current location. 356 * @param e The event triggering this translation. 357 * @param x The horizontal delta. 358 * @param y The vertical delta. 359 */ 360 @Override 361 public void translate(LayerEvent e, double x, double y) { 362 // NOTE: To get snap to grid to work right, we have to do some work. 363 // It is not sufficient to quantize the translation. What we do is 364 // find the location of the first locatable node in the selection, 365 // and find a translation for it that will lead to an acceptable 366 // quantized position. Then we use that translation. This does 367 // not ensure that all nodes in the selection get to an acceptable 368 // quantized point, but there is no way to do that without 369 // changing their relative positions. 370 // NOTE: We cannot use the location attribute of the target objects 371 // The problem is that the location as set during a drag is a 372 // queued mutation. So the translation we get isn't right. 373 Iterator<?> targets = targets(); 374 double[] originalUpperLeft = null; 375 376 while (targets.hasNext()) { 377 Figure figure = (Figure) targets.next(); 378 originalUpperLeft = new double[2]; 379 originalUpperLeft[0] = figure.getOrigin().getX(); 380 originalUpperLeft[1] = figure.getOrigin().getY(); 381 382 // Only snap the first figure in the set. 383 break; 384 } 385 386 double[] snapTranslation; 387 388 if (originalUpperLeft == null) { 389 // No location found in the selection, so we just quantize 390 // the translation. 391 double[] oldTranslation = new double[2]; 392 oldTranslation[0] = x; 393 oldTranslation[1] = y; 394 snapTranslation = _snapConstraint.constrain(oldTranslation); 395 } else { 396 double[] newUpperLeft = new double[2]; 397 newUpperLeft[0] = originalUpperLeft[0] + x; 398 newUpperLeft[1] = originalUpperLeft[1] + y; 399 400 double[] snapLocation = _snapConstraint.constrain(newUpperLeft); 401 snapTranslation = new double[2]; 402 snapTranslation[0] = snapLocation[0] - originalUpperLeft[0]; 403 snapTranslation[1] = snapLocation[1] - originalUpperLeft[1]; 404 } 405 406 // NOTE: The following seems no longer necessary, since the 407 // translation occurs as a consequence of setting the location 408 // attribute. However, for reasons that I don't understand, 409 // without this, drag doesn't work. The new location ends 410 // up identical to the old because of the snap, so no translation 411 // occurs. Oddly, the superclass call performs a translation 412 // even if the snapTranslation is zero. Beats me. EAL 7/31/02. 413 super.translate(e, snapTranslation[0], snapTranslation[1]); 414 415 // Set the location attribute of each item that is translated. 416 // NOTE: this works only because all the nodes that allow 417 // dragging are location nodes. If nodes can be dragged that 418 // aren't locatable nodes, then they shouldn't be able to be 419 // selected at the same time as a locatable node. 420 try { 421 targets = targets(); 422 423 while (targets.hasNext()) { 424 Figure figure = (Figure) targets.next(); 425 Object node = figure.getUserObject(); 426 427 if (_controller.getController().getGraphModel().isNode(node)) { 428 // NOTE: This used to get the location and then set it, 429 // but since the returned value is the internal array, 430 // then setLocation() believed there was no change, 431 // so the change would not be persistent. 432 double[] location = new double[2]; 433 location[0] = figure.getOrigin().getX(); 434 location[1] = figure.getOrigin().getY(); 435 _controller.setLocation(node, location); 436 } 437 } 438 } catch (IllegalActionException ex) { 439 MessageHandler.error("could not set location", ex); 440 } 441 } 442 443 /////////////////////////////////////////////////////////////////// 444 //// private methods //// 445 446 // Returns a constrained point from the given event 447 private double[] _getConstrainedPoint(LayerEvent e) { 448 Iterator<?> targets = targets(); 449 double[] result = new double[2]; 450 451 if (targets.hasNext()) { 452 //Figure figure = (Figure) targets.next(); 453 454 // The transform context is always (0,0) so no use 455 // NOTE: this is a bit of hack, needed to allow the undo of 456 // the movement of vertexes by themselves 457 result[0] = e.getLayerX(); 458 result[1] = e.getLayerY(); 459 return _snapConstraint.constrain(result); 460 } 461 462 /* 463 * else { 464 * AffineTransform transform 465 * = figure.getTransformContext().getTransform(); 466 * result[0] = transform.getTranslateX(); 467 * result[1] = transform.getTranslateY(); 468 * } 469 * Only snap the first figure in the set. 470 * break; 471 */ 472 return result; 473 } 474 475 /** Return the figure that is an icon of a NamedObj and is 476 * under the specified point, or null if there is none. 477 * 478 * This code is copied from {@link EditorDropTargetListener#_getFigureUnder(Point2D)} 479 * and modified for the new context. 480 * 481 * @param point The point in the graph pane. 482 * @param filteredFigures figures that are filtered from the object search 483 * @return The object under the specified point, or null if there 484 * is none or it is not a NamedObj. 485 */ 486 private Figure _getFigureUnder(Point2D point, 487 final Object[] filteredFigures) { 488 GraphPane pane = getController().getGraphPane(); 489 490 return BasicGraphFrame.getFigureUnder(pane, point, filteredFigures); 491 } 492 493 /** Return the object under the specified point, or null if there 494 * is none. 495 * 496 * This code is copied from {@link EditorDropTargetListener#_getObjectUnder(Point2D)}. 497 * 498 * @param point The point in the graph pane. 499 * @param filteredFigures figures that are filtered from the object search 500 * @return The object under the specified point, or null if there 501 * is none or it is not a NamedObj. 502 */ 503 private NamedObj _getObjectUnder(Point2D point, Object[] filteredFigures) { 504 Figure figureUnderMouse = _getFigureUnder(point, filteredFigures); 505 506 if (figureUnderMouse == null) { 507 return null; 508 } 509 510 Object objectUnderMouse = ((UserObjectContainer) figureUnderMouse) 511 .getUserObject(); 512 513 // Object might be a Location, in which case we want its container. 514 if (objectUnderMouse instanceof Location) { 515 return ((NamedObj) objectUnderMouse).getContainer(); 516 } else if (objectUnderMouse instanceof NamedObj) { 517 return (NamedObj) objectUnderMouse; 518 } 519 520 return null; 521 } 522 523 /////////////////////////////////////////////////////////////////// 524 //// private variables //// 525 526 private LocatableNodeController _controller; 527 528 // Used to undo a locatable node movement 529 private double[] _dragStart; 530 531 // Locally defined snap constraint. 532 private SnapConstraint _snapConstraint; 533}