LogoLogo
  • Home
  • Projects
  • About
  • Contact

Playing Around with the New UndoManager

Devon O. · May 01, 2010 · Actionscript, Flash · 11 comments
5

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.

C#
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:

C#
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:

C#
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:

C#
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:

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.

  Facebook   Pinterest   Twitter   Google+
command patternundomanager
  • FOTB 2010 – Day 1 in Review
    September 27, 2010 · 0 comments
    1777
    3
    Read more
  • OBO_Accordion: Another Scripted UI “Widget”
    June 03, 2008 · 10 comments
    2251
    2
    Read more
  • Rockin and Rollin with the JiglibFlash Terrain
    March 16, 2010 · 8 comments
    2496
    6
    Read more
11 Comments:
  1. Nice post, that is a cool little feature. For code high lighting I use WP-Syntax: http://wordpress.org/extend/plugins/wp-syntax/

    Jeff · May 01, 2010
  2. Thanks for the suggestion, Jeff. I’ll check it out today.

    Devon O. · May 01, 2010
  3. 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.

    Devon O. · May 02, 2010
  4. 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.

    km · May 03, 2010
  5. Might want to clear the redo stack whenever you perform a new action or it doesn’t work.

    cheezbox · May 08, 2010
  6. Do you know if the bug you mentioned has been fixed?

    judah · June 29, 2010
  7. 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.

    Aby · December 05, 2010
  8. Thank you for the tip, Aby. Perhaps that was the problem I was running into from the start.

    Devon O. · December 05, 2010
  9. 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.

    Diego Curtino · March 09, 2011
  10. 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..

    Devon O. · March 09, 2011
  11. Nice tutorial, I am trying to do same thing in my project, you made my task easy. Thanks :)

    Prashant Raut · May 19, 2011

Leave a Comment! Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Devon O. Wolfgang

AIR | Unity3D | AR/VR

Unity Certified Developer

Technical Reviewer of “The Essential Guide to Flash CS4 AIR Development” and “Starling Game Development Essentials”

Reviewer of “The Starling Handbook”

Unity Engineer at Touch Press.

Categories
  • Actionscript (95)
  • AIR (16)
  • Flash (99)
  • Games (7)
  • Liberty (13)
  • Life (53)
  • Shaders (20)
  • Unity3D (21)
Recent Comments
  • MainDepth on Unity Ripple or Shock Wave Effect
  • Devon O. on Unity Ripple or Shock Wave Effect
  • Feral_Pug on Unity Ripple or Shock Wave Effect
  • bavvireal on Unity3D Endless Runner Part I – Curved Worlds
  • Danielius Vargonas on Custom Post Processing with the LWRP
Archives
  • December 2020 (1)
  • December 2019 (1)
  • September 2019 (1)
  • February 2019 (2)
  • December 2018 (1)
  • July 2018 (1)
  • June 2018 (1)
  • May 2018 (2)
  • January 2018 (1)
  • December 2017 (2)
  • October 2017 (1)
  • September 2017 (2)
  • January 2017 (1)
  • July 2016 (1)
  • December 2015 (2)
  • March 2015 (1)
  • September 2014 (1)
  • January 2014 (1)
  • August 2013 (1)
  • July 2013 (1)
  • May 2013 (1)
  • March 2013 (2)
  • December 2012 (1)
  • November 2012 (1)
  • September 2012 (3)
  • June 2012 (2)
  • May 2012 (1)
  • April 2012 (1)
  • December 2011 (2)
  • October 2011 (3)
  • September 2011 (1)
  • August 2011 (1)
  • July 2011 (1)
  • May 2011 (2)
  • April 2011 (2)
  • March 2011 (1)
  • February 2011 (1)
  • January 2011 (2)
  • December 2010 (3)
  • October 2010 (5)
  • September 2010 (1)
  • July 2010 (2)
  • May 2010 (5)
  • April 2010 (2)
  • March 2010 (7)
  • February 2010 (5)
  • January 2010 (5)
  • December 2009 (3)
  • November 2009 (1)
  • October 2009 (5)
  • September 2009 (5)
  • August 2009 (1)
  • July 2009 (1)
  • June 2009 (2)
  • May 2009 (6)
  • April 2009 (4)
  • March 2009 (2)
  • February 2009 (4)
  • January 2009 (1)
  • December 2008 (5)
  • November 2008 (2)
  • September 2008 (1)
  • August 2008 (6)
  • July 2008 (6)
  • June 2008 (9)
  • May 2008 (4)
  • April 2008 (3)
  • March 2008 (4)
  • February 2008 (9)
  • January 2008 (7)
  • December 2007 (6)
Copyright © 2021 Devon O. Wolfgang