001/* Base class for graph controllers for objects that can have icons. 002 003 Copyright (c) 2003-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.event.ActionEvent; 031import java.awt.geom.AffineTransform; 032import java.awt.geom.NoninvertibleTransformException; 033import java.awt.geom.Point2D; 034import java.awt.geom.Rectangle2D; 035 036import javax.swing.Action; 037 038import diva.canvas.Figure; 039import diva.canvas.FigureLayer; 040import diva.graph.GraphException; 041import diva.graph.GraphPane; 042import diva.graph.NodeRenderer; 043import diva.gui.GUIUtilities; 044import diva.gui.toolbox.FigureIcon; 045import ptolemy.actor.IOPort; 046import ptolemy.actor.gui.Configuration; 047import ptolemy.kernel.Entity; 048import ptolemy.kernel.util.InternalErrorException; 049import ptolemy.kernel.util.Location; 050import ptolemy.kernel.util.NamedObj; 051import ptolemy.moml.MoMLChangeRequest; 052import ptolemy.vergil.actor.ExternalIOPortController; 053import ptolemy.vergil.kernel.AttributeController; 054import ptolemy.vergil.toolbox.EditIconAction; 055import ptolemy.vergil.toolbox.FigureAction; 056import ptolemy.vergil.toolbox.MenuActionFactory; 057import ptolemy.vergil.toolbox.RemoveIconAction; 058import ptolemy.vergil.toolbox.SnapConstraint; 059 060/////////////////////////////////////////////////////////////////// 061//// WithIconGraphController 062 063/** 064 A base class for Ptolemy II graph controllers for objects that can have 065 icons. This adds to the base class the context menu items "Edit Custom Icon" 066 and "Remove Custom Icon". This also adds a port controller. 067 068 @author Edward A. Lee 069 @version $Id$ 070 @since Ptolemy II 4.0 071 @Pt.ProposedRating Red (eal) 072 @Pt.AcceptedRating Red (johnr) 073 */ 074public abstract class WithIconGraphController extends BasicGraphController { 075 /** Create a new controller. 076 */ 077 public WithIconGraphController() { 078 super(); 079 } 080 081 /////////////////////////////////////////////////////////////////// 082 //// public methods //// 083 084 /** Get a location for a port that hasn't got a location yet. 085 * @param pane The GraphPane. 086 * @param frame The BasicGraphFrame. 087 * @param _prototype The port. 088 * @return The location. 089 */ 090 static public double[] getNewPortLocation(GraphPane pane, 091 BasicGraphFrame frame, IOPort _prototype) { 092 Point2D center = frame.getCenter(); 093 094 // If we are zoomed in, then place the ports in the canvas 095 // view, not way off yonder. 096 //Rectangle2D visiblePart = frame.getVisibleRectangle(); 097 BasicGraphFrame basicGraphFrame = frame; 098 Rectangle2D visiblePart = basicGraphFrame.getVisibleCanvasRectangle(); 099 100 double[] p; 101 if (_prototype.isInput() && _prototype.isOutput()) { 102 p = _offsetFigure(center.getX(), 103 visiblePart.getY() + visiblePart.getHeight() - _PORT_OFFSET, 104 FigureAction.PASTE_OFFSET * 2, 0, pane, frame); 105 } else if (_prototype.isInput()) { 106 p = _offsetFigure(visiblePart.getX() + _PORT_OFFSET, center.getY(), 107 0, FigureAction.PASTE_OFFSET * 2, pane, frame); 108 } else if (_prototype.isOutput()) { 109 p = _offsetFigure( 110 visiblePart.getX() + visiblePart.getWidth() - _PORT_OFFSET, 111 center.getY(), 0, FigureAction.PASTE_OFFSET * 2, pane, 112 frame); 113 } else { 114 p = _offsetFigure(center.getX(), center.getY(), 115 FigureAction.PASTE_OFFSET * 2, 116 FigureAction.PASTE_OFFSET * 2, pane, frame); 117 } 118 return p; 119 } 120 121 /** Set the configuration. This is used by some of the controllers 122 * when opening files or URLs. 123 * @param configuration The configuration. 124 */ 125 @Override 126 public void setConfiguration(Configuration configuration) { 127 super.setConfiguration(configuration); 128 _portController.setConfiguration(configuration); 129 _editIconAction.setConfiguration(configuration); 130 _removeIconAction.setConfiguration(configuration); 131 } 132 133 /////////////////////////////////////////////////////////////////// 134 //// protected methods //// 135 136 /** Create the controllers for nodes in this graph. 137 * In this base class, a port controller with PARTIAL access is created. 138 * This is called by the constructor, so derived classes that 139 * override this must be careful not to reference local variables 140 * defined in the derived classes, because the derived classes 141 * will not have been fully constructed by the time this is called. 142 */ 143 @Override 144 protected void _createControllers() { 145 super._createControllers(); 146 _portController = new ExternalIOPortController(this, 147 AttributeController.PARTIAL); 148 } 149 150 // NOTE: The following method name does not have a leading underscore 151 // because it is a diva method. 152 153 /** Initialize all interaction on the graph pane. This method 154 * is called by the setGraphPane() method of the superclass. 155 * This initialization cannot be done in the constructor because 156 * the controller does not yet have a reference to its pane 157 * at that time. Regrettably, the canvas is not yet associated 158 * with the GraphPane, so you can't do any initialization that 159 * involves the canvas. 160 */ 161 @Override 162 protected void initializeInteraction() { 163 super.initializeInteraction(); 164 165 //GraphPane pane = getGraphPane(); 166 _menuFactory.addMenuItemFactory(new MenuActionFactory(_editIconAction)); 167 _menuFactory 168 .addMenuItemFactory(new MenuActionFactory(_removeIconAction)); 169 } 170 171 /////////////////////////////////////////////////////////////////// 172 //// private methods //// 173 174 /** Offset a figure if another figure is already at that location. 175 * @param x The x value of the proposed location. 176 * @param y The y value of the proposed location. 177 * @param xOffset The x offset to be used if a figure is found. 178 * @param yOffset The x offset to be used if a figure is found. 179 * @param pane The GraphPane. 180 * @param frame The BasicGraphFrame. 181 * @return An array of two doubles (x and y) that represents either 182 * the original location or an offset location that does not obscure 183 * an object of class <i>figure</i>. 184 */ 185 static private double[] _offsetFigure(double x, double y, double xOffset, 186 double yOffset, GraphPane pane, BasicGraphFrame frame) { 187 FigureLayer foregroundLayer = pane.getForegroundLayer(); 188 189 Rectangle2D visibleRectangle; 190 if (frame != null) { 191 visibleRectangle = frame.getVisibleRectangle(); 192 } else { 193 visibleRectangle = pane.getCanvas().getVisibleSize(); 194 } 195 double[] point = FigureAction.offsetFigure(x, y, xOffset, yOffset, 196 diva.canvas.connector.TerminalFigure.class, foregroundLayer, 197 visibleRectangle); 198 return point; 199 } 200 201 /////////////////////////////////////////////////////////////////// 202 //// protected variables //// 203 204 /** The edit custom icon action. */ 205 protected static final EditIconAction _editIconAction = new EditIconAction(); 206 207 /** The port controller. */ 208 protected NamedObjController _portController; 209 210 /** The remove custom icon action. */ 211 protected static final RemoveIconAction _removeIconAction = new RemoveIconAction(); 212 213 /////////////////////////////////////////////////////////////////// 214 //// private variables //// 215 216 /** Offset of ports from the visible border. */ 217 private static double _PORT_OFFSET = 20.0; 218 219 /////////////////////////////////////////////////////////////////// 220 //// inner classes //// 221 222 /////////////////////////////////////////////////////////////////// 223 //// NewPortAction 224 225 /** An action to create a new port. */ 226 @SuppressWarnings("serial") 227 public class NewPortAction extends FigureAction { 228 /** Create a new port that has the same input, output, and 229 * multiport properties as the specified port. If the specified 230 * port is null, then a new port that is neither an input, an 231 * output, nor a multiport will be created. 232 * @param prototype Prototype port. 233 * @param description The description used for menu entries and 234 * tooltips. 235 * @param mnemonicKey The KeyEvent field for the mnemonic key to 236 * use in the menu. 237 */ 238 public NewPortAction(IOPort prototype, String description, 239 int mnemonicKey) { 240 // null as the fourth arg means get the figure from the 241 // _portController 242 this(prototype, description, mnemonicKey, null); 243 } 244 245 /** Create a new port that has the same input, output, and 246 * multiport properties as the specified port and has icons 247 * associated with being unselected, rollover, rollover 248 * selected, and selected. If the specified port is null, 249 * then a new port that is neither an input, an output, nor a 250 * multiport will be created. 251 * 252 * @param prototype Prototype port. 253 * @param description The description used for menu entries and 254 * tooltips. 255 * @param mnemonicKey The KeyEvent field for the mnemonic key to 256 * use in the menu. 257 * @param iconRoles A matrix of Strings, where each element 258 * consists of two Strings, the absolute URL of the icon 259 * and the key that represents the role of the icon. The keys 260 * are usually static fields from this class, such as 261 * {@link diva.gui.GUIUtilities#LARGE_ICON}, 262 * {@link diva.gui.GUIUtilities#ROLLOVER_ICON}, 263 * {@link diva.gui.GUIUtilities#ROLLOVER_SELECTED_ICON} or 264 * {@link diva.gui.GUIUtilities#SELECTED_ICON}. 265 * If this parameter is null, then the icon comes from 266 * the calling getNodeRenderer() on the {@link #_portController}. 267 * @see diva.gui.GUIUtilities#addIcons(Action, String[][]) 268 */ 269 public NewPortAction(IOPort prototype, String description, 270 int mnemonicKey, String[][] iconRoles) { 271 super(description); 272 _prototype = prototype; 273 274 if (iconRoles != null) { 275 GUIUtilities.addIcons(this, iconRoles); 276 } else { 277 // Creating the renderers this way is rather nasty.. 278 // Standard toolbar icons are 25x25 pixels. 279 NodeRenderer renderer = _portController.getNodeRenderer(); 280 281 Object location = null; 282 283 if (_prototype != null) { 284 location = _prototype.getAttribute("_location"); 285 } 286 287 Figure figure = renderer.render(location); 288 289 FigureIcon icon = new FigureIcon(figure, 25, 25, 1, true); 290 putValue(GUIUtilities.LARGE_ICON, icon); 291 } 292 putValue("tooltip", description); 293 putValue(GUIUtilities.MNEMONIC_KEY, Integer.valueOf(mnemonicKey)); 294 } 295 296 /** Create a new port. */ 297 @Override 298 public void actionPerformed(ActionEvent e) { 299 super.actionPerformed(e); 300 301 double x; 302 double y; 303 304 if (getSourceType() == TOOLBAR_TYPE 305 || getSourceType() == MENUBAR_TYPE) { 306 // No location in the action, so put it in the middle. 307 BasicGraphFrame frame = WithIconGraphController.this.getFrame(); 308 GraphPane pane = getGraphPane(); 309 310 if (frame != null) { 311 if (_prototype != null) { 312 // Put in the middle of the visible part. 313 double[] p = getNewPortLocation(pane, frame, 314 _prototype); 315 x = p[0]; 316 y = p[1]; 317 318 } else { 319 // Put in the middle of the visible part. 320 Point2D center = frame.getCenter(); 321 322 x = center.getX(); 323 y = center.getY(); 324 } 325 } else { 326 // Put in the middle of the pane. 327 Point2D center = pane.getSize(); 328 x = center.getX() / 2; 329 y = center.getY() / 2; 330 } 331 } else { 332 // Transform 333 AffineTransform current = getGraphPane().getTransformContext() 334 .getTransform(); 335 AffineTransform inverse; 336 337 try { 338 inverse = current.createInverse(); 339 } catch (NoninvertibleTransformException ex) { 340 throw new RuntimeException(ex.toString()); 341 } 342 343 Point2D point = new Point2D.Double(getX(), getY()); 344 345 inverse.transform(point, point); 346 x = point.getX(); 347 y = point.getY(); 348 } 349 350 AbstractBasicGraphModel graphModel = (AbstractBasicGraphModel) getGraphModel(); 351 final double[] point = SnapConstraint.constrainPoint(x, y); 352 final NamedObj toplevel = graphModel.getPtolemyModel(); 353 354 if (!(toplevel instanceof Entity)) { 355 throw new InternalErrorException( 356 "Cannot invoke NewPortAction on an object " 357 + "that is not an instance of Entity."); 358 } 359 360 String name = "port"; 361 if (_prototype != null) { 362 if (_prototype.isInput() && !_prototype.isOutput()) { 363 name = "in"; 364 } 365 if (!_prototype.isInput() && _prototype.isOutput()) { 366 name = "out"; 367 } 368 } 369 370 final String portName = toplevel.uniqueName(name); 371 final String locationName = "_location"; 372 373 // Create the port. 374 StringBuffer moml = new StringBuffer(); 375 moml.append("<port name=\"" + portName + "\">\n"); 376 moml.append("<property name=\"" + locationName 377 + "\" class=\"ptolemy.kernel.util.Location\"/>\n"); 378 379 if (_prototype != null) { 380 if (_prototype.isInput()) { 381 moml.append("<property name=\"input\"/>"); 382 } 383 384 if (_prototype.isOutput()) { 385 moml.append("<property name=\"output\"/>"); 386 } 387 388 if (_prototype.isMultiport()) { 389 // Set the width of the multiport to -1 so that the width is inferred. 390 // See ptolemy/actor/lib/test/auto/VectorDisassemblerComposite.xml 391 moml.append( 392 "<property name=\"width\" class=\"ptolemy.data.expr.Parameter\" value=\"-1\"/>"); 393 moml.append("<property name=\"multiport\"/>"); 394 } 395 } 396 397 moml.append("</port>"); 398 399 MoMLChangeRequest request = new MoMLChangeRequest(this, toplevel, 400 moml.toString()) { 401 @Override 402 protected void _execute() throws Exception { 403 super._execute(); 404 405 // Set the location of the icon. 406 // Note that this really needs to be done after 407 // the change request has succeeded, which is why 408 // it is done here. When the graph controller 409 // gets around to handling this, it will draw 410 // the icon at this location. 411 // NOTE: The cast is safe because it is checked 412 // above, and presumably a reasonable GUI would 413 // provide no mechanism for creating a port on 414 // something that is not an entity. 415 NamedObj newObject = ((Entity) toplevel).getPort(portName); 416 Location location = (Location) newObject 417 .getAttribute(locationName); 418 location.setLocation(point); 419 } 420 }; 421 422 request.setUndoable(true); 423 toplevel.requestChange(request); 424 425 try { 426 request.waitForCompletion(); 427 } catch (Exception ex) { 428 ex.printStackTrace(); 429 throw new GraphException(ex); 430 } 431 } 432 433 private IOPort _prototype; 434 435 /** Offset a figure if another figure is already at that location. 436 * @param x The x value of the proposed location. 437 * @param y The y value of the proposed location. 438 * @param xOffset The x offset to be used if a figure is found. 439 * @param yOffset The x offset to be used if a figure is found. 440 * @return An array of two doubles (x and y) that represents either 441 * the original location or an offset location that does not obscure 442 * an object of class <i>figure</i>. 443 */ 444 protected double[] _offsetFigure(double x, double y, double xOffset, 445 double yOffset) { 446 447 double[] point = WithIconGraphController._offsetFigure(x, y, 448 xOffset, yOffset, getGraphPane(), 449 WithIconGraphController.this.getFrame()); 450 return point; 451 } 452 } 453}