X

Geertjan's Blog

  • April 15, 2011

org.openide.actions.ReorderAction

Geertjan Wielenga
Product Manager
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.

Join the discussion

Comments ( 4 )
  • Ernest Friday, April 15, 2011

    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?


  • Geertjan Wielenga Friday, April 15, 2011

    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.


  • Ernest Friday, April 15, 2011

    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. :)


  • metator Wednesday, October 12, 2011

    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


Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha