Witam na mojej prywatnej stronie internetowej!
[If this is all Polish to you, click here: English]
Uwaga: z oczywistych powodów nie mogę zagwarantować swojej nieomylności, choć staram się o zgodność tego, co piszę, z Prawdą. Jest również oczywiste, że nie gwarantuję takiej zgodności w przypadku komentarzy. Umieszczenie linku do strony spoza niniejszego serwisu nie musi oznaczać, że podzielam poglądy autora tej strony, a jedynie, że uważam ją za wartościową z takich czy innych powodów.
Marcin ‘mbork’ Borkowski
As you might have guessed, I pay siginificant attention to UI/UX issues. Since I’ve been implementing undo functionality recently, I thought I’d share a few thoughts about it.
On the surface, undo seems easy: you record all changes you have made and revert the last one on demand. When you look at details, however, things can get messy.
First of all, how do you actually “record all changes”? You might record the state after every change (probably simple in many applications, but potentially a memory bottleneck). You might just record the deltas (better from the memory perspective, but a bit more involved). In the case of Logeox, however, I decided to do something even different. Instead of recording the state after each “change” (i.e., turtle command), I record the commands themselves. Since they are sufficient to completely recreate the state at any point of time, undoing the last command is easy: you just delete the last link in the chain and replay them again. (Theoretically, it means that undoing a change is a lot more expensive timewise than issuing commands, but I do not expect this to be a real problem. And if it ever becomes one, I can easily add some caching, like e.g. recording the full state every 40 commands or something like that.)
Then there comes another question: what exactly a change is? In my case, I have a few commands: go forward, turn left/right, pen up/down. Should all of them be undoable? This would be the simplest thing to do (and the first one I did), but probably not the best. After consulting half of my users (i.e., my dear wife), I decided that only actual turtle movements should be undoable. (By movement, I mean a command changing the turtle’s position, so turns are not included.)
This raises another question. Assume the following sequence of user actions: go forward, turn right, undo. Should the undo restore the direction from before the “go forward” command? I think the answer is positive (and fairly obvious). Consider now this: pen down, go forward, pen up. Should the undo change the state to pen down again? This time the answer seems less obvious, but still affirmative. And so I did it this way.
And how did I actually implement all these? It turned out to be pretty simple. I added another method to the
isUndoable, which returns false for each command except
GoForward. (I briefly considered making it return false by default and only overriding it in
GoForward. After a while, I decided that I like defining it explicitly in each command a bit better. Yes, it looks like code duplication, but (1) it makes me think before deciding – for each and every command – whether it should be undoable, and (2) it felt kind of wrong to me to hardcode the choice that happens to be more common in the parent class.) And
Undo, instead of just removing the last command from the list, keeps removing commands until the first one with
isUndoable returning true. (And while at that, I fixed a small but irritating bug with the app crashing when trying to undo when no commands were yet recorded.)
return false; and nothing else.
And now that I googled it a bit and gave it a minute of thinking, I guess there’s even a better way: to have the
isUndoable() method return a field, initialized to the proper value by the constructor. And BTW, googling for that revealed a whole mess of opinions and possibilities of having fields with various qualifiers –