-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Call bringIntoView after RenderEditable updates on paste #98604
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
Conversation
justinmc
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.
One question about the _didChangeTextEditingValue case, but this approach looks good overall. Thanks for fixing this!
|
|
||
| await resetSelectionAndScrollOffset(); | ||
| textSelectionDelegate.pasteText(SelectionChangedCause.toolbar); | ||
| await textSelectionDelegate.pasteText(SelectionChangedCause.toolbar); |
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, these awaits probably should have been here.
| // Schedule a call to bringIntoView() after renderEditable updates. | ||
| SchedulerBinding.instance.addPostFrameCallback((_) { | ||
| bringIntoView(textEditingValue.selection.extent); |
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 problem where RenderEditable is out of date is a recurring problem. I think using an addPostFrameCallback is the best option for 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.
Indeed, it is a bit prone to more bugs. For bringIntoView() itself, I can see it's called from:
- text_selection.dart:
handleSelectAll(),_handleSelectionHandleChanged() - [material|cupertino] text_field.dart:
_handleSelectionChanged() - selectable_text:
_handleSelectionChanged() - editable_text:
copySelection(),cutSelection(),pasteText(),selectAll(),_scrollToDocumentBoundary(),_updateSelection(),_expandSelection()
The only two calls that may modify text contents/layout are cut and paste, handled by this PR so this should suffice for now.
BTW, another question (unrelated to this PR) is whether only calling bringIntoView() on SelectionChangedCause.toolbar is desired behaviour. This means that if one cuts/pastes in an area outside of the viewport using keyboard shortcuts, we don't scroll there so the user won't see what's been done to the text.
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.
Glad that this PR will handle the problem here at least.
About scrolling on paste, I think you're right that it's incorrect. I've created an issue: #98675.
| // If there's no change in text and selection (e.g. when selecting and | ||
| // pasting identical text), the widget won't be rebuilt on value update. | ||
| // Handle this by calling _didChangeTextEditingValue() so caret and scroll | ||
| // updates can happen. |
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.
What corner case is this handling exactly? Maybe consider adding a test that covers this (unless I misunderstand and the existing test does cover it).
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.
Imagine you type "Lorem ipsum", select "ipsum", copy it and paste it into the same selection. Theoretically this is user input so you'd like to scroll, reset the caret animation, update selection/toolbar overlays etc. But
_didChangeTextEditingValue() won't be called because:
- it's a listener on
TextEditingControllerwhich is aValueNotifier<TextEditingValue> TextEditingValueequality operator will see the same value (text, selection, composing)ValueNotifier's value setter won't callnotifyListeners()
So further, since _didChangeTextEditingValue() isn't called, the widget won't be updated and there won't be a post frame callback with bringIntoView() either.
The interesting thing is that the "Selection will be scrolled into view with SelectionChangedCause" test in editable_text_test.dart already caught that although perhaps it wasn't intentional. What it does is to set a (0, 1) selection on each call to resetSelectionAndScrollOffset() and then do 2x cut and 2x paste. So you always end up pasting the same character that's already there.
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.
In the example you gave, wouldn't there be a selection change?
- "Lorem [ipsum]"
- Paste "ipsum"
- "Lorem ipsum|"
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.
Just tried in a TextField - no selection change on paste. In MS Word though, selection changes to collapsed on pasted text end, so it does in Chrome and VS Studio so it looks like this is the natural way of 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.
Ah you're right, that's a bug in Flutter. Ok then I think this PR is fine. I'll look into fixing this bug. I tried on native Mac and Android and it collapses after a paste on both.
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.
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.
Great, thanks for submitting the two issues!
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.
Does it make sense to move this to the pasteText 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.
Currently _replaceText() is called from:
cutSelection()pasteText()ReplaceTextIntentused by_DeleteTextAction
On cut and delete, the extra if(...) _didChangeTextEditingValue will be a no-op as text value should change. Given possible future uses of ReplaceTextIntent won't it be better to leave this in _replaceText()? (e.g. an intent to replace a selected text with the same text coming from semantics, scribble etc.
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.
My intention was to avoid bundling side effects that are sometimes unexpected (such as this one) into generic operations like replacing text. It's a bit cleaner that way IMO especially when you are reading the code in pasteText, and you'll know immediately this is going to bring the new selection into view, instead of having to trace to _replaceText and find the "bring into view" logic there. But it's just a nit.
justinmc
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 👍
Thanks for fixing this! I'm looking into the collapse-after-paste thing.
| // If there's no change in text and selection (e.g. when selecting and | ||
| // pasting identical text), the widget won't be rebuilt on value update. | ||
| // Handle this by calling _didChangeTextEditingValue() so caret and scroll | ||
| // updates can happen. |
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.
Does it make sense to move this to the pasteText method?
This PR ensures EditableText.bringIntoView() is called after the RenderEditable is updated with the new text value. This avoids incorrect scroll offset changes on paste.
Fixes #96658.
All relevants tests are passing:
Pre-launch Checklist
///).