-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for directed acyclic graph undo/redo #8
Comments
The So, it would be best if the UndoManager interface could be expanded to also support non-linear undos and redos. For example, 5 views display a different portion of the same document. Each view tracks their own edits separately. If a user undoes some action in one view, it can only be redone within that same view. Trying to redo an action in some other view will do nothing. To expand UndoManager in this way, we would also need to include an argument to keep track of which source (e.g. which view in the above example) is calling the corresponding method and thus which ChangeQueue to use to undo/redo the stored action. Then, we'd need to rename |
I'm not familiar with how one implements a Directed Acyclic Graph, so that's to my disadvantage.... Still, I'll attempt to piece together how this should work. Using arrows, let's say there are 2 areas, each of which have made the following changes where the order of changes is alphabetical (change A comes before change B):
With RichTextFX, only one change at a time can be applied to the underlying document shared by multiple clones. However, the above example is still very linear: to get back to "Start", one would need to call So, the change stored would need to know what conditions are needed for it to be considered "valid" and "invalid." For example, D and B are still "valid" because they can still delete their corresponding text. Therefore, a change could keep track of the respective text to delete and its actual location. Thus, D would not only know to delete "initial " but also that "initial " was originally the range [0, 7]. But how should changes in other areas be accounted for?
Thus, despite changes from other areas, the undo would still function correctly. However, if some new text is inserted in-between it, the change would no longer be valid. For example, what should happen in the following scenario? The user types some text in one area ("text"), then inserts text in-between that first insertion using a second area("te___xt"), and then calls undo in the first area.
In the above scenario, the change would not be "valid." However, if the user, whether via the second area or perhaps a third area, delete the text he/she inserted in-between the original text, that change is now "valid" again. Thus, if the user calls |
Let the edit graph define as an acyclic graph where vertices are edits made to the document, and there is an edge b -> a iff edit b was made after edit a and overlaps with it. Let's call edits a and b mutually independent iff there is no directed path from a to b and no directed path from b to a. Then edits that can be undone are the ones with no incoming edges. When an edit is undone, all edits mutually independent with it must have their ranges updated.
What I described above would give rise to the third behavior—the change would not be undoable at the moment, because another change depends on it. There might be some attempt to bubble up a change so that it becomes undoable. This would try to recalculate changes as if they occurred in a different order, but yielding the same result. For example, to bubble up change b in this graph (assume all edges are oriented top-to-bottom)
would result, if successful, in graph
in which b can be undone. |
(I'm restating your words, so I understand it myself better).
a is the first edit done and b follows a because it points to a. Given this graph:
Despite pointing to the same object a, e is not mutually independent with d because d points to it. Thus, d is the only edit that can be undone because it's the only one without something pointing at it. Now, returning to the first graph, there might be a time where one wants b to be undoable, even if it isn't in the first graph. Thus, we can "bubble b up" (essentially reorganize the graph) and thus make b undoable. For example, the first graph could be reorganized to the following one in which b has nothing pointing to it:
Now, however the graph is reorganized, the final state of the content must be the same as before. So, if the code tries to reorganize the graph in all its possible ways, and yet it cannot end up with a graph where the final state matches the original graph's state, then the desired edit cannot be "bubbled up". |
The reorganizing will not be just changing the arrows and adjusting ranges of the edits, but also changing the content of the edits. (If it was just adjusting ranges, the edits would have been independent already.) With allowing modifications of edits, a reorganization yielding the same final document will always be possible, but not always meaningful. What are meaningful bubbling operations remains to be determined. |
Could you give one example of a meaningful bubble operation? I understand how the idea works but don't understand its relevance. The only example that comes to mind where a bubble operation would be useful is the following graph:
Let's say Area 1 made edits
|
I'm going to give an example with a much simpler edit graph:
Edit
Let's say the initial text was Edit
Edit
A meaningful bubble up operation could result in new edits being
When (Note that I didn't give you an algorithm how to compute |
Yeah... I can see how that can get complicated very quickly with more complex edits... |
Referring to your previous comment, we'll say the rules of "bubbling up" edits start with the following ideas:
Using the above photo as an example, edit Additionally, if Another possible approach would be to pass on the "lost" portion to another edit. For example, Back to our previous example, edits In this slightly altered example (where G is split into two edits), now edit Thus, the graph from
So, the rules so far seem to be:
Finally, I wonder how many of these rules will be subjective to the context of the object being undone itself. For example, these are the rules necessary for undoing text, but if some other application uses them, will the same rules apply? |
On second thought, this really doesn't make sense. If |
Due to UndoManager's To model a DAG, let's use a The process for undoing a change would be something like:
If an edit needs to be bubbled up, something like the following would need to occur:
One issue I can foresee is if one wants to implement a NLUM with a fixed size of changes. In the above example, one change was made into three. How should that be dealt with? If the limit and number of changes is at 100 and the bubbling process splits one change into 3, what happens to the other two? To implement |
I've thought more about the current position relativity issue. Given a Similarly, there's another situation that can arise. Let's say Queue 2 adds 6 which invalidates Queue 1's 5 and Queue 3 adds 7 which invalidates Queue 1's 4. Let's also say that 6 and 7 are mutually independent from another. Thus, Queue 1's current position points to 3.
Thus, there are really 2 "current position" values that need to be kept track of: the "relative" current position (the one that changes when a change becomes invalid or valid again, the one used in |
Another issue I thought of this morning: the list of redoable changes for each ChangeQueue needs to be filtered each time a new change is done/redone to see which of those redoable changes are still valid. |
Additionally, I realized another issue. When I try to close a
Edit: 5/16/16: The second approach should be chosen for another reason: what happens if the underlying undoable content (or |
Due to this situation, it seems that even the redoable changes could also be "bubbled" (though it would be in the opposite direction when compared to the undoable changes). |
Thinking about how to implement a fixed size non linear change queue... "Fixed size" implies that there are only so many changes to keep in memory before casting out extra ones. However, what does one do when the queue has already reached its limit and a change is bubbled? Bubbling a change can work for both undos and redos. Let's assume a queue has reached it's maximum size. If a redoable change is bubbled, then the extra change will propagate down to the queue's undo list. If an undoable change is bubbled, it is later added to the redo list. In either case, it isn't reasonable for the change that was just undone/redone to then be impossible to redo/undo because it was popped off. Rather, an item at the end of the list should be removed. For example:
However, which list's last item should be popped off? Thus, it seems wiser for the fixed size implementation to have two size limits: one for the undoable changes list and another for the redoable changes list. The issue here lies in deciding which list to use to determine when the size limit has been reached and thus when to pop off a change. Do we use the Given a queue where
Then, we can consider a few situations.
The size limit could be calculated using both lists. For example, remove a change from the undo list if Another possible approach is to not implement a fixed-size non-linear change queue in the first place. |
@TomasMikula In light of my previous comment, I did want to clarify something. Is the fixed size queue comparable to a glass full of sand or a tunnel full of objects?
My previous comment assumes the second approach, but I wasn't sure if that's actually what you're fixed-size linear change queue does. |
Also @TomasMikula, I'm not sure how / whether to implement the UndoPosition and ChangeQueue.Position interfaces for a non-linear approach (mostly because of the queue's relative nature). Any ideas? |
I looked over your FixedSizeChangeQueue's test and it indicates it's the second approach. I also looked over your current implementation of the interface UndoManager {
interface UndoPosition {
/**
* Sets the mark of the underlying UndoManager at this position,.
* whether that position is valid or not.
*/
void mark();
/**
* Checks if the UndoManager's current position
* points to the same change as this one
*/
boolean isValid();
}
/*
"Saves" the current position of the UndoManger
which can be used to indicate whether the UndoManager
points back to the same position as before or not.
*/
UndoPosition getCurrentPosition();
} and interface QueuePosition<C> {
/*
Indicates whether the position refers to a change
that is still within the queue. It is valid if the change
is either in the undo or redo list of changes, REGARDLESS
OF WHETHER OR NOT THE CHANGE CAN BE UNDONE/REDONE
*/
boolean isValid();
} |
To make |
Here's what I've concluded so far in trying to implement this:
Added the following on 6/4:
|
Per FXMisc/RichTextFX#233.
The text was updated successfully, but these errors were encountered: