001/* The edge controller for links.
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.actor;
029
030import java.awt.Color;
031import java.util.List;
032
033import diva.canvas.Figure;
034import diva.canvas.Site;
035import diva.canvas.connector.Connector;
036import diva.canvas.connector.ConnectorAdapter;
037import diva.canvas.connector.ConnectorEvent;
038import diva.canvas.connector.ConnectorManipulator;
039import diva.canvas.connector.ConnectorTarget;
040import diva.canvas.connector.ManhattanConnector;
041import diva.canvas.connector.PerimeterTarget;
042import diva.canvas.connector.Terminal;
043import diva.canvas.event.MouseFilter;
044import diva.canvas.interactor.ActionInteractor;
045import diva.canvas.interactor.SelectionInteractor;
046import diva.canvas.interactor.SelectionModel;
047import diva.canvas.toolbox.SVGUtilities;
048import diva.graph.BasicEdgeController;
049import diva.graph.EdgeRenderer;
050import diva.graph.GraphController;
051import diva.gui.toolbox.MenuCreator;
052import ptolemy.actor.PublisherPort;
053import ptolemy.actor.SubscriberPort;
054import ptolemy.actor.gui.ColorAttribute;
055import ptolemy.actor.gui.Configuration;
056import ptolemy.actor.gui.PtolemyPreferences;
057import ptolemy.data.DoubleToken;
058import ptolemy.data.Token;
059import ptolemy.kernel.Port;
060import ptolemy.kernel.Relation;
061import ptolemy.kernel.util.IllegalActionException;
062import ptolemy.kernel.util.Locatable;
063import ptolemy.kernel.util.StringAttribute;
064import ptolemy.moml.Vertex;
065import ptolemy.util.MessageHandler;
066import ptolemy.vergil.basic.ContextMenuFactoryCreator;
067import ptolemy.vergil.basic.PopupMouseFilter;
068import ptolemy.vergil.kernel.Link;
069import ptolemy.vergil.toolbox.ConfigureAction;
070import ptolemy.vergil.toolbox.MenuActionFactory;
071import ptolemy.vergil.toolbox.PtolemyMenuFactory;
072
073///////////////////////////////////////////////////////////////////
074//// LinkController
075
076/**
077 This class provides interaction techniques for edges that are to be
078 connected between ports and relations.  Standard interaction
079 techniques for an undirected edge are allowed.
080
081 @author Steve Neuendorffer, Contributor: Edward A. Lee, Bert Rodiers
082 @version $Id$
083 @since Ptolemy II 2.0
084 @Pt.ProposedRating Red (eal)
085 @Pt.AcceptedRating Red (johnr)
086 */
087public class LinkController extends BasicEdgeController {
088    /** Create a link controller associated with the specified graph
089     *  controller.
090     *  @param controller The associated graph controller.
091     */
092    public LinkController(final GraphController controller) {
093        super(controller);
094
095        SelectionModel sm = controller.getSelectionModel();
096        SelectionInteractor interactor = (SelectionInteractor) getEdgeInteractor();
097        interactor.setSelectionModel(sm);
098
099        // Create and set up the manipulator for connectors
100        ConnectorManipulator manipulator = new ConnectorManipulator();
101        manipulator.setSnapHalo(4.0);
102        manipulator.addConnectorListener(new LinkDropper());
103        interactor.setPrototypeDecorator(manipulator);
104
105        // The mouse filter needs to accept regular click or control click
106        MouseFilter handleFilter = new MouseFilter(1, 0, 0);
107        manipulator.setHandleFilter(handleFilter);
108
109        ConnectorTarget ct = new LinkTarget();
110        setConnectorTarget(ct);
111        setEdgeRenderer(new LinkRenderer());
112
113        _menuCreator = new MenuCreator(null);
114        _menuCreator.setMouseFilter(new PopupMouseFilter());
115        interactor.addInteractor(_menuCreator);
116
117        // The contents of the menu is determined by the associated
118        // menu factory, which is a protected member of this class.
119        // Derived classes can add menu items to it.
120
121        // BEGIN CONFIGURABLE CONTEXT MENUS ////////////////////////////////////
122        /** FIXME
123         * @todo This location picks up all rt-click menu actions except for
124         * those on links (relations) and on the
125         */
126
127        List<?> configsList = Configuration.configurations();
128
129        Configuration config = null;
130        for (Object name : configsList) {
131            config = (Configuration) name;
132            if (config != null) {
133                break;
134            }
135        }
136
137        //If a MenuFactory has been defined in the configuration, use this
138        //one; otherwise, use the default Ptolemy one:
139        if (config != null && cmfCreator == null) {
140            cmfCreator = (ContextMenuFactoryCreator) config
141                    .getAttribute("contextMenuFactory");
142        }
143        if (cmfCreator != null) {
144            try {
145                _menuFactory = (PtolemyMenuFactory) cmfCreator
146                        .createContextMenuFactory(controller);
147            } catch (Exception ex) {
148                //do nothing - will default to ptii right-click menus
149                System.out.println(
150                        "Unable to use the alternative right-click menu "
151                                + "handler that was specified in the "
152                                + "configuration; defaulting to ptii handler. "
153                                + "Exception was: " + ex);
154            }
155
156        }
157        //if the above has failed in any way, _menuFactory will still be null,
158        //in which case we should default to ptii context menus
159        if (_menuFactory == null) {
160            _menuFactory = new PtolemyMenuFactory(controller);
161        }
162
163        _configureMenuFactory = new MenuActionFactory(_configureAction);
164        _menuFactory.addMenuItemFactory(_configureMenuFactory);
165        _menuCreator.setMenuFactory(_menuFactory);
166
167        // Add a double click interactor.
168        ActionInteractor doubleClickInteractor = new ActionInteractor(
169                _configureAction);
170        doubleClickInteractor.setConsuming(false);
171        doubleClickInteractor.setMouseFilter(new MouseFilter(1, 0, 0, 2));
172
173        interactor.addInteractor(doubleClickInteractor);
174    }
175
176    ///////////////////////////////////////////////////////////////////
177    ////                         public methods                    ////
178
179    /** Set the configuration.  This is may be used by derived controllers
180     *  to open files or URLs.
181     *  @param configuration The configuration.
182     */
183    public void setConfiguration(Configuration configuration) {
184        _configuration = configuration;
185    }
186
187    ///////////////////////////////////////////////////////////////////
188    ////                         inner classes                     ////
189
190    /** Render a visual representation of a link. */
191    public static class LinkRenderer implements EdgeRenderer {
192
193        /** Render a visual representation of the given edge.
194         *
195         *  <p>If a StringAttribute named "_color", or a
196         *  ColorAttribute is set then use
197         *  that color to draw the line.</p>
198         *
199         *  <p>If the attribute is named "_color", then the value of
200         *  the attribute is passed to
201         *  {@link diva.canvas.toolbox.SVGUtilities#getColor(String)}, which
202         *  has accepts the following format: If the first character
203         *  is "#" or "0", then the value of the attribute is expected
204         *  to be in a format suitable for java.awt.Color.decode().
205         *  Otherwise, the value of the attribute is passed to checked
206         *  against a list of color names defined in
207         *  {@link diva.canvas.toolbox.SVGUtilities}, if the color name is
208         *  not found, then the value of the attribute is passed to
209         *  java.awt.Color.getColor(String) and if there is no match,
210         *  then the color black is used.</p>
211         *
212         *  <p>If the attribute is an instance of
213         *  {@link ptolemy.actor.gui.ColorAttribute}, then the
214         *  javax.swing.JColorChooser gui will be offered as a way to
215         *  edit the color.</p>
216         *
217         *  <p>If the StringAttribute "_explanation" of the edge is set
218         *  then use it to set the tooltip.</p>
219         *
220         *  <p>If the "_linkBendRadius" preference is read from the
221         *  {@link ptolemy.actor.gui.PtolemyPreferences} and used to set
222         *  the bend radius.  The default bend radius is 20.</p>
223         *
224         *  @param edge The edge.
225         *  @param tailSite The tail site.
226         *  @param headSite The head site.
227         *  @return The Connector that represents the edge.
228         */
229        @Override
230        public Connector render(Object edge, Site tailSite, Site headSite) {
231            Link link = (Link) edge;
232            ManhattanConnector connector = new KielerLayoutConnector(tailSite,
233                    headSite, link);
234
235            if (link.getHead() != null && link.getTail() != null) {
236                connector.setLineWidth((float) 2.0);
237            }
238
239            connector.setUserObject(edge);
240
241            // The default bend radius of 50 is too large...
242            // parallel curves look bad.
243            connector.setBendRadius(20);
244
245            Relation relation = link.getRelation();
246
247            if (relation != null) {
248                String tipText = relation.getName();
249                String displayName = relation.getDisplayName();
250                if (!tipText.equals(displayName)) {
251                    tipText = displayName + " (" + tipText + ")";
252                }
253                connector.setToolTipText(tipText);
254
255                try {
256                    // Old style of colors.
257                    // FIXME: This isn't quite right for relation groups.
258                    StringAttribute colorAttribute = (StringAttribute) relation
259                            .getAttribute("_color", StringAttribute.class);
260
261                    if (colorAttribute != null) {
262                        String color = colorAttribute.getExpression();
263                        if (color != null && !color.trim().equals("")) {
264                            connector.setStrokePaint(
265                                    SVGUtilities.getColor(color));
266                        }
267                    }
268                } catch (IllegalActionException e) {
269                    // Ignore;
270                }
271                // New way to specify colors.
272                List<ColorAttribute> colorAttributes = relation
273                        .attributeList(ColorAttribute.class);
274                if (colorAttributes != null && colorAttributes.size() > 0) {
275                    // Use the last color added.
276                    Color color = colorAttributes
277                            .get(colorAttributes.size() - 1).asColor();
278                    connector.setStrokePaint(color);
279                }
280
281                StringAttribute _explAttr = (StringAttribute) relation
282                        .getAttribute("_explanation");
283
284                if (_explAttr != null) {
285                    connector.setToolTipText(_explAttr.getExpression());
286                }
287
288                // NOTE: The preferences mechanism may set this.
289                Token radiusValue = PtolemyPreferences.preferenceValue(relation,
290                        "_linkBendRadius");
291
292                if (radiusValue instanceof DoubleToken) {
293                    double overrideRadius = ((DoubleToken) radiusValue)
294                            .doubleValue();
295                    connector.setBendRadius(overrideRadius);
296                }
297            }
298
299            return connector;
300        }
301    }
302
303    /** A connector target that returns sites on a link. */
304    public static class LinkTarget extends PerimeterTarget {
305        // FindBugs suggests making this class static so as to decrease
306        // the size of instances and avoid dangling references.
307
308        /** Accept the head of the connector.
309         *  @param c The connector.
310         *  @param f The figure.
311         *  @return True if the object is a Port, a Vertex or a Locatable
312         *  contained by a Port and the super class accepts the head.
313         *  Otherwise, return false.
314         */
315        @Override
316        public boolean acceptHead(Connector c, Figure f) {
317            Object object = f.getUserObject();
318
319            boolean isPubSubPort = object instanceof PublisherPort
320                    || object instanceof SubscriberPort;
321
322            if (object instanceof Port && !isPubSubPort
323                    || object instanceof Vertex
324                    || object instanceof Link && c != f
325                    || object instanceof Locatable && ((Locatable) object)
326                            .getContainer() instanceof Port) {
327
328                // It is possible to link with an existing link.
329                // If this existing link has a vertex as head or tail,
330                // we will connect with the vertex, otherwise we will
331                // remove the old link, create a new vertex, link the
332                // head and tail of the existing link with the
333                // vertex and link the new link with the vertex.
334                // We don't allow connecting with with yourself, hence the
335                // test c != f.
336
337                return super.acceptHead(c, f);
338            } else {
339                return false;
340            }
341        }
342
343        /** Accept the tail of the connector.
344         *  @param c The connector.
345         *  @param f The figure.
346         *  @return True if the object is a Port, a Vertex or a Locatable
347         *  contained by a Port and the super class accepts the tail
348         *  Otherwise, return false.
349         */
350        @Override
351        public boolean acceptTail(Connector c, Figure f) {
352            Object object = f.getUserObject();
353
354            boolean isPubSubPort = object instanceof PublisherPort
355                    || object instanceof SubscriberPort;
356
357            if (object instanceof Port && !isPubSubPort
358                    || object instanceof Vertex
359                    || object instanceof Link && c != f
360                    || object instanceof Locatable && ((Locatable) object)
361                            .getContainer() instanceof Port) {
362
363                // It is possible to link with an existing link.
364                // If this existing link has a vertex as head or tail,
365                // we will connect with the vertex, otherwise we will
366                // remove the old link, create a new vertex, link the
367                // head and tail of the existing link with the
368                // vertex and link the new link with the vertex.
369                // We don't allow connecting with with yourself, hence the
370                // test c != f.
371
372                return super.acceptTail(c, f);
373            } else {
374                return false;
375            }
376        }
377
378        /** Get the head site.
379         *  @param f The figure.
380         *  @param x The x location.
381         *  @param y The y location.
382         *  @return The head site.
383         */
384        @Override
385        public Site getHeadSite(Figure f, double x, double y) {
386            if (f instanceof Terminal) {
387                Site site = ((Terminal) f).getConnectSite();
388                return site;
389            } else {
390                return super.getHeadSite(f, x, y);
391            }
392        }
393
394        // Tail sites are the same as head sites.
395    }
396
397    ///////////////////////////////////////////////////////////////////
398    ////                     protected members                     ////
399
400    /** The configuration. */
401    protected Configuration _configuration;
402
403    /** The configure action, which handles edit parameters requests. */
404    protected static ConfigureAction _configureAction = new ConfigureAction(
405            "Configure");
406
407    /** The submenu for configure actions. */
408    protected MenuActionFactory _configureMenuFactory;
409
410    /** The menu creator. */
411    protected MenuCreator _menuCreator;
412
413    /** The factory belonging to the menu creator. */
414    protected PtolemyMenuFactory _menuFactory;
415
416    /** An inner class that handles interactive changes to connectivity.
417     */
418    protected class LinkDropper extends ConnectorAdapter {
419        /**
420         * Called when a connector end is dropped--attach or
421         * detach the edge as appropriate.
422         * @param evt The connector event.
423         */
424        @Override
425        public void connectorDropped(ConnectorEvent evt) {
426            Connector c = evt.getConnector();
427            Figure f = evt.getTarget();
428            Link link = (Link) c.getUserObject();
429            Object node = f == null ? null : f.getUserObject();
430            ActorGraphModel model = (ActorGraphModel) getController()
431                    .getGraphModel();
432
433            switch (evt.getEnd()) {
434            case ConnectorEvent.HEAD_END:
435                if (node == link.getTail()) {
436                    MessageHandler
437                            .error("Cannot link both ends to the same object.");
438                    // FIXME: The panner needs to repaint.  How to get it to do that?
439                    return;
440                }
441                model.getLinkModel().setHead(link, node);
442                break;
443
444            case ConnectorEvent.TAIL_END:
445                if (node == link.getHead()) {
446                    MessageHandler
447                            .error("Cannot link both ends to the same object.");
448                    // FIXME: The panner needs to repaint.  How to get it to do that?
449                    return;
450                }
451                model.getLinkModel().setTail(link, node);
452                break;
453
454            default:
455                throw new IllegalStateException(
456                        "Cannot handle both ends of an edge being dragged.");
457            }
458
459            // Set the width correctly, so we know whether or not it
460            // is connected.  Note that this happens *after* the model
461            // is modified.
462            if (link.getHead() != null && link.getTail() != null) {
463                ((ManhattanConnector) c).setLineWidth((float) 2.0);
464            } else {
465                ((ManhattanConnector) c).setLineWidth((float) 1.0);
466            }
467        }
468    }
469
470    ///////////////////////////////////////////////////////////////////
471    ////                     private members                       ////
472
473    /** a configurable object that allows a different MenuFactory
474     * to be specified instead of the default ptII one.
475     * The MenuFactory constructs the right-click context menus
476     */
477    private static ContextMenuFactoryCreator cmfCreator;
478
479}