-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Undo/redo #96968
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
Undo/redo #96968
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any better name than onChanged or is that ok? It's a little confusing because it's only called on undo/redo, not when widget.controller changes for other reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe onTriggered?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't matter for UndoRedo, but I couldn't get this to work with more than one argument because it doesn't seem possible to refer to all arguments in Dart. Maybe Dart isn't meant for this kind of functional programming.
chunhtai
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something that cross my mind.
- The redo/undo from both rightclick contextmenu or system menu bar.
- accessibility, for example voiceover is enabled, redo/undo will be pronounced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe define a constant instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also curious how we are going to support different platform behavior? macOS seems to have a vary unique way of determine the redo undo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do the constant 👍 .
My plan is to not support different platform behavior for now. The different platforms seem so subtly different in weird ways and I'm hoping users don't care. I will monitor this and be ready to implement platform-specific behavior if anyone wants it, though.
The worst difference I can see now is that if you type very slowly, then the undo/redo chunks are small. On native Mac, the chunks are of a more consistent size regardless of the speed that you type.
If I do need to implement platform specific behavior here, and it's more complicated than just different Durations, then my general plan would be to make UndoRedoMac etc. widgets.
packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can be private
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| UndoRedoState createState() => UndoRedoState(); | |
| State<UndoRedo> createState() => _UndoRedoState(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, that's why I should autogenerate these things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it's too late for this topic and you've probably had the discussion, but UndoRedo feels too "real" for how abstract its functionality is. Have we considered something like TextHistoryProvider?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I think you're probably right... Maybe TextEditingHistory?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe?
| /// Listens for keyboard undo/redo shortcuts depending on the platform. Calls | |
| /// Listens to keyboard undo/redo shortcuts depending on the platform. Calls |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's more accurate 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// [onChanged] when a shortcut is received that would change the state of these | |
| /// [onChanged] when a shortcut is triggered that would affect the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I think "these" was a typo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that UndoRedo is split for code modularity and is meant to be private but testable. But can we just test EditableText in all of the following tests? The behavior of EditableText is what the users care and we maintain backward compatibility of. By making UndoRedo a private class, you're saying it is subject to refactory or breaking changes in the future, but at that time these tests will have to be migrated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually write small building blocks like this and then test them to convince myself that they work the way that I expect them to. It's useful for me personally to have the tests even if we refactor or change behavior. I don't want to burden everyone else if that's not Flutter's style though...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is mostly for visibleForTesting since it's not enforced by the compiler but by the linter, which the developer can turn off. The last time (which is a long time ago) I was told that it was discouraged (but not actually banned.) But I do think it reveals a problem where you're testing something that the users won't see, so might not be kept compatible during future development at all, i.e. adding unnecessary compatibility requirements. Anyway, just a soft suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok thanks for the explanation. I probably am going against Flutter trends a little bit. I'll keep an eye out for similar patterns and problems with this code and consider changing it then. It wouldn't be terrible to just test the widget.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to what @dkwingsmt has said, it feels like these tests are testing implementation detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright I'm outnumbered, I'll remove the tests and make the class fully private. I'll also add another test to TextEditingHistory to make sure all of those cases are covered.
|
@chunhtai About your ideas:
|
It is probably treats it as text changes, if you type a character, it will pronounce that character, if you delete a character, it will pronounce that character with a deeper tone. If you try on a native text field, does it pronounce redo/undo? |
|
Is it possible to somehow disable/override your undo work? I like it, just some people already have their own undo solution, so I'm afraid it can be a breaking change. |
|
@chunhtai No, it never said "undo" or "redo" on native. |
oh great! then we don't need to worry about it. |
|
@bernaferrari Good point, thanks for bringing that up. If I make my actions overridable, you could disable it like this: Actions(
actions: {
UndoTextIntent: DoNothingAction(),
RedoTextIntent: DoNothingAction(),
},
child: ...
)You could put this near the root of your application so that it will apply to all EditableTexts in your app. I could also add a parameter to EditableText and TextField, etc. but maybe it's best just to disable it with Actions like this. |
|
That could somehow work. In my app I have a custom undo implementation (which right now doesn't work inside a TextField, lol) but that could be handy eventually. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we keep this private?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes good catch, I'm no longer using this directly in the tests, so it can be fully private.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be reduced to value notifier according to the doc comment.
not too much of an issue if this is a private class though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah this is a comment that I forgot to update, I'll fix it.
Originally I tried to make this widget work with any type of ValueNotifier, but it turns out that there is a good amount of text-editing-specific logic that is needed to get undo/redo to work with EditableText. For example, ignoring selection and composing updates. I think it makes sense to just put that here since we have no use for a more generic undo/redo widget now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we are not modifying the value in controller in this widget a ValueNotifier of TextEditingValue should be enough, no?
but since this class can be private, I think it is probably fine as is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe explain a bit why the state may be the same?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the code prevent duplicate history in _push method.
if (widget.controller.value == TextEditingValue.empty
|| widget.controller.text == _stack.currentValue?.text) {
return;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does, but it's still possible to be called without a state change, such as when the user presses cmd+shift+z to redo when there is nothing to redo. I'll make this clear in the docs, good catch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is redundant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, I think I was thinking this was public and needed a comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am curious whether we may want to reset the history in this case?
If we don't, we may need to call _push() here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. I'm going to clear the stack here. I'm not sure if there is a real use case for ever changing the controller like this, but I'm guessing that if there is, the whole field is being reset and the history should reset too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_list is a private property, we should avoid mention it in public doc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels weird that it has three different behavior.
- returns null if empty
- returns entry if undo
- return same entry if can't undo
I feel 1 and 3 should be treated the same so that the caller doesn't need to handle three cases
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same for redo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also went back and forth on this when I wrote this code. I think of it like this:
- returns the current value (after any operation) in all cases.
If the stack is empty, then there is no current value, so it returns null. Otherwise it returns what the state should be after the undo/redo, even if nothing changed.
I'll make this a little more clear in the comments and the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to what @dkwingsmt has said, it feels like these tests are testing implementation detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also went back and forth on this when I wrote this code. I think of it like this:
returns the current value (after any operation) in all cases.
If the stack is empty, then there is no current value, so it returns null. Otherwise it > returns what the state should be after the undo/redo, even if nothing changed.
This is also related the onChange called with the same state. why the onChange needs to be fired if the state is the same? what is the difference to the caller for return null vs returning the same state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also just noticed the same state will get short-circuited in _update method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah good catch. I think it should only be called when the value changes so I'll update the docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
continue on the called with the same state.
Isn't the _update method short-circuited if the state is the same (line 4233)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also just noticed the same state will get short-circuited in _update method
chunhtai
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm, except for whether we need to expose the throttle function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to expose the throttle and its related function signature? I think we can testing the edtiabletext direction should be good enough
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it's the same argument as the other comment thread. I'll remove it.
Undo/redo for text editing when using the keyboard.
A new private UndoRedo widget saves a stack of recent TextEditingValues, and provides the current state after undo/redo operations.
Currently does not hook up to 3 finger swipe to undo/redo on iOS, but we should in the future. I think this might be possible even inside of UndoRedo by adding a gesture detector there... #34749
The throttling behavior is kind of a "best fit" of all of our platforms. It seems like there are very subtle differences between how all of these platforms do this (see #24222 (comment)). I'll keep an eye out if users want different per-platform behavior or if they want control over it themselves, but for now hopefully mine is good enough that most users won't notice a difference.
Fixes #24222