Monday Oct 06, 2008

Custom Toolbar Loaded via Lookup

Let's say you want the toolbar to be on the right side of your application. Let's also say that you want the icons to be larger than 16x16 or 24x24. At the same time, you want to use the NetBeans Actions system, so you can't simply create a TopComponent and then put buttons on the TopComponent. How to solve this problem? Use Lookup! Here's how the result looks for me:

First, I removed all the existing toolbars, by simply adding this to the layer:

<file name="Toolbars_hidden"/>

So, now the toolbars were all gone when I ran the application. Then I created a new TopComponent. I also created a new mode specifically for the TopComponent, to position it along the right side of the application.

Next, I exposed my CallableSystemActions via the META-INF/services technique:

This is the complete content of the file in META-INF/services:

org.nb.customtoolbar.Action1
org.nb.customtoolbar.Action2
org.nb.customtoolbar.Action3
org.nb.customtoolbar.Action4
org.nb.customtoolbar.Action5

Each of the actions looks as follows:

public final class Action1 extends CallableSystemAction {

    @Override
    public void performAction() {
        JOptionPane.showMessageDialog(null, "hello from 1");
    }

    @Override
    protected String iconResource() {
        return "/org/nb/customtoolbar/pic1.png";
    }

    @Override
    public String getName() {
        return "rightSideToolBar";
    }

    @Override
    public HelpCtx getHelpCtx() {
        return HelpCtx.DEFAULT_HELP;
    }

    @Override
    protected boolean asynchronous() {
        return false;
    }

}

The only interesting thing is the getName(). I use that to determine whether or not the action should be displayed in the TopComponent. And how are they displayed in the TopComponent? Like this:

private CustomToolbarDisplayerTopComponent() {

    initComponents();
    
    //Here we iterate over implementations of 'CallableSystemAction':
    for (CallableSystemAction action : Lookup.getDefault().lookupAll(CallableSystemAction.class)) {
        //We're only interested in an implementation if it has the right name and an icon:
        if (action.getName().equals("rightSideToolBar") && action.getIcon() != null) {
            //Then we add a new button to the JPanel:
            addButton(action, jPanel1);
        }
    }

}

private static void addButton(final CallableSystemAction action, JPanel container) {
    JButton button = new JButton();
    button.setAlignmentX(Component.CENTER_ALIGNMENT);
    //The icon is set by calling 'getIcon' on the implementation:
    button.setIcon(action.getIcon());
    button.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            //The action is performed by calling 'actionPerformed' on the implementation:
            action.actionPerformed(e);
        }
    });
    container.add(button);
}

And that's all! The TopComponent is in a different module to where the actions are found; there is no direct dependency on them, only on the CallableSystemAction class. And my toolbar is pluggable because anyone can add their own module that implements CallableSystemAction. All that's needed is for their implementation to include the correct name and an icon and then the additional toolbar button will be loaded. Currently, it will be loaded upon restart. But I could add a LookupListener that would detect whenever a new module fitting the requirements is available, at which point the toolbar button would be loaded at runtime.

One potential problem will occur when the user drags the TopComponent to a different position. Potentially the images will then need to be reorganized, so that they're horizontal when they're in a horizontal mode. But apart from that, I think this is a good solution.

Sunday Oct 05, 2008

Serializing Marilyn Monroe

I made a lot of progress with node serialization. Now, when I start up my application, I see ALL of the following selected:

The above are selected, without me doing anything at start up, because those were the ones I had selected at the time the application shut down. Hence, I serialized ALL the selected nodes. Before, I only managed to work with a single selection, instead of the multiple selection you see above, because I hadn't figured out how to iterate over my handles correctly and add them to the array of nodes that is selected at start up. The important changes over the previous blog entry is in bold below, within the context of everything else that is relevant here:

@Override
public Object writeReplace() {

    Handle[] selectedHandles = NodeOp.toHandles(em.getSelectedNodes());
    return new ResolvableHelper(selectedHandles);
    
}

public final static class ResolvableHelper implements Serializable {

    private static final long serialVersionUID = 1L;
    public Handle[] selectedHandles;

    private ResolvableHelper(Handle[] selectedHandles) {
        this.selectedHandles = selectedHandles;
    }

