/* A simple graph view for Ptolemy models Copyright (c) 1998-2005 The Regents of the University of California. All rights reserved. Permission is hereby granted, without written agreement and without license or royalty fees, to use, copy, modify, and distribute this software and its documentation for any purpose, provided that the above copyright notice and the following two paragraphs appear in all copies of this software. IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. PT_COPYRIGHT_VERSION_2 COPYRIGHTENDKEY 2 */ package ptolemy.vergil.basic; import ptolemy.actor.IOPort; import ptolemy.actor.IORelation; import ptolemy.actor.TypedCompositeActor; import ptolemy.actor.gui.Configuration; import ptolemy.actor.gui.PtolemyEffigy; import ptolemy.actor.gui.PtolemyFrame; import ptolemy.actor.gui.RunTableau; import ptolemy.actor.gui.SizeAttribute; import ptolemy.actor.gui.Tableau; import ptolemy.actor.gui.WindowPropertiesAttribute; import ptolemy.data.ArrayToken; import ptolemy.data.DoubleToken; import ptolemy.data.Token; import ptolemy.data.expr.ExpertParameter; import ptolemy.data.expr.Parameter; import ptolemy.kernel.ComponentEntity; import ptolemy.kernel.CompositeEntity; import ptolemy.kernel.Entity; import ptolemy.kernel.undo.RedoChangeRequest; import ptolemy.kernel.undo.UndoChangeRequest; import ptolemy.kernel.undo.UndoStackAttribute; import ptolemy.kernel.util.Attribute; import ptolemy.kernel.util.ChangeListener; import ptolemy.kernel.util.ChangeRequest; import ptolemy.kernel.util.IllegalActionException; import ptolemy.kernel.util.InternalErrorException; import ptolemy.kernel.util.KernelException; import ptolemy.kernel.util.Locatable; import ptolemy.kernel.util.Location; import ptolemy.kernel.util.NamedObj; import ptolemy.kernel.util.Settable; import ptolemy.kernel.util.Workspace; import ptolemy.moml.LibraryAttribute; import ptolemy.moml.MoMLChangeRequest; import ptolemy.moml.MoMLUndoEntry; import ptolemy.util.CancelException; import ptolemy.util.MessageHandler; import ptolemy.util.StringUtilities; import ptolemy.vergil.toolbox.MenuItemFactory; import ptolemy.vergil.toolbox.MoveAction; import ptolemy.vergil.tree.EntityTreeModel; import ptolemy.vergil.tree.PTree; import ptolemy.vergil.tree.PTreeMenuCreator; import ptolemy.vergil.tree.VisibleTreeModel; import diva.canvas.CanvasUtilities; import diva.canvas.Figure; import diva.canvas.JCanvas; import diva.canvas.Site; import diva.canvas.connector.FixedNormalSite; import diva.canvas.connector.Terminal; import diva.canvas.event.LayerAdapter; import diva.canvas.event.LayerEvent; import diva.canvas.interactor.SelectionModel; import diva.graph.GraphController; import diva.graph.GraphEvent; import diva.graph.GraphModel; import diva.graph.GraphPane; import diva.graph.GraphUtilities; import diva.graph.JGraph; import diva.graph.basic.BasicLayoutTarget; import diva.graph.layout.LayoutTarget; import diva.graph.layout.LevelLayout; import diva.gui.GUIUtilities; import diva.gui.toolbox.JCanvasPanner; import diva.gui.toolbox.JContextMenu; import diva.util.java2d.ShapeUtilities; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Event; import java.awt.Frame; import java.awt.Graphics; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.ClipboardOwner; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.print.PageFormat; import java.awt.print.Printable; import java.awt.print.PrinterException; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.net.URL; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JToolBar; import javax.swing.JTree; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; ////////////////////////////////////////////////////////////////////////// //// BasicGraphFrame /** A simple graph view for ptolemy models. This represents a level of the hierarchy of a ptolemy model as a diva graph. Cut, copy and paste operations are supported using MoML. @author Steve Neuendorffer, Edward A. Lee, Contributor: Chad Berkeley (Kepler) @version $Id$ @since Ptolemy II 2.0 @Pt.ProposedRating Red (neuendor) @Pt.AcceptedRating Red (johnr) */ public abstract class BasicGraphFrame extends PtolemyFrame implements Printable, ClipboardOwner, ChangeListener { /** Construct a frame associated with the specified Ptolemy II model * or object. After constructing this, it is necessary * to call setVisible(true) to make the frame appear. * This is typically done by calling show() on the controlling tableau. * This constructor results in a graph frame that obtains its library * either from the model (if it has one) or the default library defined * in the configuration. * @see Tableau#show() * @param entity The model or object to put in this frame. * @param tableau The tableau responsible for this frame. */ public BasicGraphFrame(NamedObj entity, Tableau tableau) { this(entity, tableau, null); } /** Construct a frame associated with the specified Ptolemy II model. * After constructing this, it is necessary * to call setVisible(true) to make the frame appear. * This is typically done by calling show() on the controlling tableau. * This constructor results in a graph frame that obtains its library * either from the model (if it has one), or the defaultLibrary * argument (if it is non-null), or the default library defined * in the configuration. * @see Tableau#show() * @param entity The model or object to put in this frame. * @param tableau The tableau responsible for this frame. * @param defaultLibrary An attribute specifying the default library * to use if the model does not have a library. */ public BasicGraphFrame(NamedObj entity, Tableau tableau, LibraryAttribute defaultLibrary) { super(entity, tableau); entity.addChangeListener(this); getContentPane().setLayout(new BorderLayout()); GraphPane pane = _createGraphPane(); pane.getForegroundLayer().setPickHalo(2); _jgraph = new JGraph(pane); _dropTarget = new EditorDropTarget(_jgraph); ActionListener deletionListener = new ActionListener() { /** Delete any nodes or edges from the graph that are * currently selected. In addition, delete any edges * that are connected to any deleted nodes. */ public void actionPerformed(ActionEvent e) { delete(); } }; _jgraph.registerKeyboardAction(deletionListener, "Delete", KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); _jgraph.registerKeyboardAction(deletionListener, "BackSpace", KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); _jgraph.setRequestFocusEnabled(true); pane.getForegroundEventLayer().setConsuming(false); pane.getForegroundEventLayer().setEnabled(true); pane.getForegroundEventLayer().addLayerListener(new LayerAdapter() { /** Invoked when the mouse is pressed on a layer * or figure. */ public void mousePressed(LayerEvent event) { Component component = event.getComponent(); if (!component.hasFocus()) { component.requestFocus(); } } }); // We used to do this, but it would result in context menus // getting lost on the mac. // _jgraph.addMouseListener(new FocusMouseListener()); _jgraph.setAlignmentX(1); _jgraph.setAlignmentY(1); _jgraph.setBackground(BACKGROUND_COLOR); try { // The SizeAttribute property is used to specify the size // of the JGraph component. Unfortunately, with Swing's // mysterious and undocumented handling of component sizes, // there appears to be no way to control the size of the // JGraph from the size of the Frame, which is specified // by the WindowPropertiesAttribute. SizeAttribute size = (SizeAttribute) getModel().getAttribute("_vergilSize", SizeAttribute.class); if (size != null) { size.setSize(_jgraph); } else { // Set the default size. // Note that the location is of the frame, while the size // is of the scrollpane. _jgraph.setMinimumSize(new Dimension(200, 200)); _jgraph.setPreferredSize(new Dimension(600, 400)); _jgraph.setSize(600, 400); } // Set the zoom factor. Parameter zoom = (Parameter) getModel().getAttribute("_vergilZoomFactor", Parameter.class); if (zoom != null) { zoom(((DoubleToken) zoom.getToken()).doubleValue()); // Make sure the visibility is only expert. zoom.setVisibility(Settable.EXPERT); } // Set the pan position. Parameter pan = (Parameter) getModel().getAttribute("_vergilCenter", Parameter.class); if (pan != null) { ArrayToken panToken = (ArrayToken) pan.getToken(); Point2D center = new Point2D.Double(((DoubleToken) panToken .getElement(0)).doubleValue(), ((DoubleToken) panToken.getElement(1)).doubleValue()); setCenter(center); // Make sure the visibility is only expert. pan.setVisibility(Settable.EXPERT); } } catch (Throwable throwable) { // Ignore problems here. Errors simply result in a default // size and location. } // Create the panner. _graphPanner = new JCanvasPanner(_jgraph); _graphPanner.setPreferredSize(new Dimension(200, 150)); _graphPanner.setMaximumSize(new Dimension(200, 150)); _graphPanner.setSize(200, 150); // NOTE: Border causes all kinds of problems! // _graphPanner.setBorder(BorderFactory.createEtchedBorder()); // Create the library of actors, or use the one in the entity, // if there is one. // FIXME: How do we make changes to the library persistent? boolean gotLibrary = false; try { LibraryAttribute libraryAttribute = (LibraryAttribute) entity .getAttribute("_library", LibraryAttribute.class); if (libraryAttribute != null) { // The model contains a library. try { _topLibrary = libraryAttribute.getLibrary(); gotLibrary = true; } catch (SecurityException ex) { System.out.println("Warning: failed to parse " + "_library attribute (running in an applet " + "or sandbox always causes this)"); } } } catch (Exception ex) { try { MessageHandler.warning("Invalid library in the model.", ex); } catch (CancelException e) { } } if (!gotLibrary) { try { if (defaultLibrary != null) { // A default library has been specified. _topLibrary = defaultLibrary.getLibrary(); gotLibrary = true; } } catch (SecurityException ex) { // Ignore, we are in an applet or sandbox. // We already printed a message, why print it again? } catch (Exception ex) { try { MessageHandler.warning("Invalid default library for the frame.", ex); } catch (CancelException e) { } } } if (!gotLibrary) { // Neither the model nor the argument have specified a library. // See if there is a default library in the configuration. _topLibrary = _createDefaultLibrary(entity.workspace()); } _libraryModel = new VisibleTreeModel(_topLibrary); _library = new PTree(_libraryModel); _library.setRootVisible(false); _library.setBackground(BACKGROUND_COLOR); // If you want to expand the top-level libraries, uncomment this. /* Object[] path = new Object[2]; path[0] = topLibrary; Iterator libraries = topLibrary.entityList().iterator(); while (libraries.hasNext()) { path[1] = libraries.next(); _library.expandPath(new TreePath(path)); } */ _libraryContextMenuCreator = new PTreeMenuCreator(); _libraryContextMenuCreator.addMenuItemFactory(new OpenLibraryMenuItemFactory()); _library.addMouseListener(_libraryContextMenuCreator); _libraryScrollPane = new JScrollPane(_library); _libraryScrollPane.setMinimumSize(new Dimension(200, 200)); _libraryScrollPane.setPreferredSize(new Dimension(200, 200)); // create the palette on the left. _palettePane = new JPanel(); _palettePane.setBorder(null); _palettePane.setLayout(new BoxLayout(_palettePane, BoxLayout.Y_AXIS)); _palettePane.add(_libraryScrollPane, BorderLayout.CENTER); _palettePane.add(_graphPanner, BorderLayout.SOUTH); _splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true); _splitPane.setLeftComponent(_palettePane); _splitPane.setRightComponent(_jgraph); getContentPane().add(_splitPane, BorderLayout.CENTER); _toolbar = new JToolBar(); getContentPane().add(_toolbar, BorderLayout.NORTH); GUIUtilities.addToolBarButton(_toolbar, _zoomInAction); GUIUtilities.addToolBarButton(_toolbar, _zoomResetAction); GUIUtilities.addToolBarButton(_toolbar, _zoomFitAction); GUIUtilities.addToolBarButton(_toolbar, _zoomOutAction); _cutAction = new CutAction(); _copyAction = new CopyAction(); _pasteAction = new PasteAction(); _moveToBackAction = new MoveToBackAction(); _moveToFrontAction = new MoveToFrontAction(); } /////////////////////////////////////////////////////////////////// //// public methods //// /** React to the fact that a change has been successfully executed * by marking the data associated with this window modified. This * will trigger a dialog when the window is closed, prompting the * user to save the data. * @param change The change that has been executed. */ public void changeExecuted(ChangeRequest change) { boolean persistent = true; // If the change is null, do not mark the model modified, // but do update the graph panner. if (change != null) { persistent = change.isPersistent(); // Note that we don't want to accidently reset to false here. if (persistent) { setModified(persistent); } } if (_graphPanner != null) { _graphPanner.repaint(); } } /** React to the fact that a change has triggered an error by * doing nothing (the effigy is also listening and will report * the error). * @param change The change that was attempted. * @param exception The exception that resulted. */ public void changeFailed(ChangeRequest change, Exception exception) { // Do not report if it has already been reported. if (change == null) { MessageHandler.error("Change failed", exception); } else if (!change.isErrorReported()) { change.setErrorReported(true); MessageHandler.error("Change failed", exception); } } /** Get the currently selected objects from this document, if any, * and place them on the clipboard in MoML format. */ public void copy() { HashSet namedObjSet = _getSelectionSet(); StringWriter buffer = new StringWriter(); try { NamedObj container = (NamedObj) _getGraphModel().getRoot(); // NOTE: The order in the model must be respected. Iterator elements = container.sortContainedObjects(namedObjSet) .iterator(); while (elements.hasNext()) { NamedObj element = (NamedObj) elements.next(); // first level to avoid obnoxiousness with // toplevel translations. element.exportMoML(buffer, 0); } if (container instanceof CompositeEntity) { buffer.write(((CompositeEntity) container).exportLinks(1, namedObjSet)); } Clipboard clipboard = java.awt.Toolkit.getDefaultToolkit() .getSystemClipboard(); // The code below does not use a PtolemyTransferable, // to work around // a bug in the JDK that should be fixed as of jdk1.3.1. The bug // is that cut and paste through the system clipboard to native // applications doesn't work unless you use string selection. clipboard.setContents(new StringSelection(buffer.toString()), this); } catch (IOException ex) { MessageHandler.error("Copy failed", ex); } } /** Create a typed composite actor that contains the selected actors * and connections. The created typed composite actor is transparent. * The resulting topology is the same in the sense * of deep connectivities. */ public void createHierarchy() { GraphController controller = _getGraphController(); SelectionModel model = controller.getSelectionModel(); GraphModel graphModel = controller.getGraphModel(); Object[] selection = model.getSelectionAsArray(); // A set, because some objects may represent the same // ptolemy object. HashSet namedObjSet = new HashSet(); HashSet nodeSet = new HashSet(); StringBuffer newPorts = new StringBuffer(); StringBuffer extRelations = new StringBuffer(); StringBuffer extConnections = new StringBuffer(); StringBuffer intRelations = new StringBuffer(); StringBuffer intConnections = new StringBuffer(); // First get all the nodes. try { final NamedObj container = (NamedObj) graphModel.getRoot(); if (!(container instanceof CompositeEntity)) { // This is an internal error because a reasonable GUI should not // provide access to this functionality. throw new InternalErrorException( "Cannot create hierarchy if the container is not a CompositeEntity."); } final String name = container.uniqueName("CompositeActor"); final TypedCompositeActor compositeActor = new TypedCompositeActor((CompositeEntity) container, name); double[] location = new double[2]; boolean gotLocation = false; for (int i = 0; i < selection.length; i++) { if (selection[i] instanceof Figure) { if (!gotLocation) { location[0] = ((Figure) selection[i]).getBounds() .getCenterX(); location[1] = ((Figure) selection[i]).getBounds() .getCenterY(); gotLocation = true; } Object userObject = ((Figure) selection[i]).getUserObject(); if (graphModel.isNode(userObject)) { nodeSet.add(userObject); NamedObj actual = (NamedObj) graphModel .getSemanticObject(userObject); namedObjSet.add(actual); } } } for (int i = 0; i < selection.length; i++) { if (selection[i] instanceof Figure) { Object userObject = ((Figure) selection[i]).getUserObject(); if (graphModel.isEdge(userObject)) { // Check to see if the head and tail are both being // selected. Object head = graphModel.getHead(userObject); //System.out.println("head:" +((NamedObj)head).getName()); Object tail = graphModel.getTail(userObject); //System.out.println("tail:" +((NamedObj)tail).getName()); boolean headOK = nodeSet.contains(head); boolean tailOK = nodeSet.contains(tail); Iterator objects = nodeSet.iterator(); while (!(headOK && tailOK) && objects.hasNext()) { Object object = objects.next(); if (!headOK && GraphUtilities.isContainedNode( head, object, graphModel)) { headOK = true; } if (!tailOK && GraphUtilities.isContainedNode( tail, object, graphModel)) { tailOK = true; } } // For the edges at the boundary. if ((!headOK && tailOK) || (headOK && !tailOK)) { IOPort port = null; IORelation relation = null; boolean duplicateRelation = false; if (head instanceof IOPort) { port = (IOPort) head; if (tail instanceof IOPort) { relation = (IORelation) graphModel .getSemanticObject(userObject); duplicateRelation = true; } else { relation = (IORelation) graphModel .getSemanticObject(tail); } } else if (tail instanceof IOPort) { port = (IOPort) tail; relation = (IORelation) graphModel .getSemanticObject(head); } if (port != null) { ComponentEntity entity = (ComponentEntity) ((IOPort) port) .getContainer(); String portName = "port_" + i; boolean isInput = ((IOPort) port).isInput(); boolean isOutput = ((IOPort) port).isOutput(); newPorts.append("\n"); if (namedObjSet.contains(entity)) { // The port is inside the hierarchy. // The relation must be outside. // Create composite port. if (isInput) { newPorts.append( ""); } if (isOutput) { newPorts.append( ""); } newPorts.append("\n\n"); // Create internal relation and links. // Note we can only partially reuse // the relation name, one original relation // can be two internal relations. String relationName = relation.getName() + "_" + i; intRelations.append("\n"); intConnections.append("\n"); intConnections.append("\n"); // Create external links. if (duplicateRelation) { extRelations.append("\n"); IOPort otherPort = (IOPort) tail; ComponentEntity otherEntity = (ComponentEntity) otherPort .getContainer(); if (otherEntity == container) { // This is a boundy port at a higher level. extConnections.append( "\n"); } else { extConnections.append( "\n"); } } extConnections.append("\n"); } else { // The port is outside the hierarchy. // The relation must be inside. if (isInput) { newPorts.append( ""); } if (isOutput) { newPorts.append( ""); } newPorts.append("\n\n"); String relationName = relation.getName() + "_" + i; extRelations.append("\n"); extConnections.append("\n"); extConnections.append("\n"); // Create external links. if (duplicateRelation) { intRelations.append("\n"); IOPort otherPort = (IOPort) tail; ComponentEntity otherEntity = (ComponentEntity) otherPort .getContainer(); intConnections.append("\n"); } intConnections.append("\n"); } } } else if (!headOK && !tailOK) { // We only selected an edge. Build one input // port, one output port for it, and build // a direct connection. } } } } //System.out.println(" new port:" + newPorts); //final Point2D point = new Point2D.Double(); // Copy the selection. copy(); _deleteWithoutUndo(); // Create the MoML command. StringBuffer moml = new StringBuffer(); // If the dropObj defers to something else, then we // have to check the parent of the object // for import attributes, and then we have to // generate import statements. Note that everything // imported by the parent will be imported now by // the object into which this is dropped. moml.append("\n"); moml.append("\n"); moml.append("\t\n"); moml.append("\t\n"); moml.append(newPorts); // additional ports. Clipboard clipboard = java.awt.Toolkit.getDefaultToolkit() .getSystemClipboard(); Transferable transferable = clipboard.getContents(this); try { moml.append((String) transferable.getTransferData( DataFlavor.stringFlavor)); } catch (Exception ex) { MessageHandler.error("Paste within Create Hierarchy failed", ex); } // internal connections moml.append(intRelations); moml.append(intConnections); moml.append("\n"); // external relations. moml.append(extRelations); moml.append(extConnections); // external connections. moml.append("\n"); //System.out.println(moml.toString()); ChangeRequest request = null; request = new MoMLChangeRequest(this, container, moml.toString()) { protected void _execute() throws Exception { super._execute(); NamedObj newObject = ((CompositeEntity) container) .getEntity(name); //_setLocation(compositeActor, point); } }; container.requestChange(request); } catch (Throwable throwable) { MessageHandler.error("Creating hierarchy failed", throwable); } } /** Remove the currently selected objects from this document, if any, * and place them on the clipboard. */ public void cut() { copy(); delete(); } /** Delete the currently selected objects from this document. */ public void delete() { // Note that we previously a delete was handled at the model level. // Now a delete is handled by generating MoML to carry out the delete // and handing that MoML to the parser GraphController controller = _getGraphController(); SelectionModel model = controller.getSelectionModel(); AbstractBasicGraphModel graphModel = (AbstractBasicGraphModel) controller .getGraphModel(); Object[] selection = model.getSelectionAsArray(); // First collect selected objects into the userObjects array // and deselect them. Object[] userObjects = new Object[selection.length]; for (int i = 0; i < selection.length; i++) { userObjects[i] = ((Figure) selection[i]).getUserObject(); model.removeSelection(selection[i]); } // Create a set to hold those elements whose deletion // does not go through MoML. This is only links that // are not connected to another port or a relation. HashSet edgeSet = new HashSet(); // Generate the MoML to carry out the deletion StringBuffer moml = new StringBuffer("\n"); // Delete edges then nodes, since deleting relations may // result in deleting links to that relation. for (int i = 0; i < selection.length; i++) { Object userObject = userObjects[i]; if (graphModel.isEdge(userObject)) { NamedObj actual = (NamedObj) graphModel.getSemanticObject(userObject); // If there is no semantic object, then this edge is // not fully connected, so we can't go through MoML. if (actual == null) { edgeSet.add(userObject); } else { moml.append(graphModel.getDeleteEdgeMoML(userObject)); } } } for (int i = 0; i < selection.length; i++) { Object userObject = userObjects[i]; if (graphModel.isNode(userObject)) { moml.append(graphModel.getDeleteNodeMoML(userObject)); } } moml.append("\n"); // Have both MoML to perform deletion and set of objects whose // deletion does not go through MoML. This set of objects // should be very small and so far consists of only links that are not // connected to a relation try { // First manually delete any objects whose deletion does not go // through MoML and so are not undoable // Note that we turn off event dispatching so that each individual // removal does not trigger graph redrawing. graphModel.setDispatchEnabled(false); Iterator edges = edgeSet.iterator(); while (edges.hasNext()) { Object nextEdge = edges.next(); if (graphModel.isEdge(nextEdge)) { graphModel.disconnectEdge(this, nextEdge); } } } finally { graphModel.setDispatchEnabled(true); } // Next process the deletion MoML. This should be the large majority // of most deletions. try { // Finally create and request the change NamedObj container = graphModel.getPtolemyModel(); MoMLChangeRequest change = new MoMLChangeRequest(this, container, moml.toString()); change.setUndoable(true); container.requestChange(change); } catch (Exception ex) { MessageHandler.error("Delete failed, changeRequest was:" + moml, ex); } graphModel.dispatchGraphEvent(new GraphEvent(this, GraphEvent.STRUCTURE_CHANGED, graphModel.getRoot())); } /** Override the dispose method to unattach any listeners that may keep * this model from getting garbage collected. */ public void dispose() { // Remove the association with the library. This is necessary to allow // this frame, and the rest of the model to be properly garbage // collected _libraryModel.setRoot(null); super.dispose(); } /** Return the center location of the visible part of the pane. * @return The center of the visible part. */ public Point2D getCenter() { Rectangle2D rect = getVisibleCanvasRectangle(); return new Point2D.Double(rect.getCenterX(), rect.getCenterY()); } /** Return the JGraph instance that this view uses to represent the * ptolemy model. */ public JGraph getJGraph() { return _jgraph; } /** Return the rectangle representing the visible part of the * pane, transformed into canvas coordinates. This is the range * of locations that are visible, given the current pan and zoom. * @return The rectangle representing the visible part. */ public Rectangle2D getVisibleCanvasRectangle() { AffineTransform current = _jgraph.getCanvasPane().getTransformContext() .getTransform(); AffineTransform inverse; try { inverse = current.createInverse(); } catch (NoninvertibleTransformException e) { throw new RuntimeException(e.toString()); } Rectangle2D visibleRect = getVisibleRectangle(); return ShapeUtilities.transformBounds(visibleRect, inverse); } /** Return the rectangle representing the visible part of the * pane, in pixel coordinates on the screen. * @return A rectangle whose upper left corner is at (0, 0) and whose * size is the size of the canvas component. */ public Rectangle2D getVisibleRectangle() { Dimension size = _jgraph.getSize(); return new Rectangle2D.Double(0, 0, size.getWidth(), size.getHeight()); } /** Layout the graph view. */ public void layoutGraph() { GraphController controller = _getGraphController(); AbstractBasicGraphModel model = _getGraphModel(); LayoutTarget target = new PtolemyLayoutTarget(controller); PtolemyLayout layout = new PtolemyLayout(target); layout.setOrientation(LevelLayout.HORIZONTAL); layout.setRandomizedPlacement(false); // Before doing the layout, need to take a copy of all the current // node locations which can be used to undo the effects of the move. try { NamedObj composite = model.getPtolemyModel(); StringBuffer moml = new StringBuffer(); moml.append("\n"); // NOTE: this gives at iteration over locations. Iterator nodes = model.nodes(composite); while (nodes.hasNext()) { Location location = (Location) nodes.next(); // Get the containing element NamedObj element = (NamedObj) location.getContainer(); // Give default values in case the previous locations value // has not yet been set String expression = location.getExpression(); if (expression == null) { expression = "0, 0"; } // Create the MoML, wrapping the location attribute // in an element refering to the container String containingElementName = element.getElementName(); moml.append("<" + containingElementName + " name=\"" + element.getName() + "\" >\n"); // NOTE: use the moml info element name here in case the // location is a vertex moml.append("<" + location.getElementName() + " name=\"" + location.getName() + "\" value=\"" + expression + "\" />\n"); moml.append("\n"); } moml.append("\n"); // Push the undo entry onto the stack MoMLUndoEntry undoEntry = new MoMLUndoEntry(composite, moml.toString()); UndoStackAttribute undoInfo = UndoStackAttribute.getUndoInfo(composite); undoInfo.push(undoEntry); } catch (Throwable throwable) { // operation not undoable } // Perform the layout and repaint layout.layout(model.getRoot()); _jgraph.repaint(); _graphPanner.repaint(); } /** Do nothing. */ public void lostOwnership(Clipboard clipboard, Transferable transferable) { } /** Assuming the contents of the clipboard is MoML code, paste it into * the current model by issuing a change request. */ public void paste() { Clipboard clipboard = java.awt.Toolkit.getDefaultToolkit() .getSystemClipboard(); Transferable transferable = clipboard.getContents(this); GraphModel model = _getGraphModel(); if (transferable == null) { return; } try { NamedObj container = (NamedObj) model.getRoot(); StringBuffer moml = new StringBuffer(); // The pasted version will have the names generated by the // uniqueName() method of the container, to ensure that they // do not collide with objects already in the container. moml.append("\n"); // Pasted items no longer line up on top of each other. moml.append(offsetPastedMomlLocation( (String) transferable.getTransferData( DataFlavor.stringFlavor), 10, 10)); moml.append("\n"); MoMLChangeRequest change = new MoMLChangeRequest(this, container, moml.toString()); change.setUndoable(true); container.requestChange(change); } catch (Exception ex) { MessageHandler.error("Paste failed", ex); } } /** Print the visible portion of the graph to a printer, * which is represented by the specified graphics object. * @param graphics The context into which the page is drawn. * @param format The size and orientation of the page being drawn. * @param index The zero based index of the page to be drawn. * @return PAGE_EXISTS if the page is rendered successfully, or * NO_SUCH_PAGE if pageIndex specifies a non-existent page. * @exception PrinterException If the print job is terminated. */ public int print(Graphics graphics, PageFormat format, int index) throws PrinterException { if (_jgraph != null) { Rectangle2D view = getVisibleRectangle(); return _jgraph.print(graphics, format, index, view); } else { return NO_SUCH_PAGE; } } /** Redo the last undone change on the model */ public void redo() { GraphModel model = _getGraphModel(); try { NamedObj toplevel = (NamedObj) model.getRoot(); RedoChangeRequest change = new RedoChangeRequest(this, toplevel); toplevel.requestChange(change); } catch (Exception ex) { MessageHandler.error("Redo failed", ex); } } /** Open a file browser and save the given entity in the file specified * by the user. * @param entity The entity to save. * @exception If something goes wrong. * @since Ptolemy 4.0 */ public void saveComponentInFile(Entity entity) throws Exception { // NOTE: This mirrors similar code in Top and TableauFrame, but // I can't find any way to re-use that code, since the details // are slightly different at each step here. JFileChooser fileDialog = new JFileChooser(); fileDialog.setDialogTitle("Save actor as..."); if (_directory != null) { fileDialog.setCurrentDirectory(_directory); } else { // The default on Windows is to open at user.home, which is // typically not what we want. // So we use the current directory instead. // This will fail with a security exception in applets. String currentWorkingDirectory = StringUtilities.getProperty( "user.dir"); if (currentWorkingDirectory != null) { fileDialog.setCurrentDirectory(new File(currentWorkingDirectory)); } } fileDialog.setSelectedFile(new File(fileDialog.getCurrentDirectory(), entity.getName() + ".xml")); // Show the dialog. int returnVal = fileDialog.showSaveDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { File file = fileDialog.getSelectedFile(); if (!_confirmFile(entity, file)) { return; } // Record the selected directory. _directory = fileDialog.getCurrentDirectory(); java.io.FileWriter fileWriter = null; try { fileWriter = new java.io.FileWriter(file); // Make sure the entity name saved matches the file name. String name = entity.getName(); String filename = file.getName(); int period = filename.indexOf("."); if (period > 0) { name = filename.substring(0, period); } else { name = filename; } fileWriter.write("\n" + "\n"); entity.exportMoML(fileWriter, 0, name); } finally { if (fileWriter != null) { fileWriter.close(); } } } } /** Save the given entity in the user library in the given * configuration. * @param configuration The configuration. * @param entity The entity to save. * @since Ptolemy 2.1 */ public static void saveComponentInLibrary(Configuration configuration, Entity entity) { try { CompositeEntity libraryInstance = (CompositeEntity) configuration.getEntity( "actor library." + VERGIL_USER_LIBRARY_NAME); if (libraryInstance == null) { MessageHandler.error("Save In Library failed: " + "Could not find user library with name \"" + VERGIL_USER_LIBRARY_NAME + "\"."); return; } // Note that the library in the configuration is an // instance of another model. We have to go get the // original model to make sure that the change propagates // back to the file from which the library is loaded from. Tableau libraryTableau = configuration.openModel(libraryInstance); PtolemyEffigy libraryEffigy = (PtolemyEffigy)libraryTableau.getContainer(); CompositeEntity library = (CompositeEntity)libraryEffigy.getModel(); StringWriter buffer = new StringWriter(); // Check whether there is already something existing in the // user library with this name. if (library.getEntity(entity.getName()) != null) { MessageHandler.error("Save In Library failed: An object" + " already exists in the user library with name " + "\"" + entity.getName() + "\"."); return; } entity.exportMoML(buffer, 1); ChangeRequest request = new MoMLChangeRequest(entity, library, buffer.toString()); library.requestChange(request); } catch (IOException ex) { // Ignore. } catch (KernelException ex) { // Ignore. } } /** Set the center location of the visible part of the pane. * This will cause the panner to center on the specified location * with the current zoom factor. * @param center The center of the visible part. */ public void setCenter(Point2D center) { Rectangle2D visibleRect = getVisibleCanvasRectangle(); AffineTransform newTransform = _jgraph.getCanvasPane() .getTransformContext() .getTransform(); newTransform.translate(visibleRect.getCenterX() - center.getX(), visibleRect.getCenterY() - center.getY()); _jgraph.getCanvasPane().setTransform(newTransform); } /** Undo the last undoable change on the model */ public void undo() { GraphModel model = _getGraphModel(); try { NamedObj toplevel = (NamedObj) model.getRoot(); UndoChangeRequest change = new UndoChangeRequest(this, toplevel); toplevel.requestChange(change); } catch (Exception ex) { MessageHandler.error("Undo failed", ex); } } /** Zoom in or out to magnify by the specified factor, from the current * magnification. * @param factor The magnification factor (relative to 1.0). */ public void zoom(double factor) { JCanvas canvas = _jgraph.getGraphPane().getCanvas(); AffineTransform current = canvas.getCanvasPane().getTransformContext() .getTransform(); // Save the center, so we remember what we were looking at. Point2D center = getCenter(); current.scale(factor, factor); canvas.getCanvasPane().setTransform(current); // Reset the center. setCenter(center); if (_graphPanner != null) { _graphPanner.repaint(); } } /** Zoom to fit the current figures. */ public void zoomFit() { GraphPane pane = _jgraph.getGraphPane(); Rectangle2D bounds = pane.getForegroundLayer().getLayerBounds(); if (bounds.isEmpty()) { // Empty diagram. return; } Rectangle2D viewSize = getVisibleRectangle(); AffineTransform newTransform = CanvasUtilities.computeFitTransform(bounds, viewSize); JCanvas canvas = pane.getCanvas(); canvas.getCanvasPane().setTransform(newTransform); if (_graphPanner != null) { _graphPanner.repaint(); } } /** Set zoom to the nominal. */ public void zoomReset() { JCanvas canvas = _jgraph.getGraphPane().getCanvas(); AffineTransform current = canvas.getCanvasPane().getTransformContext() .getTransform(); current.setToIdentity(); canvas.getCanvasPane().setTransform(current); if (_graphPanner != null) { _graphPanner.repaint(); } } /////////////////////////////////////////////////////////////////// //// public variables //// /** The name of the user library. The default value is * "UserLibrary". The value of this variable is what appears * in the Vergil left hand tree menu. */ public static String VERGIL_USER_LIBRARY_NAME = "UserLibrary"; /////////////////////////////////////////////////////////////////// //// protected methods //// /** Create the menus that are used by this frame. */ protected void _addMenus() { super._addMenus(); _editMenu = new JMenu("Edit"); _editMenu.setMnemonic(KeyEvent.VK_E); _menubar.add(_editMenu); // Add the undo action, followed by a separator then the editing actions diva.gui.GUIUtilities.addHotKey(_jgraph, _undoAction); diva.gui.GUIUtilities.addMenuItem(_editMenu, _undoAction); diva.gui.GUIUtilities.addHotKey(_jgraph, _redoAction); diva.gui.GUIUtilities.addMenuItem(_editMenu, _redoAction); _editMenu.addSeparator(); GUIUtilities.addHotKey(_jgraph, _cutAction); GUIUtilities.addMenuItem(_editMenu, _cutAction); GUIUtilities.addHotKey(_jgraph, _copyAction); GUIUtilities.addMenuItem(_editMenu, _copyAction); GUIUtilities.addHotKey(_jgraph, _pasteAction); GUIUtilities.addMenuItem(_editMenu, _pasteAction); _editMenu.addSeparator(); GUIUtilities.addHotKey(_jgraph, _moveToBackAction); GUIUtilities.addMenuItem(_editMenu, _moveToBackAction); GUIUtilities.addHotKey(_jgraph, _moveToFrontAction); GUIUtilities.addMenuItem(_editMenu, _moveToFrontAction); // Hot key for configure (edit parameters). GUIUtilities.addHotKey(_jgraph, BasicGraphController._configureAction); // May be null if there are not multiple views in the configuration. if (_viewMenu == null) { _viewMenu = new JMenu("View"); _viewMenu.setMnemonic(KeyEvent.VK_V); _menubar.add(_viewMenu); } else { _viewMenu.addSeparator(); } GUIUtilities.addHotKey(_jgraph, _zoomInAction); GUIUtilities.addMenuItem(_viewMenu, _zoomInAction); GUIUtilities.addHotKey(_jgraph, _zoomResetAction); GUIUtilities.addMenuItem(_viewMenu, _zoomResetAction); GUIUtilities.addHotKey(_jgraph, _zoomFitAction); GUIUtilities.addMenuItem(_viewMenu, _zoomFitAction); GUIUtilities.addHotKey(_jgraph, _zoomOutAction); GUIUtilities.addMenuItem(_viewMenu, _zoomOutAction); } /** Return true if any element of the specified list is implied. * An element is implied if its getDerivedLevel() method returns * anything smaller than Integer.MAX_VALUE. * @param elements A list of instances of NamedObj. * @return True if any element in the list is implied. * @see NamedObj#getDerivedLevel() */ protected boolean _checkForImplied(List elements) { Iterator elementIterator = elements.iterator(); while (elementIterator.hasNext()) { NamedObj element = (NamedObj) elementIterator.next(); if (element.getDerivedLevel() < Integer.MAX_VALUE) { MessageHandler.error("Cannot change the position of " + element.getFullName() + " because the position is set by the class."); return true; } } return false; } /** Override the base class to remove the listeners we have * created when the frame closes. Specifically, * remove our panner-updating listener from the entity. * Also remove the listeners our graph model has created. * @return True if the close completes, and false otherwise. */ protected boolean _close() { boolean result = super._close(); if (result) { getModel().removeChangeListener(this); AbstractBasicGraphModel graphModel = _getGraphModel(); graphModel.removeListeners(); } return result; } /** Create the default library to use if an entity has no * LibraryAttribute. Note that this is called in the * constructor and therefore overrides in subclasses * should not refer to any members that may not have been * initialized. If no library is found in the configuration, * then an empty one is created in the specified workspace. * @param workspace The workspace in which to create * the library, if one needs to be created. * @return The new library, or null if there is no * configuration. */ protected CompositeEntity _createDefaultLibrary(Workspace workspace) { Configuration configuration = getConfiguration(); if (configuration != null) { CompositeEntity result = (CompositeEntity) configuration.getEntity( "actor library"); if (result == null) { // Create an empty library by default. result = new CompositeEntity(workspace); try { result.setName("topLibrary"); // Put a marker in so that this is // recognized as a library. new Attribute(result, "_libraryMarker"); } catch (Exception ex) { throw new InternalErrorException( "Library configuration failed: " + ex); } } return result; } else { return null; } } /** Create a new graph pane. Subclasses will override this to change * the pane that is created. Note that this method is called in * constructor, so derived classes must be careful to not reference * local variables that may not have yet been created. * @return The pane that is created. */ protected abstract GraphPane _createGraphPane(); /** Get the directory that was last accessed by this window. * @see #_setDirectory * @return The directory last accessed. */ protected File _getDirectory() { // NOTE: This method is necessary because we wish to have // this accessed by inner classes, and there is a bug in // jdk1.2.2 where inner classes cannot access protected // static members. return _directory; } /** Return the graph controller associated with this frame. * @return The graph controller associated with this frame. */ protected GraphController _getGraphController() { GraphPane graphPane = _jgraph.getGraphPane(); return (GraphController) graphPane.getGraphController(); } /** Return the graph model associated with this frame. * @return The graph model associated with this frame. */ protected AbstractBasicGraphModel _getGraphModel() { GraphController controller = _getGraphController(); return (AbstractBasicGraphModel) controller.getGraphModel(); } /** Return a set of instances of NamedObj representing the objects * that are currently selected. This set has no particular order * to it. If you need the selection objects in proper order, as * defined by the container, then call sortContainedObjects() * on the container to sort the result. * @return The set of selected objects. */ protected HashSet _getSelectionSet() { GraphController controller = _getGraphController(); GraphModel graphModel = controller.getGraphModel(); SelectionModel model = controller.getSelectionModel(); Object[] selection = model.getSelectionAsArray(); // A set, because some objects may represent the same // ptolemy object. HashSet namedObjSet = new HashSet(); HashSet nodeSet = new HashSet(); // First get all the nodes. for (int i = 0; i < selection.length; i++) { if (selection[i] instanceof Figure) { Object userObject = ((Figure) selection[i]).getUserObject(); if (graphModel.isNode(userObject)) { nodeSet.add(userObject); NamedObj actual = (NamedObj) graphModel.getSemanticObject(userObject); namedObjSet.add(actual); } } } for (int i = 0; i < selection.length; i++) { if (selection[i] instanceof Figure) { Object userObject = ((Figure) selection[i]).getUserObject(); if (graphModel.isEdge(userObject)) { // Check to see if the head and tail are both being // copied. Only if so, do we actually take the edge. Object head = graphModel.getHead(userObject); Object tail = graphModel.getTail(userObject); boolean headOK = nodeSet.contains(head); boolean tailOK = nodeSet.contains(tail); Iterator objects = nodeSet.iterator(); while (!(headOK && tailOK) && objects.hasNext()) { Object object = objects.next(); if (!headOK && GraphUtilities.isContainedNode( head, object, graphModel)) { headOK = true; } if (!tailOK && GraphUtilities.isContainedNode( tail, object, graphModel)) { tailOK = true; } } if (headOK && tailOK) { // Add the relation. NamedObj actual = (NamedObj) graphModel .getSemanticObject(userObject); namedObjSet.add(actual); } } } } return namedObjSet; } /** Set the directory that was last accessed by this window. * @see #_getDirectory * @param directory The directory last accessed. */ protected void _setDirectory(File directory) { // NOTE: This method is necessary because we wish to have // this accessed by inner classes, and there is a bug in // jdk1.2.2 where inner classes cannot access protected // static members. _directory = directory; } /** Write the model to the specified file. This overrides the base * class to record the current size and position of the window * in the model. * @param file The file to write to. * @exception IOException If the write fails. */ protected void _writeFile(File file) throws IOException { // First, record size and position. try { // Record the position of the top-level frame, assuming // there is one. Component component = _jgraph.getParent(); Component parent = component.getParent(); while ((parent != null) && !(parent instanceof Frame)) { component = parent; parent = component.getParent(); } // If there is no parent that is a Frame, do nothing. if (parent instanceof Frame) { WindowPropertiesAttribute properties = (WindowPropertiesAttribute) getModel() .getAttribute("_windowProperties", WindowPropertiesAttribute.class); if (properties == null) { properties = new WindowPropertiesAttribute(getModel(), "_windowProperties"); } properties.recordProperties((Frame) parent); } // Have to also record the size of the JGraph because // setting the size of the frame is ignored if we don't // also set the size of the JGraph. Why? Who knows. Swing. SizeAttribute size = (SizeAttribute) getModel().getAttribute("_vergilSize", SizeAttribute.class); if (size == null) { size = new SizeAttribute(getModel(), "_vergilSize"); } size.recordSize(_jgraph); // Also record zoom and pan state. JCanvas canvas = _jgraph.getGraphPane().getCanvas(); AffineTransform current = canvas.getCanvasPane() .getTransformContext() .getTransform(); // We assume the scaling in the X and Y directions are the same. double scale = current.getScaleX(); Parameter zoom = (Parameter) getModel().getAttribute("_vergilZoomFactor", Parameter.class); if (zoom == null) { // NOTE: This will not propagate. zoom = new ExpertParameter(getModel(), "_vergilZoomFactor"); } zoom.setToken(new DoubleToken(scale)); // Make sure the visibility is only expert. zoom.setVisibility(Settable.EXPERT); // Save the center, to record the pan state. Point2D center = getCenter(); Parameter pan = (Parameter) getModel().getAttribute("_vergilCenter", Parameter.class); if (pan == null) { // NOTE: This will not propagate. pan = new ExpertParameter(getModel(), "_vergilCenter"); } Token[] centerArray = new Token[2]; centerArray[0] = new DoubleToken(center.getX()); centerArray[1] = new DoubleToken(center.getY()); pan.setToken(new ArrayToken(centerArray)); // Make sure the visibility is only expert. pan.setVisibility(Settable.EXPERT); } catch (Throwable throwable) { // Ignore problems here. Errors simply result in a default // size and location. } super._writeFile(file); } /////////////////////////////////////////////////////////////////// //// protected variables //// /** Default background color is a light grey. */ protected static Color BACKGROUND_COLOR = new Color(0xe5e5e5); /** The cut action. */ protected Action _cutAction; /** The copy action. */ protected Action _copyAction; /** The instance of EditorDropTarget associated with this object. */ protected EditorDropTarget _dropTarget; /** The edit menu. */ protected JMenu _editMenu; /** The panner. */ protected JCanvasPanner _graphPanner; /** The instance of JGraph for this editor. */ protected JGraph _jgraph; /** The library display widget. */ protected JTree _library; /** The library context menu creator. */ protected PTreeMenuCreator _libraryContextMenuCreator; /** The library model. */ protected EntityTreeModel _libraryModel; /** The library scroll pane. */ protected JScrollPane _libraryScrollPane; /** Action to move to the back. */ protected MoveToBackAction _moveToBackAction; /** Action to move to the front. */ protected MoveToFrontAction _moveToFrontAction; /** The library display panel. */ protected JPanel _palettePane; /** The paste action. */ protected Action _pasteAction; /** The split pane for library and editor. */ protected JSplitPane _splitPane; /** The toolbar. */ protected JToolBar _toolbar; /** The library. */ protected CompositeEntity _topLibrary; /////////////////////////////////////////////////////////////////// //// private methods //// /** Delete the currently selected objects from this document without * undo */ private void _deleteWithoutUndo() { // FIXME: This is the old delete() method, before undo was added. // createHierarch() calls this method. GraphController controller = _getGraphController(); AbstractBasicGraphModel graphModel = _getGraphModel(); // Note that we turn off event dispatching so that each individual // removal does not trigger graph redrawing. try { graphModel.setDispatchEnabled(false); SelectionModel model = controller.getSelectionModel(); Object[] selection = model.getSelectionAsArray(); Object[] userObjects = new Object[selection.length]; // First remove the selection. for (int i = 0; i < selection.length; i++) { userObjects[i] = ((Figure) selection[i]).getUserObject(); model.removeSelection(selection[i]); } // Remove all the edges first, // since if we remove the nodes first, // then removing the nodes might remove some of the edges. for (int i = 0; i < userObjects.length; i++) { Object userObject = userObjects[i]; if (graphModel.isEdge(userObject)) { graphModel.disconnectEdge(this, userObject); } } for (int i = 0; i < selection.length; i++) { Object userObject = userObjects[i]; if (graphModel.isNode(userObject)) { graphModel.removeNode(this, userObject); } } } finally { graphModel.setDispatchEnabled(true); graphModel.dispatchGraphEvent(new GraphEvent(this, GraphEvent.STRUCTURE_CHANGED, graphModel.getRoot())); } } /////////////////////////////////////////////////////////////////// //// private variables //// /** Action to redo the last undone MoML change. */ private Action _redoAction = new RedoAction(); /** Action to undo the last MoML change. */ private Action _undoAction = new UndoAction(); /** Action for zooming in. */ private Action _zoomInAction = new ZoomInAction("Zoom In"); /** Action for zoom reset. */ private Action _zoomResetAction = new ZoomResetAction("Zoom Reset"); /** Action for zoom fitting. */ private Action _zoomFitAction = new ZoomFitAction("Zoom Fit"); /** Action for zooming out. */ private Action _zoomOutAction = new ZoomOutAction("Zoom Out"); /////////////////////////////////////////////////////////////////// //// private inner classes //// /////////////////////////////////////////////////////////////////// //// CopyAction /** Action to copy the current selection. */ private class CopyAction extends AbstractAction { /** Create a new action to copy the current selection. */ public CopyAction() { super("Copy"); putValue("tooltip", "Copy the current selection onto the clipboard."); putValue(GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); putValue(GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_C)); } /** Copy the current selection. */ public void actionPerformed(ActionEvent e) { copy(); } } /////////////////////////////////////////////////////////////////// //// CutAction /** Action to copy and delete the current selection. */ private class CutAction extends AbstractAction { /** Create a new action to copy and delete the current selection. */ public CutAction() { super("Cut"); putValue("tooltip", "Cut the current selection onto the clipboard."); putValue(GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_X, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); putValue(GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_T)); } /** Copy and delete the current selection. */ public void actionPerformed(ActionEvent e) { cut(); } } /////////////////////////////////////////////////////////////////// //// ExecuteSystemAction /** An action to open a run control window. */ private class ExecuteSystemAction extends AbstractAction { /** Construct an action to execute the model. */ public ExecuteSystemAction() { super("Go"); putValue("tooltip", "Execute The Model"); putValue(GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_G, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); putValue(GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_G)); } /** Open a run control window. */ public void actionPerformed(ActionEvent e) { try { PtolemyEffigy effigy = (PtolemyEffigy) getTableau() .getContainer(); new RunTableau(effigy, effigy.uniqueName("tableau")); } catch (Exception ex) { MessageHandler.error("Execution Failed", ex); } } } /////////////////////////////////////////////////////////////////// //// MoveToBackAction /** Action to move the current selection to the back (which corresponds * to first in the ordered list). */ private class MoveToBackAction extends AbstractAction { public MoveToBackAction() { super("Send to Back"); putValue("tooltip", "Send to back of like objects"); putValue(GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_B, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); putValue(GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_B)); } public void actionPerformed(ActionEvent e) { final NamedObj container = (NamedObj) _getGraphModel().getRoot(); // Get the selection objects. // NOTE: The order in the model must be respected. HashSet namedObjSet = _getSelectionSet(); final List elements = container.sortContainedObjects(namedObjSet); // Return if any is a derived object. if (_checkForImplied(elements)) { return; } // Issue a change request, since this requires write access. ChangeRequest request = new ChangeRequest(container, "Send to back") { protected void _execute() throws IllegalActionException { MoveAction.move(elements, MoveAction.TO_FIRST, container); } }; container.requestChange(request); } } /////////////////////////////////////////////////////////////////// //// MoveToFrontAction /** Action to move the current selection to the back (which corresponds * to first in the ordered list). */ private class MoveToFrontAction extends AbstractAction { public MoveToFrontAction() { super("Bring to Front"); putValue("tooltip", "Bring to front of like objects"); putValue(GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); putValue(GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_F)); } public void actionPerformed(ActionEvent e) { final NamedObj container = (NamedObj) _getGraphModel().getRoot(); // Get the selection objects. // NOTE: The order in the model must be respected. HashSet namedObjSet = _getSelectionSet(); final List elements = container.sortContainedObjects(namedObjSet); // Return if any is a derived object. if (_checkForImplied(elements)) { return; } // Issue a change request, since this requires write access. ChangeRequest request = new ChangeRequest(container, "Bring to front") { protected void _execute() throws IllegalActionException { MoveAction.move(elements, MoveAction.TO_LAST, container); } }; container.requestChange(request); } } /////////////////////////////////////////////////////////////////// //// PasteAction /** Paste the current contents of the clipboard into the current model. */ private class PasteAction extends AbstractAction { /** Create a new action to paste the current contents of the * clipboard into the current model. */ public PasteAction() { super("Paste"); putValue("tooltip", "Paste the contents of the clipboard."); putValue(GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_V, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); putValue(GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_P)); } /** Paste the current contents of the clipboard into * the current model. */ public void actionPerformed(ActionEvent e) { paste(); } } /////////////////////////////////////////////////////////////////// //// PtolemyLayout /** A layout algorithm for laying out ptolemy graphs. Since our edges * are undirected, this layout algorithm turns them into directed edges * aimed consistently. i.e. An edge should always be "out" of an * internal output port and always be "in" of an internal input port. * Conversely, an edge is "out" of an external input port, and "in" of * an external output port. The copying operation also flattens * the graph, because the level layout algorithm doesn't understand * how to layout hierarchical nodes. */ private class PtolemyLayout extends LevelLayout { // FIXME: input ports should be on left, and output ports on right. /** Construct a new levelizing layout with a vertical orientation. */ public PtolemyLayout(LayoutTarget target) { super(target); } /** Copy the given graph and make the nodes/edges in the copied * graph point to the nodes/edges in the original. */ protected Object copyComposite(Object origComposite) { LayoutTarget target = getLayoutTarget(); GraphModel model = target.getGraphModel(); diva.graph.basic.BasicGraphModel local = getLocalGraphModel(); Object copyComposite = local.createComposite(null); HashMap map = new HashMap(); // Copy all the nodes for the graph. for (Iterator i = model.nodes(origComposite); i.hasNext();) { Object origNode = i.next(); if (target.isNodeVisible(origNode)) { Rectangle2D r = target.getBounds(origNode); LevelInfo inf = new LevelInfo(); inf.origNode = origNode; inf.x = r.getX(); inf.y = r.getY(); inf.width = r.getWidth(); inf.height = r.getHeight(); Object copyNode = local.createNode(inf); local.addNode(this, copyNode, copyComposite); map.put(origNode, copyNode); } } // Add all the edges. Iterator i = GraphUtilities.partiallyContainedEdges(origComposite, model); while (i.hasNext()) { Object origEdge = i.next(); Object origTail = model.getTail(origEdge); Object origHead = model.getHead(origEdge); if ((origHead != null) && (origTail != null)) { Figure tailFigure = (Figure) target.getVisualObject(origTail); Figure headFigure = (Figure) target.getVisualObject(origHead); // Swap the head and the tail if it will improve the // layout, since LevelLayout only uses directed edges. if (tailFigure instanceof Terminal) { Terminal terminal = (Terminal) tailFigure; Site site = terminal.getConnectSite(); if (site instanceof FixedNormalSite) { double normal = site.getNormal(); int direction = CanvasUtilities.getDirection(normal); if (direction == SwingUtilities.WEST) { Object temp = origTail; origTail = origHead; origHead = temp; } } } else if (headFigure instanceof Terminal) { Terminal terminal = (Terminal) headFigure; Site site = terminal.getConnectSite(); if (site instanceof FixedNormalSite) { double normal = site.getNormal(); int direction = CanvasUtilities.getDirection(normal); if (direction == SwingUtilities.EAST) { Object temp = origTail; origTail = origHead; origHead = temp; } } } origTail = _getParentInGraph(model, origComposite, origTail); origHead = _getParentInGraph(model, origComposite, origHead); Object copyTail = map.get(origTail); Object copyHead = map.get(origHead); if ((copyHead != null) && (copyTail != null)) { Object copyEdge = local.createEdge(origEdge); local.setEdgeTail(this, copyEdge, copyTail); local.setEdgeHead(this, copyEdge, copyHead); } } } return copyComposite; } // Unfortunately, the head and/or tail of the edge may not // be directly contained in the graph. In this case, we need to // figure out which of their parents IS in the graph // and calculate the cost of that instead. private Object _getParentInGraph(GraphModel model, Object graph, Object node) { while ((node != null) && !model.containsNode(graph, node)) { Object parent = model.getParent(node); if (model.isNode(parent)) { node = parent; } else { node = null; } } return node; } } /////////////////////////////////////////////////////////////////// //// PtolemyLayoutTarget /** A layout target that translates locatable nodes. */ private class PtolemyLayoutTarget extends BasicLayoutTarget { /** Construct a new layout target that operates * in the given pane. */ public PtolemyLayoutTarget(GraphController controller) { super(controller); } /** Return the viewport of the given graph as a rectangle * in logical coordinates. */ public Rectangle2D getViewport(Object composite) { GraphModel model = getController().getGraphModel(); if (composite == getRootGraph()) { // Take into account the current zoom and pan. Rectangle2D bounds = getVisibleCanvasRectangle(); double width = bounds.getWidth(); double height = bounds.getHeight(); double borderPercentage = (1 - getLayoutPercentage()) / 2; double x = (borderPercentage * width) + bounds.getX(); double y = (borderPercentage * height) + bounds.getY(); double w = getLayoutPercentage() * width; double h = getLayoutPercentage() * height; return new Rectangle2D.Double(x, y, w, h); } else { return super.getViewport(composite); } } /** Translate the figure associated with the given node in the * target's view by the given delta. */ public void translate(Object node, double dx, double dy) { super.translate(node, dx, dy); if (node instanceof Locatable) { double[] location = ((Locatable) node).getLocation(); if (location == null) { location = new double[2]; Figure figure = getController().getFigure(node); location[0] = figure.getBounds().getCenterX(); location[1] = figure.getBounds().getCenterY(); } else { location[0] += dx; location[1] += dy; } try { ((Locatable) node).setLocation(location); } catch (IllegalActionException ex) { throw new InternalErrorException(ex.getMessage()); } } } } /////////////////////////////////////////////////////////////////// //// OpenLibraryMenuItemFactory /** * Create a menu item that will open a library in editable form. */ private class OpenLibraryMenuItemFactory implements MenuItemFactory { /** * Add an item to the given context menu that will open the * given object as an editable model. */ public JMenuItem create(final JContextMenu menu, final NamedObj object) { Action action = new AbstractAction("Open for Editing") { public void actionPerformed(ActionEvent e) { try { getConfiguration().openModel(object); } catch (KernelException ex) { MessageHandler.error("Open failed.", ex); } } }; action.putValue("tooltip", "Open library for editing."); action.putValue(diva.gui.GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_O)); return menu.add(action, (String) action.getValue(Action.NAME)); } } /////////////////////////////////////////////////////////////////// //// RedoAction /** * Redo the last undone MoML change on the current current model. */ private class RedoAction extends AbstractAction { /** * Create a new action to paste the current contents of the clipboard * into the current model. */ public RedoAction() { super("Redo"); putValue("tooltip", "Redo the last change undone."); putValue(diva.gui.GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); putValue(diva.gui.GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_R)); } /** * Redo the last undone MoML change on the current current model. * * @param e The event for the action. */ public void actionPerformed(ActionEvent e) { redo(); } } /////////////////////////////////////////////////////////////////// //// UndoAction /** * Undo the last undoable MoML change on the current current model. */ private class UndoAction extends AbstractAction { /** * Create a new action to paste the current contents of the clipboard * into the current model. */ public UndoAction() { super("Undo"); putValue("tooltip", "Undo the last change."); putValue(diva.gui.GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); putValue(diva.gui.GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_U)); } /** * Undo the last undoable MoML change on the current current model. * * @param e The event for the action. */ public void actionPerformed(ActionEvent e) { undo(); } } /////////////////////////////////////////////////////////////////// //// ZoomInAction // An action to zoom in. public class ZoomInAction extends AbstractAction { public ZoomInAction(String description) { super(description); // Load the image by using the absolute path to the gif. // Using a relative location should work, but it does not. // Use the resource locator of the class. // For more information, see // jdk1.3/docs/guide/resources/resources.html URL img = getClass().getResource("/ptolemy/vergil/basic/img/zoomin.gif"); if (img != null) { ImageIcon icon = new ImageIcon(img); putValue(GUIUtilities.LARGE_ICON, icon); } putValue("tooltip", description + " (Ctrl+Shift+=)"); // NOTE: The following assumes that the + key is the same // as the = key. Unfortunately, the VK_PLUS key event doesn't // work, so we have to do it this way. putValue(GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | Event.SHIFT_MASK)); putValue(GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_Z)); } public void actionPerformed(ActionEvent e) { zoom(1.25); } } /////////////////////////////////////////////////////////////////// //// ZoomResetAction // An action to reset zoom. public class ZoomResetAction extends AbstractAction { public ZoomResetAction(String description) { super(description); // Load the image by using the absolute path to the gif. // Using a relative location should work, but it does not. // Use the resource locator of the class. // For more information, see // jdk1.3/docs/guide/resources/resources.html URL img = getClass().getResource("/ptolemy/vergil/basic/img/zoomreset.gif"); if (img != null) { ImageIcon icon = new ImageIcon(img); putValue(GUIUtilities.LARGE_ICON, icon); } putValue("tooltip", description + " (Ctrl+=)"); putValue(GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); putValue(GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_M)); } public void actionPerformed(ActionEvent e) { zoomReset(); } } /////////////////////////////////////////////////////////////////// //// ZoomFitAction // An action to zoom fit. public class ZoomFitAction extends AbstractAction { public ZoomFitAction(String description) { super(description); // Load the image by using the absolute path to the gif. // Using a relative location should work, but it does not. // Use the resource locator of the class. // For more information, see // jdk1.3/docs/guide/resources/resources.html URL img = getClass().getResource("/ptolemy/vergil/basic/img/zoomfit.gif"); if (img != null) { ImageIcon icon = new ImageIcon(img); putValue(GUIUtilities.LARGE_ICON, icon); } putValue("tooltip", description + " (Ctrl+Shift+-)"); putValue(GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | Event.SHIFT_MASK)); putValue(GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_F)); } public void actionPerformed(ActionEvent e) { zoomFit(); } } /////////////////////////////////////////////////////////////////// //// ZoomOutAction // An action to zoom out. public class ZoomOutAction extends AbstractAction { public ZoomOutAction(String description) { super(description); // Load the image by using the absolute path to the gif. // Using a relative location should work, but it does not. // Use the resource locator of the class. // For more information, see // jdk1.3/docs/guide/resources/resources.html URL img = getClass().getResource("/ptolemy/vergil/basic/img/zoomout.gif"); if (img != null) { ImageIcon icon = new ImageIcon(img); putValue(GUIUtilities.LARGE_ICON, icon); } putValue("tooltip", description + " (Ctrl+-)"); putValue(GUIUtilities.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); putValue(GUIUtilities.MNEMONIC_KEY, new Integer(KeyEvent.VK_U)); } public void actionPerformed(ActionEvent e) { zoom(1.0 / 1.25); } } /////////////////////////////////////////////////////////////////// //// private methods //// /** * Offset the moml object by xOffset and yOffset. This makes it * so pasted items do not appear directly on top of the copied * source in vergil. * @param moml the moml to change * @param xOffset the number of x pixels to move the pasted object * @param yOffset the number of y pixels to move the pasted object * @return moml with the location modified by xOffset and yOffset */ private String offsetPastedMomlLocation(String moml, int xOffset, int yOffset) { // Go through the moml and look for the _location property // when it is found, acquire the value in [x,y] form // add xOffset onto x and yOffset onto y and replace the value int index = 0; while (true) { index = moml.indexOf("