org.openide.actions.ReorderAction

Here is a root node with support for reordering its children:

When the menu item shown above is clicked, the dialog below is shown, which works as you might expect:

I did not define the dialog myself, it comes from the NetBeans Platform. To get that dialog to appear, you need to have a root node that has content as shown below:

class RootNode extends AbstractNode {

    public RootNode(final CarList model) {
        super(Children.create(new CarChildFactory(model), true));
        getCookieSet().add(new Index.Support() {
            @Override
            public Node[] getNodes() {
                return getChildren().getNodes(true);
            }
            @Override
            public int getNodesCount() {
                return getNodes().length;
            }
            @Override
            public void reorder(int[] perm) {
                model.reorder(perm);
            }
        });
    }

    @Override
    public Action[] getActions(boolean context) {
        return new Action[]{SystemAction.get(ReorderAction.class)};
    }
    
}

Or, the more modern way:

class RootNode extends AbstractNode {

    public RootNode(final CarList model, InstanceContent ic) {
        super(Children.create(new CarChildFactory(model), true), new AbstractLookup(ic));
        ic.add(new Index.Support() {
            @Override
            public Node[] getNodes() {
                return getChildren().getNodes(true);
            }
            @Override
            public int getNodesCount() {
                return getNodes().length;
            }
            @Override
            public void reorder(int[] perm) {
                model.reorder(perm);
            }
        });
    }

    @Override
    public Action[] getActions(boolean context) {
        return new Action[]{SystemAction.get(ReorderAction.class)};
    }
    
}

The key to understanding the above is "org.openide.actions.ReorderAction". Read the source code and you'll find that the class extends "CookieAction", with this method, indicating that the "Index" class needs to be in the "Lookup" of the "Node":

protected Class[] cookieClasses() {
    return new Class[] { Index.class };
}

The "Index.Support" class is a simplified "Index" class and by adding it to "getCookieSet", you've added it to the "Lookup" of the "Node".

Next, we'll create a model of our list of Car objects, including a method for listening to changes and for creating a new list of Cars based on the new order provided by "Index.Support":

public final class CarList {
    
    private final List<Car> cars;
    private final ChangeSupport cs = new ChangeSupport(this);

    public CarList(List<Car> Cars) {
        this.cars = new ArrayList<Car>(Cars);
    }

    public List<? extends Car> list() {
        return cars;
    }

    public void reorder(int[] perm) {
        Car[] reordered = new Car[cars.size()];
        for (int i = 0; i < perm.length; i++) {
            int j = perm[i];
            Car c = cars.get(i);
            reordered[j] = c;
        }
        cars.clear();
        cars.addAll(Arrays.asList(reordered));
        cs.fireChange();
    }

    public void addChangeListener(ChangeListener l) {
        cs.addChangeListener(l);
    }

    public void removeChangeListener(ChangeListener l) {
        cs.removeChangeListener(l);
    }

}

Next, we add a bit of extra code to a typical "ChildFactory". The new code consists of usage of "ChildFactory.Detachable", which gives us access to "addNotify/removeNotify", for adding/removing the listener from the model, together with a "ChangeListener" for refreshing the node hierarchy:

public class CarChildFactory extends ChildFactory.Detachable<Car> implements ChangeListener {

    private final CarList model;

    CarChildFactory(CarList model) {
        this.model = model;
    }

    @Override
    protected boolean createKeys(List<Car> list) {
        list.addAll(model.list());
        return true;
    }

    @Override
    protected Node createNodeForKey(Car key) {
        CarNode node = null;
        try {
            node = new CarNode(key);
        } catch (IntrospectionException ex) {
            //do something
        }
        return node;
    }

    @Override
    public void stateChanged(ChangeEvent e) {
        refresh(false);
    }

    @Override
    protected void addNotify() {
        model.addChangeListener(this);
    }

    @Override
    protected void removeNotify() {
        model.removeChangeListener(this);
    }
   
}

Above, we receive the "CarList" from somewhere. That "somewhere" is the "TopComponent", as follows:

ArrayList cars = new ArrayList();
cars.add(new Car("Mazda", 1983, Color.BLUE));
cars.add(new Car("Honda", 1972, Color.GREEN));
cars.add(new Car("Toyota", 1997, Color.DARK_GRAY));

Node rootNode = new RootNode(new CarList(cars));
rootNode.setDisplayName("Cars");

controler.setRootContext(rootNode);

The above code is based on code from Toni's blog on reordering, as well as on comments from Jesse.

Comments:

Hi Geertjan

I would be very tempted to, in your Lookup-based RootNode rather made the constructor with the InstanceContent as second parameter private, and exposed a public constructor taking only the CarList and passing new InstanceContent() to the private constructor. What is best practice with respect to this? Hide away InstanceContent at all costs, and only expose the readonly Lookup?

Posted by Ernest on April 15, 2011 at 03:23 AM PDT #

Hi Ernest, I agree with you, that would be the better approach, which conforms to the NetBeans source code and various books on this topic, and I will do it in that way from now onwards.

Posted by Geertjan Wielenga on April 15, 2011 at 05:14 AM PDT #

Of course, I must add - many thanks to you and Toni for the interesting material about ordering of nodes and Index.Support - I've got a perfect scenario where this can be put to good use. :)

Posted by Ernest on April 15, 2011 at 06:30 AM PDT #

Hi,

This example does not work as is. I was able to successfully add the ReorderAction to root Node but it's always disabled (and also MoveUpAction and MoveDownAction, though these are not used in this example).
The reason for this is related to CookieAction.enable(Node[]) method. The first thing it does is to check what nodes are selected and if there aren't any, it simply does not enable the Action.
So, to fix this, one must do the following after calling controller.setRootContext(rootNode):

associateLookup(ExplorerUtils.createLookup(controller, getActionMap()));

Regards

Posted by metator on October 12, 2011 at 03:02 AM PDT #

Post a Comment:
  • HTML Syntax: NOT allowed
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
« April 2014
SunMonTueWedThuFriSat
  
12
13
14
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today