Skip to content

Commit

Permalink
Reverts "Native ios context menu (#143002)" (#148237)
Browse files Browse the repository at this point in the history
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 (flutter/flutter#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 flutter/engine#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.
  • Loading branch information
auto-submit[bot] authored May 13, 2024
1 parent 1255435 commit 14d88ee
Show file tree
Hide file tree
Showing 13 changed files with 43 additions and 1,321 deletions.

This file was deleted.

This file was deleted.

43 changes: 1 addition & 42 deletions packages/flutter/lib/src/services/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -357,23 +357,15 @@ 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) {
await _systemUiChangeCallback!(args[0] as bool);
}
case 'System.requestAppExit':
return <String, dynamic>{'response': (await handleRequestAppExit()).name};
default:
throw AssertionError('Method "$method" not handled.');
}
}

Expand Down Expand Up @@ -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].
Expand Down Expand Up @@ -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();
}
178 changes: 1 addition & 177 deletions packages/flutter/lib/src/services/text_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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()));
Expand Down Expand Up @@ -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;
}
}
Loading

0 comments on commit 14d88ee

Please sign in to comment.