Features

Features > AJAX Features > Web Apps Features > Dev

As apps move from the desktop to the Web, one of the pieces of functionality that users expect to see but is often overlooked, perhaps because it is hard to implement, is undo/redo. In this tutorial, Francisco Tolmasky explains how easy it is is to add using the Cappucino web framework.

Undo and redo are two of the most essential features in any real rich application experience. In many cases, your user has already turned these commands into reflexes, automatically hitting the proper keys and expecting the right thing to happen. Unfortunately, this is often left unimplemented by developers when making the transition from the desktop to the web, serving as a rude awakening to your users when they make a mistake that can’t be undone. It makes sense that this doesn’t receive the attention it deserves since the actual functionality of your application should obviously come first, and it doesn’t help that implementing these features from scratch can be quite difficult. However, they add a necessary amount of polish that you should seriously consider adding to your web application.

Luckily for us, Cappuccino has built-in support that can allow you to plug undo and redo right in by just by adding a few lines of code. In this tutorial, we will be exploring how to add sophisticated undo and redo support to a graphical application in the browser. We won’t be creating the entire application from scratch however, but instead building off of an existing example. We’re doing this for two reasons. For starters, we don’t want to get distracted from our main task by having to wade through unrelated code. Instead we’ll simply review whatever code we need to as we get to it. More importantly, the sample provided is complex enough to serve as a true real-world example, as opposed to the contrived code we’d be forced to put together in the limited scope of space and time of this tutorial. This also has the benefit of displaying the modular nature of undo support in Cappuccino, and how we can add it to an application without knowing every detail of its implementation.

That being said, you can, of course, feel free to review the entire source of the application as well. The application itself is written entirely using Cappuccino, but you don’t really need any prior knowledge of Cappuccino to follow along with this tutorial. Feel free to review Cappuccino and Objective-J at cappuccino.org before you dig in, but it’s not a requirement to follow along.

Let’s start by taking a look at the application we’ll be modifying. You can see it and play with it live here. You’ll need to download the source in order to follow along with the changes in this tutorial. As you’ve probably noticed, it’s a simple floor planning app that lets you drag and drop and arrange furniture into the layout of a small apartment:

screenshot of floor planning application, showing furniture on left and floorplan on right

Have a play with the app. As you can see, it supports three actions that we’ll want the user to be able to undo:

actions the user should be able to undo: Drag-and-drop addition, drag-and-drop positioning, and rotation

  • drag-and-drop to add furniture: should remove the furniture when hitting undo, and add back in when hitting redo
  • drag-and-drop to a new location: should revert to previous location when hitting undo
  • rotate: should revert to previous rotation when hitting undo

Now, the main thing you should know before we dig into the code is we’ll be dealing with two classes, FloorPlanView, which represents the background and layout of the apartment:

screenshot of application with layout highlighted

The other is FurnitureView, which displays each individual piece of furniture in the apartment:

screenshot of application with furniture highlighted

The first action we’re going to tackle is undoing the movement of furniture pieces. All the code to handle moving furniture views is currently contained in two methods in FurnitureView.j: mouseDown: and mouseDragged:. Let’s review their current implementations:


- (void)mouseDown:(CPEvent)anEvent
{
    dragLocation = [anEvent locationInWindow];

    [[EditorView sharedEditorView] setFurnitureView:self];
}

- (void)mouseDragged:(CPEvent)anEvent
{
    var location = [anEvent locationInWindow],
        origin = [self frame].origin;

    [self setFrameOrigin:CGPointMake(origin.x + location.x - dragLocation.x, origin.y + location.y - dragLocation.y)];

    dragLocation = location;
}

mouseDown: simply stores the mouse down position, and additionally sets the furniture view as selected through the EditorView, which we won’t need to concern ourselves with. mouseDragged: then proceeds to update the origin of the view on every drag event. Despite being where the actual changes take place, we don’t want to register our undo action in mouseDragged: because if we do we’ll be registering an undo action for every pixel the user drags! So instead, we’ll want to add a new method right below it called mouseUp:. The mouseUp: method gets called when the user lifts the mouse and is thus done dragging:


- (void)mouseUp:(CPEvent)anEvent
{
// Register undo here.
}

Now, before we continue we’ll want to create one additional new method, setEditedOrigin:. The reason for this is that we want a way to change the furniture locations that notifies the undo architecture, and a way that doesn’t. Currently we’ve fullfilled one of these requirements: setFrameOrigin: can be used to change the position of a FurnitureView without registering any undos, so let’s now create an analog that will:


- (void)setEditedOrigin:(CGPoint)aPoint
{
    if (CGPointEqualToPoint(editedOrigin, aPoint))
        return;

    [[[self window] undoManager] registerUndoWithTarget:self selector:@selector(setEditedOrigin:) object:editedOrigin];

    editedOrigin = aPoint;

    [self setFrameOrigin:aPoint];
}

This method is relatively straightforward and shows us how to talk to Cappuccino’s undo support. We create a new instance variable called editedOrigin which keeps track of the last “undoable” position. This way we can ignore all the origin changes that take place during while a drag is mid-flight. When the user passes in a new position, we compare it to editedOrigin to make sure they’re not the same. We do this in order to prevent undos being placed in the stack that have no percievable difference to our user (making it appear that the undo “didn’t register”). After this we do the most important step, which is tell our undo manager that it should register an undo. Every view has a window, and every window has its own undo manager (that way, different windows can have different undo stacks). So we grab our undo manager from our window. We then call registerUndoWithTarget:selector:object:. This method tells the undo manager what to do when the user hits undo. In this case, we want to just call this very same method, but with the old origin, editedOrigin. We then set editedOrigin to the current position, and of course update our actual origin, and we’re done. We do need a bit of book-keeping however; namely we need to declare this new instance variable in the class definition:


@implementation FurnitureView : CPView
{
    CPString    name;
    CPImage     image;

    float       rotationRadians;

    CGPoint     dragLocation;
    CGPoint     editedOrigin;
}

as well as set its initial value in mouseDown::


- (void)mouseDown:(CPEvent)anEvent
{
    editedOrigin = [self frame].origin;

    dragLocation = [anEvent locationInWindow];

    [[EditorView sharedEditorView] setFurnitureView:self];
}

You may be wondering why we need this new instance variable at all, since we can just query our frame for our current origin. The reason is because when we only want to “remember” origins that are to be undone. If you hit refresh in your browser, you should be able to undo moving a furniture item by hitting command-z on a Mac and ctrl-z on a PC. You’ll also notice that you are capable of redoing these actions as well. That’s because setEditedOrigin: gets called for the undo action as well, thus registering another undo on the stack, which Cappuccino is smart enough to know should actually be a redo.

We can now move on to undoing the rotations which are slightly more complex. As we saw earlier, the goal is always to register an undo once editing is complete. You’ll notice that there are two empty methods in FurnitureView.j, willBeginLiveRotation and didEndLiveRotation, which the editing system is kind enough to send to us when the user begins and ends rotating, respectively:


- (void)willBeginLiveRotation
{
}

- (void)didEndLiveRotation
{
}

Clearly we’ll want to add our actual undo action in didEndLiveRotation, but just as before we’re going to created an “Edited” version of setRotationRadians: for rotation actions that we want to register with the undo system:


- (void)setEditedRotationRadians:(float)radians
{
    if (editedRotationRadians == radians)
        return;

    [[[self window] undoManager] registerUndoWithTarget:self selector:@selector(setEditedRotationRadians:) object:editedRotationRadians];

    [self setRotationRadians:radians];

    editedRotationRadians = radians;
}

This should look very familiar. It’s almost identical to our previous implementation, except we are dealing with radians instead of positions. Let’s not forget to add our new editedRotationRadians to the other necessary places, namely the class declaration:


