Playing Around with the New UndoManager

Included in the Flex 4.0 SDK and the, just released, Flash Professional CS5 lies a new hidden little gem of a class: flashx.undo.UndoManager (although the Flex 4.0 SDK’s been out for awhile, I have to admit I didn’t even notice this until I installed Flash CS5 and started poking around the documentation looking for new stuff to play with). Now, actually, the UndoManager is a part of the brand new shiny TextLayout framework, but, because it follows a basic Command design pattern, it’s very easy to adopt it to other aspects of your applications and allow functionality that users expect/demand. But since an example is worth a thousand words, check out the below to get an idea of what I’m talking about.

First, let’s start off by creating just a real simple drawing application.

package {
 
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Shape;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
 
	/**
	 * Simple drawing app
	 * @author Devon O.
	 */
	[SWF(width='640', height='480', backgroundColor='#FFFFFF', frameRate='60')]
	public class Main extends Sprite {
 
		private var _canvas:Bitmap;
		private var _canvasHolder:Sprite;
		private var _drawing:BitmapData;
		private var _shapeHolder:Sprite;
		private var _currentShape:Shape;
 
		public function Main():void {
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}
 
		private function init(event:Event = null):void {
			removeEventListener(Event.ADDED_TO_STAGE, init);
 
			initDrawing();
 
			_canvasHolder.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
		}
 
		private function initDrawing():void {
			_drawing = new BitmapData(stage.stageWidth, stage.stageHeight - 20, false, 0x000000);
			_canvas = new Bitmap(_drawing, "auto", true);
			_canvasHolder = new Sprite();
			_canvasHolder.addChild(_canvas);
			_canvasHolder.y = stage.stageHeight - _canvas.height;
			addChild(_canvasHolder);
 
			_shapeHolder = new Sprite();
			_shapeHolder.graphics.beginFill(0x000000);
			_shapeHolder.graphics.drawRect(0, 0, _canvas.width, _canvas.height);
			_shapeHolder.graphics.endFill();
		}
 
		private function mouseDownHandler(event:MouseEvent):void {
			_currentShape = new Shape();
			_shapeHolder.addChild(_currentShape);
			_currentShape.graphics.lineStyle(0, 0xFFFFFF);
			_currentShape.graphics.moveTo(_canvas.mouseX, _canvas.mouseY);
			stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
			addEventListener(Event.ENTER_FRAME, draw);
		}
 
		private function mouseUpHandler(event:MouseEvent):void {
			stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
			removeEventListener(Event.ENTER_FRAME, draw);
		}
 
		private function draw(event:Event):void {
			_currentShape.graphics.lineTo(_canvas.mouseX, _canvas.mouseY);
			_drawing.draw(_shapeHolder);
		}
	}
}

Now, if you compile that, you’ll see you can mouse down (with your ancient and anachronistic mouse that apparently no Mac users require anymore – don’t even get me started on Steve Jobs’ blatantly lying drivel) on the black area and draw white lines around (told you it was real simple). But what if you wanted to get rid of the last line you drew? If you have a quick read through the code above, you’ll see that the app works by adding a new shape to a Sprite instance every time you mouse down. The graphics property of that shape is used for your drawing and the sprite in which it resides is drawn into a BitmapData instance. To get rid of a drawn line then, all you would need to do is remove that particular shape from its sprite parent, then re-draw that sprite into the BitmapData. And to add the line back (redo, that is), you’d just add the child Shape back to the sprite and again, draw the the sprite into the BitmapData. Now this would be easy enough to implement in your own way, but the new UndoManager class makes it all the easier, and also allows you to easily set the number of levels of undos/redos a user is allowed.

As mentioned, the UndoManager follows a basic Command design pattern. In a nutshell, the command pattern essentially stashes a collection of commands (or operations) in a manager and executes them when requested. Obviously, the manger in this case is the UndoManager. The operations in question are instances of a class which implements the flashx.undo.IOperation interface. The IOperation interface requires only two methods: performUndo() and performRedo().

So let’s encapsulate the info above (how we would remove or re-add a drawn line in the simple drawing app) into a DrawingOperation class:

package {
 
	import flash.display.BitmapData;
	import flash.display.DisplayObjectContainer;
	import flash.display.Shape;
	import flashx.undo.IOperation;
 
	/**
	 * Undo/Redo operation for simple drawing application
	 * @author Devon O.
	 */
	public class DrawingOperation implements IOperation {
 
		private var _shape:Shape;
		private var _drawing:BitmapData;
		private var _parent:DisplayObjectContainer;
 
		public function DrawingOperation(shape:Shape, drawing:BitmapData) {
			_shape = shape;
			_drawing = drawing;
			_parent = _shape.parent as DisplayObjectContainer;
		}
 
		public function performUndo():void {
			if (!_parent.contains(_shape)) return;
 
			_parent.removeChild(_shape);
			draw();
		}
 
		public function performRedo():void {
			if (_parent.contains(_shape)) return;
 
			_parent.addChild(_shape);
			draw();
		}
 
		private function draw():void {
			_drawing.draw(_parent);
		}
	}
}

And, since we’re working on creating operations and I know ahead of time that I’m going to be using the great Bit-101 MinimalComps for some simple UI elements, let’s create an operation that will undo/redo changing the value of the ColorChooser component:

package  {
 
	import com.bit101.components.ColorChooser;
	import flashx.undo.IOperation;
 
	/**
	 * undo/redo changing the value of Minimalcomps ColorChooser Component
	 * @author Devon O.
	 */
	public class ColorChooserOperation implements IOperation {
 
		private var _previousColor:uint;
		private var _currentColor:uint;
		private var _colorChooser:ColorChooser;
 
		public function ColorChooserOperation(previousColor:uint, currentColor:uint, colorChooser:ColorChooser) {
			_previousColor = previousColor;
			_currentColor = currentColor;
			_colorChooser = colorChooser;
		}
 
		public function performUndo():void {
			_colorChooser.value = _previousColor;
		}
 
		public function performRedo():void {
			_colorChooser.value = _currentColor;
		}
	}
}

Now, back in our document class for the drawing application, we’ll add some GUI elements (via MinimalComps) and two UndoManager instances – one to hold our undo operations and one to hold our redo operations.

Ideally, I’d like to be able to do this with only a single UndoManager instance, but this isn’t possible due to some quirk in the class. Although the documentation seems to indicate that the manager maintains two separate stacks for redo and undo operations, this doesn’t seem to be the case. If you max out the number of allowed undos, you will not be able to push a redo without first removing an undo. This is the reason I’m using two here. If someone sees I’m doing something fishy to cause this behavior, please let me know.

Notice, now that every time we draw a line or change the value of the ColorChooser instance we push a DrawingOperation or ColorChooserOperation instance into our undo manager. And every time we perform an undo, we first push the undo about to be performed into our redo manager (and vice versa). This then, is the final document class:

package {
 
	import com.bit101.components.ColorChooser;
	import com.bit101.components.PushButton;
	import com.bit101.components.Style;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Shape;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flashx.undo.UndoManager;
 
 
 
 
	/**
	 * Simple drawing app with undo/redo capabilities via flashx.undo.UndoManager
	 * @author Devon O.
	 */
	[SWF(width='640', height='480', backgroundColor='#FFFFFF', frameRate='60')]
	public class Main extends Sprite {
 
		public static const UNDO_LIMIT:int = 5;
 
		private var _canvas:Bitmap;
		private var _canvasHolder:Sprite;
		private var _drawing:BitmapData;
		private var _shapeHolder:Sprite;
		private var _currentShape:Shape;
 
		private var _undoManager:UndoManager;
		private var _redoManager:UndoManager;
		private var _undoButton:PushButton;
		private var _redoButton:PushButton;
		private var _colorChooser:ColorChooser;
 
		private var _currentColor:uint = 0xFFFFFF;
 
		public function Main():void {
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}
 
		private function init(event:Event = null):void {
			removeEventListener(Event.ADDED_TO_STAGE, init);
 
			initDrawing();
			initManager();
			initUI();
 
			_canvasHolder.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
		}
 
		private function initDrawing():void {
			_drawing = new BitmapData(stage.stageWidth, stage.stageHeight - 20, false, 0x000000);
			_canvas = new Bitmap(_drawing, "auto", true);
			_canvasHolder = new Sprite();
			_canvasHolder.addChild(_canvas);
			_canvasHolder.y = stage.stageHeight - _canvas.height;
			addChild(_canvasHolder);
 
			_shapeHolder = new Sprite();
			_shapeHolder.graphics.beginFill(0x000000);
			_shapeHolder.graphics.drawRect(0, 0, _canvas.width, _canvas.height);
			_shapeHolder.graphics.endFill();
		}
 
		private function mouseDownHandler(event:MouseEvent):void {
			_currentShape = new Shape();
			_shapeHolder.addChild(_currentShape);
			_currentShape.graphics.lineStyle(0, _colorChooser.value);
			_currentShape.graphics.moveTo(_canvas.mouseX, _canvas.mouseY);
			stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
			addEventListener(Event.ENTER_FRAME, draw);
		}
 
		private function mouseUpHandler(event:MouseEvent):void {
			stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
			removeEventListener(Event.ENTER_FRAME, draw);
			var operation:DrawingOperation = new DrawingOperation(_currentShape, _drawing);
			_undoManager.pushUndo(operation);
			setButtonStates();
		}
 
		private function draw(event:Event):void {
			_currentShape.graphics.lineTo(_canvas.mouseX, _canvas.mouseY);
			_drawing.draw(_shapeHolder);
		}
 
		private function initManager():void {
			_undoManager = new UndoManager();
			_redoManager = new UndoManager();
			_undoManager.undoAndRedoItemLimit = _redoManager.undoAndRedoItemLimit = UNDO_LIMIT;
		}
 
		private function initUI():void {
			Style.BUTTON_FACE = 0x000000;
			_undoButton = new PushButton(this, 0, 0, "Undo", undoHandler);
			_redoButton = new PushButton(this, _undoButton.width, 0, "Redo", redoHandler);
			_colorChooser = new ColorChooser(this, _redoButton.x + _redoButton.width, 0, _currentColor, colorChooserHandler);
			_colorChooser.usePopup = true;
			setButtonStates();
		}
 
		private function redoHandler(event:MouseEvent):void {
			_undoManager.pushUndo(_redoManager.peekRedo());
			_redoManager.redo();
			setButtonStates();
		}
 
		private function undoHandler(event:MouseEvent):void {
			_redoManager.pushRedo(_undoManager.peekUndo());
			_undoManager.undo();
			setButtonStates();
		}
 
		private function colorChooserHandler(event:Event):void {
			var operation:ColorChooserOperation = new ColorChooserOperation(_currentColor, _colorChooser.value, _colorChooser);
			_undoManager.pushUndo(operation);
			_currentColor = _colorChooser.value;
			setButtonStates();
		}	
 
		private function setButtonStates():void {
			_undoButton.enabled = _undoManager.canUndo();
			_redoButton.enabled = _redoManager.canRedo();
		}
	}
}

And compiled, it gives you this:

Get Adobe Flash player

And that’s how easy it is to now add basic (and expected) undo/redo functionality to your Flash platform apps these days.


By the way, the code highlighter plugin I use here, Dean’s Code Highlighter, is starting to drive me nuts. If anyone has suggestions for other WordPress code highlighters, please post them in a comment.

11 Comments »

  1. Jeff says:

    Nice post, that is a cool little feature. For code high lighting I use WP-Syntax: http://wordpress.org/extend/plugins/wp-syntax/

  2. Devon O. says:

    Thanks for the suggestion, Jeff. I’ll check it out today.

  3. Devon O. says:

    So I switched over to wp-syntax. Took a bit of work to get it formatted the way I like, but so far it seems much better. Thanks again for the link, Jeff.

  4. km says:

    Wow thanks 10x for bringing this feature to light! Truly is a powerful little gem :)

    Also thanks for your blog and past articles… They have been a tremendous help to myself and I’m sure many others.

  5. cheezbox says:

    Might want to clear the redo stack whenever you perform a new action or it doesn’t work.

  6. judah says:

    Do you know if the bug you mentioned has been fixed?

  7. Aby says:

    Devon,

    This helped me a lot in a project Ive been working on. Thanks.

    The bug you mentioned seems to be fixed now. I used just one undoManager and it worked fine.

    Although, you need to call _undoManager.clearRedo() before _undoManager.pushUndo(operation) in the mouseUpHandler method.

    Thanks again.

  8. Devon O. says:

    Thank you for the tip, Aby. Perhaps that was the problem I was running into from the start.

  9. Diego Curtino says:

    I tried to transfer the code done here to my app in a trivial app with 1 TextInput control and two buttons. One to change the text and one to undo the operation but it didn’t work.

    I searched everything I could for 3 days in a row now, and yours seems to be the only example in the web that uses the UndoManager, so I have no other references. Is there any chance that anyone here take a look to my code (it might be 50 lines between the two classes) to point me out what I am doing wrong? I’d appreciate that a lot. I have also published my question in Adobe Forums but no answer so far. Any hint?

    Thanks in advance.

  10. Devon O. says:

    Hey Diego,

    Can’t claim to be an expert in the matter, but I can check out the code you have. I’m in Amsterdam for FITC right now, but I’ll send an email when I get settled back in at home..

  11. Prashant Raut says:

    Nice tutorial, I am trying to do same thing in my project, you made my task easy. Thanks :)

RSS feed for comments on this post. / TrackBack URI

Leave a Reply

Devon O. Wolfgang

Technical Reviewer of “The Essential Guide to Flash CS4 AIR Development”

Contributing Author of “Flash AS3 for Interactive Agencies”

Senior Flash Engineer PopCap Games, International Ltd.

Portfolio

Santabot: A Unity3D Flash Game


All right, so a Christmas game like “Santabot vs. The Flying Saucers from Mars” may be a day late[...]

Magnify – a jQuery Plugin


Let me begin by saying right up front, I have not given up on Flash[...]

It’s a Starling Halloween


Getting some practice for the upcoming Zombie Apocalypse[...]

Getting Started with Proscenium

So I had a chance this weekend to sit down and play around with Adobe’s new 3D framework for Stage3D, Proscenium, and thought I’d share a few of the results (a word of caution, there are no preloaders for any of the examples and may load a bit slowly). The first shows some reflections and [...]

Particle Editor for Starling Framework

An in-browser particle editor for the Starling 2D Framework for Flash Player 11.

So Long and Thanks for all the Flash on the Beach

So, another Flash on the Beach has just has just drawn to a close[...]

Game Development Tips from the Trenches of PopCap

Well, this is a post that’s a bit overdue, but, thanks to a well timed bank holiday, I finally had the opportunity to sit down and type up what I’ve been meaning to for some time now…

Old Skool Demoscene FX as 3D Textures

Many moons ago, I got the idea to create some demoscene plane deformation effects in Flash based on the formulas found here: http://www.iquilezles.org/www/articles/deform/deform.htm. I posted my less than desired results up on wonderfl. Thankfully, fellow wonderfl user, Hasufel, forked my attempt and optimized the hell out of it coming up with this. Well, today, for no [...]

Making The Gaming Scene (A Change in Careers)

And for my second blog post of the day, a much more personal note. After about three years of working with, what I would consider as objectively as possible, the best digital agency in Ireland, vStream Digital Media, I have made the immense career decision to leave the agency world and enter the arena of [...]

Feeling Lucky?

Images to dice, kick ascii style [...]

Adventures in Playbook Land

Adventures in Playbook Land


Now that the ordeal is over, I thought I’d take the time to sit down and share my account of what it was like to develop a Blackberry Playbook application using the Adobe Flex SDK[...]

Flash

Draw it for Me

So many ideas – so little time….

Kinect Application Running in Dublin

So, on Friday I just wrapped up our latest project at vStream Digital Media, a Kinect powered flipbook that lets users flip through the hand written notebooks of Philip Lynott of Thin Lizzy. The app uses OpenNI, runs in Adobe AIR and is currently on display at a pretty bitchin’ Phil Lynott exhibition running in [...]

Beach Ball Kinect Party

So we finally got a Kinect camera hooked up to a pc at work and, while it doesn’t seem to be legal to use it for commercial projects (but, hey, I’m no lawyer), the boss asked me to get it figured out and come up with some ideas just in case it would be feasible [...]

Facebook and Flash – A Book Review

Now, I should begin by saying I absolutely hate building Facebook applications. And I build a lot of them at work. Every time I get the word from above that we’re doing another FB app, I just groan – both inwardly and out. It’s become a running joke of the office. Why do I dislike [...]

Multitouch Fluid Dynamics with AIR for Android and RTMFP

The other day I was having some fun playing around with Eugene Zatepyakin’s (aka @inspirit) FluidSolverHD (Actionscript port of C++ fluid dynamics library, MSAFluid. Or is MSAFluid, the processing/java port of the C++ library? In any case it’s a very cool fluid dynamics thingamabob – the HD version using Alchemy). After a bit of tinkering, [...]