mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Reverts: flutter/flutter#148238 Initiated by: zanderso Reason for reverting: Failures in post submit https://logs.chromium.org/logs/flutter/buildbucket/cr-buildbucket/8748025189669617649/+/u/run_test.dart_for_web_canvaskit_tests_shard_and_subshard_3/stdout Original PR Author: justinmc Reviewed By: {hellohuanlin} This change reverts the following previous change: Reland of https://github.com/flutter/flutter/pull/143002, which was reverted in https://github.com/flutter/flutter/pull/148237 due to unresolved docs references. Not sure why those weren't caught in presubmit. ``` 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/flutter/lib/src/widgets/media_query.dart:579:14) dartdoc:stderr: in documentation inherited from widgets.MediaQueryData.supportsShowingSystemContextMenu: (file:///b/s/w/ir/x/w/flutter/packages/flutter/lib/src/widgets/media_query.dart:579:14) dartdoc:stderr: error: unresolved doc reference [showSystemContextMenu] dartdoc:stderr: from services.SystemContextMenuController.hide: (file:///b/s/w/ir/x/w/flutter/packages/flutter/lib/src/services/text_input.dart:2554:16) dartdoc:stderr: error: unresolved doc reference [hideSystemContextMenu] dartdoc:stderr: from services.SystemContextMenuController.show: (file:///b/s/w/ir/x/w/flutter/packages/flutter/lib/src/services/text_input.dart:2509:16) ```
This commit is contained in:
parent
42b3d9eb7f
commit
45e80fd63b
@ -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 {
|
Future<dynamic> _handlePlatformMessage(MethodCall methodCall) async {
|
||||||
final String method = methodCall.method;
|
final String method = methodCall.method;
|
||||||
|
assert(method == 'SystemChrome.systemUIChange' || method == 'System.requestAppExit');
|
||||||
switch (method) {
|
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':
|
case 'SystemChrome.systemUIChange':
|
||||||
final List<dynamic> args = methodCall.arguments as List<dynamic>;
|
final List<dynamic> args = methodCall.arguments as List<dynamic>;
|
||||||
if (_systemUiChangeCallback != null) {
|
if (_systemUiChangeCallback != null) {
|
||||||
@ -372,8 +366,6 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
|
|||||||
}
|
}
|
||||||
case 'System.requestAppExit':
|
case 'System.requestAppExit':
|
||||||
return <String, dynamic>{'response': (await handleRequestAppExit()).name};
|
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 {
|
Future<void> initializationComplete() async {
|
||||||
await SystemChannels.platform.invokeMethod('System.initializationComplete');
|
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].
|
/// 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 'package:vector_math/vector_math_64.dart' show Matrix4;
|
||||||
|
|
||||||
import 'autofill.dart';
|
import 'autofill.dart';
|
||||||
import 'binding.dart';
|
|
||||||
import 'clipboard.dart' show Clipboard;
|
import 'clipboard.dart' show Clipboard;
|
||||||
import 'keyboard_inserted_content.dart';
|
import 'keyboard_inserted_content.dart';
|
||||||
import 'message_codec.dart';
|
import 'message_codec.dart';
|
||||||
@ -1809,7 +1808,7 @@ class TextInput {
|
|||||||
|
|
||||||
Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
|
Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
|
||||||
final String method = methodCall.method;
|
final String method = methodCall.method;
|
||||||
switch (method) {
|
switch (methodCall.method) {
|
||||||
case 'TextInputClient.focusElement':
|
case 'TextInputClient.focusElement':
|
||||||
final List<dynamic> args = methodCall.arguments as List<dynamic>;
|
final List<dynamic> args = methodCall.arguments as List<dynamic>;
|
||||||
_scribbleClients[args[0]]?.onScribbleFocus(Offset((args[1] as num).toDouble(), (args[2] as num).toDouble()));
|
_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:
|
|
||||||
///
|
|
||||||
/// * [hide], 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:
|
|
||||||
///
|
|
||||||
/// * [show], 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
|
/// Gets the line heights at the start and end of the selection for the given
|
||||||
/// [EditableTextState].
|
/// [EditableTextState].
|
||||||
///
|
_GlyphHeights _getGlyphHeights() {
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [TextSelectionToolbarAnchors.getSelectionRect], which depends on this
|
|
||||||
/// information.
|
|
||||||
({double startGlyphHeight, double endGlyphHeight}) getGlyphHeights() {
|
|
||||||
final TextSelection selection = textEditingValue.selection;
|
final TextSelection selection = textEditingValue.selection;
|
||||||
|
|
||||||
// Only calculate handle rects if the text in the previous frame
|
// 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 prevText = span.toPlainText();
|
||||||
final String currText = textEditingValue.text;
|
final String currText = textEditingValue.text;
|
||||||
if (prevText != currText || !selection.isValid || selection.isCollapsed) {
|
if (prevText != currText || !selection.isValid || selection.isCollapsed) {
|
||||||
return (
|
return _GlyphHeights(
|
||||||
startGlyphHeight: renderEditable.preferredLineHeight,
|
start: renderEditable.preferredLineHeight,
|
||||||
endGlyphHeight: renderEditable.preferredLineHeight,
|
end: renderEditable.preferredLineHeight,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2822,9 +2817,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
start: selection.end - lastSelectedGraphemeExtent,
|
start: selection.end - lastSelectedGraphemeExtent,
|
||||||
end: selection.end,
|
end: selection.end,
|
||||||
));
|
));
|
||||||
return (
|
return _GlyphHeights(
|
||||||
startGlyphHeight: startCharacterRect?.height ?? renderEditable.preferredLineHeight,
|
start: startCharacterRect?.height ?? renderEditable.preferredLineHeight,
|
||||||
endGlyphHeight: endCharacterRect?.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 TextSelection selection = textEditingValue.selection;
|
||||||
final List<TextSelectionPoint> points =
|
final List<TextSelectionPoint> points =
|
||||||
renderEditable.getEndpointsForSelection(selection);
|
renderEditable.getEndpointsForSelection(selection);
|
||||||
return TextSelectionToolbarAnchors.fromSelection(
|
return TextSelectionToolbarAnchors.fromSelection(
|
||||||
renderBox: renderEditable,
|
renderBox: renderEditable,
|
||||||
startGlyphHeight: startGlyphHeight,
|
startGlyphHeight: glyphHeights.start,
|
||||||
endGlyphHeight: endGlyphHeight,
|
endGlyphHeight: glyphHeights.end,
|
||||||
selectionEndpoints: points,
|
selectionEndpoints: points,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -6031,6 +6026,21 @@ class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> {
|
|||||||
bool get isActionEnabled => state._value.selection.isValid && !state._value.selection.isCollapsed;
|
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
|
/// A [ClipboardStatusNotifier] whose [value] is hardcoded to
|
||||||
/// [ClipboardStatus.pasteable].
|
/// [ClipboardStatus.pasteable].
|
||||||
///
|
///
|
||||||
|
@ -74,8 +74,6 @@ enum _MediaQueryAspect {
|
|||||||
gestureSettings,
|
gestureSettings,
|
||||||
/// Specifies the aspect corresponding to [MediaQueryData.displayFeatures].
|
/// Specifies the aspect corresponding to [MediaQueryData.displayFeatures].
|
||||||
displayFeatures,
|
displayFeatures,
|
||||||
/// Specifies the aspect corresponding to [MediaQueryData.supportsShowingSystemContextMenu].
|
|
||||||
supportsShowingSystemContextMenu,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information about a piece of media (e.g., a window).
|
/// Information about a piece of media (e.g., a window).
|
||||||
@ -175,7 +173,6 @@ class MediaQueryData {
|
|||||||
this.navigationMode = NavigationMode.traditional,
|
this.navigationMode = NavigationMode.traditional,
|
||||||
this.gestureSettings = const DeviceGestureSettings(touchSlop: kTouchSlop),
|
this.gestureSettings = const DeviceGestureSettings(touchSlop: kTouchSlop),
|
||||||
this.displayFeatures = const <ui.DisplayFeature>[],
|
this.displayFeatures = const <ui.DisplayFeature>[],
|
||||||
this.supportsShowingSystemContextMenu = false,
|
|
||||||
}) : _textScaleFactor = textScaleFactor,
|
}) : _textScaleFactor = textScaleFactor,
|
||||||
_textScaler = textScaler,
|
_textScaler = textScaler,
|
||||||
assert(
|
assert(
|
||||||
@ -253,8 +250,7 @@ class MediaQueryData {
|
|||||||
alwaysUse24HourFormat = platformData?.alwaysUse24HourFormat ?? view.platformDispatcher.alwaysUse24HourFormat,
|
alwaysUse24HourFormat = platformData?.alwaysUse24HourFormat ?? view.platformDispatcher.alwaysUse24HourFormat,
|
||||||
navigationMode = platformData?.navigationMode ?? NavigationMode.traditional,
|
navigationMode = platformData?.navigationMode ?? NavigationMode.traditional,
|
||||||
gestureSettings = DeviceGestureSettings.fromView(view),
|
gestureSettings = DeviceGestureSettings.fromView(view),
|
||||||
displayFeatures = view.displayFeatures,
|
displayFeatures = view.displayFeatures;
|
||||||
supportsShowingSystemContextMenu = platformData?.supportsShowingSystemContextMenu ?? view.platformDispatcher.supportsShowingSystemContextMenu;
|
|
||||||
|
|
||||||
static TextScaler _textScalerFromView(ui.FlutterView view, MediaQueryData? platformData) {
|
static TextScaler _textScalerFromView(ui.FlutterView view, MediaQueryData? platformData) {
|
||||||
final double scaleFactor = platformData?.textScaleFactor ?? view.platformDispatcher.textScaleFactor;
|
final double scaleFactor = platformData?.textScaleFactor ?? view.platformDispatcher.textScaleFactor;
|
||||||
@ -566,19 +562,6 @@ class MediaQueryData {
|
|||||||
/// [dart:ui.DisplayFeatureType.hinge]).
|
/// [dart:ui.DisplayFeatureType.hinge]).
|
||||||
final List<ui.DisplayFeature> displayFeatures;
|
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:
|
|
||||||
///
|
|
||||||
/// * [SystemContextMenuController] and [SystemContextMenu], 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
|
/// The orientation of the media (e.g., whether the device is in landscape or
|
||||||
/// portrait mode).
|
/// portrait mode).
|
||||||
Orientation get orientation {
|
Orientation get orientation {
|
||||||
@ -615,7 +598,6 @@ class MediaQueryData {
|
|||||||
NavigationMode? navigationMode,
|
NavigationMode? navigationMode,
|
||||||
DeviceGestureSettings? gestureSettings,
|
DeviceGestureSettings? gestureSettings,
|
||||||
List<ui.DisplayFeature>? displayFeatures,
|
List<ui.DisplayFeature>? displayFeatures,
|
||||||
bool? supportsShowingSystemContextMenu,
|
|
||||||
}) {
|
}) {
|
||||||
assert(textScaleFactor == null || textScaler == null);
|
assert(textScaleFactor == null || textScaler == null);
|
||||||
if (textScaleFactor != null) {
|
if (textScaleFactor != null) {
|
||||||
@ -640,7 +622,6 @@ class MediaQueryData {
|
|||||||
navigationMode: navigationMode ?? this.navigationMode,
|
navigationMode: navigationMode ?? this.navigationMode,
|
||||||
gestureSettings: gestureSettings ?? this.gestureSettings,
|
gestureSettings: gestureSettings ?? this.gestureSettings,
|
||||||
displayFeatures: displayFeatures ?? this.displayFeatures,
|
displayFeatures: displayFeatures ?? this.displayFeatures,
|
||||||
supportsShowingSystemContextMenu: supportsShowingSystemContextMenu ?? this.supportsShowingSystemContextMenu,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -833,8 +814,7 @@ class MediaQueryData {
|
|||||||
&& other.boldText == boldText
|
&& other.boldText == boldText
|
||||||
&& other.navigationMode == navigationMode
|
&& other.navigationMode == navigationMode
|
||||||
&& other.gestureSettings == gestureSettings
|
&& other.gestureSettings == gestureSettings
|
||||||
&& listEquals(other.displayFeatures, displayFeatures)
|
&& listEquals(other.displayFeatures, displayFeatures);
|
||||||
&& other.supportsShowingSystemContextMenu == supportsShowingSystemContextMenu;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -856,7 +836,6 @@ class MediaQueryData {
|
|||||||
navigationMode,
|
navigationMode,
|
||||||
gestureSettings,
|
gestureSettings,
|
||||||
Object.hashAll(displayFeatures),
|
Object.hashAll(displayFeatures),
|
||||||
supportsShowingSystemContextMenu,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -880,7 +859,6 @@ class MediaQueryData {
|
|||||||
'navigationMode: ${navigationMode.name}',
|
'navigationMode: ${navigationMode.name}',
|
||||||
'gestureSettings: $gestureSettings',
|
'gestureSettings: $gestureSettings',
|
||||||
'displayFeatures: $displayFeatures',
|
'displayFeatures: $displayFeatures',
|
||||||
'supportsShowingSystemContextMenu: $supportsShowingSystemContextMenu',
|
|
||||||
];
|
];
|
||||||
return '${objectRuntimeType(this, 'MediaQueryData')}(${properties.join(', ')})';
|
return '${objectRuntimeType(this, 'MediaQueryData')}(${properties.join(', ')})';
|
||||||
}
|
}
|
||||||
@ -1653,26 +1631,6 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> {
|
|||||||
/// {@macro flutter.widgets.media_query.MediaQuery.dontUseMaybeOf}
|
/// {@macro flutter.widgets.media_query.MediaQuery.dontUseMaybeOf}
|
||||||
static List<ui.DisplayFeature>? maybeDisplayFeaturesOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.displayFeatures)?.displayFeatures;
|
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
|
@override
|
||||||
bool updateShouldNotify(MediaQuery oldWidget) => data != oldWidget.data;
|
bool updateShouldNotify(MediaQuery oldWidget) => data != oldWidget.data;
|
||||||
|
|
||||||
@ -1705,7 +1663,6 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> {
|
|||||||
_MediaQueryAspect.systemGestureInsets => data.systemGestureInsets != oldWidget.data.systemGestureInsets,
|
_MediaQueryAspect.systemGestureInsets => data.systemGestureInsets != oldWidget.data.systemGestureInsets,
|
||||||
_MediaQueryAspect.accessibleNavigation => data.accessibleNavigation != oldWidget.data.accessibleNavigation,
|
_MediaQueryAspect.accessibleNavigation => data.accessibleNavigation != oldWidget.data.accessibleNavigation,
|
||||||
_MediaQueryAspect.alwaysUse24HourFormat => data.alwaysUse24HourFormat != oldWidget.data.alwaysUse24HourFormat,
|
_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 double endGlyphHeight,
|
||||||
required List<TextSelectionPoint> selectionEndpoints,
|
required List<TextSelectionPoint> selectionEndpoints,
|
||||||
}) {
|
}) {
|
||||||
final Rect selectionRect = getSelectionRect(
|
final Rect editingRegion = Rect.fromPoints(
|
||||||
renderBox,
|
renderBox.localToGlobal(Offset.zero),
|
||||||
startGlyphHeight,
|
renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero)),
|
||||||
endGlyphHeight,
|
|
||||||
selectionEndpoints,
|
|
||||||
);
|
);
|
||||||
if (selectionRect == Rect.zero) {
|
|
||||||
|
if (editingRegion.left.isNaN || editingRegion.top.isNaN
|
||||||
|
|| editingRegion.right.isNaN || editingRegion.bottom.isNaN) {
|
||||||
return const TextSelectionToolbarAnchors(primaryAnchor: Offset.zero);
|
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(
|
return TextSelectionToolbarAnchors(
|
||||||
primaryAnchor: Offset(
|
primaryAnchor: Offset(
|
||||||
selectionRect.left + selectionRect.width / 2,
|
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.
|
/// The location that the toolbar should attempt to position itself at.
|
||||||
///
|
///
|
||||||
/// If the toolbar doesn't fit at this location, use [secondaryAnchor] if it
|
/// 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/spacer.dart';
|
||||||
export 'src/widgets/spell_check.dart';
|
export 'src/widgets/spell_check.dart';
|
||||||
export 'src/widgets/status_transitions.dart';
|
export 'src/widgets/status_transitions.dart';
|
||||||
export 'src/widgets/system_context_menu.dart';
|
|
||||||
export 'src/widgets/table.dart';
|
export 'src/widgets/table.dart';
|
||||||
export 'src/widgets/tap_region.dart';
|
export 'src/widgets/tap_region.dart';
|
||||||
export 'src/widgets/text.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';
|
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;
|
_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
|
@override
|
||||||
bool get brieflyShowPassword => _brieflyShowPasswordTestValue ?? _platformDispatcher.brieflyShowPassword;
|
bool get brieflyShowPassword => _brieflyShowPasswordTestValue ?? _platformDispatcher.brieflyShowPassword;
|
||||||
bool? _brieflyShowPasswordTestValue;
|
bool? _brieflyShowPasswordTestValue;
|
||||||
@ -470,7 +458,6 @@ class TestPlatformDispatcher implements PlatformDispatcher {
|
|||||||
clearTextScaleFactorTestValue();
|
clearTextScaleFactorTestValue();
|
||||||
clearNativeSpellCheckServiceDefined();
|
clearNativeSpellCheckServiceDefined();
|
||||||
resetBrieflyShowPassword();
|
resetBrieflyShowPassword();
|
||||||
resetSupportsShowingSystemContextMenu();
|
|
||||||
resetInitialLifecycleState();
|
resetInitialLifecycleState();
|
||||||
resetSystemFontFamily();
|
resetSystemFontFamily();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user