Home  >  

No Regrets: Implementing Undo/Redo in Actionscript 3.0

Author photo
AddThis Social Bookmark Button

A Rich Internet Application without undo/redo is like a series of trap-doors: your users had better be sure about every move, because there's no going back. Implementing undo/redo isn't as difficult as you might think. In this article I'll walk you through the basics, using my Pixton comic editor as the example application. These concepts can be applied to any application that lets its users do things they might regret - or unregret!

Let's start with a concrete situation where undo/redo would be useful, and then look at it more abstractly. In the Pixton editor you can design a character, articulate its body parts, and move it around the scene. Here's a sequence of actions the user might take:

Figure 1

Step 1. Initial scene
Step 2. Zoom in
Step 3. Move arm
Step 4. Rotate scene

The first requirement for undo/redo is that you keep a queue of objects in memory, corresponding to the discrete actions your user has taken. Here are the above steps drawn more abstractly:

Figure 2

Each object corresponds either to a reversible command, or to the state of your application. It's important to consider the distinction between these two choices. If each object defines a command, then it must describe exactly what was done, to what, and it must provide the means to reverse that action. If each object defines a state, then it must describe a snapshot of your application at that point in time.

Put in more concrete terms, let's take Step 4 above as an example. As a command, it's described as "rotate scene -20 degrees". Reversing the command is straightforward: "rotate scene 20 degrees." As a state, it's described as "scene positioned at (26, -65), rotation -20, zoom level 1.83, character at position (-125, 25)" and so on.

Either choice has its own merits and you should choose the one most appropriate to your application. For the Pixton editor I decided to store a queue of application states rather than commands, primarily because I needed to get and set the application state for other purposes anyway. I also prefer not having to figure out the reverse of every possible action. As long as it doesn't take up too much memory, I'll opt for saving application states. So from here on in I'll focus on application states rather than commands.

Take a sequence of applications states, then, and it looks something like this:

Figure 3

The "now" pointer indicates where you are in the queue. Invoking undo() moves "now" back one position, while redo() moves it ahead. Furthermore, any time the user performs a new action, the queue is cut off right of "now".

Let's apply these concepts in Actionscript. I'm going to start by describing a simple application, which lacks undo/redo functionality. Then I'll shown you how you can implement it in only a few simple steps.

The application presents you with a polygon and lets you change its position and rotation using some buttons. Let's quickly walk through the code used to create it, a single class called Application.

The class is contained within a generic package, but you might want to put it in a more specific namespace, such as com.mydomain.widget. We start off my importing the dependent native classes:

package 
{
 import flash.display.*;
 import flash.events.*;
 import fl.controls.*;

A couple of constants determine how much the polygon moves when you press one of the "action" buttons:

public class Application extends Sprite
{
 // increment to move polygon
 private static const XY_INCR:uint = 5;
 private static const R_INCR:uint = 10;
 
 // buttons to perform actions
 public var btnAction1:Button, btnAction2:Button, btnAction3:Button, btnAction4:Button, btnAction5:Button;
 public var lblActions:Label;

The polygon MovieClip is contained within a canvas MovieClip. The polygon's position is thus relative to the position of the canvas:

// polygon and container
public var canvas:MovieClip;
public var polygon:MovieClip;

Our very basic application will store the x, y, and rotational positions of the polygon:

// application state properties
private var _positionX:Number = 0;
private var _positionY:Number = 0;
private var _positionR:Number = 0;

I register the ADDED_TO_STAGE event, because any UI components I use won't be ready for me to initialize at this point. I also save a direct reference to the polygon MovieClip:

function Application()
{
 // hide UI while initializing
 visible = false;
 addEventListener(Event.ADDED_TO_STAGE, onReady); // get reference to polygon
 polygon = canvas.polygon;
}

The UI components are now ready for me to initialize, so I can now set the labels on the buttons:

// UI has been initialized
removeEventListener(Event.ADDED_TO_STAGE, onReady);

// initialize UI components
btnAction1.label = 'Rotate';
btnAction2.label = 'Move Up';
btnAction3.label = 'Move Down';
btnAction4.label = 'Move Left';
btnAction5.label = 'Move Right';
lblActions.text = 'Actions:';

I also register CLICK events on each button, mapping each to the same handler function:

// attach listeners to buttons
btnAction1.addEventListener(MouseEvent.CLICK, onButton);
btnAction2.addEventListener(MouseEvent.CLICK, onButton);
btnAction3.addEventListener(MouseEvent.CLICK, onButton);
btnAction4.addEventListener(MouseEvent.CLICK, onButton);
btnAction5.addEventListener(MouseEvent.CLICK, onButton);

// ready to show UI
visible = true;
}

The button handler simply executes the appropriate action, depending on which button is clicked. The render() function updates the position of the polygon to reflect the latest action:


private function onButton(evt:MouseEvent):void 
{
 // handle button click
 switch (evt.currentTarget)
 {
  case btnAction1: setR(_positionR + R_INCR); break;
  case btnAction2: setY(_positionY - XY_INCR); break;
  case btnAction3: setY(_positionY + XY_INCR); break;
  case btnAction4: setX(_positionX - XY_INCR); break;
  case btnAction5: setX(_positionX + XY_INCR); break;
 }
 
 
 // update canvas
 render();
}

private function render():void 
{
 // update canvas to reflect current application state
 polygon.x = _positionX;
 polygon.y = _positionY;
 polygon.rotation = _positionR;
}

The next three functions simply execute each kind of action. Remember, a call to the render() function will follow, updating the visible state of the application:

public function setX(setValue:Number):void 
{
 // perform action
 _positionX = setValue;
}

public function setY(setValue:Number):void 
{
 // perform action
 _positionY = setValue;
}

public function setR(setValue:Number):void 
{
 // perform action
 _positionR = setValue;
}
}
}

So that's straightforward, but there's not yet any way for the user to undo or redo any action. To implement this much-needed behaviour, we need to make a few simple additions to the Application class, and include the History class.

First, we need to add methods to the Application class which get and set the application state:

public function getState():Object 
{
 // return an object defining the current application state
 return { 'x': _positionX, 'y': _positionY, 'r': _positionR };
}

public function setState(setValue:Object):void 
{
 // set current application state
 _positionX = setValue.x;
 _positionY = setValue.y;
 _positionR = setValue.r;
}

Next, let's add undo and redo buttons to the UI. We start by declaring these UI components at the top of the class:

// buttons to undo/redo
public var btnUndo:Button, btnRedo:Button;
public var lblUndo:Label;

In onReady() we set the labels of these instances:

lblUndo.text = 'Undo / Redo:';
btnUndo.label = 'Undo';
btnRedo.label = 'Redo';

And attach event listeners to them:

btnUndo.addEventListener(MouseEvent.CLICK, onButton);
btnRedo.addEventListener(MouseEvent.CLICK, onButton);

Next comes the History class. This simple yet vital class manages our undo/redo queue. Let's walk through the History class now. This implementation uses purely static methods and variables, but alternate implementations could just as well invoke an instance of the History class, and make use of instance-level methods and variables.

The constant, MAX_LENGTH, determines how many consecutive snapshots of our application will be held in memory at any time. If your application is very complex, and each snapshot is big enough, consider placing a limit. Otherwise, leave it at zero (no limit):

package 
{
 public class History
 {
  // maximum number of undo steps (0 = unlimited)
  private static const MAX_LENGTH:uint = 0;

The queue variable stores our stack of states, and the now variable lets History remember our current position in the queue:

// stack of application states
private static var queue:Array = [];

// marks the current position in queue
private static var now:int = -1;

The save() method is called any time we want to take a new snapshot of the application and add it to the queue. But before adding it, we need to truncate the cue at the current position. In other words, if there are "future" application states in the queue, ie. if "redo" is an option, we need to remove them first:

public static function save(undoable:Application):void 
{
 // truncate the queue from now, forwards
 queue.splice(now++ + 1);
 
 // add this latest state to the queue
 queue.push(undoable.getState());
 
 // enforce maximum number of undo steps
 if (MAX_LENGTH > 0 && (queue.length - 1) > MAX_LENGTH) // Note: -1 is to account for initial application state
 {
  queue.shift();
  now--;
 }
}

Next, we add two methods which tell us whether undo or redo is an option:

public static function canUndo():Boolean
{
 // possible to go back a step?
 return (now > 0);
}

public static function canRedo():Boolean
{
 // possible to go forward a step?
 return (now < (queue.length - 1));
}

The undo() method is perhaps alarmingly straightforward. First, we decrement the now variable, to move the current position backwards one step. Then we take the corresponding state from the queue and apply it to the application.

public static function undo(undoable:Application):void 
{
 // go back one step
 canUndo() ? undoable.setState(queue[--now]) : trace('ERROR: nothing to undo');
} 

Similarly, in redo() we increment the current position, and apply the corresponding state to the application.

public static function redo(undoable:Application):void 
{
 // go forward one step
 canRedo() ? undoable.setState(queue[++now]) : trace('ERROR: nothing to redo');
}
}
}

That's it! Now, back to the Application class for a few finishing touches.