    public Object readResolve() {
        
        try {
            
            DemoTopComponent result = DemoTopComponent.getDefault();

            String path;
            int noOfHandles = selectedHandles.length;

            Node rootNode = result.getExplorerManager().getRootContext();
            Node foundNode = null;
            Node[] allFoundNodes = new Node[noOfHandles];

            //We build up an array of found nodes:  
            for (int i = 0; i < noOfHandles; i++) {
                Handle handle = selectedHandles[i];
                path = handle.getNode().getDisplayName();
                foundNode = NodeOp.findPath(rootNode, new String[]{path});
                if (foundNode != null) {
                    allFoundNodes[i] = foundNode;
                }
            }

            //Then we select the array, which means each node is selected:
            if (allFoundNodes != null) {
                try {
                    result.getExplorerManager().setSelectedNodes(allFoundNodes);
                } catch (PropertyVetoException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }

            return result;

        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }

        return null;

    }
    
}

However, in real life your children would probably have children too. (And your children's children would have children, ad nauseum.) How to serialize the entire hierarchy of selections from parent to child to grandchild and so on? Below you see my Marilyn Monroe window (search this blog for all the code, which is definitely somewhere), but this time the nodes that are selected below are selected at start up, i.e., automatically, because I serialized them at the time the application shut down:

Below is all the relevant code. Instead of serializing the entire set of selected nodes, we only serialize the first one. The cool thing is that when you serialize a node, you store the COMPLETE node, which means that all the information on the node is available to you, such as who its parent is! So, when the application restarts, we get the parent of the serialized node and, based on whether the parent is named 'Root' (which is the name of the hidden root node of the explorer view), we either select only the node or also the parent node:

@Override
public Object writeReplace() {
    Handle childHandle = NodeOp.toHandles(em.getSelectedNodes())[0];
    return new ResolvableHelper(childHandle);
}

public final static class ResolvableHelper implements Serializable {

    private static final long serialVersionUID = 1L;
    public Handle parentHandle;
    public Handle childHandle;

    private ResolvableHelper(Handle childHandle) {
        try {
            this.childHandle = childHandle;
            this.parentHandle = childHandle.getNode().getParentNode().getHandle();
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
    }

    public Object readResolve() {

        try {

            MarilynTopComponent result = MarilynTopComponent.getDefault();

            String parentName = parentHandle.getNode().getDisplayName();
            String childName = childHandle.getNode().getDisplayName();

            Node root = result.getExplorerManager().getRootContext();

            Node parentNode = null;
            Node childNode = null;

            //We know a movie has been selected if the parent is not named 'Root',
            //which is the name of the hidden root node of the explorer view,
            //so we find two nodes and select both:
            if (!parentName.equals("Root")) {
                parentNode = NodeOp.findPath(root, new String[]{parentName, childName});
                childNode = NodeOp.findPath(root, new String[]{parentName});
                result.getExplorerManager().setSelectedNodes(new Node[]{childNode, parentNode});
            } 
            //If the parent is 'Root', then we're dealing with a category,
            //so we only need to find one node, the child of the 'Root', which we then select:
            else {
                childNode = NodeOp.findPath(root, new String[]{childName});
                result.getExplorerManager().setSelectedNodes(new Node[]{childNode});
            }

            return result;

        } catch (PropertyVetoException ex) {
            Exceptions.printStackTrace(ex);
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }

        return null;

    }

}

This principle could be extended for a larger hierarchy of nodes and, if you don't only serialize a single node, your user could potentially see many hierarchies of selected nodes at the time that the application restarts.

Saturday Oct 04, 2008

"CTL_MainWindow_Title": Customizing the Title Bar

There are many ways to customize a NetBeans Platform application's title in the title bar. Below, I try to list all of them, together with all related references. One typical added requirement is that the title should change dynamically, upon selection of something different in the application. For example, in the IDE the current project's name appears in the IDE's title bar. That's also quite easy to do.
import org.openide.modules.ModuleInstall;

public class Installer extends ModuleInstall {

    @Override
    public void restored() {
        System.setProperty("netbeans.buildnumber", "");
    }

}

The above comes from an interview with Emilian Bold. The most often heard solution is to go here in an application's branding folder, in the Files window in the IDE:

In the above file you'll find keys/value like this:

CTL_MainWindow_Title=DemoApp {0}
CTL_MainWindow_Title_No_Project=DemoApp {0}

Simply remove the {0} at the end of the values and you'll not see a build number in the application.

Another approach is to grab the application's frame and change its title:

public class Installer extends ModuleInstall {

    @Override
    public void restored() {
        WindowManager.getDefault().invokeWhenUIReady(new Runnable() {
            public void run() {
                JFrame frame = (JFrame) WindowManager.getDefault().getMainWindow();
                String title = NbBundle.getMessage(Installer.class, "LBL_TITLE");
                frame.setTitle(title);
            }
        });
    }
    
}

The code above could be anywhere at all, not necessarily in the Installer class. Note that above we're referring to a bundle file that is in the same package as our class. In that bundle file, the value of the key "LBL_TITLE" (or whatever the name of your key is) sets the title of the application. This means you could have many different titles in the bundle and then dynamically switch them at runtime from the code, as done above.

However, there's more you can do with bundle files, as the code completion for NbBundle.getMessage indicates:

So, based on the current selection, you can pass something into the argument that will tweak the title of the title bar, as shown below. In this case, a listener is set on an Explorer Manager so that the currently selected node in an explorer view determines the content of the title bar:

em.addPropertyChangeListener(new PropertyChangeListener() {
    public void propertyChange(final PropertyChangeEvent evt) {
        WindowManager.getDefault().invokeWhenUIReady(new Runnable() {
            public void run() {
                JFrame frame = (JFrame) WindowManager.getDefault().getMainWindow();
                String title = NbBundle.getMessage(DemoTopComponent.class, "LBL_TITLE", em.getSelectedNodes()[0].getDisplayName());
                frame.setTitle(title);
            }
        });
    }
});

In the Bundle.properties file, which is in the same package as where the above class is found, I have the following entry:

LBL_TITLE=Selected: {0}

The {0} is a placeholder for the first additional argument sent by the NbBundle.getMessage in my Java code above. Now, whenever I choose a new node in the explorer view, the title changes in the title bar, as indicated below:

Up to 4 different objects can be passed into the argument of NbBundle.getMessage, only the first of which must be a string. The others could be any object, even an array of objects. For example, it could be like this in the Bundle.properties file:

LBL_TITLE=Selected: {0} {1} {2} {3} {4}

The whole value above is a string, so you could put other strings in between the 4 placeholders, such as shown here with some random characters thrown in:

LBL_TITLE=Selected: {0} -- {1} / {2} and {3} --- {4}

This is not useful only in the context of title bars, but is a good example of its applicability in that case.

Two other useful pieces for working with the title bar are Netbeans Platform Branding and version info and Branding custom version info into NetBeans RCP apps and the missing Bundle.Properties file.

Friday Oct 03, 2008

Serializing Nodes

By default, the NetBeans window system restores the application's customized layout, i.e., window position and size, when the application restarts (assuming the user directory hasn't been deleted). For example, even though the Projects window in the IDE is in the explorer mode, the user can move it to the properties mode. Then, when the application shuts down, the last position (and size) of the window is serialized (i.e., stored) on disk, in the user directory. When the application restarts, these settings are then loaded, so that the user's preferences are restored.

That's the default situation. I.e., only the window layout is serialized by default. Potentially, you'd also like to serialize the data in a window. That's been discussed before (here). In addition, you can use the NbPreferences class (as discussed here) or the JDK's Preferences class, if that's what you prefer. There are several different ways of doing the same thing. The advantages of one over the other depends on your scenario, as well as your personal taste.

Another further step you can take is to serialize the selected node in an explorer view. Here, for example, is what I see after restarting my application, i.e., one of the nodes in the explorer view was already selected when the application started:

And that is the case because that node was the last one I had selected before closing the application. That could be handy because it extends the amount of custom information you can restore to your user upon restart.

Providing this scenario in your application is not really trivial, but less complex than it could be. Start by reading "Serialization and traversal" in the Javadoc. There you'll find that you need Node.getHandle when writing (using writeReplace in the TopComponent) and Node.Handle.getNode when reading (using readResolve in the TopComponent) the serialized node.

Below is the most important section of code, within the TopComponent. In writeReplace I get the selected nodes from the Explorer Manager. By the way, I've limited the number of nodes the user can select, when I defined the BeanTreeView:

beanTreeView.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);

Potentially, you can also serialize ALL the selected nodes. I've managed to do that, but having them automatically selected in the hierarchy after retrieving them at startup has been something I've been unable to do. So, in this scenario, I'm assuming the user can only make a single selection. So writeReplace below turns the Nodes into Handles, which are references to the nodes for serialization purposes. The utility class org.openide.nodes.NodeOp is crucial, extremely handy for converting to/from handles and putting nodes back into the tree when reading the settings back into the application. Then the handles are passed to the ResolvableHelper class, which implements Serializable, thus automatically serializing the received handles (i.e., storing them on disk). This happens every time the user closes the application.

@Override
public Object writeReplace() {
    Handle[] selectedHandles = NodeOp.toHandles(em.getSelectedNodes());
    return new ResolvableHelper(selectedHandles);
}

public final static class ResolvableHelper implements Serializable {

