3. Step by Step

Now that you have all the background knowledge needed to understand the writing of a reference frame, it is time to step by step develop our own example reference frame providing nodes and edges for a graphical calculator.

a. The reference frame's package

All the nodes and edges we want to include in our plug-in are already given, including implementation of the SyncListener interface necessary for synchronisation if Cool Modes is coupled.

In our example, the graphical calculator, we have the following nodes and edges:

InputNode

In the InputNode you can enter a number. If you create an AdditionEdge or a SubtractionEdge from it to an OutputNode, this number will be added to (respectively subtracted from) the value of this OutputNode. Of course, it does not make any sense to create such an edge pointing towards an InputNode because its value is always determined by what the user types in.

OutputNode

An OutputNode that has no edges pointing towards it has got a value of 0. If there is an AdditionEdge or SubtractionEdge pointing towards it the value of the node this edge comes from will be added to respectively subtracted from the OutputNode's value. Of course, an OutputNode can also have an AdditionEdge or SubtractionEdge pointing towards another OutputNode.

AdditionEdge

The value of an InputNode or OutputNode this edge comes from will be added to the value of the OutputNode this edge is pointing towards.

SubtractionEdge

The value of an InputNode or OutputNode this edge comes from will be subtracted from the value of the OutputNode this edge is pointing towards.

Now we will create a new package called myplugins.calc and will put all the four classes mentioned above and their model and view classes into this package so that we have:

To provide multi-language support we also need myplugins.calc.CalcProp and its corresponding language files.

Moreover, we will create a directory myplugins/calc/resources which we will need later. Here all resources like images, sounds or movies that belong to our reference frame will be placed. For now, we will put the following files into this directory:

minus.gif plus.gif

These images will be needed when an AdditionEdge respectively a SubtractionEdge is created. The image will then be attached to the graphical representation of the StyledEdge which is the parent class of both AdditionEdge and SubtractionEdge.

b. The ReferenceFrame class

The first thing to do is to develop the controlling class, the ReferenceFrame. It provides any functionality a plug-in may have. Though it is not necessary to build a graphical user-interface, called palette, we will do so later.
myplugins.calc.CalcRefFrame.

Building the ReferenceFrame

To develop a controller class, namely ReferenceFrame is not that difficult. We need to initialize our graphical user-interface, the palette class, set node associations, package prefixes and provide rules. In the next steps you will learn how to realize this.

Let's take a look at the object variables of our example, CalcRefFrame:

protected AbstractPalette palette;
protected CalcProp prop;
					

The palette property is inherited from AbstractReferenceFrame and later holds our palette object. To support a multi-language environment we need to have a ResourceBundle prop that holds track of the current selected locale and the correct message strings. Moreover our ReferenceFrame needs to implement the LocaleListener interface to be informed of locale changes. (For more information about localization consult the JGraph HOWTO).

Initialization

To initialize our ReferenceFrame properly we will build a parameterless constructor that instantiates the palette object, and an init() method that will be called by Cool Modes after instantiation.

public CalcRefFrame() {
    palette = new CalcPalette();
}

public void init(GraphApplication manager) {
    super.init(manager);
    prop.reloadBundle();
    ((CalcPalette)palette).init();
    LocaleManager.getLocaleManager().addLocaleListener(this);
}
					

Getting drag-and-drop to work

To have a functional plug-in it should be possible to duplicate our nodes from the palette into a workspace via drag-and-drop or to draw our edges.
We simply need to create associations between the nodes respectively edges and their corresponding models. To do so, we override the getNodeAssociations() and getEdgeAssociations() methods inherited from AbstractReferenceFrame. We must return an Association array with one entry for each of our nodes or edges. The constructor of the Association class requires two parameters: a model Class and an element Class.

public Association[] getNodeAssociations() {
    Association[] list = new Association[2];
    list[0] = new Association(InputNodeModel.class,InputNode.class);
    list[1] = new Association(OutputNodeModel.class,OutputNode.class);
    return list;
}

