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}