This article describes an enhancement to GeeWhiz Prolog, a project to implement the Prolog language in the NetBeans IDE. See the GeeWhiz Prolog home page for more information.
What We'll Be Doing
In this add-on to our GeeWhiz Prolog project, we're going to add some new features and Geewhizability to our predicate modeler. Big thanks go to David Kaspar who provided the Visual Library examples from which much of this material is stolen taken. The examples are located in the "main/contrib" section of the NetBeans Mercurial repository (although I haven't updated my own library yet to reflect this). You can find them at this link: http://hg.netbeans.org/main/contrib/file/ under "visual.examples".
Adding An Export Function
The first change we're going to make to our nifty Prolog predicate diagrammer is that we're going to add a means by which we can export our diagram to a Portable Network Graphics (PNG) file. Once we have a PNG file, we can print it, email it to Aunt Margaret in Philly, or (hypothetically) stick it on our GeeWhiz Prolog home page. So let's get started.
First, create a new Java class in your project called "SceneExport". Replace the empty class with this:
public class SceneExport {
public void exportScene(VMDGraphScene scene) {
JComponent view;
Dimension dim;
BufferedImage bufImage;
Graphics2D graphics;
File file;
view = scene.getView();
dim = view.getSize();
bufImage = new BufferedImage (dim.width, dim.height,
BufferedImage.TYPE_4BYTE_ABGR);
graphics = bufImage.createGraphics ();
scene.paint (graphics);
graphics.dispose ();
file = getPNGSaveFile(view);
if (file != null) {
diagramToPNG(file, bufImage);
}
}
private File getPNGSaveFile (JComponent view) {
JFileChooser chooser;
File file;
//TODO: save selected directory as default for next time
chooser = new JFileChooser();
chooser.setDialogTitle("Export Scene As ...");
chooser.setDialogType(JFileChooser.SAVE_DIALOG);
chooser.setMultiSelectionEnabled(false);
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
chooser.setFileFilter(new FileFilter() {
public boolean accept(File file) {
if (file.isDirectory())
return true;
return file.getName().toLowerCase().endsWith(".png");
}
public String getDescription () {
return "Portable Network Graphics (.png)";
}
});
if (chooser.showSaveDialog (view) != JFileChooser.APPROVE_OPTION) {
return null;
}
file = chooser.getSelectedFile ();
if (! file.getName ().toLowerCase ().endsWith (".png"))
file = new File (file.getParentFile (), file.getName () + ".png");
if (file.exists ()) {
DialogDescriptor descriptor = new DialogDescriptor (
"File (" + file.getAbsolutePath () +
") already exists. Do you want to overwrite it?",
"File Exists", true, DialogDescriptor.YES_NO_OPTION,
DialogDescriptor.NO_OPTION, null);
DialogDisplayer.getDefault().createDialog(descriptor).setVisible(true);
if (descriptor.getValue() != DialogDescriptor.YES_OPTION) {
return null;
}
}
return file;
}
private void diagramToPNG(File file, BufferedImage bufImage) {
try {
ImageIO.write (bufImage, "png", file); // NOI18N
} catch (IOException e) {
ErrorManager.getDefault ().notify (e);
}
}
}
What the above code does is copy the predicate diagram graphics to a BufferedImage then save the image to a PNG file. Pretty straightforward, and taken from Visual Library example code that I haven't been able to find again. So fix imports, save and close the SceneExport class then create a new Java class called "SceneUtils". Replace the empty class with this code:
public class SceneUtils {
private static final String EXPORT_SCENE = "export";
private static final String ACTION_MOVE = "move";
private static final String REMOVE_LABEL = "remove";
private static final String REPLACE_LABEL = "replace";
private LabelWidget label;
private Scene scene = null;
private boolean isVMD;
public SceneUtils(Scene scene) {
this.scene = scene;
isVMD = scene instanceof VMDGraphScene;
}
public void addContextMenu() {
WidgetAction popup;
if (scene == null) {
return;
}
label = new LabelWidget (scene, "Right-click to open popup menu.");
label.setPreferredLocation (new Point (100, 100));
scene.addChild (label);
popup = ActionFactory.createPopupMenuAction (new MyPopupProvider ());
scene.getActions ().addAction (popup);
// leave popup menu up (?)
scene.createActions (ACTION_MOVE).addAction (popup);
label.createActions (ACTION_MOVE).addAction (ActionFactory.
createMoveAction ());
}
private void exportScene() {
SceneExport exporter;
if (isVMD) {
// TODO: thread
exporter = new SceneExport();
exporter.exportScene((VMDGraphScene) scene);
}
}
private void addMoveAction() {
scene.setActiveTool(ACTION_MOVE);
}
private void removeLabel() {
// removes it from the scene, doesn't delete it
this.label.removeFromParent();
}
private void replaceLabel() {
if (label.getParentWidget() == null) {
scene.addChild(label);
scene.validate();
}
}
private final class MyPopupProvider implements PopupMenuProvider,
ActionListener {
private JPopupMenu menu;
public MyPopupProvider () {
menu = new JPopupMenu ("Popup menu");
JMenuItem item;
if (isVMD) {
item = new JMenuItem ("Export scene to PNG...");
item.setActionCommand (EXPORT_SCENE);
item.addActionListener (this);
menu.add (item);
}
item = new JMenuItem ("Move the label");
item.setActionCommand (ACTION_MOVE);
item.addActionListener (this);
menu.add (item);
item = new JMenuItem ("Delete the label");
item.setActionCommand (REMOVE_LABEL);
item.addActionListener (this);
menu.add (item);
item = new JMenuItem ("Replace the label");
item.setActionCommand (REPLACE_LABEL);
item.addActionListener (this);
menu.add (item);
}
public JPopupMenu getPopupMenu (Widget widget, Point localLocation) {
return menu;
}
public void actionPerformed (ActionEvent e) {
String cmd;
cmd = e.getActionCommand ();
if (cmd.equals(EXPORT_SCENE)) {
exportScene();
} else if (cmd.equals(ACTION_MOVE)) {
addMoveAction();
} else if (cmd.equals(REMOVE_LABEL)) {
removeLabel();
} else if (cmd.equals(REPLACE_LABEL)) {
replaceLabel();
}
}
}
}
In SceneUtils we're creating a context menu that pops up when you right-click on our lovely model diagram. In this incarnation it was written to cope with any kind of Scene even though the export function only handles VMDGraphScene. The menu includes some choices that are of limited utility in practice but serve to demonstrate the capability of the right-click popup. We also introduce the LabelWidget, which we haven't seen before. So fix imports, save and close the SceneUtils file.
Finally, we're going to change the VPrologGraphScene class to use our SceneUtils class. At the beginning of each constructor, add the following code:
SceneUtils sceneUtils;
sceneUtils = new SceneUtils(this);
sceneUtils.addContextMenu();
At the very end of each constructor, add this line:
layoutScene();
I found the "layoutScene" method by accident when I was working through some of David Kaspar's Visual Library examples. It automatically cleans up our diagram! Evidentally there is a more robust orthogonal link router in the works, but for our purposes this one works just fine and eliminates much of the haphazardness of the random way we were placing nodes before. Woohoo!
Save and close the file. Now we're ready to test the beast. Clean and build the project, then install it to the target IDE. (You missed doing that since completing the original GeeWhiz project, didn't you?) Open your Algorithms project and wait for the indexing to stop. Now we're ready to check out our new popup menu. Open the sieve.pro file (or any other Prolog file) and wait for the Navigator to fill in the statements and for the syntax to color. Then go to the menu and click "View" "Show Prolog Diagram" to bring up the predicate model of our program. ("Gee whiz!") Right-click on the diagram to get our popup menu.

