001/* The edge controller for transitions in an FSM.
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.modal;
029
030import java.awt.BasicStroke;
031import java.awt.Color;
032import java.awt.Font;
033import java.awt.Stroke;
034import java.awt.Toolkit;
035import java.awt.event.ActionEvent;
036import java.awt.event.KeyEvent;
037
038import javax.swing.KeyStroke;
039
040import diva.canvas.Figure;
041import diva.canvas.Site;
042import diva.canvas.connector.ArcConnector;
043import diva.canvas.connector.ArcManipulator;
044import diva.canvas.connector.Arrowhead;
045import diva.canvas.connector.Blob;
046import diva.canvas.connector.Connector;
047import diva.canvas.connector.ConnectorAdapter;
048import diva.canvas.connector.ConnectorEvent;
049import diva.canvas.connector.ConnectorManipulator;
050import diva.canvas.connector.ConnectorTarget;
051import diva.canvas.connector.PerimeterTarget;
052import diva.canvas.event.MouseFilter;
053import diva.canvas.interactor.ActionInteractor;
054import diva.canvas.interactor.SelectionInteractor;
055import diva.canvas.interactor.SelectionModel;
056import diva.canvas.toolbox.LabelFigure;
057import diva.graph.BasicEdgeController;
058import diva.graph.EdgeRenderer;
059import diva.graph.GraphController;
060import diva.graph.JGraph;
061import diva.gui.GUIUtilities;
062import diva.gui.toolbox.MenuCreator;
063import ptolemy.actor.TypedActor;
064import ptolemy.actor.gui.Configuration;
065import ptolemy.data.DoubleToken;
066import ptolemy.domains.modal.kernel.State;
067import ptolemy.domains.modal.kernel.Transition;
068import ptolemy.kernel.ComponentRelation;
069import ptolemy.kernel.Entity;
070import ptolemy.kernel.util.IllegalActionException;
071import ptolemy.kernel.util.Locatable;
072import ptolemy.kernel.util.NameDuplicationException;
073import ptolemy.kernel.util.NamedObj;
074import ptolemy.moml.MoMLChangeRequest;
075import ptolemy.util.MessageHandler;
076import ptolemy.util.StringUtilities;
077import ptolemy.vergil.basic.PopupMouseFilter;
078import ptolemy.vergil.kernel.Link;
079import ptolemy.vergil.toolbox.ConfigureAction;
080import ptolemy.vergil.toolbox.FigureAction;
081import ptolemy.vergil.toolbox.MenuActionFactory;
082import ptolemy.vergil.toolbox.PtolemyMenuFactory;
083
084///////////////////////////////////////////////////////////////////
085//// TransitionController
086
087/**
088 This class provides interaction techniques for transitions in an FSM.
089
090 @author Steve Neuendorffer, Contributor: Edward A. Lee
091 @version $Id$
092 @since Ptolemy II 8.0
093 @Pt.ProposedRating Red (eal)
094 @Pt.AcceptedRating Red (johnr)
095 */
096public class TransitionController extends BasicEdgeController {
097    /** Create a transition controller associated with the specified graph
098     *  controller.
099     *  @param controller The associated graph controller.
100     */
101    public TransitionController(final GraphController controller) {
102        super(controller);
103
104        SelectionModel sm = controller.getSelectionModel();
105        SelectionInteractor interactor = (SelectionInteractor) getEdgeInteractor();
106        interactor.setSelectionModel(sm);
107
108        // Create and set up the manipulator for connectors.
109        // This overrides the manipulator created by the base class.
110        ConnectorManipulator manipulator = new ArcManipulator();
111        manipulator.setSnapHalo(4.0);
112        manipulator.addConnectorListener(new LinkDropper());
113        interactor.setPrototypeDecorator(manipulator);
114
115        // The mouse filter needs to accept regular click or control click
116        MouseFilter handleFilter = new MouseFilter(1, 0, 0);
117        manipulator.setHandleFilter(handleFilter);
118
119        ConnectorTarget ct = new LinkTarget();
120        setConnectorTarget(ct);
121        _createEdgeRenderer();
122
123        _menuCreator = new MenuCreator(null);
124        _menuCreator.setMouseFilter(new PopupMouseFilter());
125        interactor.addInteractor(_menuCreator);
126
127        // The contents of the menu is determined by the associated
128        // menu factory, which is a protected member of this class.
129        // Derived classes can add menu items to it.
130        _menuFactory = new PtolemyMenuFactory(controller);
131        _configureMenuFactory = new MenuActionFactory(_configureAction);
132        _menuFactory.addMenuItemFactory(_configureMenuFactory);
133        _menuCreator.setMenuFactory(_menuFactory);
134
135        // Add a double click interactor.
136        ActionInteractor doubleClickInteractor = new ActionInteractor(
137                _configureAction);
138        doubleClickInteractor.setConsuming(false);
139        doubleClickInteractor.setMouseFilter(new MouseFilter(1, 0, 0, 2));
140
141        interactor.addInteractor(doubleClickInteractor);
142
143        _setUpLookInsideAction();
144    }
145
146    ///////////////////////////////////////////////////////////////////
147    ////                         public methods                    ////
148
149    /** Add hot keys to the actions in the given JGraph.
150     *   It would be better that this method was added higher in the hierarchy. Now
151     *   most controllers
152     *  @param jgraph The JGraph to which hot keys are to be added.
153     */
154    public void addHotKeys(JGraph jgraph) {
155        GUIUtilities.addHotKey(jgraph, _lookInsideAction);
156    }
157
158    /** Set the configuration.  This is may be used by derived controllers
159     *  to open files or URLs.
160     *  @param configuration The configuration.
161     */
162    public void setConfiguration(Configuration configuration) {
163        _configuration = configuration;
164
165        _setUpLookInsideAction();
166    }
167
168    ///////////////////////////////////////////////////////////////////
169    ////                  public inner classes                     ////
170
171    /** Render a link.
172     */
173    public static class LinkRenderer implements EdgeRenderer {
174        /** Render a visual representation of the given edge. */
175        @Override
176        public Connector render(Object edge, Site tailSite, Site headSite) {
177
178            ArcConnector c = new KielerLayoutArcConnector(tailSite, headSite);
179            c.setLineWidth((float) 2.0);
180            c.setUserObject(edge);
181
182            Link link = (Link) edge;
183            Transition transition = (Transition) link.getRelation();
184
185            // When first dragging out a transition, the relation
186            // may still be null.
187            if (transition != null) {
188                boolean isHistory = false;
189                try {
190                    isHistory = transition.isHistory();
191                } catch (IllegalActionException e2) {
192                    // Ignore and render as a non-history transition.
193                }
194                if (isHistory) {
195                    Blob blob = new Blob(0, 0, 0, Blob.ARROW_CIRCLE_H, 6.0,
196                            Color.white);
197                    c.setHeadEnd(blob);
198                } else {
199                    Arrowhead arrowhead = new Arrowhead();
200                    c.setHeadEnd(arrowhead);
201                }
202
203                try {
204                    if (transition.isPreemptive()
205                            && !transition.isImmediate()) {
206                        Blob blob = new Blob(0, 0, 0, Blob.BLOB_CIRCLE, 4.0,
207                                Color.red);
208                        blob.setFilled(true);
209                        c.setTailEnd(blob);
210                    } else if (transition.isImmediate()
211                            && !transition.isPreemptive()) {
212                        Blob blob = new Blob(0, 0, 0, Blob.BLOB_DIAMOND, 5.0,
213                                Color.red);
214                        blob.setFilled(true);
215                        c.setTailEnd(blob);
216                    } else if (transition.isImmediate()
217                            && transition.isPreemptive()) {
218                        Blob blob = new Blob(0, 0, 0, Blob.BLOB_CIRCLE_DIAMOND,
219                                5.0, Color.red);
220                        blob.setFilled(true);
221                        c.setTailEnd(blob);
222                    } else if (transition.isErrorTransition()) {
223                        Blob blob = new Blob(0, 0, 0, Blob.STAR, 5.0,
224                                Color.red);
225                        blob.setFilled(true);
226                        c.setTailEnd(blob);
227                    } else if (transition.isTermination()) {
228                        Blob blob = new Blob(0, 0, 0, Blob.TRIANGLE, 5.0,
229                                Color.green);
230                        blob.setFilled(true);
231                        c.setTailEnd(blob);
232                    }
233                } catch (IllegalActionException ex) {
234                    Blob blob = new Blob(0, 0, 0, Blob.ERROR, 5.0, Color.red);
235                    blob.setFilled(true);
236                    c.setTailEnd(blob);
237                }
238                if (transition.isNondeterministic()) {
239                    c.setStrokePaint(Color.RED);
240                }
241                try {
242                    if (transition.isDefault()) {
243                        float[] dashvalues = new float[2];
244                        dashvalues[0] = (float) 2.0;
245                        dashvalues[1] = (float) 2.0;
246                        Stroke dashed = new BasicStroke(1.0f,
247                                BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0,
248                                dashvalues, 0);
249                        c.setStroke(dashed);
250                    }
251                } catch (IllegalActionException e) {
252                    // Ignore and don't render dashed line if default parameter fails to evaluate.
253                }
254
255                try {
256                    TypedActor[] refinements = transition.getRefinement();
257                    if (refinements != null && refinements.length > 0) {
258                        c.setLineWidth(4.0f);
259                    }
260                } catch (IllegalActionException e1) {
261                    // Ignore. Unable to get refinement.
262                }
263
264                c.setToolTipText(transition.getName());
265
266                String labelStr = transition.getLabel();
267
268                try {
269                    double exitAngle = ((DoubleToken) transition.exitAngle
270                            .getToken()).doubleValue();
271
272                    // If the angle is too large, then truncate it to
273                    // a reasonable value.
274                    double maximum = 99.0 * Math.PI;
275
276                    if (exitAngle > maximum) {
277                        exitAngle = maximum;
278                    } else if (exitAngle < -maximum) {
279                        exitAngle = -maximum;
280                    }
281
282                    // If the angle is zero, then the link does not get
283                    // drawn.  So we restrict it so that it can't quite
284                    // go to zero.
285                    double minimum = Math.PI / 999.0;
286
287                    if (exitAngle < minimum && exitAngle > -minimum) {
288                        if (exitAngle > 0.0) {
289                            exitAngle = minimum;
290                        } else {
291                            exitAngle = -minimum;
292                        }
293                    }
294
295                    c.setAngle(exitAngle);
296
297                    // Set the gamma angle
298                    double gamma = ((DoubleToken) transition.gamma.getToken())
299                            .doubleValue();
300                    c.setGamma(gamma);
301                } catch (IllegalActionException ex) {
302                    // Ignore, accepting the default.
303                    // This exception should not occur.
304                }
305
306                if (!labelStr.equals("")) {
307                    // FIXME: get label position modifier, if any.
308                    LabelFigure label = new LabelFigure(labelStr, _labelFont);
309                    label.setFillPaint(Color.black);
310                    c.setLabelFigure(label);
311                }
312            }
313
314            return c;
315        }
316    }
317
318    /** A Link target.
319     */
320    public static class LinkTarget extends PerimeterTarget {
321        @Override
322        public boolean acceptHead(Connector c, Figure f) {
323            Object object = f.getUserObject();
324
325            if (object instanceof Locatable) {
326                Locatable location = (Locatable) object;
327
328                if (location.getContainer() instanceof Entity) {
329                    return true;
330                } else {
331                    return false;
332                }
333            }
334
335            return false;
336        }
337
338        @Override
339        public boolean acceptTail(Connector c, Figure f) {
340            return acceptHead(c, f);
341        }
342    }
343
344    ///////////////////////////////////////////////////////////////////
345    ////                         protected methods                 ////
346
347    /** Create an edge renderer specifically for instances of Transition.
348     */
349    protected void _createEdgeRenderer() {
350        setEdgeRenderer(new LinkRenderer());
351    }
352
353    /** Open the instance or the model.  In this base class, the default
354     *  look inside action is to open the model.  In derived classes such
355     *  as the Ptera editor, the default action is to open the instance.
356     *  @param configuration  The configuration with which to open the model or instance.
357     *  @param refinement The model or instance to open.
358     *  @exception IllegalActionException If constructing an effigy or tableau
359     *   fails.
360     *  @exception NameDuplicationException If a name conflict occurs (this
361     *   should not be thrown).
362     *  @see ptolemy.actor.gui.Configuration#openInstance(NamedObj)
363     *  @see ptolemy.actor.gui.Configuration#openModel(NamedObj)
364     */
365    protected void _openInstanceOrModel(Configuration configuration,
366            NamedObj refinement)
367            throws IllegalActionException, NameDuplicationException {
368        configuration.openModel(refinement);
369    }
370
371    /** Set up look inside actions, if appropriate.
372     */
373    protected void _setUpLookInsideAction() {
374        if (_configuration != null && _lookInsideActionFactory == null) {
375            _lookInsideActionFactory = new MenuActionFactory(_lookInsideAction);
376            // NOTE: The following requires that the configuration be
377            // non-null, or it will report an error.
378            _menuFactory.addMenuItemFactory(_lookInsideActionFactory);
379        }
380    }
381
382    ///////////////////////////////////////////////////////////////////
383    ////                     protected members                     ////
384
385    /** The configuration. */
386    protected Configuration _configuration;
387
388    /** The configure action, which handles edit parameters requests. */
389    protected static ConfigureAction _configureAction = new ConfigureAction(
390            "Configure");
391
392    /** The submenu for configure actions. */
393    protected MenuActionFactory _configureMenuFactory;
394
395    /** The action that handles look inside. */
396    protected LookInsideAction _lookInsideAction = new LookInsideAction();
397
398    /** The menu factory for _lookInsideAction. null if the factory has not been
399    added to the context menu. */
400    protected MenuActionFactory _lookInsideActionFactory;
401
402    /** The menu creator. */
403    protected MenuCreator _menuCreator;
404
405    /** The factory belonging to the menu creator. */
406    protected PtolemyMenuFactory _menuFactory;
407
408    ///////////////////////////////////////////////////////////////////
409    ////               protected inner classes                     ////
410
411    /** An inner class that handles interactive changes to connectivity. */
412    protected class LinkDropper extends ConnectorAdapter {
413        /** Called when a connector end is dropped.  Attach or
414         *  detach the edge as appropriate.
415         */
416        @Override
417        public void connectorDropped(ConnectorEvent evt) {
418            Connector c = evt.getConnector();
419            Figure f = evt.getTarget();
420            Object edge = c.getUserObject();
421            Object node = f == null ? null : f.getUserObject();
422            FSMGraphModel model = (FSMGraphModel) getController()
423                    .getGraphModel();
424
425            switch (evt.getEnd()) {
426            case ConnectorEvent.HEAD_END:
427                model.getArcModel().setHead(edge, node);
428                break;
429
430            case ConnectorEvent.TAIL_END:
431                model.getArcModel().setTail(edge, node);
432                break;
433
434            case ConnectorEvent.MIDPOINT:
435                break;
436
437            default:
438                throw new IllegalStateException(
439                        "Cannot handle both ends of an edge being dragged.");
440            }
441
442            // Make the link rerender itself so that geometry is preserved
443            Link link = (Link) edge;
444            ComponentRelation transition = link.getRelation();
445
446            if (transition != null && c instanceof ArcConnector) {
447                double angle = ((ArcConnector) c).getAngle();
448                double gamma = ((ArcConnector) c).getGamma();
449
450                // Set the new exitAngle and gamma parameter values based
451                // on the current link. These will be created if they
452                // don't already exist.
453                String moml = "<group><property name=\"exitAngle\" value=\""
454                        + angle + "\" class=\"ptolemy.data.expr.Parameter\"/>"
455                        + "<property name=\"gamma\" value=\"" + gamma
456                        + "\" class=\"ptolemy.data.expr.Parameter\"/></group>";
457                MoMLChangeRequest request = new MoMLChangeRequest(this,
458                        transition, moml);
459                transition.requestChange(request);
460            }
461
462            // rerender the edge.  This is necessary for several reasons.
463            // First, the edge is only associated with a relation after it
464            // is fully connected.  Second, edges that aren't
465            // connected should be erased (which this will rather
466            // conveniently take care of for us).
467            getController().rerenderEdge(edge);
468        }
469    }
470
471    ///////////////////////////////////////////////////////////////////
472    ////                         inner classes                     ////
473
474    /** An action to look inside a transition at its refinement, if it has one.
475     *  NOTE: This requires that the configuration be non null, or it
476     *  will report an error with a fairly cryptic message.
477     */
478    @SuppressWarnings("serial")
479    private class LookInsideAction extends FigureAction {
480        public LookInsideAction() {
481            super("Look Inside");
482
483            // If we are in an applet, so Control-L or Command-L will
484            // be caught by the browser as "Open Location", so we don't
485            // supply Control-L or Command-L as a shortcut under applets.
486            if (!StringUtilities.inApplet()) {
487                // For some inexplicable reason, the I key doesn't work here.
488                // So we use L.
489                putValue(GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(
490                        KeyEvent.VK_L,
491                        Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
492            }
493        }
494
495        @Override
496        public void actionPerformed(ActionEvent e) {
497            if (_configuration == null) {
498                MessageHandler
499                        .error("Cannot look inside without a configuration.");
500                return;
501            }
502
503            try {
504                super.actionPerformed(e);
505
506                NamedObj target = getTarget();
507
508                // If the target is not an instance of
509                // State or Transition, do nothing.
510
511                TypedActor[] refinements = null;
512
513                if (target instanceof Transition) {
514                    refinements = ((Transition) target).getRefinement();
515                } else if (target instanceof State) {
516                    refinements = ((State) target).getRefinement();
517                }
518
519                if (refinements != null && refinements.length > 0) {
520                    for (TypedActor refinement : refinements) {
521                        // Open each refinement.
522                        // Derived classes may open the instance, this class opens the model.
523                        _openInstanceOrModel(_configuration,
524                                (NamedObj) refinement);
525                    }
526                } else {
527                    MessageHandler.error("No refinement.");
528                }
529            } catch (Exception ex) {
530                MessageHandler.error("Look inside failed: ", ex);
531            }
532        }
533    }
534
535    ///////////////////////////////////////////////////////////////////
536    ////                         private variables                 ////
537
538    private static Font _labelFont = new Font("SansSerif", Font.PLAIN, 10);
539}