-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Add CopyInterceptor support to SelectionArea #146625
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 CopyInterceptor support to SelectionArea #146625
Conversation
|
@chunhtai Let's see if we like this one |
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.
Just a quick review with some questions and ideas. I think I'm inclined to like this approach at a high level. Thanks @Rexios80 for jumping in and getting the conversation started with this PR.
| abstract class CopyInterceptor { | ||
| /// Creates a [CopyInterceptor]. | ||
| const CopyInterceptor(); |
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'm wondering if this needs to be a class instead of just a function, or otherwise if this should be a non-abstract class that takes the intercept function as a parameter. Just details to consider.
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 originally had it as just a function, but it quickly became a mess. Having this as a class has the added benefit of letting us have const factory constructors.
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 is the _InlineCopyInterceptor basically what you're talking about?
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 thanks, and I guess it's made public with CopyInterceptor.inline 👍
| final StringBuffer buffer = StringBuffer(); | ||
| for (final SelectedContent selection in selections) { | ||
| buffer.write(selection.plainText); | ||
| } | ||
| return SelectedContent( | ||
| plainText: buffer.toString(), | ||
| ); | ||
| return SelectedContent(plainText: copyInterceptor.intercept(selections)); |
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 this all there is to how we currently do copy? If so, I kind of like this approach at a high level. Just give users the ability to use their own function at this point, so they can pretty much do whatever they want here.
| text: data, | ||
| children: textSpan != null ? <InlineSpan>[textSpan!] : null, | ||
| ), | ||
| copyInterceptor: copyInterceptor, |
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 have slight reservations about adding this parameter to every relevant widget. I guess it's not a lot of them, though, and I don't have a better alternative off the top of my head.
|
|
||
| @override | ||
| String intercept(List<SelectedContent> selections) => _interceptInline(selections); | ||
| } |
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 if a user wants to do something like this: every Row widget copies in a special way, and every other widget copies in a different way? Row is just an example here, could be any specific widget. I'm wondering whether this approach will give users this kind of flexibility or not.
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 PR adds the ability to nest SelectionAreas so there is no reason you couldn't have whatever behavior you want
Renzo-Olivares
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.
Thank you for the contribution @Rexios80!
Just some small comments and thoughts about this approach.
SelectionArea/SelectableRegioncan now be nested.
My main concern is thatSelectionArea/SelectableRegionincludes logic for gesture handling and managing selection overlays which will not be used in the case of nesting. It also forces the user to use nestedSelectionAreas if they want to accomplish a space separated row inside of a vertical List.
child: SelectionArea(
child: Container(
height: 600.0,
width: 600.0,
child: ListView(
children: <Widget>[
SelectionArea(
copyInterceptor: CopyInterceptor.space,
child: Row(
children: <Widget>[
const Text(
'Text widget 1',
),
const Text(
'Text widget 2',
),
const Text(
'Text widget 3',
),
const Text(
'Text widget 4',
),
],
),
),
],
),
),
),instead I would expect something like
child: SelectionArea(
child: Container(
height: 600.0,
width: 600.0,
child: ListView(
children: <Widget>[
SelectionContainer(
copyInterceptor: CopyInterceptor.space,
child: Row(
children: <Widget>[
const Text(
'Text widget 1',
),
const Text(
'Text widget 2',
),
const Text(
'Text widget 3',
),
const Text(
'Text widget 4',
),
],
),
),
],
),
),
),The SelectionContainer has a required SelectionContainerDelegate field so it may also be appropriate to make CopyInterceptor a part of MultiSelectableSelectionContainerDelegate.
@protected
CopyInterceptor get copyInterceptor => _defaultCopyInterceptor;|
|
|
Something like this maybe? |
|
Well that code didn't work and these delegates seem overly complicated to design. Can we just reuse the class SelectionInterceptor extends StatelessWidget {
const SelectionInterceptor({
super.key,
required this.copyInterceptor,
required this.child,
});
final CopyInterceptor copyInterceptor;
final Widget child;
@override
Widget build(BuildContext context) {
return SelectionContainer(
registrar: SelectionContainer.maybeOf(context),
delegate: _SelectableRegionContainerDelegate(copyInterceptor: copyInterceptor),
child: child
);
}
} |
|
@Rexios80 I agree, I think having to create a delegate right now is complicated. We can make it easier for users to create a Then they can do the following, without an intermediate widget: SelectionContainer(
delegate: MySpacesCopyInterceptorSelectionContainerDelegate(),
child: Row(
children: <Widget>[
const Text(
'Text widget 1',
),
const Text(
'Text widget 2',
),
const Text(
'Text widget 3',
),
const Text(
'Text widget 4',
),
],
),
),class MySpacesCopyInterceptorSelectionContainerDelegate extends _SelectableRegionContainerDelegate {
@override
CopyIterator get copyIterator => CopyIterator.spaces;
}What do you think? |
|
I think having users create any class at all is too much work to achieve this behavior. If the SelectableRegionContainerDelegate is public couldn't they just use it directly instead of subclassing? |
I think your code in this post works #146625 (comment) , but I don't think we need an intermediate widget if we are making My other concern is similar to https://github.com/flutter/flutter/pull/146625/files#r1563001791 . For I think the expected behavior of |
|
If that's the case then we can just create a delegate such as this class CopyInterceptorContainerDelegate extends _SelectableRegionContainerDelegate {
CopyInterceptorContainerDelegate({
required this.interceptor,
});
final CopyInterceptor interceptor;
@override
SelectedContent? getSelectedContent() {
final List<SelectedContent> selections = <SelectedContent>[];
for (final Selectable selectable in selectables) {
final SelectedContent? data = selectable.getSelectedContent();
if (data != null) {
selections.add(data);
}
}
if (selections.isEmpty) {
return null;
}
return SelectedContent(plainText: interceptor.intercept(selections));
}
}and revert the rest of the changes in this PR. My concern with doing it this way is user discoverability of this feature. With a |
|
@Rexios80 I understand your concern about discoverability but I'm a little hesitant to add We should also make it easier for users to create a I think we could always add a |
|
@Renzo-Olivares I agree that having the default separator as I also agree that it makes more sense to use |
…rea-copy-interceptor
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.
I have not looked into the code but I can't see a way to really apply a setting pass another selection container.
For example if you do this
SelectionContainer(
copyInterceptor: myInterceptor,
child: Column(children: ...)
);
if at some point, we decided to add a selection container as part of Column for whatever reason, the myInterceptor will not work for children of the Column. I think we need something that can penetrate through SelectionContainer.
One way I can think of is through InheritedWidget. However, I am not sure how the API will look like.
| this.restorationId, | ||
| this.scrollBehavior, | ||
| this.clipBehavior = Clip.hardEdge, | ||
| this.copyInterceptor, |
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 is not ideal, we will have to drill this down for every scrollable subclass or scrollview.
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 you have any examples? Since MultiSelectableSelectionContainerDelegate forces you to implement the copyInterceptor parameter every current implementation of MultiSelectableSelectionContainerDelegate now has this parameter.
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 meant you also have CustomScrollView, ListView, PageView, TabView, GridView...etc are all using Scrollable. if we do it this way, we have to add this copyInterceptor to all of them. and if we add more parameter, we will add to all of them as well.
I also not a fan of adding selection related API to Widgets. it will be confusing for people who do not use SelectionArea.
|
I'm not sure we would need to "penetrate" |
What I meant is if we add a SelectionContainer for a inside a widget, it will override all copyinterceptor above. My Idea is we can do it through inheritedwidget, so that customer would have full control over the copy. One Idea is to add it to DefaultSelectionStyle |
|
@chunhtai I made a proof of concept for using a widget for this. Please let me know what you think. |
|
Hey @Rexios80 thanks for the updates. It looks like there are some failing checks, can you take a look? |
|
@Piinks I've already rewritten this PR 4 times. I don't want to put effort into fixing CI if we don't like the idea. |
|
@chunhtai do you have any thoughts? |
| child: const Column( | ||
| children: <Widget>[ | ||
| Text('How are you?'), | ||
| CopyInterceptor( |
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 wonder if the copyInterceptor functionality should go under DefaultSelectionStyle. Then the user can wrap their subtree with DefaultSelectionStyle.merge(copyInterceptor: CopyIntercept.space) to override only the copyInterceptor.
Overall, I like this approach since it is able to propagate the copyInterceptor down a subtree. I'm just weary about adding a new widget to support this kind of functionality since it feels like this should be part of some overall selection configuration.
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.
Hi @Rexios80, wanted to follow up on this comment. Do you have any thoughts on this approach? I think the SelectionContainer would be the one inheriting the copyInterceptor from its nearest DefaultSelectionStyle.
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.
How will that work for defaults on a per-widget basis? Would they usa a DefaultSelectionStyle in their build method unless there is a parent DefaultSelectionStyle?
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 I understand correctly, a descendant widget of a DefaultSelectionStyle would use DefaultSelectionStyle.of(context) to inherit any ancestor configuration, or use a local DefaultSelectionStyle to override any ancestor configuration.
Given SelectableRegion it should have a DefaultSelectionStyle.merge in its build method defining a default (probably newline). And then descendant widgets can override this. Let me know if this is what you were asking.
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.
Yes I think this overall is better than having it on the selectioncontainerdelegate, but I also agree that this can be something in the DefaultSelectionStyle as well
|
(Triage) Hi @Rexios80 what is the status of this PR? |
|
@Piinks to move on I need to know what method we want to use to accomplish this. If someone could point me in the right direction I can rewrite this (hopefully) one last time. |
|
Going along with what @chunhtai and @Renzo-Olivares suggested, I'm probably going to rewrite this PR using I just haven't had time yet. If anyone has an objection to this approach or would like to implement it themselves please comment. |
|
I will make a new PR when I have a chance to make the new implementation |
Spiritual continuation of #126835
Right now, copying text in a
SelectionAreacopies it with no separation. SinceSelectionAreais very useful with vertical lists ofTextwidgets this is not ideal. This PR adds acopyInterceptorfield toSelectionAreaso that developers can add custom copy behavior toSelectionAreawidgets.Fixes #104548
I added a new test for this
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.