public Association[] getEdgeAssociations() {
    Association[] list = new Association[2];
    list[0] = new Association(AdditionEdgeModel.class,AdditionEdge.class);
    list[1] = new Association(SubtractionEdgeModel.class, SubtractionEdge.class);
    return list;
}
					

Rules

Being familiar with the JGraph, you should know about the classes EdgeRule and ForbiddenCycleRule. If necessary, you can get more information on them in the JGraph HOWTO. If you need to make use of edge rules and forbidden cycle rules, you must override getRules().
In our example reference frame, it does not make any sense to allow our edges pointing towards an InputNode or to allow more than one instance of our edges between two of our nodes. Apart from that, creating cycles like in the picture below is obviously not only senseless but also causes problems and exceptions.

The result of constructing a cycle of OutputNodes

So, we decide to override the above mentioned method: no edges pointing towards an InputNode, only one edge between each instance of our two node classes, no addition edges or subtraction edges between other kinds of nodes and no cycles consisting of addition edges and/or subtraction edges:

public Rule[] getRules() {
    Rule[] rules = new Rule[6];
    Locale es = new Locale("es");
    rules[0] = new EdgeRule(null, new InputNode(), null, 0,
                new LocalizedMessage(prop.getProp("rule.0", Locale.ENGLISH), prop.getProp("rule.0", Locale.GERMAN), prop.getProp("rule.0", es)));
    rules[1] = new EdgeRule(new InputNode(), new OutputNode(), null, 1,
                new LocalizedMessage(prop.getProp("rule.12", Locale.ENGLISH), prop.getProp("rule.12", Locale.GERMAN), prop.getProp("rule.12", es)));
    rules[2] = new EdgeRule(new OutputNode(), new OutputNode(), null, 1,
                new LocalizedMessage(prop.getProp("rule.12", Locale.ENGLISH), prop.getProp("rule.12", Locale.GERMAN), prop.getProp("rule.12", es)));
    rules[3] = new EdgeRule(null, null, new AdditionEdge(), 0,
                new LocalizedMessage(prop.getProp("rule.3", Locale.ENGLISH), prop.getProp("rule.3", Locale.GERMAN), prop.getProp("rule.3", es)));
    rules[4] = new EdgeRule(null, null, new SubtractionEdge(), 0,
                new LocalizedMessage(prop.getProp("rule.4", Locale.ENGLISH), prop.getProp("rule.4", Locale.GERMAN), prop.getProp("rule.4", es)));
    Edge[] edges = { new AdditionEdge(), new SubtractionEdge() };
    rules[5] = new ForbiddenCycleRule(edges,
        new LocalizedMessage(prop.getProp("cyclerule.0", Locale.ENGLISH), prop.getProp("cyclerule.0", Locale.GERMAN), prop.getProp("cyclerule.0", es)));
    return rules;
}
					

The final touches

The identifier

To destinguish between the number of plug-ins, each reference frame has to indentify itself by a unique identifier. This is done by defining the public abstract String getIdentifier() method.

public String getIdentifier() {
    return "Graphical Calculator";
}
					

The package prefixes

Here we must return the prefixes of our reference frame's package (containing the classes of our nodes and edges). For our Graphical Calculator this is:

public String[] getPackagePrefixes() {
    String[] s = {"myplugins.calc"};
    return s;
}
					

Localization

As mentioned above our ReferenceFrame implements the LocaleListener interface to provide localization support. Because we registered it as listener in the init() method, everytime Cool Modes changes the language the following method will be called to handle the change.

public void localeChanged(LocaleEvent event) {
    prop.reloadBundle();
    ((CalcPalette)palette).changeLanguage();
}
					

c. The Palette class

Now that we have our reference frame's package, it is time to take a look at how to write the actual palette class. In our example, we will now develop the palette class myplugins.calc.CalcPalette.

Building the GUI

When writing a palette class, we will for the most part be busy building the plug-in's graphical user interface. This consists of a JGraph and usually a JPanel containing other Swing elements, namely instances of JToggleButton and a JCheckBox. Of course, it is principally possible to design any user interface for your reference frame.

Let's take a look at the object variables of our example, CalcPalette:

