001/* The node controller 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.BasicStroke;
031import java.awt.Color;
032import java.awt.Stroke;
033import java.awt.geom.Rectangle2D;
034
035import diva.canvas.CanvasUtilities;
036import diva.canvas.CompositeFigure;
037import diva.canvas.Figure;
038import diva.canvas.connector.TerminalFigure;
039import diva.canvas.toolbox.BasicFigure;
040import diva.graph.BasicNodeController;
041import diva.graph.GraphController;
042import diva.graph.GraphModel;
043import diva.graph.NodeInteractor;
044import ptolemy.data.BooleanToken;
045import ptolemy.data.Token;
046import ptolemy.data.expr.Parameter;
047import ptolemy.kernel.util.Attribute;
048import ptolemy.kernel.util.IllegalActionException;
049import ptolemy.kernel.util.Locatable;
050import ptolemy.kernel.util.NamedObj;
051
052///////////////////////////////////////////////////////////////////
053//// LocatableNodeController
054
055/**
056 This node controller provides interaction techniques for nodes that are
057 locations.   This is common when the node has some concept of its
058 graphical location, but does not know about the figure that it
059 is associated with.  This class provides the connection between the
060 figure's notion of location and the node's concept of location.
061 <p>
062 When nodes are drawn, they are automatically placed at the
063 coordinate given by the location.  A LocatableNodeDragInteractor
064 is used to update the location of the node as the figure moves.
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 LocatableNodeController extends BasicNodeController {
073    /** Create an instance associated with the specified graph controller.
074     *  @param controller The graph controller.
075     */
076    public LocatableNodeController(GraphController controller) {
077        super(controller);
078
079        NodeInteractor nodeInteractor = (NodeInteractor) getNodeInteractor();
080        _dragInteractor = new LocatableNodeDragInteractor(this);
081        nodeInteractor.setDragInteractor(_dragInteractor);
082    }
083
084    ///////////////////////////////////////////////////////////////////
085    ////                         public methods                    ////
086
087    /** Add a node to this graph editor and render it
088     * at the given location.
089     */
090    @Override
091    public void addNode(Object node, double x, double y) {
092        throw new UnsupportedOperationException("Cannot add node.");
093    }
094
095    /** Draw the node at its location. This overrides the base class
096     *  to assign a location to the object.
097     */
098    @Override
099    public Figure drawNode(Object node) {
100        Figure nf = super.drawNode(node);
101        locateFigure(node);
102        return nf;
103    }
104
105    /** Return the desired location of this node.  Throw a runtime
106     *  exception if the node does not have a desired location.
107     *  @param node The node.
108     *  @return The desired location of the node.
109     *  @see #setLocation(Object, double[])
110     */
111    public double[] getLocation(Object node) {
112        if (hasLocation(node)) {
113            return ((Locatable) node).getLocation();
114        } else {
115            throw new RuntimeException(
116                    "The node " + node + "does not have a desired location");
117        }
118    }
119
120    /** Return true if the node is associated with a desired location.
121     *  In this base class, return true if the the node's semantic object is
122     *  an instance of Locatable.
123     *  @param node The node.
124     *  @return True if the node is associated with a desired location.
125     */
126    public boolean hasLocation(Object node) {
127        if (node instanceof Locatable) {
128            Locatable object = (Locatable) node;
129            double[] location = object.getLocation();
130
131            if (location != null && location.length == 2) {
132                return true;
133            }
134        }
135
136        return false;
137    }
138
139    /** Move the node's figure to the location specified in the node's
140     *  semantic object, if that object is an instance of Locatable.
141     *  If the semantic object is not a location, then do nothing.
142     *  If the figure associated with the semantic object is an instance
143     *  of TerminalFigure, then modify the location to ensure that the
144     *  connect site snaps to grid.
145     *  @param node The object to locate.
146     */
147    public void locateFigure(Object node) {
148        Figure nf = getController().getFigure(node);
149
150        try {
151            if (hasLocation(node)) {
152                double[] location = getLocation(node);
153                CanvasUtilities.translateTo(nf, location[0], location[1]);
154            }
155        } catch (Exception ex) {
156            // FIXME: Ignore if there is no valid location.  This
157            // happens occasionally due to a race condition in the
158            // Bouncer demo.  Occasionally, the repaint thread will
159            // attempt to locate the bouncing icon before the location
160            // parameter has been evaluated, causing an exception to
161            // be thrown.  Basically the lazy parameter evaluation
162            // mechanism causes rerendering in Diva to be rentrant,
163            // which it shouldn't be.  Unfortunately, I have no idea
164            // how to fix it... SN 5/5/2003
165        }
166    }
167
168    /** Set the desired location of this node.  Throw an exception if the
169     *  node can not be given a desired location.
170     *  @param node The node
171     *  @param location The location
172     *  @exception IllegalActionException Not thrown in this base class.
173     *  @see #getLocation(Object)
174     */
175    public void setLocation(Object node, double[] location)
176            throws IllegalActionException {
177        if (location == null) {
178            throw new RuntimeException(
179                    "The location is not valid, it should " + "not be null.");
180        } else if (location.length != 2) {
181            throw new RuntimeException("The location is not valid, the length "
182                    + "should be 2, but it is " + location.length);
183        }
184
185        if (node instanceof Locatable) {
186            ((Locatable) node).setLocation(location);
187        } else {
188            throw new RuntimeException(
189                    "The node " + node + "cannot have a desired location");
190        }
191    }
192
193    /** Specify the snap resolution. The default snap resolution is 5.0.
194     *  @param resolution The snap resolution.
195     */
196    public void setSnapResolution(double resolution) {
197        _dragInteractor.setSnapResolution(resolution);
198    }
199
200    ///////////////////////////////////////////////////////////////////
201    ////                         public variables                  ////
202
203    /** A fourth argument would this highlight translucent, which would enable
204     *  combination with other highlights. However, this causes printing to
205     *  PDF to rasterize, which significantly degrades the quality of the
206     *  graphic output. Used to have value 200.
207     */
208    public static final Color CLASS_ELEMENT_HIGHLIGHT_COLOR = new Color(255,
209            128, 128);
210
211    ///////////////////////////////////////////////////////////////////
212    ////                         protected methods                 ////
213
214    /** Render the children of the specified node.
215     *  This overrides the base class to do nothing if the node
216     *  contains a parameter named "_hide" with value true.
217     *  @param node The node with children to render.
218     */
219    @Override
220    protected void _drawChildren(java.lang.Object node) {
221        if (!_hide(node)) {
222            super._drawChildren(node);
223        }
224    }
225
226    /** Get the CompositeFigure from the given Figure.
227     *
228     *  @param nf The figure that should be a CompositeFigure itself, or be a
229     *   TerminalFigure whose getFigure() method returns a CompositeFigure.
230     *  @return The CompositeFigure, or null of it cannot be found.
231     */
232    protected static CompositeFigure _getCompositeFigure(Figure nf) {
233        CompositeFigure cf;
234
235        // Try to get a composite figure that we can add the
236        // annotation to.  This is complicated by the fact that
237        // ExternalIOPortController wraps its figure in a
238        // TerminalFigure, and that there is nothing in the API
239        // that enforces that a CompositeFigure is returned.
240        // *sigh*
241        if (nf instanceof CompositeFigure) {
242            cf = (CompositeFigure) nf;
243        } else if (nf instanceof TerminalFigure) {
244            Figure f = ((TerminalFigure) nf).getFigure();
245
246            if (f instanceof CompositeFigure) {
247                cf = (CompositeFigure) f;
248            } else {
249                cf = null;
250            }
251        } else {
252            cf = null;
253        }
254
255        return cf;
256    }
257
258    /** In this base class, return true if the specified node contains a
259     *  parameter named "_hide" with value true or an attribute that is not
260     *  a parameter named "_hide". Derived classes can override this method
261     *  to provide more sophisticated methods of choosing which nodes to
262     *  display.
263     *  @param node The node
264     *  @return true if the specified node should be hidden.
265     */
266    protected boolean _hide(java.lang.Object node) {
267        if (node instanceof Locatable) {
268            if (_isPropertySet(((Locatable) node).getContainer(), "_hide")) {
269                return true;
270            }
271        }
272
273        if (node instanceof NamedObj) {
274            if (_isPropertySet((NamedObj) node, "_hide")) {
275                return true;
276            }
277        }
278
279        return false;
280    }
281
282    /** Return true if the property of the specified name is set for
283     *  the specified object. A property is specified if the specified
284     *  object contains an attribute with the specified name and that
285     *  attribute is either not a boolean-valued parameter, or it is
286     *  a boolean-valued parameter with value true.
287     *  @param object The object.
288     *  @param name The property name.
289     *  @return True if the property is set.
290     */
291    protected boolean _isPropertySet(NamedObj object, String name) {
292        Attribute attribute = object.getAttribute(name);
293
294        if (attribute == null) {
295            return false;
296        }
297
298        if (attribute instanceof Parameter) {
299            try {
300                Token token = ((Parameter) attribute).getToken();
301
302                if (token instanceof BooleanToken) {
303                    if (!((BooleanToken) token).booleanValue()) {
304                        return false;
305                    }
306                }
307            } catch (IllegalActionException e) {
308                // Ignore, using default of true.
309            }
310        }
311
312        return true;
313    }
314
315    /** Render the specified node.  This overrides the base class to
316     *  return an invisible figure if the node contains a parameter
317     *  named "_hide" with value true.  This overrides the base class
318     *  to assign a location and to highlight the node if it is an
319     *  inherited object, and hence cannot be deleted.
320     *  @param node The node to render.
321     *  @return the newly created figure.
322     */
323    @Override
324    protected Figure _renderNode(java.lang.Object node) {
325        if (node == null || _hide(node)) {
326            // Return an empty figure.
327            Figure newFigure = new CompositeFigure();
328            newFigure.setVisible(false);
329            newFigure.setInteractor(getNodeInteractor());
330            newFigure.setUserObject(node);
331            getController().setFigure(node, newFigure);
332            return newFigure;
333        } else {
334            Figure nf = super._renderNode(node);
335            GraphModel model = getController().getGraphModel();
336            Object object = model.getSemanticObject(node);
337            CompositeFigure cf = _getCompositeFigure(nf);
338
339            if (_decoratable && object instanceof NamedObj
340                    && ((NamedObj) object).getDerivedLevel() < Integer.MAX_VALUE
341                    && cf != null) {
342                // float[] dash = { 2.0f, 5.0f };
343                Stroke stroke = new BasicStroke(2f, /* width */
344                        BasicStroke.CAP_SQUARE, /* cap   */
345                        BasicStroke.JOIN_MITER, /* join  */
346                        10.0f); /* mitre limit */
347                // To get a dashed line, add the following two arguments above:
348                // dash, /* dash  */
349                // 0.0f); /* dash_phase  */
350                // Pad the figure so that this highlight composes properly
351                // with other highlights.
352                Rectangle2D bounds = cf.getBackgroundFigure().getBounds();
353                double padding = 3.0;
354                bounds = new Rectangle2D.Double(bounds.getX() - padding,
355                        bounds.getY() - padding,
356                        bounds.getWidth() + padding * 2.0,
357                        bounds.getHeight() + padding * 2.0);
358                BasicFigure bf = new BasicFigure(bounds);
359                bf.setStroke(stroke);
360                bf.setStrokePaint(CLASS_ELEMENT_HIGHLIGHT_COLOR);
361                // Put the highlighting in the background,
362                // behind the actor label.
363                int index = cf.getFigureCount();
364                if (index < 0) {
365                    index = 0;
366                }
367                cf.add(index, bf);
368            }
369
370            return nf;
371        }
372    }
373
374    ///////////////////////////////////////////////////////////////////
375    ////                         protected variables               ////
376
377    /** A flag indicating that the figures associated with this
378     *  controller can be decorated to indicate that they are derived.
379     *  Some derived classes (like IOPortController) override this
380     *  to suppress such decoration. This is true by default.
381     */
382    protected boolean _decoratable = true;
383
384    ///////////////////////////////////////////////////////////////////
385    ////                         private variables                 ////
386
387    /** The drag interactor, which is remembered so we can change the
388     *  snap resolution.
389     */
390    private LocatableNodeDragInteractor _dragInteractor;
391}