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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
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:
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.
Nice post, that is a cool little feature. For code high lighting I use WP-Syntax: http://wordpress.org/extend/plugins/wp-syntax/
Thanks for the suggestion, Jeff. I’ll check it out today.
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.
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.
Might want to clear the redo stack whenever you perform a new action or it doesn’t work.
Do you know if the bug you mentioned has been fixed?
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.
Thank you for the tip, Aby. Perhaps that was the problem I was running into from the start.
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.
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..
Nice tutorial, I am trying to do same thing in my project, you made my task easy. Thanks :)