We need to add to the render() method, and update the enabled state of the undo and redo buttons. This way, any time undo() or redo() are invoked, the buttons get updated accordingly:

// update enabled state of undo/redo buttons
btnUndo.enabled = History.canUndo();
btnRedo.enabled = History.canRedo();

In the onReady() method, we need to capture the initial state of the application. This state will always be the first element in the History queue (unless MAX_DEPTH is given a positive value). Note that we must pass the Application instance to History, so that History may invoke getState() and setState() on it.

// save initial application state
History.save(this);

// call to render() updates enabled states of undo/redo buttons
render();

In the onButton event handler, we must include the two new buttons:

case btnUndo: History.undo(this); break;
case btnRedo: History.redo(this); break;

And, last but not least, we must save the application state anew any time an undoable action is invoked. This affects setX(), setY(), and setR():

// save application state
History.save(this);

Click here to download the final source code, and run the code for yourself. The .FLA file includes the user interface referred to in this example.

There are many perfectly acceptable variations on this implementation of undo/redo. For example, you could use events exclusively to communicate between the Application instance and an instance of the History class. Or, rather than keeping a single queue you could maintain a series or hierarchy of History instances, corresponding to different areas or levels of your application. It depends on your preference, and on the specific requirements of the application you're building. What's important is that you're providing your users with a way to say "Oops, I didn't mean to do that", rather than "Oh @!$#, what the @&*% have I done!? I HATE this @*!$ thing!"

Read more from Clive Goodinson. Clive Goodinson's Atom feed pixton on Twitter

Comments

12 Comments

mpc said:

This stuff is great! Really nice tutorial thank you Clive!

J said:

Thanks!!

Freddy said:

Can't beat the quality from a real expert.

ryan said:

Great stuff. I especially like the last line of the article. I agree back/forward support and redo/undo is a key element of flash being able to be as usable as html and desktop apps.

mrm said:

queue.splice(now++ + 1);
That's just bad practice. What happens if you have:
queue.splice(now++ + then);
Can the compiler decide if you want to post-increment the variable "now" and add it to the variable "then" OR you want to add the variable "now" to the pre-incremented variable "then". No, it can't, and you got yourself a nice stealthy bug.

mrm, while I admit this particular line is perhaps unnecessarily concise, there's no potential for the compiler to be unable to "decide". The line:

queue.splice(now++ + then);

is equivalent to:

queue.splice(now + then);
now = now + 1;

There may be potential for human error, by inadvertently adding or removing a space where it's not appropriate, but otherwise it's perfectly acceptable. Am I missing something?

Hey Clive,
This is a nice solution for when you know that your application is only going to change certain things, as in your case x, y, and rotation. But what happens if you don't know what exactly the application is changing? sometimes it may be x/y position, sometimes it may be rotation, sometimes it may be color of an object, sometimes it may be a filter, etc. Whats the best way to handle that then?

I'm not even sure if my question makes real sense, I think I'm a bit overwhelmed with what all my application is doing and how to handle it best, but hopefully you understand what i'm trying to accomplish.

Hey Clive,
This is a nice solution for when you know that your application is only going to change certain things, as in your case x, y, and rotation. But what happens if you don't know what exactly the application is changing? sometimes it may be x/y position, sometimes it may be rotation, sometimes it may be color of an object, sometimes it may be a filter, etc. Whats the best way to handle that then?

I'm not even sure if my question makes real sense, I think I'm a bit overwhelmed with what all my application is doing and how to handle it best, but hopefully you understand what i'm trying to accomplish.

Hi Matt,

In the method I've described, the History class only cares that an action was performed, it doesn't care about the type of action performed.

The variables saved in each "snapshot" can be used to reconstruct the state of the application.

So there are two main things you need to do: first, make sure that every time the user performs an action, the History class takes a new snapshot. Second, make sure you can reconstruct your application state from the data saved in the snapshot.

Does this help at all? If I haven't understood correctly, please elaborate.

Arindam Mojumder said:

Can't open the Flash file in cs 3. version. Is the raw .fla file in cs 4 version?

Bryan said:

Hi Daniel, with regards your relatged article, do you have any idea how to combine the Adobe API with the Facebook API? lightweight rucksacks

Leave a comment


Tag Cloud

Question of the Week: Dream App

If you had an unlimited budget and unlimited resources what application would you build and why would you build it?

Answer

Latest Features

Recommended for You

@InsideRIA on Twitter

Archives

  • Or, visit our complete archive.  

About This Site

Welcome to the premiere community site for all things RIA sponsored by O'Reilly Media and Adobe Systems Incorporated.