Skip to main content

How to replace the selection model in the NetBeans Platform

Posted by timboudreau on January 23, 2007 at 12:48 AM PST

NetBeans has a selection model where whatever logical window has focus owns the selection (determines what actions are enabled/disabled, what the property sheet shows, etc.). Some applications need something different - for example, an image editor probably wants the current image to own the selection no matter what logical window has focus. You can completely replace the selection management mechanism in the NetBeans platform, to accomodate any selection model you want. Here's how...

First, let's go over how NetBeans defines selection. The selection is a Lookup. That's basically a bag-o-stuff owned (by default) by whatever component has focus. So, an Action in a toolbar or menu that should be enabled is written to be sensitive to the presence or absence of some Java type in the selection lookup (the Lookup returned buy Utilities.actionsGlobalContext()). For example, a Save All action which can save one or more files (or whatever) listens on the global context for one or more SaveCookies. If there is at least one, the action is enabled, if not, it's disabled.

Let me back up a bit...a Lookup is like a Map, where the keys are Class objects and the value for any Class key is a Collection of zero or more instances of that class. A lot of lookup-like APIs use Strings for keys (think JNDI - when you realize that could have been done in a type-safe way and wasn't, it seems kind of grotesque and depressing, doesn't it? A Lookup may be a magic bag-o-stuff, but it's a type-safe magic bag-o-stuff - I want all the help the compiler can give me!); Lookup is nicer in that you have type-safety - if I call Map.get ("foo"), I have no idea what I will get back - the compiler cannot help me; if I call Lookup.lookup (MyObject.class), I know I'm getting back instances of MyObject.

Boat aground on the street in New Orleans

By default, in NetBeans, the Lookup you get back from Utilities.actionsGlobalContext() is provided by the window system implementation. It tracks which logical window has focus, and proxies that window's Lookup. It provides a central point for listening for selection changes without the code interested in selection changes having to care that there even is a window system that is causing the changes - an object that cares about selection simply subscribes to changes in the presence or absence of some Java type in the global selection - the fact that the global selection, by default, is the Lookup that belongs to the focused logical window, and when focus moves to a different logical window, appropriate changes are fired (so if you're listening for a Customer object and the focus moves from a window that does have one to one that doesn't have one, a change is fired to tell you a Customer object disappeared from the global selection, your code doesn't have to care why that happened), is immaterial to things interested in selection. Selection is completely divorced from the UI that happens to provide it.

For some applications, that's not what you want. I discovered this when working on the Imagine project - this is an image editor. There are palettes that display attributes of the image being edited. It would be very disconcerting if the component that shows all of the individual layers that make up the currently edited image suddenly disappeared when you clicked the tool palette to select a different tool - the image you are editing hasn't changed.

I spent the last couple of days here in New Orleans working with some folks with a similar problem - they are building a CRM application on the NetBeans Platform, and the appropriate context for actions and palettes is the selected Customer - it doesn't matter which window has focus.

Flood Damage In New Orleans
Fortunately, in NetBeans, if you want a different selection management model, it's easy. The Lookup that is returned by Utilities.actionsGlobalContext() is what determines the enablement of context sensitive actions. You can completely replace it by implementing org.openide.util.ContextGlobalProvider and placing your implementation of that interface in the default lookup. Here's an example.

public final class Ctx implements ContextGlobalProvider {
    private final InstanceContent content = new InstanceContent();
    private final Lookup lkp = new AbstractLookup (content);
    public Lookup getLookup() {
        return lkp;

The next step is to actually put something in the empty lookup, whose content we control. So we add a method:
public void setContent (Collection c) {
    ic.set (c, null);

Depending on what kind of contents you want the lookup to have, you might expose add, remove, or whatever methods you want. If, as in the CRM application, there is only one selected customer, you might do something like
public void setCustomer (Customer customer) {
     Customer old = lkp.lookup (Customer.class);
     if (((old == null) != (customer == null)) || (customer != null && !customer.equals(old))) {
          ic.remove (old);
          if (customer != null) {
               ic.add (customer);

Now you have a guarantee that there is only either zero or one Customer objects selectable. You can always put addition objects of other types into the global selection - that's the whole point - you're in control. If a customer might have multiple addresses or names, you can put as many Address objects in there as you need - the UI that displays them just needs to be able to handle getting more than one.

Then you have the question of event model; in the case of the CRM app, there are a bunch of UI pieces that display the address, etc. of a customer. Different user roles will see different attributes. There are two ways to solve this: First is the Swing-like way, where you can add property change listeners to a Customer object, and the UI that cares about it can listen for changes. But this introduces boatloads of bookkeeping, because probably Address, etc. objects all ought to have listeners, and pretty soon you are drowning in potential memory leaks and objects listening to each other. Particularly in this case, where the Customer object arrived from a web service and its object-identity is determined by its database id.

So we can simply make the model more granular - object identity remains the same, but we need to update all UI's showing an attribute of a customer when it changes. Since we control the selection model, we can simply remove the current customer and re-add it. Slightly less efficient, but one line of code and a thousand trips through a heap-dump averted - just remove old and re-add it, regardless if it equals() the incoming object or not. Unbeautiful, perhaps, but eminently practical and a world of listener-leaks is avoided.

The point here isn't so much that the above is good or bad, but that if you're writing an application on the NetBeans Platform, you can create whatever selection model makes sense for your application.

The final step is to register the custom implementation of ContextGlobalProvider in the default lookup. The default lookup is just the Java Extension Mechanism which has been around since JDK 1.3 (JDK 6's ServiceLoader, while painfully limited, does the same thing as the default lookup) - you include a flat file in your JAR in META-INF/services. That file's name is the name of the interface it is installing an instance of - in this case, its name is org.openide.util.ContextGlobalProvider. Its content looks like this:

These lines do two things. The first is an extension to the spec, legally embedded in a comment - Lookup lets you delete an entry by putting its name in a comment line preceded by a minus sign. So the first line ensures that the default selection model provided by the NetBeans window system is removed. The following line, in accordance with the spec, registers our own selection model implementation - an implementation of ContextGlobalProvider, by listing its fully-qualified class name.

While all of this may take a moment to digest, the important fact is that the NetBeans Platform and window system are not married to the NetBeans IDE's notion of what is selection context - you can customize how selection works to your heart's content.

Related Topics >>