diff --git a/examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart b/examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart deleted file mode 100644 index b648c116b0..0000000000 --- a/examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; - -/// Flutter code sample for [SystemContextMenu]. - -void main() => runApp(const SystemContextMenuExampleApp()); - -class SystemContextMenuExampleApp extends StatelessWidget { - const SystemContextMenuExampleApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('SystemContextMenu Basic Example'), - ), - body: Center( - child: TextField( - contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { - // If supported, show the system context menu. - if (SystemContextMenu.isSupported(context)) { - return SystemContextMenu.editableText( - editableTextState: editableTextState, - ); - } - // Otherwise, show the flutter-rendered context menu for the current - // platform. - return AdaptiveTextSelectionToolbar.editableText( - editableTextState: editableTextState, - ); - }, - ), - ), - ), - ); - } -} diff --git a/examples/api/test/widgets/system_context_menu/system_context_menu.0_test.dart b/examples/api/test/widgets/system_context_menu/system_context_menu.0_test.dart deleted file mode 100644 index f8052e500b..0000000000 --- a/examples/api/test/widgets/system_context_menu/system_context_menu.0_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_api_samples/widgets/system_context_menu/system_context_menu.0.dart' as example; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - testWidgets('only shows the system context menu on iOS when MediaQuery says it is supported', (WidgetTester tester) async { - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - final MediaQueryData mediaQueryData = MediaQuery.of(context); - return MediaQuery( - data: mediaQueryData.copyWith( - // Faking this value, which is usually set to true only on - // devices running iOS 16+. - supportsShowingSystemContextMenu: defaultTargetPlatform == TargetPlatform.iOS, - ), - child: const example.SystemContextMenuExampleApp(), - ); - }, - ), - ); - - expect(find.byType(SystemContextMenu), findsNothing); - - // Show the context menu. - final Finder textFinder = find.byType(EditableText); - await tester.longPress(textFinder); - tester.state(textFinder).showToolbar(); - await tester.pumpAndSettle(); - - switch (defaultTargetPlatform) { - case TargetPlatform.iOS: - expect(find.byType(SystemContextMenu), findsOneWidget); - expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); - expect(find.byType(SystemContextMenu), findsNothing); - } - }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - - testWidgets('does not show the system context menu when not supported', (WidgetTester tester) async { - await tester.pumpWidget( - // By default, MediaQueryData.supportsShowingSystemContextMenu is false. - const example.SystemContextMenuExampleApp(), - ); - - expect(find.byType(SystemContextMenu), findsNothing); - - // Show the context menu. - final Finder textFinder = find.byType(EditableText); - await tester.longPress(textFinder); - tester.state(textFinder).showToolbar(); - await tester.pumpAndSettle(); - - expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); - expect(find.byType(SystemContextMenu), findsNothing); - }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] -} diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart index b513f18238..14825107a9 100644 --- a/packages/flutter/lib/src/services/binding.dart +++ b/packages/flutter/lib/src/services/binding.dart @@ -357,14 +357,8 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { Future _handlePlatformMessage(MethodCall methodCall) async { final String method = methodCall.method; + assert(method == 'SystemChrome.systemUIChange' || method == 'System.requestAppExit'); switch (method) { - // Called when the system dismisses the system context menu, such as when - // the user taps outside the menu. Not called when Flutter shows a new - // system context menu while an old one is still visible. - case 'ContextMenu.onDismissSystemContextMenu': - for (final SystemContextMenuClient client in _systemContextMenuClients) { - client.handleSystemHide(); - } case 'SystemChrome.systemUIChange': final List args = methodCall.arguments as List; if (_systemUiChangeCallback != null) { @@ -372,8 +366,6 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { } case 'System.requestAppExit': return {'response': (await handleRequestAppExit()).name}; - default: - throw AssertionError('Method "$method" not handled.'); } } @@ -518,19 +510,6 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { Future initializationComplete() async { await SystemChannels.platform.invokeMethod('System.initializationComplete'); } - - final Set _systemContextMenuClients = {}; - - /// Registers a [SystemContextMenuClient] that will receive system context - /// menu calls from the engine. - static void registerSystemContextMenuClient(SystemContextMenuClient client) { - instance._systemContextMenuClients.add(client); - } - - /// Unregisters a [SystemContextMenuClient] so that it is no longer called. - static void unregisterSystemContextMenuClient(SystemContextMenuClient client) { - instance._systemContextMenuClients.remove(client); - } } /// Signature for listening to changes in the [SystemUiMode]. @@ -609,23 +588,3 @@ class _DefaultBinaryMessenger extends BinaryMessenger { } } } - -/// An interface to receive calls related to the system context menu from the -/// engine. -/// -/// Currently this is only supported on iOS 16+. -/// -/// See also: -/// * [SystemContextMenuController], which uses this to provide a fully -/// featured way to control the system context menu. -/// * [MediaQuery.maybeSupportsShowingSystemContextMenu], which indicates -/// whether the system context menu is supported. -/// * [SystemContextMenu], which provides a widget interface for displaying the -/// system context menu. -mixin SystemContextMenuClient { - /// Handles the system hiding a context menu. - /// - /// This is called for all instances of [SystemContextMenuController], so it's - /// not guaranteed that this instance was the one that was hidden. - void handleSystemHide(); -} diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart index ecea3a576a..f46a753e30 100644 --- a/packages/flutter/lib/src/services/text_input.dart +++ b/packages/flutter/lib/src/services/text_input.dart @@ -17,7 +17,6 @@ import 'package:flutter/foundation.dart'; import 'package:vector_math/vector_math_64.dart' show Matrix4; import 'autofill.dart'; -import 'binding.dart'; import 'clipboard.dart' show Clipboard; import 'keyboard_inserted_content.dart'; import 'message_codec.dart'; @@ -1809,7 +1808,7 @@ class TextInput { Future _handleTextInputInvocation(MethodCall methodCall) async { final String method = methodCall.method; - switch (method) { + switch (methodCall.method) { case 'TextInputClient.focusElement': final List args = methodCall.arguments as List; _scribbleClients[args[0]]?.onScribbleFocus(Offset((args[1] as num).toDouble(), (args[2] as num).toDouble())); @@ -2404,178 +2403,3 @@ class _PlatformTextInputControl with TextInputControl { ); } } - -/// Allows access to the system context menu. -/// -/// The context menu is the menu that appears, for example, when doing text -/// selection. Flutter typically draws this menu itself, but this class deals -/// with the platform-rendered context menu. -/// -/// Only one instance can be visible at a time. Calling [show] while the system -/// context menu is already visible will hide it and show it again at the new -/// [Rect]. An instance that is hidden is informed via [onSystemHide]. -/// -/// Currently this system context menu is bound to text input. The buttons that -/// are shown and the actions they perform are dependent on the currently -/// active [TextInputConnection]. Using this without an active -/// [TextInputConnection] is a noop. -/// -/// Call [dispose] when no longer needed. -/// -/// See also: -/// -/// * [ContextMenuController], which controls Flutter-drawn context menus. -/// * [SystemContextMenu], which wraps this functionality in a widget. -/// * [MediaQuery.maybeSupportsShowingSystemContextMenu], which indicates -/// whether the system context menu is supported. -class SystemContextMenuController with SystemContextMenuClient { - /// Creates an instance of [SystemContextMenuController]. - /// - /// Not shown until [show] is called. - SystemContextMenuController({ - this.onSystemHide, - }) { - ServicesBinding.registerSystemContextMenuClient(this); - } - - /// Called when the system has hidden the context menu. - /// - /// For example, tapping outside of the context menu typically causes the - /// system to hide it directly. Flutter is made aware that the context menu is - /// no longer visible through this callback. - /// - /// This is not called when [show]ing a new system context menu causes another - /// to be hidden. - final VoidCallback? onSystemHide; - - static const MethodChannel _channel = SystemChannels.platform; - - static SystemContextMenuController? _lastShown; - - /// The target [Rect] that was last given to [show]. - /// - /// Null if [show] has not been called. - Rect? _lastTargetRect; - - /// True when the instance most recently [show]n has been hidden by the - /// system. - bool _hiddenBySystem = false; - - bool get _isVisible => this == _lastShown && !_hiddenBySystem; - - /// After calling [dispose], this instance can no longer be used. - bool _isDisposed = false; - - // Begin SystemContextMenuClient. - - @override - void handleSystemHide() { - assert(!_isDisposed); - // If this instance wasn't being shown, then it wasn't the instance that was - // hidden. - if (!_isVisible) { - return; - } - if (_lastShown == this) { - _lastShown = null; - } - _hiddenBySystem = true; - onSystemHide?.call(); - } - - // End SystemContextMenuClient. - - /// Shows the system context menu anchored on the given [Rect]. - /// - /// The [Rect] represents what the context menu is pointing to. For example, - /// for some text selection, this would be the selection [Rect]. - /// - /// There can only be one system context menu visible at a time. Calling this - /// while another system context menu is already visible will remove the old - /// menu before showing the new menu. - /// - /// Currently this system context menu is bound to text input. The buttons - /// that are shown and the actions they perform are dependent on the - /// currently active [TextInputConnection]. Using this without an active - /// [TextInputConnection] will be a noop. - /// - /// This is only supported on iOS 16.0 and later. - /// - /// See also: - /// - /// * [hideSystemContextMenu], which hides the menu shown by this method. - /// * [MediaQuery.supportsShowingSystemContextMenu], which indicates whether - /// this method is supported on the current platform. - Future show(Rect targetRect) { - assert(!_isDisposed); - assert( - TextInput._instance._currentConnection != null, - 'Currently, the system context menu can only be shown for an active text input connection', - ); - - // Don't show the same thing that's already being shown. - if (_lastShown != null && _lastShown!._isVisible && _lastShown!._lastTargetRect == targetRect) { - return Future.value(); - } - - assert( - _lastShown == null || _lastShown == this || !_lastShown!._isVisible, - 'Attempted to show while another instance was still visible.', - ); - - _lastTargetRect = targetRect; - _lastShown = this; - _hiddenBySystem = false; - return _channel.invokeMethod>( - 'ContextMenu.showSystemContextMenu', - { - 'targetRect': { - 'x': targetRect.left, - 'y': targetRect.top, - 'width': targetRect.width, - 'height': targetRect.height, - }, - }, - ); - } - - /// Hides this system context menu. - /// - /// If this hasn't been shown, or if another instance has hidden this menu, - /// does nothing. - /// - /// Currently this is only supported on iOS 16.0 and later. - /// - /// See also: - /// - /// * [showSystemContextMenu], which shows the menu hidden by this method. - /// * [MediaQuery.supportsShowingSystemContextMenu], which indicates whether - /// the system context menu is supported on the current platform. - Future hide() async { - assert(!_isDisposed); - // This check prevents a given instance from accidentally hiding some other - // instance, since only one can be visible at a time. - if (this != _lastShown) { - return; - } - _lastShown = null; - // This may be called unnecessarily in the case where the user has already - // hidden the menu (for example by tapping the screen). - return _channel.invokeMethod( - 'ContextMenu.hideSystemContextMenu', - ); - } - - @override - String toString() { - return 'SystemContextMenuController(onSystemHide=$onSystemHide, _hiddenBySystem=$_hiddenBySystem, _isVisible=$_isVisible, _isDiposed=$_isDisposed)'; - } - - /// Used to release resources when this instance will never be used again. - void dispose() { - assert(!_isDisposed); - hide(); - ServicesBinding.unregisterSystemContextMenuClient(this); - _isDisposed = true; - } -} diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 069230666e..f5b549e763 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -2786,12 +2786,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien /// Gets the line heights at the start and end of the selection for the given /// [EditableTextState]. - /// - /// See also: - /// - /// * [TextSelectionToolbarAnchors.getSelectionRect], which depends on this - /// information. - ({double startGlyphHeight, double endGlyphHeight}) getGlyphHeights() { + _GlyphHeights _getGlyphHeights() { final TextSelection selection = textEditingValue.selection; // Only calculate handle rects if the text in the previous frame @@ -2805,9 +2800,9 @@ class EditableTextState extends State with AutomaticKeepAliveClien final String prevText = span.toPlainText(); final String currText = textEditingValue.text; if (prevText != currText || !selection.isValid || selection.isCollapsed) { - return ( - startGlyphHeight: renderEditable.preferredLineHeight, - endGlyphHeight: renderEditable.preferredLineHeight, + return _GlyphHeights( + start: renderEditable.preferredLineHeight, + end: renderEditable.preferredLineHeight, ); } @@ -2822,9 +2817,9 @@ class EditableTextState extends State with AutomaticKeepAliveClien start: selection.end - lastSelectedGraphemeExtent, end: selection.end, )); - return ( - startGlyphHeight: startCharacterRect?.height ?? renderEditable.preferredLineHeight, - endGlyphHeight: endCharacterRect?.height ?? renderEditable.preferredLineHeight, + return _GlyphHeights( + start: startCharacterRect?.height ?? renderEditable.preferredLineHeight, + end: endCharacterRect?.height ?? renderEditable.preferredLineHeight, ); } @@ -2843,14 +2838,14 @@ class EditableTextState extends State with AutomaticKeepAliveClien ); } - final (startGlyphHeight: double startGlyphHeight, endGlyphHeight: double endGlyphHeight) = getGlyphHeights(); + final _GlyphHeights glyphHeights = _getGlyphHeights(); final TextSelection selection = textEditingValue.selection; final List points = renderEditable.getEndpointsForSelection(selection); return TextSelectionToolbarAnchors.fromSelection( renderBox: renderEditable, - startGlyphHeight: startGlyphHeight, - endGlyphHeight: endGlyphHeight, + startGlyphHeight: glyphHeights.start, + endGlyphHeight: glyphHeights.end, selectionEndpoints: points, ); } @@ -6031,6 +6026,21 @@ class _CopySelectionAction extends ContextAction { bool get isActionEnabled => state._value.selection.isValid && !state._value.selection.isCollapsed; } +/// The start and end glyph heights of some range of text. +@immutable +class _GlyphHeights { + const _GlyphHeights({ + required this.start, + required this.end, + }); + + /// The glyph height of the first line. + final double start; + + /// The glyph height of the last line. + final double end; +} + /// A [ClipboardStatusNotifier] whose [value] is hardcoded to /// [ClipboardStatus.pasteable]. /// diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart index c0289d9914..58a2bcf444 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -74,8 +74,6 @@ enum _MediaQueryAspect { gestureSettings, /// Specifies the aspect corresponding to [MediaQueryData.displayFeatures]. displayFeatures, - /// Specifies the aspect corresponding to [MediaQueryData.supportsShowingSystemContextMenu]. - supportsShowingSystemContextMenu, } /// Information about a piece of media (e.g., a window). @@ -175,7 +173,6 @@ class MediaQueryData { this.navigationMode = NavigationMode.traditional, this.gestureSettings = const DeviceGestureSettings(touchSlop: kTouchSlop), this.displayFeatures = const [], - this.supportsShowingSystemContextMenu = false, }) : _textScaleFactor = textScaleFactor, _textScaler = textScaler, assert( @@ -253,8 +250,7 @@ class MediaQueryData { alwaysUse24HourFormat = platformData?.alwaysUse24HourFormat ?? view.platformDispatcher.alwaysUse24HourFormat, navigationMode = platformData?.navigationMode ?? NavigationMode.traditional, gestureSettings = DeviceGestureSettings.fromView(view), - displayFeatures = view.displayFeatures, - supportsShowingSystemContextMenu = platformData?.supportsShowingSystemContextMenu ?? view.platformDispatcher.supportsShowingSystemContextMenu; + displayFeatures = view.displayFeatures; static TextScaler _textScalerFromView(ui.FlutterView view, MediaQueryData? platformData) { final double scaleFactor = platformData?.textScaleFactor ?? view.platformDispatcher.textScaleFactor; @@ -566,18 +562,6 @@ class MediaQueryData { /// [dart:ui.DisplayFeatureType.hinge]). final List displayFeatures; - /// Whether showing the system context menu is supported. - /// - /// For example, on iOS 16.0 and above, the system text selection context menu - /// may be shown instead of the Flutter-drawn context menu in order to avoid - /// the iOS clipboard access notification when the "Paste" button is pressed. - /// - /// See also: - /// - /// * [TextInput.showSystemContextMenu], which may be used to show the system - /// context menu when this flag indicates it's supported. - final bool supportsShowingSystemContextMenu; - /// The orientation of the media (e.g., whether the device is in landscape or /// portrait mode). Orientation get orientation { @@ -614,7 +598,6 @@ class MediaQueryData { NavigationMode? navigationMode, DeviceGestureSettings? gestureSettings, List? displayFeatures, - bool? supportsShowingSystemContextMenu, }) { assert(textScaleFactor == null || textScaler == null); if (textScaleFactor != null) { @@ -639,7 +622,6 @@ class MediaQueryData { navigationMode: navigationMode ?? this.navigationMode, gestureSettings: gestureSettings ?? this.gestureSettings, displayFeatures: displayFeatures ?? this.displayFeatures, - supportsShowingSystemContextMenu: supportsShowingSystemContextMenu ?? this.supportsShowingSystemContextMenu, ); } @@ -832,8 +814,7 @@ class MediaQueryData { && other.boldText == boldText && other.navigationMode == navigationMode && other.gestureSettings == gestureSettings - && listEquals(other.displayFeatures, displayFeatures) - && other.supportsShowingSystemContextMenu == supportsShowingSystemContextMenu; + && listEquals(other.displayFeatures, displayFeatures); } @override @@ -855,7 +836,6 @@ class MediaQueryData { navigationMode, gestureSettings, Object.hashAll(displayFeatures), - supportsShowingSystemContextMenu, ); @override @@ -879,7 +859,6 @@ class MediaQueryData { 'navigationMode: ${navigationMode.name}', 'gestureSettings: $gestureSettings', 'displayFeatures: $displayFeatures', - 'supportsShowingSystemContextMenu: $supportsShowingSystemContextMenu', ]; return '${objectRuntimeType(this, 'MediaQueryData')}(${properties.join(', ')})'; } @@ -1652,26 +1631,6 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { /// {@macro flutter.widgets.media_query.MediaQuery.dontUseMaybeOf} static List? maybeDisplayFeaturesOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.displayFeatures)?.displayFeatures; - /// Returns [MediaQueryData.supportsShowingSystemContextMenu] for the nearest - /// [MediaQuery] ancestor or throws an exception, if no such ancestor exists. - /// - /// Use of this method will cause the given [context] to rebuild any time that - /// the [MediaQueryData.supportsShowingSystemContextMenu] property of the - /// ancestor [MediaQuery] changes. - /// - /// {@macro flutter.widgets.media_query.MediaQuery.dontUseOf} - static bool supportsShowingSystemContextMenu(BuildContext context) => _of(context, _MediaQueryAspect.supportsShowingSystemContextMenu).supportsShowingSystemContextMenu; - - /// Returns [MediaQueryData.supportsShowingSystemContextMenu] for the nearest - /// [MediaQuery] ancestor or null, if no such ancestor exists. - /// - /// Use of this method will cause the given [context] to rebuild any time that - /// the [MediaQueryData.supportsShowingSystemContextMenu] property of the - /// ancestor [MediaQuery] changes. - /// - /// {@macro flutter.widgets.media_query.MediaQuery.dontUseMaybeOf} - static bool? maybeSupportsShowingSystemContextMenu(BuildContext context) => _maybeOf(context, _MediaQueryAspect.supportsShowingSystemContextMenu)?.supportsShowingSystemContextMenu; - @override bool updateShouldNotify(MediaQuery oldWidget) => data != oldWidget.data; @@ -1704,7 +1663,6 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { _MediaQueryAspect.systemGestureInsets => data.systemGestureInsets != oldWidget.data.systemGestureInsets, _MediaQueryAspect.accessibleNavigation => data.accessibleNavigation != oldWidget.data.accessibleNavigation, _MediaQueryAspect.alwaysUse24HourFormat => data.alwaysUse24HourFormat != oldWidget.data.alwaysUse24HourFormat, - _MediaQueryAspect.supportsShowingSystemContextMenu => data.supportsShowingSystemContextMenu != oldWidget.data.supportsShowingSystemContextMenu, }); } } diff --git a/packages/flutter/lib/src/widgets/system_context_menu.dart b/packages/flutter/lib/src/widgets/system_context_menu.dart deleted file mode 100644 index 05fd4b412c..0000000000 --- a/packages/flutter/lib/src/widgets/system_context_menu.dart +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; - -import 'basic.dart'; -import 'editable_text.dart'; -import 'framework.dart'; -import 'media_query.dart'; -import 'text_selection_toolbar_anchors.dart'; - -/// Displays the system context menu on top of the Flutter view. -/// -/// Currently, only supports iOS 16.0 and above and displays nothing on other -/// platforms. -/// -/// The context menu is the menu that appears, for example, when doing text -/// selection. Flutter typically draws this menu itself, but this class deals -/// with the platform-rendered context menu instead. -/// -/// There can only be one system context menu visible at a time. Building this -/// widget when the system context menu is already visible will hide the old one -/// and display this one. A system context menu that is hidden is informed via -/// [onSystemHide]. -/// -/// To check if the current device supports showing the system context menu, -/// call [isSupported]. -/// -/// {@tool dartpad} -/// This example shows how to create a [TextField] that uses the system context -/// menu where supported and does not show a system notification when the user -/// presses the "Paste" button. -/// -/// ** See code in examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart ** -/// {@end-tool} -/// -/// See also: -/// -/// * [SystemContextMenuController], which directly controls the hiding and -/// showing of the system context menu. -class SystemContextMenu extends StatefulWidget { - /// Creates an instance of [SystemContextMenu] that points to the given - /// [anchor]. - const SystemContextMenu._({ - super.key, - required this.anchor, - this.onSystemHide, - }); - - /// Creates an instance of [SystemContextMenu] for the field indicated by the - /// given [EditableTextState]. - factory SystemContextMenu.editableText({ - Key? key, - required EditableTextState editableTextState, - }) { - final ( - startGlyphHeight: double startGlyphHeight, - endGlyphHeight: double endGlyphHeight, - ) = editableTextState.getGlyphHeights(); - return SystemContextMenu._( - key: key, - anchor: TextSelectionToolbarAnchors.getSelectionRect( - editableTextState.renderEditable, - startGlyphHeight, - endGlyphHeight, - editableTextState.renderEditable.getEndpointsForSelection( - editableTextState.textEditingValue.selection, - ), - ), - onSystemHide: () { - editableTextState.hideToolbar(); - }, - ); - } - - /// The [Rect] that the context menu should point to. - final Rect anchor; - - /// Called when the system hides this context menu. - /// - /// For example, tapping outside of the context menu typically causes the - /// system to hide the menu. - /// - /// This is not called when showing a new system context menu causes another - /// to be hidden. - final VoidCallback? onSystemHide; - - /// Whether the current device supports showing the system context menu. - /// - /// Currently, this is only supported on newer versions of iOS. - static bool isSupported(BuildContext context) { - return MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false; - } - - @override - State createState() => _SystemContextMenuState(); -} - -class _SystemContextMenuState extends State { - late final SystemContextMenuController _systemContextMenuController; - - @override - void initState() { - super.initState(); - _systemContextMenuController = SystemContextMenuController( - onSystemHide: widget.onSystemHide, - ); - _systemContextMenuController.show(widget.anchor); - } - - @override - void didUpdateWidget(SystemContextMenu oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.anchor != oldWidget.anchor) { - _systemContextMenuController.show(widget.anchor); - } - } - - @override - void dispose() { - _systemContextMenuController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - assert(SystemContextMenu.isSupported(context)); - return const SizedBox.shrink(); - } -} diff --git a/packages/flutter/lib/src/widgets/text_selection_toolbar_anchors.dart b/packages/flutter/lib/src/widgets/text_selection_toolbar_anchors.dart index f6072b2b49..616a13aea1 100644 --- a/packages/flutter/lib/src/widgets/text_selection_toolbar_anchors.dart +++ b/packages/flutter/lib/src/widgets/text_selection_toolbar_anchors.dart @@ -30,17 +30,30 @@ class TextSelectionToolbarAnchors { required double endGlyphHeight, required List selectionEndpoints, }) { - final Rect selectionRect = getSelectionRect( - renderBox, - startGlyphHeight, - endGlyphHeight, - selectionEndpoints, + final Rect editingRegion = Rect.fromPoints( + renderBox.localToGlobal(Offset.zero), + renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero)), ); - if (selectionRect == Rect.zero) { + + if (editingRegion.left.isNaN || editingRegion.top.isNaN + || editingRegion.right.isNaN || editingRegion.bottom.isNaN) { return const TextSelectionToolbarAnchors(primaryAnchor: Offset.zero); } - final Rect editingRegion = _getEditingRegion(renderBox); + final bool isMultiline = selectionEndpoints.last.point.dy - selectionEndpoints.first.point.dy > + endGlyphHeight / 2; + + final Rect selectionRect = Rect.fromLTRB( + isMultiline + ? editingRegion.left + : editingRegion.left + selectionEndpoints.first.point.dx, + editingRegion.top + selectionEndpoints.first.point.dy - startGlyphHeight, + isMultiline + ? editingRegion.right + : editingRegion.left + selectionEndpoints.last.point.dx, + editingRegion.top + selectionEndpoints.last.point.dy, + ); + return TextSelectionToolbarAnchors( primaryAnchor: Offset( selectionRect.left + selectionRect.width / 2, @@ -53,44 +66,6 @@ class TextSelectionToolbarAnchors { ); } - /// Returns the [Rect] of the [RenderBox] in global coordinates. - static Rect _getEditingRegion(RenderBox renderBox) { - return Rect.fromPoints( - renderBox.localToGlobal(Offset.zero), - renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero)), - ); - } - - /// Returns the [Rect] covering the given selection in the given [RenderBox] - /// in global coordinates. - static Rect getSelectionRect( - RenderBox renderBox, - double startGlyphHeight, - double endGlyphHeight, - List selectionEndpoints, - ) { - final Rect editingRegion = _getEditingRegion(renderBox); - - if (editingRegion.left.isNaN || editingRegion.top.isNaN - || editingRegion.right.isNaN || editingRegion.bottom.isNaN) { - return Rect.zero; - } - - final bool isMultiline = selectionEndpoints.last.point.dy - selectionEndpoints.first.point.dy > - endGlyphHeight / 2; - - return Rect.fromLTRB( - isMultiline - ? editingRegion.left - : editingRegion.left + selectionEndpoints.first.point.dx, - editingRegion.top + selectionEndpoints.first.point.dy - startGlyphHeight, - isMultiline - ? editingRegion.right - : editingRegion.left + selectionEndpoints.last.point.dx, - editingRegion.top + selectionEndpoints.last.point.dy, - ); - } - /// The location that the toolbar should attempt to position itself at. /// /// If the toolbar doesn't fit at this location, use [secondaryAnchor] if it diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index c53eed2261..41eb3e9a77 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -141,7 +141,6 @@ export 'src/widgets/snapshot_widget.dart'; export 'src/widgets/spacer.dart'; export 'src/widgets/spell_check.dart'; export 'src/widgets/status_transitions.dart'; -export 'src/widgets/system_context_menu.dart'; export 'src/widgets/table.dart'; export 'src/widgets/tap_region.dart'; export 'src/widgets/text.dart'; diff --git a/packages/flutter/test/services/system_context_menu_controller_test.dart b/packages/flutter/test/services/system_context_menu_controller_test.dart deleted file mode 100644 index 38fe480be6..0000000000 --- a/packages/flutter/test/services/system_context_menu_controller_test.dart +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import './text_input_utils.dart'; - -void main() { - final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); - - test('showing and hiding one controller', () { - // Create an active connection, which is required to show the system menu. - final FakeTextInputClient client = FakeTextInputClient(const TextEditingValue(text: 'test1')); - final TextInputConnection connection = TextInput.attach(client, client.configuration); - addTearDown(() { - connection.close(); - }); - - final List> targetRects = >[]; - int hideCount = 0; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { - switch (methodCall.method) { - case 'ContextMenu.showSystemContextMenu': - final Map arguments = methodCall.arguments as Map; - final Map untypedTargetRect = arguments['targetRect'] as Map; - final Map lastTargetRect = untypedTargetRect.map((String key, dynamic value) { - return MapEntry(key, value as double); - }); - targetRects.add(lastTargetRect); - case 'ContextMenu.hideSystemContextMenu': - hideCount += 1; - } - return; - }); - addTearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, null); - }); - - final SystemContextMenuController controller = SystemContextMenuController(); - addTearDown(() { - controller.dispose(); - }); - - expect(targetRects, isEmpty); - expect(hideCount, 0); - - // Showing calls the platform. - const Rect rect1 = Rect.fromLTWH(0.0, 0.0, 100.0, 100.0); - controller.show(rect1); - expect(targetRects, hasLength(1)); - expect(targetRects.last['x'], rect1.left); - expect(targetRects.last['y'], rect1.top); - expect(targetRects.last['width'], rect1.width); - expect(targetRects.last['height'], rect1.height); - - // Showing the same thing again does nothing. - controller.show(rect1); - expect(targetRects, hasLength(1)); - - // Showing a new rect calls the platform. - const Rect rect2 = Rect.fromLTWH(1.0, 1.0, 200.0, 200.0); - controller.show(rect2); - expect(targetRects, hasLength(2)); - expect(targetRects.last['x'], rect2.left); - expect(targetRects.last['y'], rect2.top); - expect(targetRects.last['width'], rect2.width); - expect(targetRects.last['height'], rect2.height); - - // Hiding calls the platform. - controller.hide(); - expect(hideCount, 1); - - // Hiding again does nothing. - controller.hide(); - expect(hideCount, 1); - - // Showing the last shown rect calls the platform. - controller.show(rect2); - expect(targetRects, hasLength(3)); - expect(targetRects.last['x'], rect2.left); - expect(targetRects.last['y'], rect2.top); - expect(targetRects.last['width'], rect2.width); - expect(targetRects.last['height'], rect2.height); - - controller.hide(); - expect(hideCount, 2); - }); - - test('the system can hide the menu with handleSystemHide', () async { - // Create an active connection, which is required to show the system menu. - final FakeTextInputClient client = FakeTextInputClient(const TextEditingValue(text: 'test1')); - final TextInputConnection connection = TextInput.attach(client, client.configuration); - addTearDown(() { - connection.close(); - }); - - final List> targetRects = >[]; - int hideCount = 0; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { - switch (methodCall.method) { - case 'ContextMenu.showSystemContextMenu': - final Map arguments = methodCall.arguments as Map; - final Map untypedTargetRect = arguments['targetRect'] as Map; - final Map lastTargetRect = untypedTargetRect.map((String key, dynamic value) { - return MapEntry(key, value as double); - }); - targetRects.add(lastTargetRect); - case 'ContextMenu.hideSystemContextMenu': - hideCount += 1; - } - return; - }); - addTearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, null); - }); - - int systemHideCount = 0; - final SystemContextMenuController controller = SystemContextMenuController( - onSystemHide: () { - systemHideCount += 1; - }, - ); - addTearDown(() { - controller.dispose(); - }); - - expect(targetRects, isEmpty); - expect(hideCount, 0); - expect(systemHideCount, 0); - - // Showing calls the platform. - const Rect rect1 = Rect.fromLTWH(0.0, 0.0, 100.0, 100.0); - controller.show(rect1); - expect(targetRects, hasLength(1)); - expect(targetRects.last['x'], rect1.left); - expect(targetRects.last['y'], rect1.top); - expect(targetRects.last['width'], rect1.width); - expect(targetRects.last['height'], rect1.height); - - // If the system hides the menu, onSystemHide is called. - final ByteData? messageBytes = const JSONMessageCodec().encodeMessage({ - 'method': 'ContextMenu.onDismissSystemContextMenu', - }); - await binding.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/platform', - messageBytes, - (ByteData? data) {}, - ); - expect(hideCount, 0); - expect(systemHideCount, 1); - - // Hiding does not call the platform, since the menu was already hidden. - controller.hide(); - expect(hideCount, 0); - }); - - test('showing a second controller while one is visible is an error', () { - // Create an active connection, which is required to show the system menu. - final FakeTextInputClient client = FakeTextInputClient(const TextEditingValue(text: 'test1')); - final TextInputConnection connection = TextInput.attach(client, client.configuration); - addTearDown(() { - connection.close(); - }); - - final SystemContextMenuController controller1 = SystemContextMenuController(); - addTearDown(() { - controller1.dispose(); - }); - const Rect rect1 = Rect.fromLTWH(0.0, 0.0, 100.0, 100.0); - expect(() { controller1.show(rect1); }, isNot(throwsAssertionError)); - - final SystemContextMenuController controller2 = SystemContextMenuController(); - addTearDown(() { - controller2.dispose(); - }); - const Rect rect2 = Rect.fromLTWH(1.0, 1.0, 200.0, 200.0); - expect(() { controller2.show(rect2); }, throwsAssertionError); - - controller1.hide(); - }); - - test('showing and hiding two controllers', () { - // Create an active connection, which is required to show the system menu. - final FakeTextInputClient client = FakeTextInputClient(const TextEditingValue(text: 'test1')); - final TextInputConnection connection = TextInput.attach(client, client.configuration); - addTearDown(() { - connection.close(); - }); - - final List> targetRects = >[]; - int hideCount = 0; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { - switch (methodCall.method) { - case 'ContextMenu.showSystemContextMenu': - final Map arguments = methodCall.arguments as Map; - final Map untypedTargetRect = arguments['targetRect'] as Map; - final Map lastTargetRect = untypedTargetRect.map((String key, dynamic value) { - return MapEntry(key, value as double); - }); - targetRects.add(lastTargetRect); - case 'ContextMenu.hideSystemContextMenu': - hideCount += 1; - } - return; - }); - addTearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, null); - }); - - final SystemContextMenuController controller1 = SystemContextMenuController(); - addTearDown(() { - controller1.dispose(); - }); - - expect(targetRects, isEmpty); - expect(hideCount, 0); - - // Showing calls the platform. - const Rect rect1 = Rect.fromLTWH(0.0, 0.0, 100.0, 100.0); - controller1.show(rect1); - expect(targetRects, hasLength(1)); - expect(targetRects.last['x'], rect1.left); - - // Hiding calls the platform. - controller1.hide(); - expect(hideCount, 1); - - // Showing a new controller calls the platform. - final SystemContextMenuController controller2 = SystemContextMenuController(); - addTearDown(() { - controller2.dispose(); - }); - const Rect rect2 = Rect.fromLTWH(1.0, 1.0, 200.0, 200.0); - controller2.show(rect2); - expect(targetRects, hasLength(2)); - expect(targetRects.last['x'], rect2.left); - expect(targetRects.last['y'], rect2.top); - expect(targetRects.last['width'], rect2.width); - expect(targetRects.last['height'], rect2.height); - - // Hiding the old controller does nothing. - controller1.hide(); - expect(hideCount, 1); - - // Hiding the new controller calls the platform. - controller2.hide(); - expect(hideCount, 2); - }); -} diff --git a/packages/flutter/test/services/text_input_utils.dart b/packages/flutter/test/services/text_input_utils.dart index 3de6786540..e1644e5648 100644 --- a/packages/flutter/test/services/text_input_utils.dart +++ b/packages/flutter/test/services/text_input_utils.dart @@ -90,81 +90,3 @@ class FakeScribbleElement implements ScribbleClient { latestMethodCall = 'onScribbleFocus'; } } - -class FakeTextInputClient with TextInputClient { - FakeTextInputClient(this.currentTextEditingValue); - - String latestMethodCall = ''; - final List performedSelectors = []; - late Map? latestPrivateCommandData; - - @override - TextEditingValue currentTextEditingValue; - - @override - AutofillScope? get currentAutofillScope => null; - - @override - void performAction(TextInputAction action) { - latestMethodCall = 'performAction'; - } - - @override - void performPrivateCommand(String action, Map? data) { - latestMethodCall = 'performPrivateCommand'; - latestPrivateCommandData = data; - } - - @override - void insertContent(KeyboardInsertedContent content) { - latestMethodCall = 'commitContent'; - } - - @override - void updateEditingValue(TextEditingValue value) { - latestMethodCall = 'updateEditingValue'; - } - - @override - void updateFloatingCursor(RawFloatingCursorPoint point) { - latestMethodCall = 'updateFloatingCursor'; - } - - @override - void connectionClosed() { - latestMethodCall = 'connectionClosed'; - } - - @override - void showAutocorrectionPromptRect(int start, int end) { - latestMethodCall = 'showAutocorrectionPromptRect'; - } - - @override - void showToolbar() { - latestMethodCall = 'showToolbar'; - } - - TextInputConfiguration get configuration => const TextInputConfiguration(); - - @override - void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) { - latestMethodCall = 'didChangeInputControl'; - } - - @override - void insertTextPlaceholder(Size size) { - latestMethodCall = 'insertTextPlaceholder'; - } - - @override - void removeTextPlaceholder() { - latestMethodCall = 'removeTextPlaceholder'; - } - - @override - void performSelector(String selectorName) { - latestMethodCall = 'performSelector'; - performedSelectors.add(selectorName); - } -} diff --git a/packages/flutter/test/widgets/system_context_menu_test.dart b/packages/flutter/test/widgets/system_context_menu_test.dart deleted file mode 100644 index a7a7c474d2..0000000000 --- a/packages/flutter/test/widgets/system_context_menu_test.dart +++ /dev/null @@ -1,415 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('asserts when built on an unsupported device', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'one two three', - ); - await tester.pumpWidget( - // By default, MediaQueryData.supportsShowingSystemContextMenu is false. - MaterialApp( - home: Scaffold( - body: Center( - child: TextField( - controller: controller, - contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { - return SystemContextMenu.editableText( - editableTextState: editableTextState, - ); - }, - ), - ), - ), - ), - ); - - await tester.tap(find.byType(TextField)); - final EditableTextState state = tester.state(find.byType(EditableText)); - expect(state.showToolbar(), true); - await tester.pump(); - - expect(tester.takeException(), isAssertionError); - }, variant: TargetPlatformVariant.all()); - - testWidgets('can be shown and hidden like a normal context menu', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'one two three', - ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - final MediaQueryData mediaQueryData = MediaQuery.of(context); - return MediaQuery( - data: mediaQueryData.copyWith( - supportsShowingSystemContextMenu: true, - ), - child: MaterialApp( - home: Scaffold( - body: Center( - child: TextField( - controller: controller, - contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { - return SystemContextMenu.editableText( - editableTextState: editableTextState, - ); - }, - ), - ), - ), - ), - ); - }, - ), - ); - - expect(find.byType(SystemContextMenu), findsNothing); - - await tester.tap(find.byType(TextField)); - final EditableTextState state = tester.state(find.byType(EditableText)); - expect(state.showToolbar(), true); - await tester.pump(); - expect(find.byType(SystemContextMenu), findsOneWidget); - - state.hideToolbar(); - await tester.pump(); - expect(find.byType(SystemContextMenu), findsNothing); - }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); - - testWidgets('can be updated.', (WidgetTester tester) async { - final List> targetRects = >[]; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { - if (methodCall.method == 'ContextMenu.showSystemContextMenu') { - final Map arguments = methodCall.arguments as Map; - final Map untypedTargetRect = arguments['targetRect'] as Map; - final Map lastTargetRect = untypedTargetRect.map((String key, dynamic value) { - return MapEntry(key, value as double); - }); - targetRects.add(lastTargetRect); - } - return; - }); - addTearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, null); - }); - - final TextEditingController controller = TextEditingController( - text: 'one two three', - ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - final MediaQueryData mediaQueryData = MediaQuery.of(context); - return MediaQuery( - data: mediaQueryData.copyWith( - supportsShowingSystemContextMenu: true, - ), - child: MaterialApp( - home: Scaffold( - body: Center( - child: TextField( - controller: controller, - contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { - return SystemContextMenu.editableText( - editableTextState: editableTextState, - ); - }, - ), - ), - ), - ), - ); - }, - ), - ); - - expect(targetRects, isEmpty); - - await tester.tap(find.byType(TextField)); - final EditableTextState state = tester.state(find.byType(EditableText)); - expect(state.showToolbar(), true); - await tester.pump(); - - expect(targetRects, hasLength(1)); - expect(targetRects.last, containsPair('width', 0.0)); - - controller.selection = const TextSelection( - baseOffset: 4, - extentOffset: 7, - ); - await tester.pumpAndSettle(); - - expect(targetRects, hasLength(2)); - expect(targetRects.last['width'], greaterThan(0.0)); - }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); - - testWidgets('can be rebuilt', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'one two three', - ); - late StateSetter setState; - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - final MediaQueryData mediaQueryData = MediaQuery.of(context); - return MediaQuery( - data: mediaQueryData.copyWith( - supportsShowingSystemContextMenu: true, - ), - child: MaterialApp( - home: Scaffold( - body: Center( - child: StatefulBuilder( - builder: (BuildContext context, StateSetter localSetState) { - setState = localSetState; - return TextField( - controller: controller, - contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { - return SystemContextMenu.editableText( - editableTextState: editableTextState, - ); - }, - ); - }, - ), - ), - ), - ), - ); - }, - ), - ); - - await tester.tap(find.byType(TextField)); - final EditableTextState state = tester.state(find.byType(EditableText)); - expect(state.showToolbar(), true); - await tester.pump(); - - setState(() {}); - await tester.pumpAndSettle(); - - expect(tester.takeException(), isNull); - }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); - - testWidgets('can handle multiple instances', (WidgetTester tester) async { - final TextEditingController controller1 = TextEditingController( - text: 'one two three', - ); - final TextEditingController controller2 = TextEditingController( - text: 'four five six', - ); - final GlobalKey field1Key = GlobalKey(); - final GlobalKey field2Key = GlobalKey(); - final GlobalKey menu1Key = GlobalKey(); - final GlobalKey menu2Key = GlobalKey(); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - final MediaQueryData mediaQueryData = MediaQuery.of(context); - return MediaQuery( - data: mediaQueryData.copyWith( - supportsShowingSystemContextMenu: true, - ), - child: MaterialApp( - home: Scaffold( - body: Center( - child: Column( - children: [ - TextField( - key: field1Key, - controller: controller1, - contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { - return SystemContextMenu.editableText( - key: menu1Key, - editableTextState: editableTextState, - ); - }, - ), - TextField( - key: field2Key, - controller: controller2, - contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { - return SystemContextMenu.editableText( - key: menu2Key, - editableTextState: editableTextState, - ); - }, - ), - ], - ), - ), - ), - ), - ); - }, - ), - ); - - expect(find.byType(SystemContextMenu), findsNothing); - - await tester.tap(find.byKey(field1Key)); - final EditableTextState state1 = tester.state( - find.descendant( - of: find.byKey(field1Key), - matching: find.byType(EditableText), - ), - ); - expect(state1.showToolbar(), true); - await tester.pump(); - expect(find.byKey(menu1Key), findsOneWidget); - expect(find.byKey(menu2Key), findsNothing); - - // In a real app, this message is sent by iOS when the user taps anywhere - // outside of the system context menu. - final ByteData? messageBytes = const JSONMessageCodec().encodeMessage({ - 'method': 'ContextMenu.onDismissSystemContextMenu', - }); - await binding.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/platform', - messageBytes, - (ByteData? data) {}, - ); - await tester.pump(); - expect(find.byType(SystemContextMenu), findsNothing); - - await tester.tap(find.byKey(field2Key)); - final EditableTextState state2 = tester.state( - find.descendant( - of: find.byKey(field2Key), - matching: find.byType(EditableText), - ), - ); - expect(state2.showToolbar(), true); - await tester.pump(); - expect(find.byKey(menu1Key), findsNothing); - expect(find.byKey(menu2Key), findsOneWidget); - }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); - - testWidgets('asserts when built with no text input connection', (WidgetTester tester) async { - SystemContextMenu? systemContextMenu; - late StateSetter setState; - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - final MediaQueryData mediaQueryData = MediaQuery.of(context); - return MediaQuery( - data: mediaQueryData.copyWith( - supportsShowingSystemContextMenu: true, - ), - child: MaterialApp( - home: Scaffold( - body: StatefulBuilder( - builder: (BuildContext context, StateSetter localSetState) { - setState = localSetState; - return Column( - children: [ - const TextField(), - if (systemContextMenu != null) - systemContextMenu!, - ], - ); - }, - ), - ), - ), - ); - }, - ), - ); - - // No SystemContextMenu yet, so no assertion error. - expect(tester.takeException(), isNull); - - // Add the SystemContextMenu and receive an assertion since there is no - // active text input connection. - setState(() { - final EditableTextState state = tester.state(find.byType(EditableText)); - systemContextMenu = SystemContextMenu.editableText( - editableTextState: state, - ); - }); - - final FlutterExceptionHandler? oldHandler = FlutterError.onError; - dynamic exception; - FlutterError.onError = (FlutterErrorDetails details) { - exception ??= details.exception; - }; - addTearDown(() { - FlutterError.onError = oldHandler; - }); - - await tester.pump(); - expect(exception, isAssertionError); - expect(exception.toString(), contains('only be shown for an active text input connection')); - }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); - - testWidgets('does not assert when built with an active text input connection', (WidgetTester tester) async { - SystemContextMenu? systemContextMenu; - late StateSetter setState; - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - final MediaQueryData mediaQueryData = MediaQuery.of(context); - return MediaQuery( - data: mediaQueryData.copyWith( - supportsShowingSystemContextMenu: true, - ), - child: MaterialApp( - home: Scaffold( - body: StatefulBuilder( - builder: (BuildContext context, StateSetter localSetState) { - setState = localSetState; - return Column( - children: [ - const TextField(), - if (systemContextMenu != null) - systemContextMenu!, - ], - ); - }, - ), - ), - ), - ); - }, - ), - ); - - // No SystemContextMenu yet, so no assertion error. - expect(tester.takeException(), isNull); - - // Tap the field to open a text input connection. - await tester.tap(find.byType(TextField)); - await tester.pump(); - - // Add the SystemContextMenu and expect no error. - setState(() { - final EditableTextState state = tester.state(find.byType(EditableText)); - systemContextMenu = SystemContextMenu.editableText( - editableTextState: state, - ); - }); - - final FlutterExceptionHandler? oldHandler = FlutterError.onError; - dynamic exception; - FlutterError.onError = (FlutterErrorDetails details) { - exception ??= details.exception; - }; - addTearDown(() { - FlutterError.onError = oldHandler; - }); - - await tester.pump(); - expect(exception, isNull); - }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); -} diff --git a/packages/flutter_test/lib/src/window.dart b/packages/flutter_test/lib/src/window.dart index b108c0588e..e94d6de46e 100644 --- a/packages/flutter_test/lib/src/window.dart +++ b/packages/flutter_test/lib/src/window.dart @@ -307,18 +307,6 @@ class TestPlatformDispatcher implements PlatformDispatcher { _nativeSpellCheckServiceDefinedTestValue = null; } - @override - bool get supportsShowingSystemContextMenu => _supportsShowingSystemContextMenu ?? _platformDispatcher.supportsShowingSystemContextMenu; - bool? _supportsShowingSystemContextMenu; - set supportsShowingSystemContextMenu(bool value) { // ignore: avoid_setters_without_getters - _supportsShowingSystemContextMenu = value; - } - - /// Resets [supportsShowingSystemContextMenu] to the default value. - void resetSupportsShowingSystemContextMenu() { - _supportsShowingSystemContextMenu = null; - } - @override bool get brieflyShowPassword => _brieflyShowPasswordTestValue ?? _platformDispatcher.brieflyShowPassword; bool? _brieflyShowPasswordTestValue; @@ -470,7 +458,6 @@ class TestPlatformDispatcher implements PlatformDispatcher { clearTextScaleFactorTestValue(); clearNativeSpellCheckServiceDefined(); resetBrieflyShowPassword(); - resetSupportsShowingSystemContextMenu(); resetInitialLifecycleState(); resetSystemFontFamily(); }