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}