mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Reverts: flutter/flutter#143002 Initiated by: cbracken Reason for reverting: unresolved docs links. See failure here: https://ci.chromium.org/ui/p/flutter/builders/prod/Linux%20docs_test/16540/overview ``` dartdoc:stdout: Generating docs for package flutter... dartdoc:stderr: error: unresolved doc reference [TextInput.showSystemContextMenu] dartdoc:stderr: from widgets.MediaQueryData.supportsShowingSystemContextMenu: (file:///b/s/w/ir/x/w/flutter/packages/flutt Original PR Author: justinmc Reviewed By: {Renzo-Olivares, hellohuanlin} This change reverts the following previous change: In order to work around the fact that iOS 15 shows a notification on pressing Flutter's paste button (https://github.com/flutter/flutter/issues/103163), this PR allows showing the iOS system context menu in text fields. <img width="385" alt="Screenshot 2024-02-06 at 11 52 25 AM" src="https://github.com/flutter/flutter/assets/389558/d82e18ee-b8a3-4082-9225-cf47fa7f3674"> It is currently opt-in, which a user would typically do like this (also in example system_context_menu.0.dart): ```dart 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, ); }, ``` Requires engine PR https://github.com/flutter/engine/pull/50095. ## API changes ### SystemContextMenu A widget that shows the system context menu when built, and removes it when disposed. The main high-level way that most users would use this PR. Only works on later versions of iOS. ### SystemContextMenuController Used under the hood to hide and show a system context menu. There can only be one visible at a time. ### MediaQuery.supportsShowingSystemContextMenu Sent by the iOS embedder to tell the framework whether or not the platform supports showing the system context menu. That way the framework, or Flutter developers, can decide to show a different menu. ### `flutter/platform ContextMenu.showSystemContextMenu` Sent by the framework to show the menu at a given `targetRect`, which is the current selection rect. ### `flutter/platform ContextMenu.hideSystemContextMenu` Sent by the framework to hide the menu. Typically not needed, because the platform will hide the menu when the user taps outside of it and after the user presses a button, but it handles edge cases where the user programmatically rebuilds the context menu, for example. ### `flutter/platform System.onDismissSystemContextMenu` Sent by the iOS embedder to indicate that the system context menu has been hidden by the system, such as when the user taps outside of the menu. This is useful when there are multiple instances of SystemContextMenu, such as with multiple text fields.
This commit is contained in:
parent
125543505d
commit
14d88ee0c8
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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<EditableTextState>(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<EditableTextState>(textFinder).showToolbar();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||||
expect(find.byType(SystemContextMenu), findsNothing);
|
||||
}, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended]
|
||||
}
|
@ -357,14 +357,8 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
|
||||
|
||||
Future<dynamic> _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<dynamic> args = methodCall.arguments as List<dynamic>;
|
||||
if (_systemUiChangeCallback != null) {
|
||||
@ -372,8 +366,6 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
|
||||
}
|
||||
case 'System.requestAppExit':
|
||||
return <String, dynamic>{'response': (await handleRequestAppExit()).name};
|
||||
default:
|
||||
throw AssertionError('Method "$method" not handled.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -518,19 +510,6 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
|
||||
Future<void> initializationComplete() async {
|
||||
await SystemChannels.platform.invokeMethod('System.initializationComplete');
|
||||
}
|
||||
|
||||
final Set<SystemContextMenuClient> _systemContextMenuClients = <SystemContextMenuClient>{};
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
@ -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<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
|
||||
final String method = methodCall.method;
|
||||
switch (method) {
|
||||
switch (methodCall.method) {
|
||||
case 'TextInputClient.focusElement':
|
||||
final List<dynamic> args = methodCall.arguments as List<dynamic>;
|
||||
_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<void> 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<void>.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<Map<String, dynamic>>(
|
||||
'ContextMenu.showSystemContextMenu',
|
||||
<String, dynamic>{
|
||||
'targetRect': <String, double>{
|
||||
'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<void> 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<void>(
|
||||
'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;
|
||||
}
|
||||
}
|
||||
|
@ -2786,12 +2786,7 @@ class EditableTextState extends State<EditableText> 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<EditableText> 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<EditableText> 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<EditableText> with AutomaticKeepAliveClien
|
||||
);
|
||||
}
|
||||
|
||||
final (startGlyphHeight: double startGlyphHeight, endGlyphHeight: double endGlyphHeight) = getGlyphHeights();
|
||||
final _GlyphHeights glyphHeights = _getGlyphHeights();
|
||||
final TextSelection selection = textEditingValue.selection;
|
||||
final List<TextSelectionPoint> 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<CopySelectionTextIntent> {
|
||||
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].
|
||||
///
|
||||
|
@ -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 <ui.DisplayFeature>[],
|
||||
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<ui.DisplayFeature> 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<ui.DisplayFeature>? 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<ui.DisplayFeature>? 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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<SystemContextMenu> createState() => _SystemContextMenuState();
|
||||
}
|
||||
|
||||
class _SystemContextMenuState extends State<SystemContextMenu> {
|
||||
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();
|
||||
}
|
||||
}
|
@ -30,17 +30,30 @@ class TextSelectionToolbarAnchors {
|
||||
required double endGlyphHeight,
|
||||
required List<TextSelectionPoint> 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<TextSelectionPoint> 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
|
||||
|
@ -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';
|
||||
|
@ -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<Map<String, double>> targetRects = <Map<String, double>>[];
|
||||
int hideCount = 0;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||
switch (methodCall.method) {
|
||||
case 'ContextMenu.showSystemContextMenu':
|
||||
final Map<String, dynamic> arguments = methodCall.arguments as Map<String, dynamic>;
|
||||
final Map<String, dynamic> untypedTargetRect = arguments['targetRect'] as Map<String, dynamic>;
|
||||
final Map<String, double> lastTargetRect = untypedTargetRect.map((String key, dynamic value) {
|
||||
return MapEntry<String, double>(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<Map<String, double>> targetRects = <Map<String, double>>[];
|
||||
int hideCount = 0;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||
switch (methodCall.method) {
|
||||
case 'ContextMenu.showSystemContextMenu':
|
||||
final Map<String, dynamic> arguments = methodCall.arguments as Map<String, dynamic>;
|
||||
final Map<String, dynamic> untypedTargetRect = arguments['targetRect'] as Map<String, dynamic>;
|
||||
final Map<String, double> lastTargetRect = untypedTargetRect.map((String key, dynamic value) {
|
||||
return MapEntry<String, double>(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(<String, dynamic>{
|
||||
'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<Map<String, double>> targetRects = <Map<String, double>>[];
|
||||
int hideCount = 0;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||
switch (methodCall.method) {
|
||||
case 'ContextMenu.showSystemContextMenu':
|
||||
final Map<String, dynamic> arguments = methodCall.arguments as Map<String, dynamic>;
|
||||
final Map<String, dynamic> untypedTargetRect = arguments['targetRect'] as Map<String, dynamic>;
|
||||
final Map<String, double> lastTargetRect = untypedTargetRect.map((String key, dynamic value) {
|
||||
return MapEntry<String, double>(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);
|
||||
});
|
||||
}
|
@ -90,81 +90,3 @@ class FakeScribbleElement implements ScribbleClient {
|
||||
latestMethodCall = 'onScribbleFocus';
|
||||
}
|
||||
}
|
||||
|
||||
class FakeTextInputClient with TextInputClient {
|
||||
FakeTextInputClient(this.currentTextEditingValue);
|
||||
|
||||
String latestMethodCall = '';
|
||||
final List<String> performedSelectors = <String>[];
|
||||
late Map<String, dynamic>? latestPrivateCommandData;
|
||||
|
||||
@override
|
||||
TextEditingValue currentTextEditingValue;
|
||||
|
||||
@override
|
||||
AutofillScope? get currentAutofillScope => null;
|
||||
|
||||
@override
|
||||
void performAction(TextInputAction action) {
|
||||
latestMethodCall = 'performAction';
|
||||
}
|
||||
|
||||
@override
|
||||
void performPrivateCommand(String action, Map<String, dynamic>? 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);
|
||||
}
|
||||
}
|
||||
|
@ -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<EditableTextState>(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<EditableTextState>(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<Map<String, double>> targetRects = <Map<String, double>>[];
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||
if (methodCall.method == 'ContextMenu.showSystemContextMenu') {
|
||||
final Map<String, dynamic> arguments = methodCall.arguments as Map<String, dynamic>;
|
||||
final Map<String, dynamic> untypedTargetRect = arguments['targetRect'] as Map<String, dynamic>;
|
||||
final Map<String, double> lastTargetRect = untypedTargetRect.map((String key, dynamic value) {
|
||||
return MapEntry<String, double>(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<EditableTextState>(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<EditableTextState>(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: <Widget>[
|
||||
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<EditableTextState>(
|
||||
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(<String, dynamic>{
|
||||
'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<EditableTextState>(
|
||||
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: <Widget>[
|
||||
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<EditableTextState>(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: <Widget>[
|
||||
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<EditableTextState>(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));
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user