private JGraph palette;
private JPanel ui;
private TitledBorder border;
private JScrollPane pane;
private JToggleButton drawAddEdgeButton;
private JToggleButton drawSubEdgeButton;
private JToggleButton deleteEdgeButton;
private JCheckBox onceBox;
private boolean onceMode = false;
private CalcProp prop;
					

Except for the once mode flag and the prop, all the object variables are elements of our plug-in's graphical user interface. In our instance of JGraph, all the nodes that are part of our reference frame are placed so that the user can drag and drop them into the workspace.
One JToggleButton object is needed for each type of edge we want to offer in our reference frame.. These may be custom edges that we put into our plug-in's package and/or standard edges belonging to the com.spiriteam.graph package, for example SimpleEdge or StyledEdge. In our example, we need two such buttons, one for the AdditionEdge and one for the SubtractionEdge. Moreover, one such button is always necessary for the delete edge mode.
A JCheckBox is provided to offer the possibility to switch to once mode and back. See The modes for more information on this topic. When our reference frame is loaded, it is not in once mode so our onceMode flag is set to false.
prop is used for multi-language support. How to realize this easily is discussed in the JGraph HOWTO.

The palette's JGraph

The first element of the user interface is the JGraph object palette. As mentioned above, this is supposed to contain all the nodes belonging to our reference frame but not the edges.
There are actually two different approaches. One is to manually add the nodes to the JGraph, the other one is to load the JGraph from an XML file. In the example sources, we have made use of the latter, but of course, both ways will be explained here. In both cases, the JGraph will be built in the constructor of our palette class.

Manually building the JGraph

If you choose to manually build the JGraph, you will have to make use of its addNode(Node node,Point location) method. In case of our graphical calculator example, it might look like this:

public CalcPalette() {
    palette = new JGraph();
    palette.addNode(new InputNode(), new Point(30,30));
    palette.addNode(new CalendarNode(), new Point(30,110));
    palette.setInteractionMode(JGraph.COPYONLY_MODE);
}
					

The Point parameter gives the JGraph information on the location in the view of the graph, i.e. where to place the new node.
The last line is necessary since we do not want the user to be able to move our nodes or drag other nodes into the palette's JGraph but merely to create instances of our nodes in a workspace via drag-and-drop.

Loading the JGraph from an XML file

If you want to load the JGraph from an XML file, in our example the constructor will look like this:

public CalcPalette() {}

// will be called by CalcRefFrame
public void init() {
    palette=loadJGraph(prop.getProp("location.nodegraph"));
    palette.setInteractionMode(JGraph.COPYONLY_MODE);
    initUI();
}
					

The loadJGraph(String location) method is inherited from AbstractPalette. Place the file calc.xml in the appropriate directory to make the example work.

Now we should take a closer look at the XML file itself, so here it is:

<?xml version="1.0" encoding="ISO-8859-1"?>

<JGraph visible="true" mode="copypaste">
  <ARRAY>
    <NodeInfo>
      <InputNode ID="1" text="Input value" value="0" >
      </InputNode>
      <Point x="30" y="30" />
    </NodeInfo>
    <NodeInfo>
      <OutputNode ID="2" text="Output val." value="0" >
      </OutputNode>
      <Point x="30" y="110" />
    </NodeInfo>
  </ARRAY>
</JGraph>
				 

When you write your XML file you have to stick to this basic structure: a JGraph element containing an ARRAY element which then contains one or more NodeInfo elements. These NodeInfo elements consist of a node element and a Point element giving information on the position of the node in the JGraph.

Even if at the moment the parsing of our XML document works without writing our own DTD for our plug-in, it is recommended to write one. This DTD is then placed in the info/collide/coolmodes/config/ directory. In our example, the file CalcData.dtd looks like this:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!-- DTD for Calc Plug-In -->

<!ELEMENT InputNode EMPTY>
<!ATTLIST InputNode ID CDATA #REQUIRED
                       text CDATA #IMPLIED
                       value CDATA #IMPLIED>

<!ELEMENT OutputNode EMPTY>
<!ATTLIST OutputNode ID CDATA #REQUIRED
                     text CDATA #IMPLIED
                     value CDATA #IMPLIED>
			 

This, of course, must correspond to the configureByXML(Element element) method of our nodes' models.

Now, to get this incomplete version of our reference frame's graphical user interface on the screen, we have to override AbstractPalette's getUI() method and develop an initUI() method.
public Component getUI() {
    return ui;
}

private void initUI() {
    ui = new JPanel();
    ui.setLayout(new BorderLayout());
    ui.add(pane = new JScrollPane(palette), BorderLayout.CENTER);
}
					

Completing the UI

To complete the user interface of our Graphical Calculator plug-in, we still have to add the control elements for the edges. All this will also be done in the getUI() method.

First, we need another instance of JPanel which we will fit up with a GridLayout. We need one row for the once mode checkbox and one row for each type of edge mode including the delete edge mode:

private void initUI() {
    JPanel southPanel = new JPanel();
    southPanel.setLayout(new GridLayout(2,2));
    southPanel.setBorder(border = BorderFactory.createTitledBorder(prop.getProp("titledborder.edges")));

    ui = new JPanel();
    ui.setLayout(new BorderLayout());
    ui.add(pane = new JScrollPane(palette), BorderLayout.CENTER);
}
					
The once mode checkbox

The first row of our GridLayout will only include the JCheckBox needed to enable switching to once mode and back. Since the right column of the first row is supposed to be empty we add an empty JLabel to it:

private void initUI() {
    JPanel southPanel = new JPanel();
    southPanel.setLayout(new GridLayout(2,2));
    southPanel.setBorder(border = BorderFactory.createTitledBorder(prop.getProp("titledborder.edges")));

    onceBox = new JCheckBox(prop.getProp("checkbox.once"),false);
    onceBox.addActionListener(this);
    southPanel.add(onceBox);

    ui = new JPanel();
    ui.setLayout(new BorderLayout());
    ui.add(pane = new JScrollPane(palette), BorderLayout.CENTER);
}
					
The edges

As already mentioned, we need a JToggleButton for each kind of edge belonging to our reference frame (at least this is suitable, feel free to do it differently). This will usually be added to the left column of a row in the GridLayout. Additionally, we need a graphical representation of each kind of edge which will be part of the JToggleButton. This graphical representation is actually realized by another image resource.

private void initUI() {
    JPanel southPanel = new JPanel();
    southPanel.setLayout(new GridLayout(2,2));
    southPanel.setBorder(border = BorderFactory.createTitledBorder(prop.getProp("titledborder.edges")));

    drawAddEdgeButton = new JToggleButton(prop.getProp("button.additionedge"), new ImageIcon(getClass().getResource("/info/collide/coolmodes/palettes/calc/resources/Icon.Edges.Add.gif")), false);
    drawAddEdgeButton.addActionListener(this);
    drawAddEdgeButton.setMargin(new Insets(1, 1, 1, 1));
    drawAddEdgeButton.setIconTextGap(2);
    drawAddEdgeButton.setHorizontalAlignment(SwingConstants.LEFT);
    southPanel.add(drawAddEdgeButton);

    drawSubEdgeButton = new JToggleButton(prop.getProp("button.subtractionedge"), new ImageIcon(getClass().getResource("/info/collide/coolmodes/palettes/calc/resources/Icon.Edges.Substract.gif")), false);
    drawSubEdgeButton.addActionListener(this);
    drawSubEdgeButton.setMargin(new Insets(1, 1, 1, 1));
    drawSubEdgeButton.setIconTextGap(2);
    drawSubEdgeButton.setHorizontalAlignment(SwingConstants.LEFT);
    southPanel.add(drawSubEdgeButton);

    onceBox = new JCheckBox(prop.getProp("checkbox.once"),false);
    onceBox.addActionListener(this);
    southPanel.add(onceBox);

    ui = new JPanel();
    ui.setLayout(new BorderLayout());
    ui.add(pane = new JScrollPane(palette), BorderLayout.CENTER);
}
					
Delete edge