@implementation FurnitureView : CPView
{
    CPString    name;
    CPImage     image;

    float       rotationRadians;
    float       editedRotationRadians;

    CGPoint     dragLocation;
    CGPoint     editedOrigin;
}

and willBeginLiveRotation, which is analogous to our mouseDown: since it’s what kick starts the rotation process:


- (void)willBeginLiveRotation
{
    editedRotationRadians = rotationRadians;
}

Now all that’s left to do is to actually call setEditedRotationRadians when the user finishes rotating a furniture piece:


- (void)didEndLiveRotation
{
    [self setEditedRotationRadians:rotationRadians];
}

Once again, if you refresh you should be able to undo and redo rotating furniture items, as well as being able to undo their positioning.

We are now done with editing the FurnitureView class, and we can move on to the last action that the user should be able to undo: actually adding furniture to the apartment layout. To do this, we’ll have to move to FloorPlanView.j, where the FloorPlanView class is contained. The two methods we are primarily concerned with are addFurnitureView: and removeFurnitureView:, which do just that:


- (void)addFurnitureView:(FurnitureView)aFurnitureView
{
    [self addSubview:aFurnitureView];

    [[EditorView sharedEditorView] setFurnitureView:aFurnitureView];
}

- (void)removeFurnitureView:(FurnitureView)aFurnitureView
{
    var editorView = [EditorView sharedEditorView];

    if ([editorView furnitureView] == aFurnitureView)
        [editorView setFurnitureView:nil];

    [aFurnitureView removeFromSuperview];
}

As you can see there’s quite a bit going on here, but again most of it is not of our concern. In fact, in this case, none of it is. This comes from one simple realization: these methods are opposites of each other, and thus undo each other. So to undo addFurnitureView:, we need to removeFurnitureView:, and vice versa, so the code is actually quite simple:


- (void)addFurnitureView:(FurnitureView)aFurnitureView
{
    [[[self window] undoManager] registerUndoWithTarget:self selector:@selector(removeFurnitureView:) object:aFurnitureView];

    [self addSubview:aFurnitureView];

    [[EditorView sharedEditorView] setFurnitureView:aFurnitureView];
}
- (void)removeFurnitureView:(FurnitureView)aFurnitureView
{
    [[[self window] undoManager] registerUndoWithTarget:self selector:@selector(addFurnitureView:) object:aFurnitureView];

    var editorView = [EditorView sharedEditorView];

    if ([editorView furnitureView] == aFurnitureView)
        [editorView setFurnitureView:nil];

    [aFurnitureView removeFromSuperview];
}

And there it is, it’s that easy. All we had to do is register the opposite action in each method, and both of them are undable (and redoable) now.

Make undo and redo discoverable

Something interesting that I’ve noticed with undo and redo in our own applications is that many times people don’t realize they have this feature. When we first launched 280 Slides we got a number of “feature requests” asking for undo and redo support. They were thrilled when we told them they could just use the key commands they were used to, but apparently this was not discoverable enough. Because of this, we decided to add actual undo and redo buttons to make these actions more explicit. Let’s go ahead and do the same here by adding the following snippet of code to the end of the applicationDidFinishLaunching: method in AppController.j:


var undoButton = [[CPButton alloc] initWithFrame:CGRectMake(20.0, 400.0, 60.0, 18.0)],
    redoButton = [[CPButton alloc] initWithFrame:CGRectMake(90.0, 400.0, 60.0, 18.0)];

[undoButton setTitle:“Undo”];
[undoButton setTarget:[theWindow undoManager]];
[undoButton setAction:@selector(undo)];

[redoButton setTitle:“Redo”];
[redoButton setTarget:[theWindow undoManager]];
[redoButton setAction:@selector(redo)];

[view addSubview:undoButton];
[view addSubview:redoButton];

Now if our users have “unlearned” expecting undo and redo to work, they’ll be able to idenitfy these buttons. With that, we’ve now made all the current actions in this application undoable and provided an easy way for our users to use this feature. As we continue to add new features to this application we can also incrementally add their associated undos. As we’ve seen here, it’s often a matter of just adding one or two lines of code per method, so it’s good to start early and not wait until the very end to begin incorporating this functionality. I hope you’ve enjoyed this tutorial and make sure to leave any questions you may have in the comments! The completed source with all the above additions is available here and you can give it a spin here.

