Skip to content

Commit 3f34b48

Browse files
authored
[Framework] Add Share to selection controls (#132599)
In native iOS, users are able to select text and initiate a share menu, which provides several standard services, such as copy, sharing to social media, direct ability to send to various contacts through messaging apps, etc. https://github.com/flutter/engine/assets/36148254/d0af7034-31fd-412e-8636-a06bbff54765 This PR is the framework portion of the changes that will allow Share to be implemented. The corresponding merged engine PR is [here](flutter/engine#44554) This PR addresses #107578 More details are available in this [design doc](https://github.com/flutter/engine/pull/flutter.dev/go/add-missing-features-to-selection-controls)
1 parent ac66bdb commit 3f34b48

File tree

175 files changed

+1108
-285
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

175 files changed

+1108
-285
lines changed

packages/flutter/lib/src/cupertino/adaptive_text_selection_toolbar.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget {
9696
required VoidCallback? onSelectAll,
9797
required VoidCallback? onLookUp,
9898
required VoidCallback? onSearchWeb,
99+
required VoidCallback? onShare,
99100
required VoidCallback? onLiveTextInput,
100101
required this.anchors,
101102
}) : children = null,
@@ -107,6 +108,7 @@ class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget {
107108
onSelectAll: onSelectAll,
108109
onLookUp: onLookUp,
109110
onSearchWeb: onSearchWeb,
111+
onShare: onShare,
110112
onLiveTextInput: onLiveTextInput
111113
);
112114