First, try moving, deleting and re-adding the label. Then, when you're tired of playing with that, try exporting the diagram to a PNG file. Cool, eh? If you monkey around with it enough, however, you'll notice that if you zoom and pan the diagram prior to exporting the PNG the export doesn't work exactly right. For now my solution for this glitch is: don't zoom or pan the diagram prior to exporting the PNG. Someday I'll look into fixing this; I think that maybe I need to get the dimensions from something other than the view graphics. But we have other things to do now, so let's leave it alone.
And by the way, don't forget to gasp with delight at the way layoutScene() fixes up our diagram when it is first displayed! When you're done testing and fooling around you can close the target IDE.
Adding Editor Windows To Nodes
The next feature we're going to add is a window containing the text Prolog source code for the predicate that opens up when we double-click on a predicate node. You wondered why we built the text into our PrologClause class, didn't you? This is why. First, create a new JFrame Java class called "NodeContents" by right-clicking on the package and selecting "New" "JFrame Form..." This is going to be the window in which we display our source code. In the "Design" view, add a scroll pane that snaps to the preferred borders of the frame and then add a text area that fills the scroll pane. Rename the text area to "textArea", then in Properties give it a descriptive tool tip, unclick the "editable" property and change the tab size to 4. In the JFrame properties, change the defaultCloseOperation to "HIDE". Switch to "Source" and delete the Main method. Right below the constructor, insert the following code:
public void setText(String text) {
textArea.setText(text);
}
public void changeTitle(String title) {
this.setTitle(title);
}
Save and close the file. Now open VPrologGraphScene and replace the whole VPrologGraphScene class with this:
public class VPrologGraphScene extends VMDGraphScene {
private static final Image IMAGE_NODE = Utilities.
loadImage ("org/hulles/geewhiz/node.png"); // NOI18N
private static final Image IMAGE_EXTERNAL = Utilities.
loadImage ("org/hulles/geewhiz/external.png"); // NOI18N
private static final Image IMAGE_ITEM = Utilities.
loadImage ("org/hulles/geewhiz/item.gif"); // NOI18N
private static int pinID = 1;
private static int edgeID = 1;
private List<PrologClause> clauses;
/** Creates a new instance of VPrologGraphScene */
public VPrologGraphScene() { // demo
SceneUtils sceneUtils;
sceneUtils = new SceneUtils(this);
sceneUtils.addContextMenu();
createNode (this, 100, 100, IMAGE_NODE, "Clause1", "Internal", null);
createPin (this, "Clause1", "start", IMAGE_ITEM, "Start", "Element");
createNode (this, 400, 100, IMAGE_NODE, "Clause2", "External",
Arrays.asList (IMAGE_EXTERNAL));
createPin (this, "Clause2", "ok", IMAGE_ITEM, "okCommand1", "Command");
createEdge (this, "start", "Clause2");
createEdge (this, "ok", "Clause1");
layoutScene();
}
public VPrologGraphScene(DataObject dObj) {
PrologAST pTree;
List<PrologClause> embedded;
String nodeID;
Integer instances;
String newPinID;
PrologClause existingClause;
String eNodeID;
List<String> eNodes;
SceneUtils sceneUtils;
VMDNodeWidget node;
sceneUtils = new SceneUtils(this);
sceneUtils.addContextMenu();
pTree = new PrologAST(dObj);
clauses = pTree.getClauses();
// create all primary nodes first so they're available
// to create edges to....
for (PrologClause clause : clauses) {
instances = clause.getInstanceCount();
nodeID = makeNodeID(clause);
node = createNode(this, randXPoint(), randYPoint(),
IMAGE_NODE, nodeID, instances.toString() + " instances", null);
addEditor(node, clause);
}
// now create pins and edges
eNodes = new ArrayList<String>();
for (PrologClause clause : clauses) {
nodeID = makeNodeID(clause);
embedded = clause.getBody();
for (PrologClause e : embedded) {
eNodeID = makeNodeID(e);
newPinID = "pin" + VPrologGraphScene.pinID++;
createPin(this, nodeID, newPinID,
IMAGE_ITEM, eNodeID, "Embedded");
existingClause = PrologClause.findClause(clauses, e);
if (existingClause == null) {
if (!eNodes.contains(eNodeID)) {
// externally defined (?)
createNode(this, randXPoint(), randYPoint(),
IMAGE_NODE, eNodeID, "External",
Arrays.asList (IMAGE_EXTERNAL));
eNodes.add(eNodeID);
}
}
createEdge(this, newPinID, eNodeID);
}
}
layoutScene();
}
private String makeNodeID(PrologClause clause) {
String name;
Integer arity;
name = clause.getName();
arity = clause.getArity();
return name + "/" + arity.toString();
}
private int randXPoint() {
return (int) (Math.random() * 800);
}
private int randYPoint() {
return (int) (Math.random() * 800);
}
private VMDNodeWidget createNode (VMDGraphScene scene, int x, int y,
Image image, String name, String type, List<Image> glyphs) {
VMDNodeWidget widget;
widget = (VMDNodeWidget) scene.addNode (name);
widget.setPreferredLocation (new Point (x, y));
widget.setNodeProperties (image, name, type, glyphs);
scene.addPin (name, name + VMDGraphScene.PIN_ID_DEFAULT_SUFFIX);
return widget;
}
private void createPin (VMDGraphScene scene, String nodeID, String pinID,
Image image, String name, String type) {
VMDPinWidget pinWidget;
pinWidget = (VMDPinWidget) scene.addPin (nodeID, pinID);
pinWidget.setProperties (name, null);
}
private void createEdge (VMDGraphScene scene, String sourcePinID,
String targetNodeID) {
String localEdgeId;
localEdgeId = "edge" + VPrologGraphScene.edgeID ++;
scene.addEdge (localEdgeId);
scene.setEdgeSource (localEdgeId, sourcePinID);
scene.setEdgeTarget (localEdgeId, targetNodeID +
VMDGraphScene.PIN_ID_DEFAULT_SUFFIX);
}
private void addEditor(VMDNodeWidget node, PrologClause clause) {
NodeEditor editor;
WidgetAction nodeAction;
editor = new NodeEditor(clause);
nodeAction = ActionFactory.createEditAction(editor);
node.getActions().addAction(nodeAction);
}
public void newLayout() {
layoutScene();
}
private class NodeEditor implements EditProvider {
private final PrologClause clause;
private final String popupTitle;
private final String popupText;
private NodeContents popup;
public NodeEditor(PrologClause clause) {
this.clause = clause;
this.popup = null;
popupTitle = makeNodeID(clause);
popupText = clause.getText();
}
public void edit(Widget node) {
// assumes read-only text
VMDNodeWidget myNode;
myNode = (VMDNodeWidget) node;
// lazy edit window construction
if (popup == null) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
popup = new NodeContents();
popup.changeTitle(popupTitle);
popup.setText(popupText);
popup.setVisible(true);
}
});
} else {
popup.setVisible(true);
}
}
}
}
What we're doing here is, for every defined node (as opposed to external node) that we create, we're adding an EditAction that creates a NodeContents window when the node is double-clicked. We hold off actually creating the NodeContents window until the node is double-clicked so we can be a good NetBeans neighbor. Also, notice that instead of disposing of the window when we close it we just hide it so we can set it visible again the next time it's clicked.
Adding A Satellite Window
Before we test the editor functionality we're going to go ahead and make another window that shows the entire diagram in miniature, called in Visual Library terms a satellite view. We're going to show it and hide it via our context menu. So create another new JFrame called NodeSatelliteView.
In "Design" mode add a panel to the frame that takes up the whole frame and make it "BorderLayout". Change the name from jPanel1 to satellitePanel and give it a tooltip "Prolog diagram satellite view". Change the JFrame defaultCloseOperation to "HIDE" and the title to "Prolog Satellite View".
Switch to "Source" mode, remove the Main method, and add this method below the constructor.
public void addView(JComponent view) {
satellitePanel.removeAll();
satellitePanel.add(view);
}
Save and close the file. Now open "SceneUtils" and replace the entire class with this.
public class SceneUtils {
private static final String EXPORT_SCENE = "export"; // NOI18N
private static final String LAYOUT_SCENE = "layout"; // NOI18N
private static final String PRINT_SCENE = "print"; // NOI18N
private static final String SATELLITE_VIEW = "satellite"; // NOI18N
private VPrologGraphScene scene = null;
private NodeSatelliteView viewer;
public SceneUtils(VPrologGraphScene scene) {
this.scene = scene;
}
public void addContextMenu() {
WidgetAction popup;
if (scene == null) {
return;
}
popup = ActionFactory.createPopupMenuAction (new MyPopupProvider ());
scene.getActions ().addAction (popup);
// leave popup menu up (?)
scene.createActions (LAYOUT_SCENE).addAction (popup);
}
private void exportScene() {
SceneExport exporter;
// TODO: thread
exporter = new SceneExport();
exporter.exportScene((VMDGraphScene) scene);
}
private void reLayoutScene() {
scene.newLayout();
}
private void printScene() {
// not yet implemented
}
private void makeSatelliteView() {
if (viewer == null) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
JComponent view;
viewer = new NodeSatelliteView();
view = scene.createSatelliteView();
viewer.addView(view);
viewer.setVisible(true);
}
});
} else {
if (viewer.isVisible()) {
viewer.setVisible(false);
} else {
viewer.setVisible(true);
}
}
}
private final class MyPopupProvider implements PopupMenuProvider, ActionListener {
private JPopupMenu menu;
public MyPopupProvider () {
menu = new JPopupMenu ("Popup menu");
JMenuItem item;
item = new JMenuItem ("Print scene...");
item.setActionCommand (PRINT_SCENE);
item.addActionListener (this);
menu.add (item);
item = new JMenuItem ("Export scene to PNG...");
item.setActionCommand (EXPORT_SCENE);
item.addActionListener (this);
menu.add (item);
item = new JMenuItem ("Redo scene layout");
item.setActionCommand (LAYOUT_SCENE);
item.addActionListener (this);
menu.add (item);
item = new JMenuItem ("Show / hide satellite view");
item.setActionCommand (SATELLITE_VIEW);
item.addActionListener (this);
menu.add (item);
}
public JPopupMenu getPopupMenu (Widget widget, Point localLocation) {
return menu;
}
public void actionPerformed (ActionEvent e) {
String cmd;
cmd = e.getActionCommand ();
if (cmd.equals(EXPORT_SCENE)) {
exportScene();
} else if (cmd.equals(PRINT_SCENE)) {
printScene();
} else if (cmd.equals(LAYOUT_SCENE)) {
reLayoutScene();
} else if (cmd.equals(SATELLITE_VIEW)) {
makeSatelliteView();
}
}
}
}
Notice that we got rid of the label and label actions and tailored the class to be specific to our Prolog needs instead of generic. We added an empty printing function, a layout function, and a satellite view function to our context menu and implemented the satellite view within the class. Fix imports, save and close the file. Now we're ready to test the work we've done. You'll like this.
Testing The New Diagram
Clean and build the main project, then install to the target IDE. Open the "Algorithms" project and open "sieve.pro". When the Navigator comes up, start up the modeler ("View" "Show Prolog Diagram") and double-click on a node. If the node is defined and not external then a window should open with the text of all the clauses that make up the predicate.

