-
Notifications
You must be signed in to change notification settings - Fork 28.7k
Paste shows only when content on clipboard #54902
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
(haven't reviewed the code, but two questions from just reading the description)
|
void _updateClipboardStatus() { | ||
Clipboard.getData(Clipboard.kTextPlain).then((ClipboardData value) { | ||
final bool isClipboardEmpty = value?.text == null || value.text.isEmpty; | ||
if (isClipboardEmpty != _isClipboardEmpty) { |
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.
Should we check if the widget is still mounted when the future completes?
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 call.
|
||
@override | ||
void didChangeAppLifecycleState(AppLifecycleState state) { | ||
if (state == AppLifecycleState.resumed) { |
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.
nit: use switch for enums per the style guide.
@override Widget build(BuildContext context) { | ||
// Asynchronously check if the clipboard has changed, and trigger another | ||
// build if so. | ||
if (widget.handlePaste != null) { |
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.
nit: build
should be idempotent.
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 I worried about this when I wrote it. I'm going to move it into didChangeDependencies, which is still probably not the best, but I think it's necessary in this case.
Edit: didChangeDependencies, not didUpdateWidget.
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.
Is it possible to do this in initState
?
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 was worried about the menu getting out of date with the clipboard status, but it seems not to really happen in actual use cases, and it doesn't break any tests. I'll change it to initState.
@goderbauer There don't seem to be any visual glitches, and I think it's because of the fade-in animation. If I don't render the toolbar until I get the content from the clipboard, then many tests fail (over 100) because they're expecting a Paste button 1 frame after tapping. These failures seem fixable by changing About TalkBack, I just tried it and it seemed to work. TalkBack highlighted and spoke the visible toolbar buttons. When paste wasn't there, it wasn't usable. |
I would prefer updating the test. Now the tests seem just wrong: They are expecting a Paste button that may actually not become visible to the user. |
@goderbauer @LongCatIsLooong Updated to only render when clipboard data is available. I changed many tests to use |
@@ -66,33 +70,88 @@ class _TextSelectionToolbarState extends State<_TextSelectionToolbar> with Ticke | |||
); | |||
} | |||
|
|||
// Asynchronously check if the clipboard has data and update | |||
// _isClipboardEmpty if needed. | |||
void _updateClipboardStatus() { |
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.
nit: should this check if widget.handlePaste
is not null first, in case the information isn't needed at all?
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 (see my comment above)
/// | ||
/// Subclasses can use this to decide if they should expose the paste | ||
/// functionality to the user. | ||
/// | ||
/// This does not consider the current contents of the clipboard. |
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.
nit: could you elaborate a bit more on this? It confused me a little on the first read-through.
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.
void didChangeAppLifecycleState(AppLifecycleState state) { | ||
switch (state) { | ||
case AppLifecycleState.resumed: | ||
_updateClipboardStatus(); |
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.
Is it possible to control-c when the toolbar is visible?
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.
That's a good point. I wasn't able to achieve this in the simulator, but maybe on desktop it will be possible, or on a mobile device with a physical keyboard attached.
I wish there was a Clipboard listener. I'm not sure how to handle this case if it is possible.
My macbook seems to close the native context menu if I ctrl-C, so maybe we can do that if it comes up on desktop.
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
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
// Don't render the menu until the state of the clipboard is known. | ||
if (widget.handlePaste != null && _isClipboardEmpty == null) { | ||
return const SizedBox(width: 0.0, height: 0.0); |
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.
👍
// Whether the user's system clipboard is empty as of the last check. | ||
bool _isClipboardEmpty; | ||
|
||
// Asynchronously check if theh clipboard has data and update |
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.
// Asynchronously check if theh clipboard has data and update | |
// Asynchronously check if the clipboard has data and update |
if (isClipboardEmpty == _isClipboardEmpty) { | ||
return; | ||
} | ||
_isClipboardEmpty = isClipboardEmpty; |
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.
move everything except for this line outside of the setState? (Only this line is actually setting 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.
I moved everything inside because I was getting weird test failures when the widget would unmount, but I don't see anything like that now. I'll move it outside.
case AppLifecycleState.resumed: | ||
_updateClipboardStatus(); | ||
break; | ||
default: |
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.
nit: we try to avoid default cases. That way, every time we add a new value to the enum the analyzer tells us all the places in the code base where we have to consider the new value.
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.
Thanks, that's smart, I didn't know that.
super.dispose(); | ||
} | ||
|
||
@override Widget build(BuildContext context) { |
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.
nit: stange formatting
WidgetsBinding.instance.addObserver(this); | ||
// Asynchronously check if the clipboard has changed, and trigger another | ||
// build if so. | ||
_updateClipboardStatus(); |
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.
should we only do this if widget.handlePaste
is set to something? (here and elsewhere where we call 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.
(plus we'd need to do it in didUpdateWidget if it changes)
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'm going to change this to check if widget.handlePaste is set and do this in didUpdateWidget as well. You can actually cause a bug without this change by opening the menu with nothing copied, copying something while the menu is open, and then tapping Select All to make the menu rebuild. At least in the simulator it's possible.
@@ -66,33 +70,88 @@ class _TextSelectionToolbarState extends State<_TextSelectionToolbar> with Ticke | |||
); | |||
} | |||
|
|||
// Asynchronously check if the clipboard has data and update | |||
// _isClipboardEmpty if needed. | |||
void _updateClipboardStatus() { |
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 (see my comment above)
@@ -49,7 +50,10 @@ class _TextSelectionToolbar extends StatefulWidget { | |||
_TextSelectionToolbarState createState() => _TextSelectionToolbarState(); | |||
} | |||
|
|||
class _TextSelectionToolbarState extends State<_TextSelectionToolbar> with TickerProviderStateMixin { | |||
class _TextSelectionToolbarState extends State<_TextSelectionToolbar> with TickerProviderStateMixin, WidgetsBindingObserver { |
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.
Some of the comments I made on the Cupertino impl also apply here.
Code review changes made, thanks for the comments. |
@override | ||
void didChangeAppLifecycleState(AppLifecycleState state) { | ||
switch (state) { | ||
case AppLifecycleState.resumed: |
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.
You will have to list all other cases as no-ops.
Did you have a chance to check how TalkBack behaves? I think it is showing a paste option in its local context menu. Does the operating system auto-hide that if there is nothing to paste? Or do we need to hide the this pate action from the text field in that case:
(Same question for pasting with iOS and VoiceOver). |
@goderbauer I didn't realize there was a separate context menu for TalkBack! I thought you just meant how TalkBack read out our context menu. It seems that yes, the native TalkBack context menu does hide/show paste based on the content of the clipboard. I think I'll have to add this isClipboardEmpty state to EditableText unfortunately. |
Does it do that based on what's actually in the clipboard, or do we need to tell TalkBack to show/hide the option? |
It looks like we need to tell it. If I comment out the onPaste handler in the Semantics widget in EditableText, then it doesn't show the paste option. Otherwise it does, even if nothing is on the clipboard. |
@goderbauer I added the same clipboard checking logic to EditableText. Is there a nice way to abstract this code somehow? |
Could we implement a class "ClipBoardStatus" which is a Listenable. The listenable notifies its listeners whenever the clipboard status changes (Textfield and the Toolbar would listen to that). It provides two getters: BTW, all names used in the paragraph above are just placeholders. Feel free to pick better names. |
case AppLifecycleState.detached: | ||
break; | ||
case AppLifecycleState.inactive: | ||
break; | ||
case AppLifecycleState.paused: | ||
break; |
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.
nit: this can be more compact.
case AppLifecycleState.detached: | |
break; | |
case AppLifecycleState.inactive: | |
break; | |
case AppLifecycleState.paused: | |
break; | |
case AppLifecycleState.detached: | |
case AppLifecycleState.detached: | |
case AppLifecycleState.paused: | |
// Nothing to do. |
_updateClipboardStatus(); | ||
} | ||
break; | ||
case AppLifecycleState.detached: |
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 here.
Gold has detected one or more untriaged digests on patchset 29. |
Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change). Changes to golden files are considered breaking changes, so consult Handling Breaking Changes to proceed. While there are exceptions to this rule, if this patch modifies an existing golden file, it is probably not an exception. Only new golden file tests, or downstream changes like those from skia updates are considered non-breaking. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. Changes reported for pull request #54902 at sha 54a72e9ed0ec4f23ed5c2c15526b85c37f2f6246 |
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
@@ -1155,11 +1156,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien | |||
@override | |||
bool get selectAllEnabled => widget.toolbarOptions.selectAll; | |||
|
|||
// Inform the widget that the value of clipboardStatus has changed. |
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.
nit: move this comment also inside the set state to explain why the empty set state is OK 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.
Missed that, thanks.
Just had to squash all the commits in order to fix a failure. |
This reverts commit 053c388.
…4902)" (flutter#56614)" This reverts commit cd67da2.
Unreverts #54902 with fixes for a failing integration test.
Description
On native, the paste button in the text selection menu only shows when there is something to paste on the clipboard. This PR adds the same functionality for Flutter.
There was a long standing TODO in the code about making the
canPaste
method consider what's on the clipboard. I did not take this approach, because checking the clipboard is asynchronous andcanPaste
is used synchronously.Instead, the text selection menus check the clipboard asynchronously when they're rendered and rerender if needed. The clipboard is also checked when the app is resumed. This results in the selection menu always being up to date, even if it's sometimes 1 frame late. The only way I can find to trick it is by clearing the clipboard while the selection menu is still visible using the command line. This shouldn't be possible in a production app, only while running an emulator.
Related Issues
#4355
Tests