-
Notifications
You must be signed in to change notification settings - Fork 236
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 nonlinear undo/redo (clones don't currently handle undo/redo correctly) #233
Comments
I think the only way to account for this is to use one So, it seems like the best way to get around this is to wrap the UndoManager in wrapper class. See my betterCloningUndoing branch for an example of this. However, there is an additional problem to solve. As of now, when the undo manager is created, it is passed a lambda that calls the So, if there are three
....when So, what happens when initial gets disposed? When |
The only way around this second part is if the |
The same effect as by sharing an UndoManager can also be achieved by only one of the areas having a real UndoManager and the others having a fake one: area.setUndoManager(UndoManagerFactory.zeroHistoryFactory()) Then no undo/redo will be available on the other areas, but the keyboard shortcuts can be overridden to trigger undo/redo on the area with the true UndoManager. |
What happens when the initial area is disposed though? And doesn't that limit the developer if they want to be able to |
Yeah, that would be a problem.
The developer would have to know to call When first implementing the undo functionality, I almost wanted the UndoManager to live outside the StyledTextArea. I think in the end the main reason why |
What if it was placed within |
Sorry for not being responsive. The reason is that I don't know a good solution off the top of my head, and I didn't have time to think deeply about the problem. Putting Another desired feature would be that in one view, you would undo/redo only the edits made in that view. And the other view would undo/redo its own edits. So at least the UndoManager would need to distinguish the origin of the change (this applies whether there is a shared UndoManager or two separate ones). With this feature comes another complication. It is no longer sufficient to store edits as a linear sequence (possibly tagged by origin), as is done by UndoFX, but rather as a directed acyclic graph, in order to not impose some arbitrary ordering on edits that are independent (and thus can be undone independently of each other). And then someone will come and want to implement a collaborative editor on top of RichTextFX. That necessarily means there will be asynchrony involved, which opens up the possibility of conflicts, so they will need a mechanism to resolve conflicts. I don't think we can or should resolve all these problems in RichTextFX. I think the best we can do is put the user in charge, by pushing the undo/redo functionality out of |
No worries! :-) You're busy and I didn't realize just how complicated the implications could become
So, you're suggesting something like a "plug-in" sort of idea...? We can provide the current version of Edit: Perhaps "plug-in" isn't the best term to use for the idea your suggesting. However, I get the basic idea of what you're proposing. |
So, it should remain inside
Doesn't this necessarily mean that public interface UndoActions {
public void undo(StyledTextAreaModel model) { getUndoManager().undo(model); }
} However, as soon as we go that direction, now we run across the requirement of using generics: if one subclasses public class ModelSubclass extend StyledTextAreaModel {
// class content...
}
public interface UndoActions<PS, S, M extends StyledTextAreaModel<PS, S>> {
public void undo(M model) { getUndoManager().undo(model); }
} Adding another thought: Additionally, if we go that route, now |
Or maybe further out, to
That would not be necessary, or desired. (Why would a user calling
I'm not yet really on board with allowing subclassing of |
Isn't that breaking separation of concerns? UndoManager seems to operate on the model, not necessarily the view. Why stick it inside the view?
I was thinking in terms of separation of concerns. Why store model-content of a subclass of
I was thinking of an approach where the clones share one UndoManager. The public API for |
While this is correct, in trying to give the user complete control over how to instantiate the undo manager, for
I didn't imagine there would be different model for different subclasses of
Each area's UndoManager can be a thin wrapper around the shared UndoManager, that in addition remembers the source. |
Ok. That is true and would give more control over how that works.
So, when a developer does subclass For example, let's say a developer subclasses If the model cannot be subclassed, we'd have to take the following approach: // store the terms that have been defined in their own model
public class SubModel {
private final Map<String, Definition> termsToDefinitions;
public Definition getDefinitionOf(String term) { return termsToDefinitions.get(term); }
// rest of the class...
}
public class SubStyledTextArea<PS, S> extends StyledTextArea<PS, S> {
private final subModel = // a second model stored within the view
// which model do we get? Two models might lead to confusion...
// get SubStyledTextArea's model
protected final SubModel getSubModel() { return subModel; }
// get StyledTextArea's model
protected final getModel() { return model; }
public Definition getDefinitionOf(String term) { return subModel.getDefinitionOf(term); }
public SubStyledTextArea(/* args */) {}
// rest of class...
} But if the model could be subclassed, we'd only have one model in the view public class SubModel<PS, S> extends StyledTextAreaModel<PS, S> {
private final Map<String, Definition> termsToDefinitions;
public Definition getDefinitionOf(String term) { return termsToDefinitions.get(term); }
// rest of the class...
}
public class SubStyledTextArea<PS, S> extends StyledTextArea<PS, S, SubModel<PS, S> {
// get the only model within the view
public final getModel() { return model; }
public Definition getDefinitionOf(String term) { return getModel().getDefinitionOf(term); }
}
Ok. That works! |
Why store the dictionary of term definitions inside |
If you were implementing the above idea, would you compose a new class like so? class ReaderArea<PS, S> extends Region implements Virtualized {
StyledTextArea<PS, S> area;
HashMap<String, Definition> termsToDefs;
ReaderArea(/* args */) {
getChildren().add(area);
}
// rest of class code...
protected void layoutChildren() {
// layout area correctly...
}
} I don't know if my previous comment's example was a good one, but I was simply trying to show that when public class SubClassModel {
// same object is shared across clones...
protected final ObservableMap<String, Definition> getDefinitions() { return map; }
}
public class SubStyledTextArea<PS, S> extends StyledTextArea<PS, S> {
private final SubClassModel subModel;
public SubStyledTextArea(ObservableMap<String, Definition> termsToDefs,
PS initialParagraphStyle, BiConsumer<TextFlow, PS> applyParagraphStyle,
S initialTextStyle, BiConsumer<? super TextExt, S> applyStyle,
boolean preserveStyle) {
// constructor code
subModel = new SubModel(termsToDefs, /* additional args... */);
};
}
SubStyledTextArea area = // creation code....
SubStyledTextArea clone = new SubStyledTextArea<>(
area.getSubModel().getDefinitions(),
area.getInitialParagraphStyle(), area.getApplyParagraphStyle(),
area.getInitialTextStyle(), area.getApplyStyle(),
area.getModel().getContent(), area.isPreserveStyle())
); |
I would implement it like this class MyApp {
StyledTextArea<Foo, Bar> area;
Map<String, Definition> termsToDefs;
} No extending |
Oh... ok. |
You're probably getting annoyed with the number of times you've stated this principle to me 😜 |
This is only about the second time I guess, or I have a short memory ;) |
Just FYI. Now that we've migrated to the new approach in RTFX, I'll probably start to look at this again next week. I think this is the last unstable aspect that needs to be fixed before a stable release can be made. |
Just an update. I'm pretty sure I've designed a working non-linear undo/redo system in UndoFX (see FXMisc/UndoFX#11). The only issue is designing a suitable test environment to test whether this system actually works. |
Edit: Reference correct PR |
As long as it is accessed only from the JavaFX application thread, which is the assumption for most things JavaFX, thread-unsafety is not an issue. Anyway, how does that relate to performance? Do you think you could gain performance by parallel implementation? BTW, it used to be the case that the undo manager had "privileged" access to the stream of changes, in the sense that it would receive the change before any other observer of |
Aye to the parallel implementation. My current implementation in that PR iterates through a graph's queues and has each of them run code to handle their changes' updating as well as recalculating their valid changes (among other things). Theoretically, this process will take longer if such queues have a very large number of changes. Although one would probably only have one to five queues hooked into the graph at a time since one would probably only display five views at most, theoretically, one could have thousands of such queues. In which case, using concurrency would address the resulting performance problem. Again, it's not something that is desperately needed for GUIs, just my mind realizing that the code could be further optimized if so desired. However, even UndoFX's ReadMe mentions that it's code base could be used for Java applications in general, so perhaps that is desirable to some extent. However, I don't currently have a need to optimize the code in that way.
Good points and helpful for understanding the thoughts behind your design. I don't remember if I stated this here or elsewhere, but even if the UndoFX PR on which I'm working was merged into RTFX, one could still use However, it does seem that the code base you wrote and to which I and some others contributed has reached a state where it truly is a code base. |
The real issue here is whether to include this feature inside the scope of this project. Although such a feature could be added for For now, I'll label it as an enhancement. |
I think this issue itself is actually outside the scope of this project. In reality, a user should be given "full control" (in Tomas' words) over how undo/redo works and what its privilege is. This implies that #333 should be done rather than adding support for nonlinear undo/redos, which are complicated, all of the options and situations for which we will not be able to fully account, and which a developer could implement on top of the area base which RTFX provides. |
#232 (solution to #152) has been implemented. However, as noted by Tomas:
The text was updated successfully, but these errors were encountered: