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:
[kml_flashembed publishmethod=”static” fversion=”10.0.0″ movie=”http://blog.onebyonedesign.com/wp-content/uploads/2010/05/undome.swf” width=”640″ height=”480″ targetclass=”flashmovie”]
[/kml_flashembed]
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.
Post a comment