    private static final long serialVersionUID = 1L;
    public Handle[] selectedHandles;

    private ResolvableHelper(Handle[] selectedHandles) {
        this.selectedHandles = selectedHandles;
    }

    public Object readResolve() {
        try {
            DemoTopComponent result = DemoTopComponent.getDefault();
            String path = selectedHandles[0].getNode().getDisplayName();
            Node foundNode = NodeOp.findPath(result.getExplorerManager().getRootContext(), new String[]{path});
            if (foundNode != null) {
                try {
                    result.getExplorerManager().setSelectedNodes(new Node[]{foundNode});
                } catch (PropertyVetoException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
            return result;
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        return null;
    }
}

The second part of the section of code above applies to the point where the application is restarted. It simply gets the first of the serialized nodes and then gets its display name. Then NodeOp.findPath is used to put it back in the tree. Then the ExplorerManager.setSelectedNodes selects that node in the Explorer Manager.

Two other things are important—you need to specify on your Nodes that you'd like them to create handles. Secondly, it is safe to always define a name (even if you don't need it) on your Nodes, since some other parts of the API assume that your Nodes are named. For example, this is how the children shown above are created:

@Override
protected Node createNodeForKey(String key) {
    AbstractNode result = new AbstractNode(Children.LEAF, Lookups.fixed());
    result.setDisplayName(key);
    result.setName(key);
    result.setIconBaseWithExtension("/org/nb/properties/icon.png");
    result.getHandle();
    return result;
}

So above the AbstractNode.getHandle and the AbstractNode.setName are really important to provide in this scenario. For the rest, I think pretty much everything related to this scenario is discussed in this blog entry.

Wednesday Oct 01, 2008

Opening Multiple TopComponents For One File

In Convert your TopComponent to a MultiViewElement I explained, in the first part of that blog entry, how to open a file represented by a node in an explorer view into a TopComponent (instead of into the Source Editor, where a file opens by default). Basically, as described in that blog entry, you need to extend org.openide.loaders.OpenSupport and implement org.openide.cookies.OpenCookie and org.openide.cookies.CloseCookie. Not very nice code, but it works perfectly.

However, what if you want to open multiple TopComponent at the same time whenever you open a file? There might be different views onto the same file, all provided by different TopComponents. In this case, I've discovered that you need to override the open() method in your OpenSupport class. Then you can open the TopComponents in one of two ways: if you want group behavior you will create a TopComponentGroup and then call open on the group, within the overridden open().

@Override
public void open() {
    super.open();
    TopComponentGroup group = WindowManager.getDefault().findTopComponentGroup("MyGroup");
    if (group != null){
        group.open();
    }
}

Alternatively, just call open() on each of the TopComponents separately and then call active() on the TopComponent that should be active when the file is opened. Note that in this case the TopComponent must be a CloneableTopComponent, but that doesn't necessarily mean that the TopComponent will be cloneable, since only TopComponents in 'editor' modes are cloneable, never TopComponents in 'view' modes (i.e., there's no 'Clone Document' menu item on TopComponents in 'view' modes). Below, instead of creating a TopComponentGroup, I simply opened another TopComponent from the open() in the OpenSupport class:

And here's the OpenSupport class:

class DemoOpenSupport extends OpenSupport implements OpenCookie, CloseCookie {

    TwoTopComponent tc;
    String name;

    public DemoOpenSupport(Entry primaryEntry) {
        super(primaryEntry);
        DemoDataObject dobj = (DemoDataObject) primaryEntry.getDataObject();
        this.name = dobj.getName():
        this.tc = new TwoTopComponent();
    }

    @Override
    protected CloneableTopComponent createCloneableTopComponent() {
        OneTopComponent tc = new OneTopComponent();
        tc.setDisplayName(name);
        return tc;
    }
    

    @Override
    public void open() {
        super.open();
        tc.setDisplayName(name);
        tc.open();
        tc.requestActive();
    }
    
}

About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« October 2008 »
SunMonTueWedThuFriSat
   
2
7
8
11
12
20
24
25
27
31
 
       
Today