Undoable actions

From FreeMind

Jump to: navigation, search

Contents

Background

As an example, we look at the implementation of the NodeColorAction that changes the color of a node. We assume, that you want to make an action belonging to the mindmap mode undoable.

Summary

  • The mode controller is responsible for your action.
  • You make your action serializable via XML.
  • You divide your action into separate "do"- and an "undo"-(xml-)actions.
  • Each XML action is executed by "freemind.modes.mindmapmode.actions.xml.ActionFactory.executeAction".
  • To every XML action a (dynamically added) handler belongs that takes a XmlAction argument and does something useful with it.


Step 1: The ModeController implements the low level action

The interface MindMapActions describes all actions that can be performed on a mindmap in mindmap mode. We find the method

public void setNodeColor(MindMapNode node, Color color);

The implementation in MindMapController is as follows:

    public void setNodeColor(MindMapNode node, Color color) {
        nodeColor.setNodeColor(node, color);
    }

It is delegated to a separate class that handles all necessary actions (strongly recommended).

The only way to perform the action for an external programmer must be the method described in the interface. The method must carry out all redisplay of the map.

Step 2: The XML-Serialization

The undo mechanism is realized via xml serialization: you specify what data you need to perform and to undo your action. The data consists of strings. As an example: you describe the node, the action is applied to, via its identifier.

The serialization is declarated in the file
freemind_actions.xsd
.

There we find:

	<!-- Sets the color of the nodes Action -->
	<xs:element name="node_color_format_action">
	  <xs:complexType>
		<xs:complexContent>
			<xs:extension base="format_node_action">
		      <xs:attribute name="color" use="optional" type="xs:string"/>
			</xs:extension>
		</xs:complexContent>
	  </xs:complexType>
	</xs:element>

The "format_node_action" extends "node_action" which is the following:

	<!-- Node actions. These actions involve exactly one node.-->
	  <xs:complexType name="node_action">
		<xs:complexContent>
			<xs:extension base="xml_action">
		      <xs:attribute name="node" use="required" type="xs:string"/>
			</xs:extension>
		</xs:complexContent>
	  </xs:complexType>

Here, we see the string attribute "node" (the id). node_color_format_action contained the color attribute (a string, too).

Step 3: Don't forget to add your xml type to the compound type

Often forgotten: at the top of the schema there you'll find the compound type declared. It is requested that your type occures there as a list of actions are tied together in a single xml via

	<!-- Compound action.  -->
	<xs:element name="compound_action">
	  <xs:complexType>
		<xs:complexContent>
			<xs:extension base="xml_action">
				<xs:choice minOccurs="0" maxOccurs="unbounded">
					<xs:element ref="compound_action"/>
...
					<xs:element ref="node_color_format_action"/>
...
				</xs:choice>
			</xs:extension>
		</xs:complexContent>
	  </xs:complexType>
	</xs:element>

Step 4: Look at the xml serialization interface

Run "ant dist", refresh, close your eclipse project and reopen it. Then open the type "NodeColorFormatActionType" and you get:

public interface NodeColorFormatActionType
    extends freemind.controller.actions.generated.instance.FormatNodeAction
{


    /**
     * 
     * @return possible object is
     * {@link java.lang.String}
     */
    java.lang.String getColor();

    /**
     * 
     * @param value allowed object is
     * {@link java.lang.String}
     */
    void setColor(java.lang.String value);

}

Step 5: Instrument the action

The class that realizes the action (it is often derived from the "AbstractAction" class of swing) must implement "ActorXml" to serve as an actor in the undo game. This means that during the xml deserialization a XML element that is found can be linked to a Java class. In this game, no undo is present anymore.

ActorXml is:

public interface ActorXml {


	public void act(XmlAction action); 
		
	/**
	 * @return
	 */
	Class getDoActionClass();

}

"getDoActionClass" must return the Java class that belongs to the xml action we specified in step 2. We say

    public Class getDoActionClass() {
        return NodeColorFormatAction.class;
    }

And the "act" method should do the work:

    public void act(XmlAction action) {
		if (action instanceof NodeColorFormatAction) {
			NodeColorFormatAction nodeColorAction = (NodeColorFormatAction) action;
			Color color = Tools.xmlToColor(nodeColorAction.getColor());
			MindMapNode node = controller.getNodeFromID(nodeColorAction.getNode());
			Color oldColor = node.getColor() ;
			if (!Tools.safeEquals(color, oldColor)) {
                		node.setColor(color); // null
                		controller.nodeChanged(node);
            		}
		}
   }

You can see, that it basically decodes the XML information and performs the action, but only if necessary.

Step 6: Add your XML action handler dynamically

Often forgotten: During construction of your action, add

        controller.getActionFactory().registerActor(this, getDoActionClass());

Thus, the action factory knows, that it call you to perform the work belonging to the "node_color_format_action" xml code.

Step 7: Create do and undo actions

We write a helper method that fills the xml action with reasonable values:

    public NodeColorFormatAction createNodeColorFormatAction(MindMapNode node, Color color) throws JAXBException {
		NodeColorFormatAction nodeAction = controller.getActionXmlFactory().createNodeColorFormatAction();
		nodeAction.setNode(node.getObjectId(controller));
	    	nodeAction.setColor(Tools.colorToXml(color));
		return nodeAction;
    }

And we implement the main method:

    public void setNodeColor(MindMapNode node, Color color) {
		try {
			NodeColorFormatAction doAction = createNodeColorFormatAction(node, color);
			NodeColorFormatAction undoAction = createNodeColorFormatAction(node, node.getColor());
			controller.getActionFactory().startTransaction(this.getClass().getName());
			controller.getActionFactory().executeAction(new ActionPair(doAction, undoAction));
			controller.getActionFactory().endTransaction(this.getClass().getName());
		} catch (JAXBException e) {
			e.printStackTrace();
		}
    }

The procedure is always the same:

  • Collect the do action (what should be done?)
  • Collect the undo action (what should be done to undo the stuff?). Often, this is an action of a different type (think of adding something: to undo, you have to remove it...)
  • Execute the do action in a transaction (this is not used so far but will be in the future).

Grouping several actions together

If you need to carry out several actions in one step, you can use the CompoundAction we've already seen in Step 3. The reason to do this can be that you want to carry out different types of XmlActions or that an action consists of more than one step.

As an example, we look at the removal of all icons of the selected nodes. To undo this action, you have to add several icons to several different nodes. We take a look at the code:

    public ActionPair apply(MapAdapter model, MindMapNode selected) throws JAXBException {
        CompoundAction undoAction = modeController.getActionXmlFactory().createCompoundAction();
        for (Iterator i = selected.getIcons().iterator(); i.hasNext();) {
            MindIcon icon = (MindIcon) i.next();
            undoAction.getCompoundActionOrSelectNodeActionOrCutNodeAction().add(addIconAction.createAddIconAction(selected, icon));
        }
        return new ActionPair(createRemoveAllIconsXmlAction(selected), undoAction);
    }

The strange getter "getCompoundActionOrSelectNodeActionOrCutNodeAction()" is something generated by JaxB. Correction: as of version 0.9.0, we are using JibX instead. The strange getter has another name with this (free!) library.

Personal tools