1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/material.dart';
6library;
7
8import 'package:flutter/foundation.dart';
9import 'package:flutter/rendering.dart';
10import 'package:flutter/services.dart';
11
12import 'basic.dart';
13import 'editable_text.dart';
14import 'framework.dart';
15import 'localizations.dart';
16import 'media_query.dart';
17import 'text_selection_toolbar_anchors.dart';
18
19/// Displays the system context menu on top of the Flutter view.
20///
21/// Currently, only supports iOS 16.0 and above and displays nothing on other
22/// platforms.
23///
24/// The context menu is the menu that appears, for example, when doing text
25/// selection. Flutter typically draws this menu itself, but this class deals
26/// with the platform-rendered context menu instead.
27///
28/// There can only be one system context menu visible at a time. Building this
29/// widget when the system context menu is already visible will hide the old one
30/// and display this one. A system context menu that is hidden is informed via
31/// [onSystemHide].
32///
33/// Pass [items] to specify the buttons that will appear in the menu. Any items
34/// without a title will be given a default title from [WidgetsLocalizations].
35///
36/// By default, [items] will be set to the result of [getDefaultItems]. This
37/// method considers the state of the [EditableTextState] so that, for example,
38/// it will only include [IOSSystemContextMenuItemCopy] if there is currently a
39/// selection to copy.
40///
41/// To check if the current device supports showing the system context menu,
42/// call [isSupported].
43///
44/// {@tool dartpad}
45/// This example shows how to create a [TextField] that uses the system context
46/// menu where supported and does not show a system notification when the user
47/// presses the "Paste" button.
48///
49/// ** See code in examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart **
50/// {@end-tool}
51///
52/// See also:
53///
54/// * [SystemContextMenuController], which directly controls the hiding and
55/// showing of the system context menu.
56class SystemContextMenu extends StatefulWidget {
57 /// Creates an instance of [SystemContextMenu] that points to the given
58 /// [anchor].
59 const SystemContextMenu._({
60 super.key,
61 required this.anchor,
62 required this.items,
63 this.onSystemHide,
64 });
65
66 /// Creates an instance of [SystemContextMenu] for the field indicated by the
67 /// given [EditableTextState].
68 factory SystemContextMenu.editableText({
69 Key? key,
70 required EditableTextState editableTextState,
71 List<IOSSystemContextMenuItem>? items,
72 }) {
73 final (startGlyphHeight: double startGlyphHeight, endGlyphHeight: double endGlyphHeight) =
74 editableTextState.getGlyphHeights();
75
76 return SystemContextMenu._(
77 key: key,
78 anchor: TextSelectionToolbarAnchors.getSelectionRect(
79 editableTextState.renderEditable,
80 startGlyphHeight,
81 endGlyphHeight,
82 editableTextState.renderEditable.getEndpointsForSelection(
83 editableTextState.textEditingValue.selection,
84 ),
85 ),
86 items: items ?? getDefaultItems(editableTextState),
87 onSystemHide: () => editableTextState.hideToolbar(false),
88 );
89 }
90
91 /// The [Rect] that the context menu should point to.
92 final Rect anchor;
93
94 /// A list of the items to be displayed in the system context menu.
95 ///
96 /// When passed, items will be shown regardless of the state of text input.
97 /// For example, [IOSSystemContextMenuItemCopy] will produce a copy button
98 /// even when there is no selection to copy. Use [EditableTextState] and/or
99 /// the result of [getDefaultItems] to add and remove items based on the state
100 /// of the input.
101 ///
102 /// Defaults to the result of [getDefaultItems].
103 final List<IOSSystemContextMenuItem> items;
104
105 /// Called when the system hides this context menu.
106 ///
107 /// For example, tapping outside of the context menu typically causes the
108 /// system to hide the menu.
109 ///
110 /// This is not called when showing a new system context menu causes another
111 /// to be hidden.
112 final VoidCallback? onSystemHide;
113
114 /// Whether the current device supports showing the system context menu.
115 ///
116 /// Currently, this is only supported on newer versions of iOS.
117 static bool isSupported(BuildContext context) {
118 return MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false;
119 }
120
121 /// The default [items] for the given [EditableTextState].
122 ///
123 /// For example, [IOSSystemContextMenuItemCopy] will only be included when the
124 /// field represented by the [EditableTextState] has a selection.
125 ///
126 /// See also:
127 ///
128 /// * [EditableTextState.contextMenuButtonItems], which provides the default
129 /// [ContextMenuButtonItem]s for the Flutter-rendered context menu.
130 static List<IOSSystemContextMenuItem> getDefaultItems(EditableTextState editableTextState) {
131 return <IOSSystemContextMenuItem>[
132 if (editableTextState.copyEnabled) const IOSSystemContextMenuItemCopy(),
133 if (editableTextState.cutEnabled) const IOSSystemContextMenuItemCut(),
134 if (editableTextState.pasteEnabled) const IOSSystemContextMenuItemPaste(),
135 if (editableTextState.selectAllEnabled) const IOSSystemContextMenuItemSelectAll(),
136 if (editableTextState.lookUpEnabled) const IOSSystemContextMenuItemLookUp(),
137 if (editableTextState.searchWebEnabled) const IOSSystemContextMenuItemSearchWeb(),
138 if (editableTextState.liveTextInputEnabled) const IOSSystemContextMenuItemLiveText(),
139 ];
140 }
141
142 @override
143 State<SystemContextMenu> createState() => _SystemContextMenuState();
144}
145
146class _SystemContextMenuState extends State<SystemContextMenu> {
147 late final SystemContextMenuController _systemContextMenuController;
148
149 @override
150 void initState() {
151 super.initState();
152 _systemContextMenuController = SystemContextMenuController(onSystemHide: widget.onSystemHide);
153 }
154
155 @override
156 void dispose() {
157 _systemContextMenuController.dispose();
158 super.dispose();
159 }
160
161 @override
162 Widget build(BuildContext context) {
163 assert(SystemContextMenu.isSupported(context));
164
165 if (widget.items.isNotEmpty) {
166 final WidgetsLocalizations localizations = WidgetsLocalizations.of(context);
167 final List<IOSSystemContextMenuItemData> itemDatas = widget.items
168 .map((IOSSystemContextMenuItem item) => item.getData(localizations))
169 .toList();
170 _systemContextMenuController.showWithItems(widget.anchor, itemDatas);
171 }
172
173 return const SizedBox.shrink();
174 }
175}
176
177/// Describes a context menu button that will be rendered in the iOS system
178/// context menu and not by Flutter itself.
179///
180/// See also:
181///
182/// * [SystemContextMenu], a widget that can be used to display the system
183/// context menu.
184/// * [IOSSystemContextMenuItemData], which performs a similar role but at the
185/// method channel level and mirrors the requirements of the method channel
186/// API.
187/// * [ContextMenuButtonItem], which performs a similar role for Flutter-drawn
188/// context menus.
189@immutable
190sealed class IOSSystemContextMenuItem {
191 const IOSSystemContextMenuItem();
192
193 /// The text to display to the user.
194 ///
195 /// Not exposed for some built-in menu items whose title is always set by the
196 /// platform.
197 String? get title => null;
198
199 /// Returns the representation of this class used by method channels.
200 IOSSystemContextMenuItemData getData(WidgetsLocalizations localizations);
201
202 @override
203 int get hashCode => title.hashCode;
204
205 @override
206 bool operator ==(Object other) {
207 if (identical(this, other)) {
208 return true;
209 }
210 if (other.runtimeType != runtimeType) {
211 return false;
212 }
213 return other is IOSSystemContextMenuItem && other.title == title;
214 }
215}
216
217/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
218/// copy button.
219///
220/// Should only appear when there is a selection that can be copied.
221///
222/// The title and action are both handled by the platform.
223///
224/// See also:
225///
226/// * [SystemContextMenu], a widget that can be used to display the system
227/// context menu.
228/// * [IOSSystemContextMenuItemDataCopy], which specifies the data to be sent to
229/// the platform for this same button.
230final class IOSSystemContextMenuItemCopy extends IOSSystemContextMenuItem {
231 /// Creates an instance of [IOSSystemContextMenuItemCopy].
232 const IOSSystemContextMenuItemCopy();
233
234 @override
235 IOSSystemContextMenuItemDataCopy getData(WidgetsLocalizations localizations) {
236 return const IOSSystemContextMenuItemDataCopy();
237 }
238}
239
240/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
241/// cut button.
242///
243/// Should only appear when there is a selection that can be cut.
244///
245/// The title and action are both handled by the platform.
246///
247/// See also:
248///
249/// * [SystemContextMenu], a widget that can be used to display the system
250/// context menu.
251/// * [IOSSystemContextMenuItemDataCut], which specifies the data to be sent to
252/// the platform for this same button.
253final class IOSSystemContextMenuItemCut extends IOSSystemContextMenuItem {
254 /// Creates an instance of [IOSSystemContextMenuItemCut].
255 const IOSSystemContextMenuItemCut();
256
257 @override
258 IOSSystemContextMenuItemDataCut getData(WidgetsLocalizations localizations) {
259 return const IOSSystemContextMenuItemDataCut();
260 }
261}
262
263/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
264/// paste button.
265///
266/// Should only appear when the field can receive pasted content.
267///
268/// The title and action are both handled by the platform.
269///
270/// See also:
271///
272/// * [SystemContextMenu], a widget that can be used to display the system
273/// context menu.
274/// * [IOSSystemContextMenuItemDataPaste], which specifies the data to be sent
275/// to the platform for this same button.
276final class IOSSystemContextMenuItemPaste extends IOSSystemContextMenuItem {
277 /// Creates an instance of [IOSSystemContextMenuItemPaste].
278 const IOSSystemContextMenuItemPaste();
279
280 @override
281 IOSSystemContextMenuItemDataPaste getData(WidgetsLocalizations localizations) {
282 return const IOSSystemContextMenuItemDataPaste();
283 }
284}
285
286/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
287/// select all button.
288///
289/// Should only appear when the field can have its selection changed.
290///
291/// The title and action are both handled by the platform.
292///
293/// See also:
294///
295/// * [SystemContextMenu], a widget that can be used to display the system
296/// context menu.
297/// * [IOSSystemContextMenuItemDataSelectAll], which specifies the data to be
298/// sent to the platform for this same button.
299final class IOSSystemContextMenuItemSelectAll extends IOSSystemContextMenuItem {
300 /// Creates an instance of [IOSSystemContextMenuItemSelectAll].
301 const IOSSystemContextMenuItemSelectAll();
302
303 @override
304 IOSSystemContextMenuItemDataSelectAll getData(WidgetsLocalizations localizations) {
305 return const IOSSystemContextMenuItemDataSelectAll();
306 }
307}
308
309/// Creates an instance of [IOSSystemContextMenuItem] for the
310/// system's built-in look up button.
311///
312/// Should only appear when content is selected.
313///
314/// The [title] is optional, but it must be specified before being sent to the
315/// platform. Typically it should be set to
316/// [WidgetsLocalizations.lookUpButtonLabel].
317///
318/// The action is handled by the platform.
319///
320/// See also:
321///
322/// * [SystemContextMenu], a widget that can be used to display the system
323/// context menu.
324/// * [IOSSystemContextMenuItemDataLookUp], which specifies the data to be sent
325/// to the platform for this same button.
326final class IOSSystemContextMenuItemLookUp extends IOSSystemContextMenuItem with Diagnosticable {
327 /// Creates an instance of [IOSSystemContextMenuItemLookUp].
328 const IOSSystemContextMenuItemLookUp({this.title});
329
330 @override
331 final String? title;
332
333 @override
334 IOSSystemContextMenuItemDataLookUp getData(WidgetsLocalizations localizations) {
335 return IOSSystemContextMenuItemDataLookUp(title: title ?? localizations.lookUpButtonLabel);
336 }
337
338 @override
339 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
340 super.debugFillProperties(properties);
341 properties.add(DiagnosticsProperty<String>('title', title));
342 }
343}
344
345/// Creates an instance of [IOSSystemContextMenuItem] for the
346/// system's built-in search web button.
347///
348/// Should only appear when content is selected.
349///
350/// The [title] is optional, but it must be specified before being sent to the
351/// platform. Typically it should be set to
352/// [WidgetsLocalizations.searchWebButtonLabel].
353///
354/// The action is handled by the platform.
355///
356/// See also:
357///
358/// * [SystemContextMenu], a widget that can be used to display the system
359/// context menu.
360/// * [IOSSystemContextMenuItemDataSearchWeb], which specifies the data to be
361/// sent to the platform for this same button.
362final class IOSSystemContextMenuItemSearchWeb extends IOSSystemContextMenuItem with Diagnosticable {
363 /// Creates an instance of [IOSSystemContextMenuItemSearchWeb].
364 const IOSSystemContextMenuItemSearchWeb({this.title});
365
366 @override
367 final String? title;
368
369 @override
370 IOSSystemContextMenuItemDataSearchWeb getData(WidgetsLocalizations localizations) {
371 return IOSSystemContextMenuItemDataSearchWeb(
372 title: title ?? localizations.searchWebButtonLabel,
373 );
374 }
375
376 @override
377 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
378 super.debugFillProperties(properties);
379 properties.add(DiagnosticsProperty<String>('title', title));
380 }
381}
382
383/// Creates an instance of [IOSSystemContextMenuItem] for the
384/// system's built-in share button.
385///
386/// Opens the system share dialog.
387///
388/// Should only appear when shareable content is selected.
389///
390/// The [title] is optional, but it must be specified before being sent to the
391/// platform. Typically it should be set to
392/// [WidgetsLocalizations.shareButtonLabel].
393///
394/// See also:
395///
396/// * [SystemContextMenu], a widget that can be used to display the system
397/// context menu.
398/// * [IOSSystemContextMenuItemDataShare], which specifies the data to be sent
399/// to the platform for this same button.
400final class IOSSystemContextMenuItemShare extends IOSSystemContextMenuItem with Diagnosticable {
401 /// Creates an instance of [IOSSystemContextMenuItemShare].
402 const IOSSystemContextMenuItemShare({this.title});
403
404 @override
405 final String? title;
406
407 @override
408 IOSSystemContextMenuItemDataShare getData(WidgetsLocalizations localizations) {
409 return IOSSystemContextMenuItemDataShare(title: title ?? localizations.shareButtonLabel);
410 }
411
412 @override
413 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
414 super.debugFillProperties(properties);
415 properties.add(StringProperty('title', title));
416 }
417}
418
419/// Creates an instance of [IOSSystemContextMenuItem] for the
420/// system's built-in Live Text button.
421///
422/// The title and action are both handled by the platform.
423///
424/// See also:
425///
426/// * [SystemContextMenu], a widget that can be used to display the system
427/// context menu.
428/// * [IOSSystemContextMenuItemDataLiveText], which specifies the data to be sent
429/// to the platform for this same button.
430final class IOSSystemContextMenuItemLiveText extends IOSSystemContextMenuItem {
431 /// Creates an instance of [IOSSystemContextMenuItemLiveText].
432 const IOSSystemContextMenuItemLiveText();
433
434 @override
435 IOSSystemContextMenuItemData getData(WidgetsLocalizations localizations) {
436 return const IOSSystemContextMenuItemDataLiveText();
437 }
438}
439
440// TODO(justinmc): Support the "custom" type.
441// https://github.com/flutter/flutter/issues/103163
442