Notice that you can open more than one predicate window at a time but only one window per node, which makes sense within the context of the diagram.

Now right-click on the diagram to bring up our context menu.

Move a node or two then try out the "Redo layout" function. Hold your breath, then try "Show / hide satellite view".

Move the cursor around on the satellite view and watch what happens to your underlying diagram. Gee whiz! Pretty awesome, right? To close the satellite view you can click "Show / hide..." again in the context menu or just close the window. As you might suspect, the satellite view is not very useful with small diagrams like the one sieve.pro generates, but with a larger diagram like that in the screen shot it can be very useful indeed. Plus, it's just plain fun to have it around.
Going Further
One of the funky things about what we accomplished in this entry is that the screens for the node text and the screen for the satellite window are all JFrames independent of the diagram TopComponent. If you close the diagram, the frames stick around anyway. While you could possibly make a case that the little text windows are useful independently of the diagram, at least the satellite window should be dependent on the TopComponent and go away when it goes away. It is not useful without the diagram. So a reasonable next step would be to make the "pop up" screens for node text and the satellite window JDialogs or close them when the TopComponent goes away.
Now that we have all these new features on our diagram, what else could we do? The first thing that comes to mind is to allow the user to edit the code in the popup node window and have the changes reflected in the editor window source code. This would take some careful design because of the way multiple clauses make up a predicate, but it could certainly be done using techniques we've already looked at. We could disembowel the PrologAST class and store line and column numbers in PrologClause, then use the same way we pop into the source code from the output window to go into the editor and change the text. But the usefulness of editing directly in the popup window is pretty questionable, at least for Prolog programming, so we won't go that route. I won't, anyway, but you certainly can...
An even more interesting (to me) idea is to use the stuff we learned in GeeWhiz to write a whole new language interface. It would be based on Java but instead of writing code you would just manipulate nodes and edges then compile the diagram! Theoretically a person would not even have to know Java to "program" with this new language. I know, I know, it sounds like UML but it would be way cooler. Actually, this is something I've always wanted to develop, so look for "Project PipeDreams" coming your way soon....
Files From This Entry