packages/flutter/lib/src/cupertino/localizations.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ abstract class CupertinoLocalizations {
253253
// The global version uses the translated string from the arb file.
254254
String get searchWebButtonLabel;
255255

256+
/// The term used for launching a web search on a selection.
257+
// The global version uses the translated string from the arb file.
258+
String get shareButtonLabel;
259+
256260
/// The default placeholder used in [CupertinoSearchTextField].
257261
// The global version uses the translated string from the arb file.
258262
String get searchTextFieldPlaceholderLabel;
@@ -469,6 +473,9 @@ class DefaultCupertinoLocalizations implements CupertinoLocalizations {
469473
@override
470474
String get searchWebButtonLabel => 'Search Web';
471475

476+
@override
477+
String get shareButtonLabel => 'Share...';
478+
472479
@override
473480
String get searchTextFieldPlaceholderLabel => 'Search';
474481

packages/flutter/lib/src/cupertino/text_selection_toolbar_button.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ class CupertinoTextSelectionToolbarButton extends StatefulWidget {
109109
return localizations.lookUpButtonLabel;
110110
case ContextMenuButtonType.searchWeb:
111111
return localizations.searchWebButtonLabel;
112+
case ContextMenuButtonType.share:
113+
return localizations.shareButtonLabel;
112114
case ContextMenuButtonType.liveTextInput:
113115
case ContextMenuButtonType.delete:
114116
case ContextMenuButtonType.custom:
@@ -195,6 +197,7 @@ class _CupertinoTextSelectionToolbarButtonState extends State<CupertinoTextSelec
195197
case ContextMenuButtonType.delete:
196198
case ContextMenuButtonType.lookUp:
197199
case ContextMenuButtonType.searchWeb:
200+
case ContextMenuButtonType.share:
198201
case ContextMenuButtonType.custom:
199202
return textWidget;
200203
case ContextMenuButtonType.liveTextInput:

packages/flutter/lib/src/material/adaptive_text_selection_toolbar.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget {
105105
required VoidCallback? onSelectAll,
106106
required VoidCallback? onLookUp,
107107
required VoidCallback? onSearchWeb,
108+
required VoidCallback? onShare,
108109
required VoidCallback? onLiveTextInput,
109110
required this.anchors,
110111
}) : children = null,
@@ -116,6 +117,7 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget {
116117
onSelectAll: onSelectAll,
117118
onLookUp: onLookUp,
118119
onSearchWeb: onSearchWeb,
120+
onShare: onShare,
119121
onLiveTextInput: onLiveTextInput
120122
);
121123

@@ -223,6 +225,8 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget {
223225
return localizations.lookUpButtonLabel;
224226
case ContextMenuButtonType.searchWeb:
225227
return localizations.searchWebButtonLabel;
228+
case ContextMenuButtonType.share:
229+
return localizations.searchWebButtonLabel;
226230
case ContextMenuButtonType.liveTextInput:
227231
return localizations.scanTextButtonLabel;
228232
case ContextMenuButtonType.custom:

packages/flutter/lib/src/material/material_localizations.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ abstract class MaterialLocalizations {
121121
/// Label for "search web" edit buttons and menu items.
122122
String get searchWebButtonLabel;
123123

124+
/// Label for "share" edit buttons and menu items.
125+
String get shareButtonLabel;
126+
124127
/// Label for the [AboutDialog] button that shows the [LicensePage].
125128
String get viewLicensesButtonLabel;
126129

@@ -1190,6 +1193,9 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
11901193
@override
11911194
String get searchWebButtonLabel => 'Search Web';
11921195

1196+
@override
1197+
String get shareButtonLabel => 'Share...';
1198+
11931199
@override
11941200
String get viewLicensesButtonLabel => 'View licenses';
11951201

packages/flutter/lib/src/services/text_input.dart

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,24 +1038,27 @@ mixin TextSelectionDelegate {
10381038
/// input.
10391039
void bringIntoView(TextPosition position);
10401040

1041-
/// Whether cut is enabled, must not be null.
1041+
/// Whether cut is enabled.
10421042
bool get cutEnabled => true;
10431043

1044-
/// Whether copy is enabled, must not be null.
1044+
/// Whether copy is enabled.
10451045
bool get copyEnabled => true;
10461046

1047-
/// Whether paste is enabled, must not be null.
1047+
/// Whether paste is enabled.
10481048
bool get pasteEnabled => true;
10491049

1050-
/// Whether select all is enabled, must not be null.
1050+
/// Whether select all is enabled.
10511051
bool get selectAllEnabled => true;
10521052

1053-
/// Whether look up is enabled, must not be null.
1053+
/// Whether look up is enabled.
10541054
bool get lookUpEnabled => true;
10551055

1056-
/// Whether search web is enabled, must not be null.
1056+
/// Whether search web is enabled.
10571057
bool get searchWebEnabled => true;
10581058

1059+
/// Whether share is enabled.
1060+
bool get shareEnabled => true;
1061+
10591062
/// Whether Live Text input is enabled.
10601063
///
10611064
/// See also:

packages/flutter/lib/src/widgets/context_menu_button_item.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ enum ContextMenuButtonType {
3232
/// A button that launches a web search for the current text selection.
3333
searchWeb,
3434

35+
/// A button that displays the share screen for the current text selection.
36+
share,
37+
3538
/// A button for starting Live Text input.
3639
///
3740
/// See also:

packages/flutter/lib/src/widgets/editable_text.dart

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1874,6 +1874,7 @@ class EditableText extends StatefulWidget {
18741874
required final VoidCallback? onSelectAll,
18751875
required final VoidCallback? onLookUp,
18761876
required final VoidCallback? onSearchWeb,
1877+
required final VoidCallback? onShare,
18771878
required final VoidCallback? onLiveTextInput,
18781879
}) {
18791880
final List<ContextMenuButtonItem> resultButtonItem = <ContextMenuButtonItem>[];
@@ -1914,6 +1915,11 @@ class EditableText extends StatefulWidget {
19141915
onPressed: onSearchWeb,
19151916
type: ContextMenuButtonType.searchWeb,
19161917
),
1918+
if (onShare != null)
1919+
ContextMenuButtonItem(
1920+
onPressed: onShare,
1921+
type: ContextMenuButtonType.share,
1922+
),
19171923
]);
19181924
}
19191925

@@ -2291,6 +2297,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
22912297
&& textEditingValue.selection.textInside(textEditingValue.text).trim() != '';
22922298
}
22932299

2300+
@override
2301+
bool get shareEnabled {
2302+
if (defaultTargetPlatform != TargetPlatform.iOS) {
2303+
return false;
2304+
}
2305+
2306+
return !widget.obscureText
2307+
&& !textEditingValue.selection.isCollapsed
2308+
&& textEditingValue.selection.textInside(textEditingValue.text).trim() != '';
2309+
}
2310+
22942311
@override
22952312
bool get liveTextInputEnabled {
22962313
return _liveTextInputStatus?.value == LiveTextInputStatus.enabled &&
@@ -2456,8 +2473,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
24562473
}
24572474
}
24582475

2459-
/// Look up the current selection, as in the "Look Up" edit menu button on iOS.
2476+
/// Look up the current selection,
2477+
/// as in the "Look Up" edit menu button on iOS.
2478+
///
24602479
/// Currently this is only implemented for iOS.
2480+
///
24612481
/// Throws an error if the selection is empty or collapsed.
24622482
Future<void> lookUpSelection(SelectionChangedCause cause) async {
24632483
assert(!widget.obscureText);
@@ -2473,9 +2493,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
24732493
}
24742494

24752495
/// Launch a web search on the current selection,
2476-
/// as in the "Search Web" edit menu button on iOS.
2496+
/// as in the "Search Web" edit menu button on iOS.
24772497
///
24782498
/// Currently this is only implemented for iOS.
2499+
///
24792500
/// When 'obscureText' is true or the selection is empty,
24802501
/// this function will not do anything
24812502
Future<void> searchWebForSelection(SelectionChangedCause cause) async {
@@ -2493,6 +2514,28 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
24932514
}
24942515
}
24952516

