Examples

GXL to SVG Converter (Gxl2svg)

In this example, JGraph is used in a batch-processing environment, instead of a user interface. The input is a GXL file, which defines a graph with vertices and edges, but not its geometric pattern. The file is parsed into a graph model, and the vertices are arranged in a circle. The display is then stored as an SVG file, using the Batik library (http://xml.apache.org/batik/).

Here is the main method:


  // Usage: java SVGGraph gxl-file svg-file
  public static void main(String[] args) {
    (...)
    // Construct the graph to hold the ettributes
    JGraph graph = new JGraph(new DefaultGraphModel());
    // Read the GXL file into the model
    read(new File(args[0]), graph.getModel());
    // Apply Layout
    layout(graph);
    // Resize (Otherwise not Visible)
    graph.setSize(graph.getPreferredSize());
    (...)
    // Write the SVG file
    write(graph, out);
    (...)
  }
      

Read

The read method creates the cells defined in the GXL file, and constructs the arguments for the insert method, namely a list of cells, a connection set, and a map, from cells to attributes. To establish the connections, the vertices in the file are given unique IDs, which are referenced from the edge's source and target. (The ids map is used to look-up the vertices.)


  public static void read(File f, GraphModel model) (...) {
    (...)
    // List for the new Cells
    List newCells = new ArrayList();
    // ConnectionSet to Specify Connections
    ConnectionSet cs = new ConnectionSet();
    // Map from Cells to AttributesHashtable
    attributes = new Hashtable();
    // Map from ID to Vertex
    Map ids = new Hashtable();
        

In the innermost loop of the read method, cells are created based on the current XML element. (Let label point to the element's label, and type point to the element's type, which is "node" for vertices and "edge" for edges.)

The following creates a vertex with a port:


    // Create Vertex
    if (type.equals("node")) {
      (...)
      // Fetch ID Value
      id = tmp.getNodeValue();
      // Need unique valid ID
      if (id != null && !ids.keySet().contains(id)) {
        // Create Vertex with label
        DefaultGraphCell v = new DefaultGraphCell(label);
        // Add One Floating Port
        v.add(new DefaultPort());
        // Add ID, Vertex pair to Hashtable
        ids.put(id, v);
        // Add Default Attributes
        attributes.put(v, createDefaultAttributes());
        // Update New Cell List
        newCells.add(v);
      }
    }
        

Otherwise, an edge is created and its source and target IDs are looked up in the ids map. The first child, which is the port of the returned vertex is used to establish the connection in the connection set. (The source and target ID are analogous, so only the source ID is shown below.)


    // Create Edge
    else if (type.equals("edge")) {
      // Create Edge with label
      DefaultEdge edge = new DefaultEdge(label);
      // Fetch Source ID Value
      id = tmp.getNodeValue();
      // Find Source Port
      if (id != null) {
        // Fetch Vertex for Source ID
        DefaultGraphCell v = (DefaultGraphCell) ids.get(id);
        if (v != null)
          // Connect to Source Port
          cs.connect(edge, v.getChildAt(0), true);
      }
      (...)
      // Update New Cell List
      newCells.add(edge);
    }
        

When the innermost loop exits, the new cells are inserted into the model, together with the connection set and attributes. Note that the toArray method is used to create the first argument, which is an array of Objects.


    // Insert the cells (View stores attributes)
    model.insert(newCells.toArray(), cs, null, attributes);
  }
        

A helper method creates the attributes for the vertex, which are stored in a map, from keys to values. This map is then stored in another map, with the vertex as a key. The latter is used as a parameter for the insert method.


  // Create Attributes for a Black Line Border
  public static Map createDefaultAttributes() {
    // Create an AttributeMap
    Map map = GraphConstants.createMap();
    // Black Line Border (Border-Attribute must be null)
    GraphConstants.setBorderColor(map, Color.black);
    // Return the Map
    return map;
  }
      

Layout

Here is the layout method. Using this method, the vertices of the graph are arranged in a circle. The method has two loops: First, all cells are filtered for vertices, and the maximum width or height is stored. (The order of the cells in the circle is equal to the order of insertion.)


  public static void layout(JGraph graph) {
    // Fetch All Cell Views
    CellView[] views = graph.getView().getRoots();
    // List to Store the Vertices
    List vertices = new ArrayList();
    // Maximum width or height
    int max = 0;
    // Loop through all views
    for (int i = 0; i < views.length; i++) {
      // Add Vertex to List
      if (views[i] instanceof VertexView) {
        vertices.add(views[i]);
        // Fetch Bounds
        Rectangle b = views[i].getBounds();
        // Update Maximum
        if (bounds != null)
          max = Math.max(Math.max(b.width, b.height), max);
      }
    }
        

The number of vertices and the maximum width or height is then used to compute the minimum radius of the circle, and the angle step size. The step size is equal to the circle, divided by the number of vertices.


    // Compute Radius
    int r = (int) Math.max(vertices.size()*max/Math.PI, 100);
    // Compute Radial Step
    double phi = 2*Math.PI/vertices.size();
      

With the radius and step size at hand, the second loop is entered. In this loop, the position of the vertices is changed (without using the history).


    // Arrange vertices in a circle
    for (int i = 0; i < vertices.size(); i++) {
      // Cast the Object to a CellView
      CellView view = (CellView) vertices.get(i);
      // Fetch the Bounds
      Rectangle bounds = view.getBounds();
      // Update the Location
      if (bounds != null)
        bounds.setLocation(r+(int) (r*Math.sin(i*phi)),
                                     r+(int) (r*Math.cos(i*phi)));
    }
  }
        

Write

Then the graph is written to an SVG file using the write method. To implement this method, the Batik library is used. The graph is painted onto a custom Graphics2D, which is able to stream the graphics out as SVG.


  // Stream out to the Writer as SVG
  public static void write(JGraph graph, Writer out) (...) {
    // Get a DOMImplementation
    DOMImplementation dom = GenericDOMImplementation.getDOMImplementation();
    // Create an instance of org.w3c.dom.Document
    Document doc = dom.createDocument(null, "svg", null);
    // Create an instance of the SVG Generator
    SVGGraphics2D svgGenerator = new SVGGraphics2D(doc);
    // Render into the SVG Graphics2D Implementation
    graph.paint(svgGenerator);
    // Use CSS style attribute
    boolean useCSS = true;
    // Finally, stream out SVG to the Writer
    svgGenerator.stream(out, useCSS);
  }
        

Simple Diagram Editor (GraphEd)

This example provides a simple diagram editor, which has a popup menu, a command history, zoom, grouping, layering, and clipboard support. The connections between cells may be established in a special connect mode, which allows dragging the ports of a cell.

The connect mode requires a special marquee handler, which is also used to implement the popup menu. The history support is implemented by using the GraphUndoManager class, which extends Swing's UndoManager. A custom graph overrides the edge view's default bindings for adding and removing points (the right mouse button that is used for the popup menu, and shift-click is used instead). Additionally, a custom model that does not allow self-references is implemented.

The editor object extends JPanel, which can be inserted into a frame. It provides the inner classes MyGraph, MyModel and MyMarqueeHandler, which extend JGraph, DefaultGraphModel and BasicMarqueeHandler respectively. A GraphSelectionListener is used to update the toolbar buttons, and a KeyListener is responsible to handle the delete keystroke, however, the implementation of these interfaces is not shown below.

Here are the class definition and variable declarations:


public class Editor extends JPanel (...) {
  // JGraph object
  protected JGraph graph;
  // Undo Manager
  protected GraphUndoManager undoManager;
      

The main method constructs a frame with an editor and displays it:


  // Usage: java -jar graphed.jar
  public static void main(String[] args) {
    // Construct Frame
    JFrame frame = new JFrame("GraphEd");
    (...)
    // Add an Editor Panel
    frame.getContentPane().add(new Editor());
    (...)
    // Set Default Size
    frame.setSize(520, 390);
    // Show Frame
    frame.show();
  }
      

Constructor

The constructor of the editor panel creates the MyGraph object and initializes it with an instance of MyModel. Then, the GraphUndoManager is constructed, and the undoableEditHappened is overridden to enable or disable the undo and redo action using the updateHistoryButtons method (which is not shown).


  // Construct an Editor Panel
  public Editor() {
    // Use Border Layout
    setLayout(new BorderLayout());
    // Construct the Graph object
    graph = new MyGraph(new MyModel());
    // Custom Graph Undo Manager
    undoManager = new GraphUndoManager() {
      // Extend Superclass
      public void undoableEditHappened(UndoableEditEvent e) {
        // First Invoke Superclass
        super.undoableEditHappened(e);
        // Update Undo/Redo Actions
        updateHistoryButtons();
      }
    };
        

The undo manager is then registered with the graph, together with the selection and key listener.


    // Register UndoManager with the Model
    graph.getModel().addUndoableEditListener(undoManager);
    // Register the Selection Listener for Toolbar Update
    graph.getSelectionModel().addGraphSelectionListener(this);
    // Listen for Delete Keystroke when the Graph has Focus
    graph.addKeyListener(this);
        

Finally, the panel is constructed out of the toolbar and the graph object:


    // Add a ToolBar
    add(createToolBar(), BorderLayout.NORTH);
    // Add the Graph as Center Component
    add(new JScrollPane(graph), BorderLayout.CENTER);
  }
        

The Editor object provides a set of methods to change the graph, namely, an insert, connect, group, ungroup, toFront, toBack, undo and redo method, which are explained below. The cut, copy and paste actions are implemented in the JGraph core API, and are explained at the end.

Insert

The insert method is used to add a vertex at a specific point. The method creates the vertex, and adds a floating port.


  // Insert a new Vertex at point
  public void insert(Point pt) {
    // Construct Vertex with no Label
    DefaultGraphCell vertex = new DefaultGraphCell();
    // Add one Floating Port
    vertex.add(new DefaultPort());
        

Then the specified point is applied to the grid, and used to construct the bounds of the vertex, which are subsequently stored in the vertices' attributes, which are created and accessed using the GraphConstants class:


    // Snap the Point to the Grid
    pt = graph.snap(new Point(pt));
    // Default Size for the new Vertex
    Dimension size = new Dimension(25,25);
    // Create a Map that holds the attributes for the Vertex
    Map attr = GraphConstants.createMap();
    // Add a Bounds Attribute to the Map
    GraphConstants.setBounds(attr, new Rectangle(pt, size));
        

To make the layering visible, the vertices' attributes are initialized with a black border, and a white background:


    // Add a Border Color Attribute to the Map
    GraphConstants.setBorderColor(attr, Color.black);
    // Add a White Background
    GraphConstants.setBackground(attr, Color.white);
    // Make Vertex Opaque
    GraphConstants.setOpaque(attr, true);
        

Then, the new vertex and its attributes are inserted into the model. To associate the vertex with its attributes, an additional map, from the cell to its attributes is used:


    // Construct the argument for the Insert (Nested Map)
    Hashtable nest = new Hashtable();
    // Associate the Vertex with its Attributes
    nest.put(vertex, attr);
    // Insert wants an Object-Array
    Object[] arg = new Object[]{vertex};
    // Insert the Vertex and its Attributes
    graph.getModel().insert(arg, null, null, nest);
  }
        

Connect

The connect method may be used to insert an edge between the specified source and target port:


  // Insert a new Edge between source and target
  public void connect(Port source, Port target) {
    // Construct Edge with no label
    DefaultEdge edge = new DefaultEdge(edge);
    // Use Edge to Connect Source and Target
    ConnectionSet cs = new ConnectionSet(edge, source, target);
        

The new edge should have an arrow at the end. Again, the attributes are created using the GraphConstants class. The attributes are then associated with the edge using a nested map, which is passed to the model's insert method:


    // Create a Map that holds the attributes for the edge
    Map attr = GraphConstants.createMap();
    // Add a Line End Attribute
    GraphConstants.setLineEnd(attr, GraphConstants.SIMPLE);
    // Construct a Map from cells to Maps (for insert)
    Hashtable nest = new Hashtable();
    // Associate the Edge with its Attributes
    nest.put(edge, attr);
    // Insert wants an Object-Array
    Object[] arg = new Object[]{edge};
    // Insert the Edge and its Attributes
    graph.getModel().insert(arg, cs, null, nest);
  }
        

Group

The group method is used to compose a new group out of an array of cells (typically the selected cells). Because the cells to be grouped are already contained in the model, a parent map must be used to change the cells' existing parents, and the cells' layering-order must be retrieved using the order method of the GraphView object.


  // Create a Group that Contains the Cells
  public void group(Object[] cells) {
    // Order Cells by View Layering
    cells = graph.getView().order(cells);
    // If Any Cells in View
    if (cells != null && cells.length > 0) {
      // Create Group Cell
      DefaultGraphCell group = new DefaultGraphCell();
      

In the following, the entries of the parent map are created by associating the existing cells (children) with the new cell (parent). The parent map and the new cell are then passed to the model's insert method to create the group:


      // Create Change Information ParentMap
      map = new ParentMap();
      // Insert Child Parent Entries
      for (int i = 0; i < cells.length; i++)
        map.addEntry(cells[i], group);
      // Insert wants an Object-Array
      Object[] arg = new Object[]{group};
      // Insert into model
      graph.getModel().insert(arg, null, map, null);
    }
  }
        

Ungroup

The inverse of the above is the ungroup method, which is used to replace groups by their children. Since the model makes no distinction between ports and children, we use the cell's view to identify a group. While the getChildCount for vertices with ports returns the number of ports, the corresponding VertexView's isLeaf method only returns true if at least one child is not a port, which is the definition of a group:


      // Determines if a Cell is a Group
    public boolean isGroup(Object cell) {
    // Map the Cell to its View
    CellView view = graph.getView().getMapping(cell, false);
    if (view != null)
      return !view.isLeaf();
    return false;
      }
      

Using the above method, the ungroup method is able to identify the groups in the array, and store these groups in a list for later removal. Additionally, the group's children are added to a list that makes up the future selection.


  // Ungroup the Groups in Cells and Select the Children
  public void ungroup(Object[] cells) {
    // Shortcut to the model
    GraphModel m = graph.getModel();
    // If any Cells
    if (cells != null && cells.length > 0) {
      // List that Holds the Groups
      ArrayList groups = new ArrayList();
      // List that Holds the Children
      ArrayList children = new ArrayList();
      // Loop Cells
      for (int i = 0; i < cells.length; i++) {
        // If Cell is a Group
        if (isGroup(cells[i])) {
          // Add to List of Groups
          groups.add(cells[i]);
          // Loop Children of Cell
          for (int j = 0; j < m.getChildCount(cells[i]); j++)
            // Get Child from Model
            children.add(m.getChild(cells[i], j));
        }
      }
      // Remove Groups from Model (Without Children)
      m.remove(groups.toArray());
      // Select Children
      graph.setSelectionCells(children.toArray());
    }
  }
        

To Front / To Back

The following methods use the graph view's functionality to change the layering of cells, or get redirected to the model based on isOrdered.


      // Brings the Specified Cells to Front
      public void toFront(Object[] c) {
    if (c != null && c.length > 0)
      graph.getView().toFront(graph.getView().getMapping(c));
      }
    
      // Sends the Specified Cells to Back
      public void toBack(Object[] c) {
    if (c != null && c.length > 0)
      graph.getView().toBack(graph.getView().getMapping(c));
      }
      

Undo / Redo

The following methods use the undo manager's functionality to undo or redo a change to the model or the graph view:


      // Undo the last Change to the Model or the View
      public void undo() {
    try {
      undoManager.undo(graph.getView());
    } catch (Exception ex) {
      System.err.println(ex);
    } finally {
      updateHistoryButtons();
    }
      }
    
      // Redo the last Change to the Model or the View
      public void redo() {
    try {
      undoManager.redo(graph.getView());
    } catch (Exception ex) {
      System.err.println(ex);
    } finally {
      updateHistoryButtons();
    }
      }
      

Graph Component

Let's look at the implementation of the MyGraph class, which is GraphEd's main UI component. A custom graph is necessary to override the default implementation's use of the right mouse button to change an edge's points, and also to change flags, and to use the custom model and marquee handler, which are printed below. The MyGraph class is implemented as an inner class:


  // Custom Graph
  public class MyGraph extends JGraph {
    // Construct the Graph using the Model as its Data Source
    public MyGraph(GraphModel model) {
      super(model);
      // Use a Custom Marquee Handler
      setMarqueeHandler(new MyMarqueeHandler());
      // Tell the Graph to Select new Cells upon Insertion
      setSelectNewCells(true);
      // Make Ports Visible by Default
      setPortsVisible(true);
      // Use the Grid (but don't make it Visible)
      setGridEnabled(true);
      // Set the Grid Size to 6 Pixel
      setGridSize(6);
      // Set the Snap Size to 1 Pixel
      setSnapSize(1);
    }
        

To override the right mouse button trigger with a shift trigger, an indirection is used. By overriding the createEdgeView method, we can define our own EdgeView class, which in turn overrides the isAddPointEvent method, and isRemovePointEvent method to check the desired trigger:


    // Override Superclass Method to Return Custom EdgeView
    protected EdgeView createEdgeView(Edge e, CellMapper c) {
      // Return Custom EdgeView
      return new EdgeView(e, this, c) {
        // Points are Added using Shift-Click
        public boolean isAddPointEvent(MouseEvent event) {
          return event.isShiftDown();
        }
        // Points are Removed using Shift-Click
        public boolean isRemovePointEvent(MouseEvent event) {
          return event.isShiftDown();
        }
      };
    }
      }
      

Graph Model

The custom model extends DefaultGraphModel, and overrides its acceptsSource and acceptsTarget methods. The methods prevent self-references, that is, if the specified port is equal to the existing source or target port, then they return false:


  // A Custom Model that does not allow Self-References
  public class MyModel extends DefaultGraphModel {
    // Source only Valid if not Equal to Target
    public boolean acceptsSource(Object edge, Object port) {
      return (((Edge) edge).getTarget() != port);
    }
    // Target only Valid if not Equal to Source
    public boolean acceptsTarget(Object edge, Object port) {
      return (((Edge) edge).getSource() != port);
    }
  }
        

Marquee Handler

The idea of the marquee handler is to act as a "high-level" mouse handler, with additional painting capabilites. Here is the inner class definition:


      // Connect Vertices and Display Popup Menus
      public class MyMarqueeHandler extends BasicMarqueeHandler {
    // Holds the Start and the Current Point
    protected Point start, current;
    
    // Holds the First and the Current Port
    protected PortView port, firstPort;
      

The isForceMarqueeEvent method is used to fetch the subsequent mousePressed, mouseDragged and mouseReleased events. Thus, the marquee handler may be used to gain control over the mouse. The argument to the method is the event that triggered the call, namely the mousePressed event. (The graph's portsVisible flag is used to toggle the connect mode.)


  // Gain Control (for PopupMenu and ConnectMode)
  public boolean isForceMarqueeEvent(MouseEvent e) {
    // Wants to Display the PopupMenu
    if (SwingUtilities.isRightMouseButton(e))
      return true;
    // Find and Remember Port
    port = getSourcePortAt(e.getPoint());
    // If Port Found and in ConnectMode (=Ports Visible)
    if (port != null && graph.isPortsVisible())
      return true;
    // Else Call Superclass
    return super.isForceMarqueeEvent(e);
  }
        

The mousePressed method is used to display the popup menu, or to initiate the connection establishment, if the global port variable has been set.


    // Display PopupMenu or Remember Location and Port
    public void mousePressed(final MouseEvent e) {
      // If Right Mouse Button
      if (SwingUtilities.isRightMouseButton(e)) {
        // Scale From Screen to Model
        Point l = graph.fromScreen(e.getPoint());
        // Find Cell in Model Coordinates
        Object c = graph.getFirstCellForLocation(l.x,l.y);
        // Create PopupMenu for the Cell
        JPopupMenu menu = createPopupMenu(e.getPoint(), c);
        // Display PopupMenu
        menu.show(graph, e.getX(), e.getY());
        // Else if in ConnectMode and Remembered Port is Valid
      } else if (port != null && !e.isConsumed() &&
              graph.isPortsVisible()) {
        // Remember Start Location
        start = graph.toScreen(port.getLocation(null));
        // Remember First Port
        firstPort = port;
        // Consume Event
        e.consume();
      } else
        // Call Superclass
        super.mousePressed(e);
    }
      

The mouseDragged method is messaged repeatedly, before the mouseReleased method is invoked. The method is used to provide the live-preview, that is, to draw a line between the source and target port for visual feedback:


    // Find Port under Mouse and Repaint Connector
    public void mouseDragged(MouseEvent e) {
      // If remembered Start Point is Valid
      if (start != null && !e.isConsumed()) {
        // Fetch Graphics from Graph
        Graphics g = graph.getGraphics();
        // Xor-Paint the old Connector (Hide old Connector)
        paintConnector(Color.black, graph.getBackground(), g);
        // Reset Remembered Port
        port = getTargetPortAt(e.getPoint());
        // If Port was found then Point to Port Location
        if (port != null)
          current = graph.toScreen(port.getLocation(null));
        // Else If no Port found Point to Mouse Location
        else
          current = graph.snap(e.getPoint());
        // Xor-Paint the new Connector
        paintConnector(graph.getBackground(), Color.black, g);
        // Consume Event
        e.consume();
      }
      // Call Superclass
      super.mouseDragged(e);
    }
      

The following method is called when the mouse button is released. If a valid source and target port exist, the connection is established using the editor's connect method:


    // Establish the Connection
    public void mouseReleased(MouseEvent e) {
      // If Valid Event, Current and First Port
      if (e != null && !e.isConsumed() && port != null &&
        firstPort != null && firstPort != port) {
        // Fetch the Underlying Source Port
        Port source = (Port) firstPort.getCell();
        // Fetch the Underlying Target Port
        Port target = (Port) port.getCell();
        // Then Establish Connection
        connect(source, target);
        // Consume Event
        e.consume();
      } else {
        // Else Repaint the Graph
        graph.repaint();
      }
      // Reset Global Vars
      firstPort = port = null;
      start = current = null;
      // Call Superclass
      super.mouseReleased(e);
    }
      

The marquee handler also implements the mouseMoved method, which is messaged independently of the others, to change the mouse pointer when over a port:


    // Show Special Cursor if Over Port
    public void mouseMoved(MouseEvent e) {
      // Check Mode and Find Port
      if (e != null && getSourcePortAt(e.getPoint()) != null &&
        !e.isConsumed() && graph.isPortsVisible()) {
        // Set Cusor on Graph (Automatically Reset)
        graph.setCursor(new Cursor(Cursor.HAND_CURSOR));
        // Consume Event
        e.consume();
      }
      // Call Superclass
      super.mouseReleased(e);
    }
      

Here are the helper methods used by the custom marquee handler. The first is simply used to retrieve the port at a specified position. (The method is named getSourcePortAt because another method must be used to retrieve the target port.)


    // Returns the Port at the specified Position
    public PortView getSourcePortAt(Point point) {
      // Scale from Screen to Model
      Point tmp = graph.fromScreen(new Point(point));
      // Find a Port View in Model Coordinates and Remember
      return graph.getPortViewAt(tmp.x, tmp.y);
    }
      

The getTargetPortAt checks if there is a cell under the mouse pointer, and if one is found, it returns its "default" port (first port).


    // Find a Cell and Return its Defaulr Port
    protected PortView getTargetPortAt(Point p) {
      // Find Cell at point (No scaling needed here)
      Object cell = graph.getFirstCellForLocation(p.x, p.y);
      // Shortcut Variable
      GraphModel model = graph.getModel();
      // Loop Children to find first PortView
      for (int i = 0; i < model.getChildCount(cell); i++) {
        // Get Child from Model
        Object tmp = graph.getModel().getChild(cell, i);
        // Map Cell to View
        tmp = graph.getView().getMapping(tmp, false);
        // If is Port View and not equal to First Port
        if (tmp instanceof PortView && tmp != firstPort)
          // Return as PortView
          return (PortView) tmp;
      }
      // No Port View found
      return getSourcePortAt(point);
    }
      

The paintConnector method displays a preview of the edge to be inserted. (The paintPort method is not shown.)


  // Use Xor-Mode on Graphics to Paint Connector
  void paintConnector(Color fg, Color bg, Graphics g) {
    // Set Foreground
    g.setColor(fg);
    // Set Xor-Mode Color
    g.setXORMode(bg);
    // Highlight the Current Port
    paintPort(graph.getGraphics());
    // If Valid First Port, Start and Current Point
    if (firstPort != null && start != null &&
          current != null) {
      // Then Draw A Line From Start to Current Point
      g.drawLine(start.x, start.y, current.x, current.y);
    }
  } // End of Editor.MyMarqueeHandler
        

The rest of the Editor class implements the methods to create the popup menu (not shown), and the toolbar. These methods mostly deal with the creation of action objects, but the copy, paste, and cut actions are exceptions:


  // Creates a Toolbar
  public JToolBar createToolBar() {
    JToolBar toolbar = new JToolBar();
    toolbar.setFloatable(false);
    (...)
    // Copy
    action = graph.getTransferHandler().getCopyAction();
    url = getClass().getClassLoader().getResource("copy.gif");
    action.putValue(Action.SMALL_ICON, new ImageIcon(url));
    toolbar.add(copy = new EventRedirector(action));
    (...)
  }
        

Because the source of an event that is executed from the toolbar is the JToolBar instance, and the copy, cut and paste actions assume a graph as the source, an indirection must be used to change the source to point to the graph:


  // This will change the source of the actionevent to graph.
  protected class EventRedirector extends AbstractAction {
    protected Action action;
    // Construct the "Wrapper" Action
    public EventRedirector(Action a) {
      super("", (ImageIcon) a.getValue(Action.SMALL_ICON));
      this.action = a;
    }
  
    // Redirect the Actionevent to the Wrapped Action
    public void actionPerformed(ActionEvent e) {
      e = new ActionEvent(graph, e.getID(),
      e.getActionCommand(), e.getModifiers());
      action.actionPerformed(e);
    }
  }