Conversation
There was a problem hiding this comment.
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.
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.
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.
Maybe define a constant instead?
There was a problem hiding this comment.
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.
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.
| UndoRedoState createState() => UndoRedoState(); | |
| State<UndoRedo> createState() => _UndoRedoState(); |
There was a problem hiding this comment.
Good catch, that's why I should autogenerate these things.
There was a problem hiding this comment.
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.
Actually I think you're probably right... Maybe TextEditingHistory?
There was a problem hiding this comment.
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.
Yeah that's more accurate 👍
There was a problem hiding this comment.
| /// [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.
👍 I think "these" was a typo.
There was a problem hiding this comment.
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.
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.
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.
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.
+1 to what @dkwingsmt has said, it feels like these tests are testing implementation detail.
There was a problem hiding this comment.
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.
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.
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.
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.
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.
Maybe explain a bit why the state may be the same?
There was a problem hiding this comment.
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.
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.
Good call, I think I was thinking this was public and needed a comment.
There was a problem hiding this comment.
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.
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.
_list is a private property, we should avoid mention it in public doc
There was a problem hiding this comment.
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.
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.
+1 to what @dkwingsmt has said, it feels like these tests are testing implementation detail.
There was a problem hiding this comment.
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.
also just noticed the same state will get short-circuited in _update method
There was a problem hiding this comment.
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.
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.
also just noticed the same state will get short-circuited in _update method
chunhtai
left a comment
There was a problem hiding this comment.
lgtm, except for whether we need to expose the throttle function
There was a problem hiding this comment.
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.
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