Like the two previous seen buttons the delete button is a JToggleButton and works the same way.

private void initUI() {
    JPanel southPanel = new JPanel();
    southPanel.setLayout(new GridLayout(2,2));
    southPanel.setBorder(border = BorderFactory.createTitledBorder(prop.getProp("titledborder.edges")));

    drawAddEdgeButton = new JToggleButton(prop.getProp("button.additionedge"), new ImageIcon(getClass().getResource("/info/collide/coolmodes/palettes/calc/resources/Icon.Edges.Add.gif")), false);
    drawAddEdgeButton.addActionListener(this);
    drawAddEdgeButton.setMargin(new Insets(1, 1, 1, 1));
    drawAddEdgeButton.setIconTextGap(2);
    drawAddEdgeButton.setHorizontalAlignment(SwingConstants.LEFT);
    southPanel.add(drawAddEdgeButton);

    drawSubEdgeButton = new JToggleButton(prop.getProp("button.subtractionedge"), new ImageIcon(getClass().getResource("/info/collide/coolmodes/palettes/calc/resources/Icon.Edges.Substract.gif")), false);
    drawSubEdgeButton.addActionListener(this);
    drawSubEdgeButton.setMargin(new Insets(1, 1, 1, 1));
    drawSubEdgeButton.setIconTextGap(2);
    drawSubEdgeButton.setHorizontalAlignment(SwingConstants.LEFT);
    southPanel.add(drawSubEdgeButton);

    deleteEdgeButton = new JToggleButton(prop.getProp("button.deleteedge"), new ImageIcon(getClass().getResource("/info/collide/coolmodes/palettes/calc/resources/Icon.Edges.Delete.gif")), false);
    deleteEdgeButton.addActionListener(this);
    deleteEdgeButton.setMargin(new Insets(1, 1, 1, 1));
    deleteEdgeButton.setIconTextGap(2);
    deleteEdgeButton.setHorizontalAlignment(SwingConstants.LEFT);
    southPanel.add(deleteEdgeButton);

    onceBox = new JCheckBox(prop.getProp("checkbox.once"),false);
    onceBox.addActionListener(this);
    southPanel.add(onceBox);

    ui = new JPanel();
    ui.setLayout(new BorderLayout());
    ui.add(pane = new JScrollPane(palette), BorderLayout.CENTER);
}
					
The graphical calculator's palette window

Our palette window should now look like this: the JGraph containing one instance of the input node and one of the output node for drag-and-drop, below the JPanel with the three toggle buttons (each for one kind of edge mode), the once mode checkbox, two instances of JGraph representing addition edges respectively subtraction edges and an image representing the delete edge mode.

The node mode UI

Now that our getUI() is complete it is time to override the setNodeModeUI() method. This method is always called when Cool Modes is in once mode and the user has drawn an edge so that Cool Modes switches back to node mode. It is common that in such a case all the edges' toggle buttons and the delete edge toggle button are deselected and that the once mode checkbox is enabled:

public void setNodeModeUI() {
    drawAddEdgeButton.setSelected(false);
    drawSubEdgeButton.setSelected(false);
    deleteEdgeButton.setSelected(false);
    onceBox.setEnabled(true);
}
					

Changing the language

Because Cool Modes supports a multi-language environment we need to provide some related methods. Whenever the locale changes, Cool Modes will inform our ReferenceFrame which then reloads the ResourceBundle prop and calls the following method:

public void changeLanguage() {
    border.setTitle(prop.getProp("titledborder.edges"));
    drawAddEdgeButton.setText(prop.getProp("button.additionedge"));
    drawSubEdgeButton.setText(prop.getProp("button.subtractionedge"));
    deleteEdgeButton.setText(prop.getProp("button.deleteedge"));
    onceBox.setText(prop.getProp("checkbox.once"));
    palette = loadJGraph(prop.getProp("location.nodegraph"));
    palette.setInteractionMode(JGraph.COPYONLY_MODE);
    pane.setViewportView(palette);
}
					

Processing the ActionEvents