2517+
/// Launch the share interface for the current selection,
2518+
/// as in the "Share" edit menu button on iOS.
2519+
///
2520+
/// Currently this is only implemented for iOS.
2521+
///
2522+
/// When 'obscureText' is true or the selection is empty,
2523+
/// this function will not do anything
2524+
Future<void> shareSelection(SelectionChangedCause cause) async {
2525+
assert(!widget.obscureText);
2526+
if (widget.obscureText) {
2527+
return;
2528+
}
2529+
2530+
final String text = textEditingValue.selection.textInside(textEditingValue.text);
2531+
if (text.isNotEmpty) {
2532+
await SystemChannels.platform.invokeMethod(
2533+
'Share.invoke',
2534+
text,
2535+
);
2536+
}
2537+
}
2538+
24962539
void _startLiveTextInput(SelectionChangedCause cause) {
24972540
if (!liveTextInputEnabled) {
24982541
return;
@@ -2725,6 +2768,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
27252768
onSearchWeb: searchWebEnabled
27262769
? () => searchWebForSelection(SelectionChangedCause.toolbar)
27272770
: null,
2771+
onShare: shareEnabled
2772+
? () => shareSelection(SelectionChangedCause.toolbar)
2773+
: null,
27282774
onLiveTextInput: liveTextInputEnabled
27292775
? () => _startLiveTextInput(SelectionChangedCause.toolbar)
27302776
: null,

packages/flutter/test/cupertino/adaptive_text_selection_toolbar_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ void main() {
177177
onLiveTextInput: () {},
178178
onLookUp: () {},
179179
onSearchWeb: () {},
180+
onShare: () {},
180181
),
181182
),
182183
));
@@ -202,7 +203,7 @@ void main() {
202203
case TargetPlatform.macOS:
203204
case TargetPlatform.linux:
204205
case TargetPlatform.windows:
205-
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNWidgets(7));
206+
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNWidgets(8));
206207
}
207208
},
208209
skip: kIsWeb, // [intended] on web the browser handles the context menu.

0 commit comments

Comments
 (0)