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)

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

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

Open Source Applications Foundation "Dev" mailing list
http://lists.osafoundation.org/mailman/listinfo/dev

Reply via email to