Skip to content

Commit

Permalink
Secure paste milestone 2 (flutter#159013)
Browse files Browse the repository at this point in the history
Implements the framework side of secure paste milestone 2, where the iOS
system context menu items can be customized.

Depends on PR flutter#161103. Currently
I've merged that PR into this one for testing, but I think that PR
should merge separately first.

### Widget API (most users)

```dart
TextField(
  contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
    return SystemContextMenu.editableText(
      editableTextState: editableTextState,
      // items is optional
      items: <IOSSystemContextMenuItem>[
        const IOSSystemContextMenuItemCut(),
        constIOS SystemContextMenuItemCopy(),
        const IOSSystemContextMenuItemPaste(),
        const IOSSystemContextMenuItemSelectAll(),
        const IOSSystemContextMenuItemSearchWeb(
          title: 'Search!', // title is optional for this button, defaults to localized string
        ),
        // Milestone 3:
        IOSSystemContextMenuItemCustom(
          // title and onPressed are required
          title: 'custom button',
          onPressed: () {
            print('pressed the custom button.');
          }
        ),
      ],
    );
  },
),
```

### Raw Controller API

```dart

_systemContextMenuController.show(
  widget.anchor,
  <IOSSystemContextMenuItemData>[
    // Notice these are different classes than those used for the widget. That's
    // mainly because I can't provide localized defaults here, so the titles are
    // required in the classes that have titles.
    const IOSSystemContextMenuItemDataCut(),
    const IOSSystemContextMenuItemDataCopy(),
    const IOSSystemContextMenuItemDataPaste(),
    const IOSSystemContextMenuItemDataSelectAll(),
    const IOSSystemContextMenuItemDataSearchWeb(
      title: 'Search!', // title is required.
    ),
    // Milestone 3:
    IOSSystemContextMenuItemDataCustom(
      // title and onPressed are required as before.
      title: 'custom button',
      onPressed: () {
        print('pressed the custom button.');
      }
    ),
  ],
);

```

<details>

<summary>Json format</summary>

```dart
    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,
        },
        'items': <dynamic>[
          <String, dynamic>{
            'type': 'default',
            'action': 'paste',
          },
          <String, dynamic>{
            'type': 'default',
            'action': 'copy',
          },
          <String, dynamic>{
            'type': 'default',
            'title': 'Crazy Title',
            'action': 'share',
          },
        ],
      },
    );
```

</summary>

</details>

### Localization changes
This change requires the SystemContextMenu widget in the widgets library
to be able to look up the default localized label for several context
menu buttons like "Copy", etc. Those strings previously resided in
MaterialLocalizations and CupertinoLocalizations, but not in
WidgetsLocalizations, so I have copied the necessary strings into
WidgetsLocalizations.

---------

Co-authored-by: Huan Lin <[email protected]>
  • Loading branch information
justinmc and hellohuanlin authored Feb 21, 2025
1 parent 043b719 commit df65e46
Show file tree
Hide file tree
Showing 124 changed files with 4,034 additions and 309 deletions.
30 changes: 18 additions & 12 deletions packages/flutter/lib/src/services/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -413,9 +413,15 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
// 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();
if (_systemContextMenuClient == null) {
assert(
false,
'Platform sent onDismissSystemContextMenu when no SystemContextMenuClient was registered.',
);
return;
}
_systemContextMenuClient!.handleSystemHide();
_systemContextMenuClient = null;
case 'SystemChrome.systemUIChange':
final List<dynamic> args = methodCall.arguments as List<dynamic>;
if (_systemUiChangeCallback != null) {
Expand Down Expand Up @@ -571,17 +577,14 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
await SystemChannels.platform.invokeMethod('System.initializationComplete');
}

final Set<SystemContextMenuClient> _systemContextMenuClients = <SystemContextMenuClient>{};
SystemContextMenuClient? _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);
///
/// To unregister, set to null.
static set systemContextMenuClient(SystemContextMenuClient? client) {
instance._systemContextMenuClient = client;
}
}

Expand Down Expand Up @@ -673,14 +676,17 @@ class _DefaultBinaryMessenger extends BinaryMessenger {
/// See also:
/// * [SystemContextMenuController], which uses this to provide a fully
/// featured way to control the system context menu.
/// * [ServicesBinding.systemContextMenuClient], which can be set to a
/// [SystemContextMenuClient] to register it to receive events, or null to
/// unregister.
/// * [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.
/// Called only on the single active instance registered with
/// [ServicesBinding.systemContextMenuClient].
void handleSystemHide();
}
Loading

0 comments on commit df65e46

Please sign in to comment.