Can only redo once?

UndoRedo Visual Library
In the image above I have my Visual Library implementation, wrapped into a MultiViewElement. Story short, there’re 4 SubLayout widgets on the Scene, each SubLayout widget consists of SquareWidgets (squares with a number drawn at its center). Each of these four widgets can be selected through mouse selection on Scene or using the widget explorer (“sample” TopComponent on the bottom right).

I want to make the selection synchronized, either it’s triggered from Scene or from the explorer. A straightforward approach is as follows :

  • When user select a node on the explorer, put the selected SubLayout object into the Lookup of the explorer’s TC.
  • Similarly, when user select a widget on the scene, put the selected SubLayout object into the Lookup of the Scene.
  • Both action above will trigger two LookupListeners, in which the visualization of the selection will be handled. The first listener will invoke the ObjectScene.setFocusedObject, and the other one will call the ExplorerManager.setSelectedNodes

The code went well, until I want to make the selection undoable. The first reference I found was from Geertjan’s blog, as expected.

Quick and dirty, I pass the UndoRedo.Manager of MultiViewElement to the Scene’s constructor to enable the addition of UndoableEdit, which happened in the resultChanged of the first listener mentioned above. Here’s the code

private class UndoableSubLayoutSingleSelectionListener implements LookupListener {

    @Override
    public void resultChanged(LookupEvent ev) {
        if (!allSubLayoutsInLookup.allInstances().isEmpty()) {
            SubLayout c = allSubLayoutsInLookup.allInstances().iterator().next();

            for (Object o : getObjects()) {
                SubLayout localObject = (SubLayout) o;
                if (localObject.equals(c)) {
                    if (!o.equals(getFocusedObject())) {

//add to undo manager before the selection is actually happened
                        SubLayoutSingleSelectionUndoableEdit edit = new SubLayoutSingleSelectionUndoableEdit(o);
                        undomanager.undoableEditHappened(new UndoableEditEvent(o, edit));

//THE selection
                        icForSelectedSubLayoutOnCanvas.set(Collections.singleton(localObject), null);
                        setFocusedObject(o);
                        userSelectionSuggested(Collections.singleton(o), false);
                    }
                }
            }

        }
        validate();
    }

    class SubLayoutSingleSelectionUndoableEdit extends AbstractUndoableEdit {

        private Object previousSelectedObject;
        private Object suggestedSelectedObject;

        private SubLayoutSingleSelectionUndoableEdit(Object o) {
            suggestedSelectedObject = o;
            previousSelectedObject = getFocusedObject();
        }

        @Override
        public String getPresentationName() {
            return "SubLayout Selection";
        }

        @Override
        public boolean canRedo() {
            return true;
        }

        @Override
        public boolean canUndo() {
            return previousSelectedObject != null;
        }

        @Override
        public void redo() throws CannotRedoException {
            super.redo();
            icForSelectedSubLayoutOnCanvas.set(Collections.singleton((SubLayout) suggestedSelectedObject), null);
        }

        @Override
        public void undo() throws CannotUndoException {
            super.undo();
            icForSelectedSubLayoutOnCanvas.set(Collections.singleton((SubLayout) previousSelectedObject), null);
        }

    }

}

The idea is as follows :

  • Before the change of object selection is actually happened, create an UndoableEdit and add it to the manager. This way we have an edit that understands how to redo (the undo part is trivial, isn’t it?)
  • Since the selection might be triggered from the explorer, we set the content of icForSelectedSubLayoutOnCanvas (an InstanceContent) once again to make sure the listener won’t be triggered again if the same object is selected afterward from the Scene.
  • Next, we handle the selection using setFocusedObject and userSelectionSuggested

This way, the undo button will do his job excellently. But once you hit the redo button (when it’s enabled), it will correctly put back the selection to the previous object, and after that, it become disabled. It will always be disabled, no matter how many times the undo action has been triggered prior to the redo action is invoked.

After wasting an hour of imitating the code of UndoRedo.Manager, I found that when I call the redo, an edit is added. That will discard all edits in the stack that added after the just redone edit. This time I realized my fault, since setting the content of InstanceContent will trigger the resultChanged once again, so an edit is added.

I managed to make it work properly using a hackish solution : put a boolean variable that will help the resultChanged to recognize whether the listener was triggered from Undo/Redo or from another source. Here’s the revised code from the listener

private boolean triggeredFromUndoRedo;

public UndoableSubLayoutSingleSelectionListener() {
    triggeredFromUndoRedo = false;
}

public void resultChanged(LookupEvent ev) {
    ...
    if (!o.equals(getFocusedObject())) {
        if (!triggeredFromUndoRedo) {
        //add to undo manager before the selection is actually happened
            SubLayoutSingleSelectionUndoableEdit edit = new SubLayoutSingleSelectionUndoableEdit(o);
            undomanager.undoableEditHappened(new UndoableEditEvent(o, edit));
        } else {
            triggeredFromUndoRedo = false;
        }
        //THE selection
        ...
    }
    ...
}

and the UndoableEdit

public void redo() throws CannotRedoException {
    super.redo();
    triggeredFromUndoRedo = true;
    icForSelectedSubLayoutOnCanvas.set(Collections.singleton((SubLayout) suggestedSelectedObject), null);
}

public void undo() throws CannotUndoException {
    super.undo();
    triggeredFromUndoRedo = true;
    icForSelectedSubLayoutOnCanvas.set(Collections.singleton((SubLayout) previousSelectedObject), null);
}
This entry was posted in fail : reconstructed, netbeans platform, visual library api. Bookmark the permalink.

Comments are closed.