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}