Subscribe to
Posts
Comments
NSLog(); Header Image

The Pain of Undo

I've got a test application. It's got two text fields and two checkboxes. One would have sufficed, but I wanted to add another method of "entering" text - by tabbing to the other text field. I've got undo working for the checkboxes and text fields. I've got AppleScript working for both. I've got a full MVC model working throughout. What I haven't got working is undo involving a text field and a checkbox. It's ridiculous!

Here's the scenario. The user types some text in a text field, they click a checkbox, and they continue to type in the text field, eventually tabbing out of it or pressing return to "commit" that text. If they've done only these two things, the first undo should undo the text (the last thing done), and the next undo should undo the checkbox.

Unfortunately, doing this is proving to be a tremendous pain in the behind. First, let me explain the sequence of events:

  1. User clicks checkbox or enters (enter, tab) text.
  2. IBAction sent from the View (text field or checkbox) to the Controller to individualized actions (one per text field/button).
  3. Controller gets the string value or state (of the text field or checkbox) and calls the appropriate "set" method in the Model.
  4. The Model registers the undo callback and posts a notification saying "update the UI please." This notification is necessary because the AppleScript interface may change the Model without having gone through the View at all.
  5. The Controller receives the update notification and queries the Model for all of its current data, setting the appropriate text and checkbox states in the View.

This breaks down in several ways. When I'm typing, and I click a checkbox, the text has not been committed yet and the "update UI" stuff clears the checkbox (or resets it to its last value, which is "" at startup). Alternatively, if I commit the text immediately and then commit the checkbox action, the two actions become tied together so that a single undo undoes both the checkbox and the entered text. There are a few other possible actions, depending on when you endEditingFor: or makeFirstResponder: or whatever. I've tried several different things, and none simply do what I want - what I think is logical.

For example: I could simply not update the UI after someone clicks a checkbox. After all, the checkbox should be in the proper state, right? It's been clicked. When you click, it switches to on or off. Simple! Perfect! Not updating the UI means the text field both remains as firstResponder and the text remains "uncommitted."

Unfortunately, this method fails in two ways. First, undo - which uses the same setBool: method as the setting does in the first place (in the Model) doesn't properly update the checkbox. So I can click a checkbox, the document becomes "dirty," and I can undo it… but it remains checked until I commit some text (which does update the UI). Second, it fails if someone writes AppleScript like set test document 1's model's check1 to true or something - the UI doesn't get updated to reflect the fact that the checkbox is checked.

I could probably work around this last case by having about three methods per checkbox - whether something came in via AppleScript or an undo, or registering a different undo setter that updates the UI, or something… but isn't that all a bunch of bull? Shouldn't there be an easier solution out there? Why is this so fleepin' difficult to just get right?

If anyone's got any ideas, I'd love to hear 'em. I've got a test project up here (very small) if anyone wants to grab it and play with it.

It's frustrating the heck out of me.

10 Responses to "The Pain of Undo"

  1. Why do you need to provide undo for checkboxes? The only app I can think of that does that is Vermont Recipes, and that's just for demonstration purposes.

    I'd also look at the delayed committing of the text. Having to explicitly commit with Tab or Return seems weird to me. The document won't be marked dirty until you commit, so if you close the window without committing you won't get a warning about unsaved changes. I know it's standard Cocoa behavior, but it doesn't seem right. Why not have it commit as you type, not when you tab out?

  2. On second thought, I guess that would mess up the undo groups, too. Is there an app that has undo for a bunch of little text fields like this? The ones I just tried don't.

  3. In the checkbox action, use NSChangeDone to set the document's "dirty" flag, post a notification when the checkbox is changed, in the method called when above notification is posted, have the flag removed (I forget the name of the opposite flag). I haven't tried this, but it seems like it would work. Let me know by leaving a comment on my blog.

  4. You could selectively update the UI based on what was changed in the model. It might not scale, but it would solve the immediate problem. I made some changes to the test project and posted them in my Public folder (dschimpf) that does what I'm talking about.

    Undo is always the last thing that works, and the first thing to break. 🙂

  5. Hasan, that doesn't work. We need them to be undoable - simply setting "change done" doesn't help. We might have ten checkboxes - people need to be able to undo a change, not be forced to guess what they changed.

    Dan, everything needs updated in the UI. Someone can write an AppleScript to turn on a checkbox - it needs to show up as checked immediately.

  6. I've never heard of an app that lets you 'undo' checkboxes. That is silly. Don't do it.

  7. Clef - if the checkboxes are data then the need to be undoable otherwise you can undo back to an inconsistent state of the data model.

  8. I don't know what context these fields and checkboxes will be in, but the current behavior doesn't make any sense to me.

    First of all, I'd expect undo to undo typing without me needing to "commit" changes in the textbox by tabbing out of it. I can't think of any other apps that have the concept of "committing" changes before they can be undone.

    More importantly, though, I think you're being too hard on yourself. I'd expectTyping in a textboxChanging the state of a checkbox, thenTyping more in the textboxto be three separate actions. I'd expect undo to first remove the text I had typed since checking the box, then uncheck the box, then remove the text I had typed before checking the box.

    I'm happy to see you willing to do so much work to save the user from doing work, though.

  9. Daniel, it's standard Cocoa behavior to not "commit" text until you've tabbed or pressed enter. Look around - a lot of apps do it. A lot of apps.

    And secondly, I'm not being too hard on myself, because even the steps you listed are currently very difficult - Cocoa wants to make the "undo" involving the checkbox also undo - at the same time - any changes you force to be committed on the text.

    It's a mess, really, and I'm trying to find the perfect, ideal solution and the best way to do it. And as to Clef, as the anonymous poster below you points out, undo changes the state of a model, and so it should be undoable, yes.

  10. Sigh. EOF's UI layer used to handle Undo, for the most part. It handled a great deal of the MVC stuff.

    I would have gone mad writing trade-entry apps with many, many fields on the windows if not for EOF.