Alec Flett wrote:
John Anderson wrote:
Most applications I've worked on implemented undo this way. However,
one applicaiton that I worked on was tied to the repository. It was by
far the nicest because it didn't require and any extra work by the
programmer implementing a command, wasn't buggy and worked consistently
everywhere.
John and I brainstormed a bit today in person on this. One of the
things we talked about was how there are certain view things that you
actually do want to change, if possible. For instance, if like the
scenario #2 I described, you delete an event and then switch to another
collection, you could argue that the undo should also switch you back
to the previous collection. That way you could actually see that the
event "came back" rather than just appearing in another collection that
is out of view.
this is an approach that we talked about:
- An "undoable action" begins say from a key command or a menu.
Some sort of BeginTransaction() command is called along with a
user-visible name for the transaction. This BeginTransaction starts
with a commit() (just to make sure the repository state is clean) and
then stores some version information
- since all commands begin with CPIA events of some kind, we
could
actually just build the BeginTransaction() call directly into the CPIA
event code, along with the user-visible name of the command
- when the "next" undoable action comes in, then it finishes up
the
previous transaction, allowing it to be undone. The advantage of this
is that any subsequent actions, such as switching views, could be
retained, and "undone" automatically.
Now just as John and I came up with a view-specific action that we'd
like to be able to "undo" (i.e. switching collections) - Grant came up
with a view-specific action that we'd prefer NOT to "undo" (i.e.
changing the window size/position)
In practice I don't think this is a big deal -- that fact that window
size don't participate in undo in other applicaitons is simply a side
effect of the mechanism they use to implement undo. If the window size
did participate in undo, I don't think anybody would really mind -- the
situation almost never happens -- and technically you could argue it's
more correct. So my vote is not to waste any code implementing it -- at
least not until it becomes clear that it's a real problem.
Personally, I'm betting that there are more UI things we DON'T want to
undo than those we DO want to undo.
John and I both agreed that the actual mechanism for "undoing" was
almost irrelevant, as long as it involves automatic attribute
re-setting in the repository - i.e. rolling back or re-applying changes
backwards.. its six of one, half dozen of the other.
So at the moment, I think one decision we need to make is when exactly
an undoable action "ends" It sounds like there are a few possibilities:
1) open-ended: Transactions begin when particular CPIA events fire, and
stay "open" until the next transaction starts. The advantage here is
that UI changes get automatically bundled with their previous action,
allowing the UI to return to exactly the previous state when the
command ran. This is also the disadvantage, since many UI changes
shouldn't be undone.
2) explicitly closed: transactions end either when someone calls
EndTransaction(), or automatically with the CPIA events mechanism. The
advantage is that undoable transactions aren't going to "leak" by
staying open - i.e. an "open" transaction wouldn't start to accumulate
irrelevant changes
3) Some hybrid, where CPIA events Begin transactions, and certain types
of CPIA events could also "end" open transactions without starting a
new one...
Alec
I would argue that undo/redo should be
achieved by parcelling up the code into "actions", similar to what
Alec described, that know how to change the state of the model in both
directions, thus being undoable and redoable. Based on what I've seen
about how the repository is tied to both UI actions and external
actions (like new mail), I do not think it would be a good idea to tie
the undo architecture to the repository. Rather, they would make
additional repository changes in order to achieve their required state
change.
I don't really care about whether it's implemented as roll back or
playing some repository change log forward, all I care about is that we
automate the work necessary to keep track of changes so that I don't
need to write any special code when I implement a command -- except
perhaps for saying where the undo points are and what the name of the
command is.
Perhaps as an optimization, they could check for external changes, and
if there are none, then use the repository's rollback mechanism, but
even that seems difficult, based on what Andi said about not being
able to discard changes (or is that something that is coming soon?).
Regardless, it goes out the window if something else happens like
getting mail in the middle of doing undo/redo.
As I mentioned earlier, when undoing we do need to merge in changes to
the repository from other threads, so, for example newly arrived mail
isn't lost when you undo.
Is the "rerender" mechanism actually just marking items as "dirty", or
is it forcing them to draw?
Rendering creates widgets and synchronizes them to the state of the
blocks (blocks persist and widgets don't).
Reid
Here are a few scenarios that I think would cause us trouble if we
were to tie undo points directly to calls to commit()/uncommit(). I'm
using the term "undoable change" to be a user-comprehensible change,
such as deleting an event or changing data in the detail view - one
that the user would consider "undoable" by selecting Edit->Undo.
I'm using the term "uncommit" to refer to playing back a series of
changes in reverse order - this may or may not mean simply rolling
back, as you'll see below.
Scenario 1: an undoable change may actually span multiple commits
A user makes an undoable change that causes commit() to be called
three times just because of the codepath it follows happens to have an
extra commit() or two.. (maybe it goes through sharing and that causes
some repository view manipulation) Now any sort of "Undo" command
would have to be run three times to uncommit that single user action
In past systems that used a mechanism like I'm proposing, you can
avoid this problem quite easily. Organize your code into operations
that do work, but don't commit. A command calls one or more routines
that the work then the command does a commit at the end (Actually, to
be more precise, each time a command was begun it commited the last
commands changes)
Scenario 2: A user manipulates the UI after making an undoable change
A user makes an undoable change, and then clicks on another
collection. Thanks to CPIA, changes in the UI are also changes in the
repository. This means that "Undo" operation might actually just cause
the user to switch back to their preview collection. It would take a
2nd Undo operation to actually "undo" the original change.
I'm not sure I understand this, but if you mean that undo moves the
user interface back to exactly what it was when you did the last
commit, yes that's the way it works and this is what you expect --
undo should get you back to exactly where you were, view included.
Scenario 3: Some changes might occur in different repository views
A user makes an undoable change, and that change may cause changes in
multiple views - i.e. sharing. What exactly do we "undo" when we
uncommit?
If I understand this, another variation of it is the following:
Suppose while you're doing some operation, new mail arrives, when you
undo you don't want to undo the getting of the new mail. This is a
real problem. One possible solution is to save information about
changes to the repository from other threads and when you undo (roll
back) replay (merge) those changes into the repository.
Personally, I think we need to be very explicit about undoable
changes. I think we need to bracket such changes with some sort of
begin/end undoable transaction mechanism. We can still exploit all the
benefits of the repository though - for instance:
Currently, there is no obvious rule about when to do a commit, so it
ends up being a little arbitrary. This would eliminate the any
confusion about when to do commit.
1) an undoable transaction might just
be
a pair of repository version numbers - and we just play back all the
changes backwards from the newest version to the oldest. (i..e even if
we're at version 103, the previous undoable change might be from
revision 100 to 101, so we'd just play the changes backwards between
101 and 100)
Yes, I agree something like this makes sense
2) notifications would fire when we
play
back the changes, so the UI would (hopefully) stay up to date
To keep the UI up to date, all we need to do is unrender the UI, roll
back the repository, then rerender it (which also calls
synchronizeWiget)
3) perhaps we could even look ahead at
the changes to be played back, to see if an undo operation is even
valid. (i.e. if it might cause conflicts or something)
4) Redo support would be easy - even multiple redo support like in
Word/etc.
An explicit Undo mechanism gives us another advantage: we could assign
user-visible names to the undo actions.. and then we could display
that user-visible name in the edit menu, so it might say "Undo Cut"
instead of just "Undo"
In another system I used based on a begin/end mechanism like commit,
commit took an argument which was the name of the command, e.g. "Cut",
which was used to keep the menu up to date.
Here's an example of this system in action:
UndoManager.BeginTransaction(_(u"Cut"))
self.CutSomething()
UndoManger.EndTransaction()
If your BeginTransaction happened to call commit, I think we could
eliminate explicit commits and our proposals would be identical.
Maybe there's even a more pythonic way to handle this:
UndoManager.DoTransaction(_(u"Cut"), self.CutSomething)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
Open Source Applications Foundation "Dev" mailing list
http://lists.osafoundation.org/mailman/listinfo/dev
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
Open Source Applications Foundation "Dev" mailing list
http://lists.osafoundation.org/mailman/listinfo/dev
|