The palette class itself is responsible for processing the ActionEvents of its graphical user interface so it must implement the ActionListener interface. We have already assumed that this interface is implemented when we registrated our palette with the checkbox and the toggle buttons in the getUI() method. Now, let's take a look at what our actionPerformed() method has got to do.

The once mode checkbox

The palette class must always keep the once mode flag up-to-date. This is as simple as that:

public void actionPerformed(ActionEvent event) {
    if (event.getSource().equals(onceBox)) {
        onceMode = onceBox.isSelected();
    }
}
					
The edge mode toggle buttons

Since Cool Modes can only be in one type of edge mode at a time (i.e. only one type of edge that is drawn at the moment), it is necessary that the graphical user interface symbolizes this by deselecting all the other edge toggle buttons if one of those toggle buttons has been selected by the user. Moreover, changing to once mode and back is not supposed to be possible in edge mode so the checkbox has to be disabled. We tell Cool Modes with which kind of edge to enter edge mode by giving a new instance of the respective edge class and the once mode flag as parameters of the toEdgeMode(Edge e, boolean once) method inherited from AbstractPalette. If we want to enter delete edge mode the edge parameter has to be null.
If, on the other hand, the ActionEvent is caused due to the user deselecting one of the toggle buttons, we have to switch to node mode and make sure that the once mode checkbox is again enabled.

public void actionPerformed(ActionEvent event) {
    if (event.getSource().equals(onceBox)) {
        onceMode = onceBox.isSelected();
    }

    if (event.getSource() == drawAddEdgeButton) {
        if (drawAddEdgeButton.isSelected()) {
            drawSubEdgeButton.setSelected(false);
            deleteEdgeButton.setSelected(false);
            toEdgeMode(new AdditionEdge(),onceMode);
            onceBox.setEnabled(false);
        }
        else {
            toNodeMode();
            onceBox.setEnabled(true);
        }
    }
    else if (event.getSource() == drawSubEdgeButton) {
        if (drawSubEdgeButton.isSelected()) {
            drawAddEdgeButton.setSelected(false);
            deleteEdgeButton.setSelected(false);
            toEdgeMode(new SubtractionEdge(),onceMode);
            onceBox.setEnabled(false);
        }
        else {
            toNodeMode();
            onceBox.setEnabled(true);
        }
    }
    else if (event.getSource() == deleteEdgeButton) {
        if (deleteEdgeButton.isSelected()) {
            drawAddEdgeButton.setSelected(false);
            drawSubEdgeButton.setSelected(false);
            toEdgeMode(null,onceMode);
            onceBox.setEnabled(false);
        }
        else {
            toNodeMode();
            onceBox.setEnabled(true);
        }
    }

}
					
Activating the last workspace

Sometimes it can be important that immediately after performing an action in the palette window (e.g. selecting or deselecting a toggle button or a checkbox), there is an active workspace. This is why you should always call activateLastWorkspace(), inherited from AbstractPalette, at the end of your palette's actionPerformed() method.

The final touches

enterPalette()

When the plug-in is loaded or selected, its enterPalette() method is called. Usually, we want our plug-in to be in node mode in such a case, so we have to override this method:

public void enterPalette() {
    toNodeMode();
}
					

An icon for the plug-in

Up to now, our reference frame was symbolized by a question mark, the standard icon referred to in AbstractPalette. When creating your own icon, give it a size of 32 x 32 px. This icon should then be placed in the resources folder of your reference frame's package. In our example, we put the file calc.gif into the folder myplugins/calc/resources.
Once this is done we have to override thegetIconLocation() method so that the correct path is given back to the getIcon() method:

protected String getIconLocation() {
    return "myplugins/calc/calc.png";
}
					

The tooltip hint

When the cursor resides a few seconds above our icon, we want to show the plug-ins name to the user. This can be realized by our tooltip method:

public String getToolTipText() {
    return "Graphical Calculator";
}
					

d. Summary

Now that we have completed our example reference frame and discussed every step in full detail, it is time to give a short summary of the most important steps that have to be done when creating your own plug-in. This summary serves as a quick instruction. For more information, please take a look at the appropriate part of this chapter of the HOWTO.