Francisco will be speaking at The Future of Web Apps Miami

The Future of Web Apps returns to Miami on 23 and 24 Feb 2009. The awesome speaker lineup includes Michael Arrington, Daniel Burka, Jason Fried, Joel Spolsky, and Gary Vaynerchuk. Book now as there are a limited number of conference passes for just $200 (normally $395) - be very quick as they won't last long!

7 Responses to “Add Undo and Redo to Your Web Application With Cappuccino”

  1. Cappuccino Blog » Blog Archive » Tutorial on Adding Undo to Your Cappuccino Application Publish on ThinkVitamin says

    […] ThinkVitamin.com is featuring an article by Francisco that goes through the process of adding undo/redo support to an existing application. It uses a “furniture layout” application as the model, and goes through all the steps necessary to make user actions undoable. […]

  2. Ajaxian » Undo, redo, and much more with Cappuccino says

    […] Francisco Tolmasky of 280 North has written a very nice piece on adding Undo and Redo to your Web application with Cappuccino. […]

  3. Undo, redo, and much more with Cappuccino | Eroarea 403 says

    […] Francisco Tolmasky of 280 North has written a very nice piece on adding Undo and Redo to your Web application with Cappuccino. […]

  4. Undo, redo, and much more with Cappuccino | Eroarea 403 says

    […] Francisco Tolmasky of 280 North has written a very nice piece on adding Undo and Redo to your Web application with Cappuccino. […]

  5. Cappuccino : Add Undo/Redo tutorial « Cjed Audio blog says

    […] cjed mac & audio main website « Palace of Arts Budapest Pipe Organ Samples Cappuccino : Add Undo/Redo tutorial December 5, 2008 At the thinkvitamin site we can find a tutorial about adding Undo/Redo to aCappuccino application. As with Cocoa programming, the UndoManager is aware that a Redo is undoing the undo, so it cleans the actions stacks and doesn’t require to implement the Redo feature. I didn’t find the undo and redo buttons in the example here (nor managed to get Pomme-Z to produce something). […]

  6. Ben Darlow says

    Undo and redo are two of the most essential features in any real rich application experience. In many cases, your user has already turned these commands into reflexes, automatically hitting the proper keys and expecting the right thing to happen.

    Citation required? I’ve never seen a web user expect the keyboard shortcuts for undo and redo to work automatically. Ignoring the fact that there isn’t a consistent standard keyboard shortcut for redo anyway, this makes two glaring mistaken assumptions.

    Firstly, keyboard shortcuts are a power user feature. It’s quite simply wrong to assume that your users will be confused by the absence of this functionality.

    Secondly, the web browser already has a built-in mechanism somewhat analogous to undo which users are more familiar with: the back button. If you want to augment your web application with the ability to undo actions, then this should be the interface for that ability, not obscure keyboard shortcuts that a fraction of your userbase will be familiar with.

  7. Andre Angelantoni says

    Ben,

    you are so missing the point it’s difficult to know where to start….

    1. In a rich client, the back button is a poor tool for implementing undo. It would tell the browser to go to the previous document, not the previous app state.
    2. as web client tools become more powerful, they will get keyboard shortcuts just like desktop clients. This is already happening.
    3. lots of people are familiar with ctrl-z to undo, not just power users

    The web is becoming more desktop app-like…it’s already well underway. The question is how to get us there faster, because perhaps a million programming hours every year are currently being wasted solving silly problems using the current crop of tools (HTML+css+javascript+widget= hell on earth as you try to debug across all browsers).

Leave a Reply

Basic HTML (<strong>, <em>, <a>, etc.) is allowed in your comments. Please be respectful and keep your comments on-topic. If we think you're being offensive for no reason, we'll delete your comment.

Comments RSS