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.
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:
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.
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.
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.
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.
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.
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).
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);
}
|
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;
}
|
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;
}
|
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";
}
|
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;
}
|
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();
}
|
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.
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 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.
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.
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);
}
|
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 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);
}
|
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);
}
|
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.
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);
}
|
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);
}
|
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 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();
}
}
|
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);
}
}
}
|
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.
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();
}
|
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";
}
|
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";
}
|
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.