Skip to content

[iOS][Secure Paste] Custom edit menu actions #171825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: master
Choose a base branch
from

Conversation

jingshao-code
Copy link
Member

@jingshao-code jingshao-code commented Jul 8, 2025

This PR implements support for custom action items in the native edit menu on iOS, with changes to both framework and engine. This PR will be updated incrementally until the full feature is complete.

Phase 1: Add hardcoded custom menu item for iOS edit menu.
Phase 2: Add Framework API for custom iOS context menu items.
Phase 3: Add Optimization, Testing, and Documentation.

Part of #103163

Part of #140184

If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@github-actions github-actions bot added a: text input Entering text in a text field or keyboard related problems platform-ios iOS applications specifically framework flutter/packages/flutter repository. See also f: labels. engine flutter/engine repository. See also e: labels. team-ios Owned by iOS platform team labels Jul 8, 2025
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView
performCustomAction:(NSString*)customAction
withClient:(int)client {
[self.textInputChannel invokeMethod:@"TextInputClient.performCustomAction"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you use platformChannel and ContextMenu prefix? (similar to willDismissEditMenuWithTextInputClient method in this file)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reviewing! I've implemented all your suggestions, but I have a question about the method naming: The current method name is quite long. Would you prefer to keep it as is for clarity, or should we consider simplifying it? For example: performContextMenuCustomAction?

performCustomAction:(NSString*)customAction
withClient:(int)client {
[self.textInputChannel invokeMethod:@"TextInputClient.performCustomAction"
arguments:@[ @(client), customAction ]];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the customAction the ID? if so you should call it customActionID

if (customId && title) {
UIAction* action = [UIAction actionWithTitle:title
image:nil
identifier:customId
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to set identifier.

image:nil
identifier:customId
handler:^(__kindof UIAction* _Nonnull action) {
[self.textInputDelegate flutterTextInputView:self
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to use weak self

@hellohuanlin
Copy link
Contributor

The engine part is heading the right direction. Good work!

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this approach looks solid! Most of my comments are just about how we organize the code.

I left one comment about adding an example to the examples directory. I think that will help us to visualize how this will work better, and it will allow you to remove your debug code for the vibrate button.

I also left a comment about the callback map not being cleared when the system hides the menu. I'll keep an eye out for other sources of leaks. We'll have to make sure that we cover use cases like showing one SystemContextMenu widget, then immediately showing another. I think hide might get called in that case so we'll be covered, but we should make sure.

Comment on lines 1974 to 1977
if (callback != null) {
callback();
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also assert that the callback is not null so that developers can be aware that something is wrong?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is testable or not, but it would be nice if we can test it.

Comment on lines 1980 to 1983
if (actionId == 'vibrate') {
debugPrint('[HARDCODED] Vibrate action triggered');
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you write an example in the examples/api/lib directory that does something like this at the user level? Maybe a more complex version of this one: https://github.com/flutter/flutter/blob/master/examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe put your example on the items parameter of SystemContextMenu:

final List<IOSSystemContextMenuItem> items;

@@ -2118,6 +2135,9 @@ class TextInput {
firstArg['action'] as String,
firstArg['data'] == null ? <String, dynamic>{} : firstArg['data'] as Map<String, dynamic>,
);
case 'TextInputClient.performCustomAction':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should go in services/binding.dart, where we handle onDismissSystemContextMenu.

@@ -2624,6 +2644,9 @@ class SystemContextMenuController with SystemContextMenuClient, Diagnosticable {

static SystemContextMenuController? _lastShown;

/// Map to store custom action callbacks, keyed by their unique identifiers.
static final Map<String, VoidCallback> _customActionCallbacks = <String, VoidCallback>{};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the method channel call were handled in binding.dart, then you could handle this stuff via SystemContextMenuClient and SystemContextMenuController.

@@ -2809,6 +2853,7 @@ class SystemContextMenuController with SystemContextMenuClient, Diagnosticable {
}
_lastShown = null;
ServicesBinding.systemContextMenuClient = null;
_clearCustomActions();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should also get cleared when the platform hides the context menu (ContextMenu.onDismissSystemContextMenu).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how the platform hides it:

case 'ContextMenu.onDismissSystemContextMenu':

@override
IOSSystemContextMenuItemData getData(WidgetsLocalizations localizations) {
final String callbackId = '${DateTime.now().millisecondsSinceEpoch}_${title.hashCode}';
SystemContextMenuController.registerCustomAction(callbackId, onPressed);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a tricky side effect that developers probably won't expect when calling getData. See https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#getters-feel-faster-than-methods

Could you register these when the system context menu is shown?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should hopefully sort itself out when you move the action map to SystemContextMenuController, as mentioned above. So I would do that first before trying to address this comment.


@override
IOSSystemContextMenuItemData getData(WidgetsLocalizations localizations) {
final String callbackId = '${DateTime.now().millisecondsSinceEpoch}_${title.hashCode}';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just hashCode is probably a good key for this. The only possible collision would be for the exact same item with the exact same onPressed, say if you passed the same item into the items List twice. Even in that case, it would be fine to have only one entry in the _customActionCallbacks map and to call the same one for both.

Also IOSSystemContextMenuItemCustom is const, so this is equivalent to passing the same instance:

items: <IOSSystemContextMenuItem>[
  IOSSystemContextMenuItemCustom(title: 'press me', onPressed: _myOnPressed),
  IOSSystemContextMenuItemCustom(title: 'press me', onPressed: _myOnPressed),
],

But even in that case, it's fine to have only one entry in _customActionCallbacks.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Justin, I have made all the changes based on your comments and we discussed last time:

  1. Moved callbacks from static to instance-based
  2. Now routes through binding.dart
  3. getData() now only returns data, side-effect free
  4. Added proper cleanup when platform dismisses the menu
  5. Removed the hardcoded testa
  6. Added the tests and a working example

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking forward to your feedback!

@github-actions github-actions bot added d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos labels Jul 22, 2025
@jingshao-code jingshao-code requested a review from justinmc July 22, 2025 10:07
Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good, thanks for doing that refactor. I think putting the callback map inside of SystemContextMenuController is much better now. Also the example is great.

Comment on lines 87 to 83
_controller.text = text.replaceRange(
selection.start,
selection.end,
'❤️',
);
_controller.selection = TextSelection.collapsed(
offset: selection.start + 2,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I would do this as a single update by setting _controller.value.

expect(controller.text, '');
expect(find.text('Text cleared'), findsOneWidget);
},
skip: defaultTargetPlatform != TargetPlatform.iOS || kIsWeb, // [intended]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than skipping for the platform, use variant and TargetPlatformVariant.only. Skipping for kIsWeb is ok.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great example, thanks for adding it.

@@ -2770,9 +2783,18 @@ class SystemContextMenuController with SystemContextMenuClient, Diagnosticable {

ServicesBinding.systemContextMenuClient = this;

_customActionCallbacks.clear();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you also might need to call this in handleSystemHide. Then I think we will be covered and this shouldn't leak.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also test this: Show the SystemContextMenu with a certain item, then show again with a new item. Then try sending a handleCustomContextMenuAction for the original item.


for (final IOSSystemContextMenuItemData item in items) {
if (item is IOSSystemContextMenuItemDataCustom) {
_customActionCallbacks[item.callbackId] = item.onPressed;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think, would it be worth it to assert that there are no duplicate item.callbackIds? I guess it's not a problem for us if there are, and maybe users have a real reason to do something like that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe assert if there are duplicate callbackIds AND the callbacks are not equivalent? That seems like it would very likely be a mistake on the part of the app developer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making a note that we talked this over offline and seemed to agree on the comment above this one.

Comment on lines 3143 to 3146
'callbackId': callbackId,
'title': title,
'type': _jsonType,
'id': callbackId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need both an id and a callbackId?

Comment on lines 3165 to 3167
return other is IOSSystemContextMenuItemDataCustom &&
other.title == title &&
other.callbackId == callbackId;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onPressed should probably also be included here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also should include onPressed in hashCode (discussed offline).

IOSSystemContextMenuItemData getData(WidgetsLocalizations localizations) {
return IOSSystemContextMenuItemDataCustom(
title: title,
callbackId: hashCode.toString(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing the callbackId in here, could IOSSystemContextMenuItemData generate it itself?

final IOSSystemContextMenuItemCustom customItem1 = items[1] as IOSSystemContextMenuItemCustom;
final IOSSystemContextMenuItemCustom customItem2 = items[2] as IOSSystemContextMenuItemCustom;

controller2.handleCustomContextMenuAction(customItem1.hashCode.toString());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also test that a call from onPerformCustomAction works? You can simulate the platform message using TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage.

No need to use handlePlatformMessage every time, it's also fine to use handleCustomContextMenuAction directly like you are here. Just make sure that the onPerformCustomAction gets tested at least once too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also try the error state: send that message when there is no _systemContextMenuClient.

Copy link
Member Author

@jingshao-code jingshao-code Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the platform channel test as requested. Note: I had to use handlePlatformMessage for all tests (not just one) because SystemContextMenu doesn't expose its controller, it's created internally in
_SystemContextMenuState and not accessible from tests. Is this the correct approach, or did you have a different testing strategy in mind?

Comment on lines +468 to +479
/// {@tool dartpad}
/// This example shows how to add custom menu items to the iOS system context menu.
///
/// ** See code in examples/api/lib/widgets/system_context_menu/system_context_menu.1.dart **
/// {@end-tool}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be valuable to also add this example to SystemContextMenu.items, or is it too much?

expect(customAction1Called, isTrue);
expect(customAction2Called, isFalse);

controller2.handleCustomContextMenuAction(customItem2.hashCode.toString());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another test idea: What happens when you call handleCustomContextMenuAction after the menu has been hidden?

What happens when it's called with a callbackId that doesn't exist?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably put those tests in system_context_menu_controller_test.dart.

@jingshao-code jingshao-code requested a review from justinmc August 4, 2025 17:17
@jingshao-code
Copy link
Member Author

jingshao-code commented Aug 4, 2025

Justin, Thanks for the meeting! I've made all the changes:

  1. Switched to atomic updates with _controller.value
  2. Made callback ID generation internal (much cleaner now!)
  3. Confirmed memory cleanup in both hide() and handleSystemHide()
  4. Removed the duplicate id/callbackId
  5. Fixed hashCode/equality to include onPressed
  6. Added assertions for duplicate IDs
  7. Updated test variants
  8. Added all the edge case tests you suggested

Looking forward to your feedback!

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a question about whether the menu auto-closes when a button is pressed or not that came up in a few comments below.

await tester.pumpAndSettle();

if (defaultTargetPlatform == TargetPlatform.iOS) {
expect(find.byType(SystemContextMenu), findsOneWidget);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can you also check: expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);?

if (_systemContextMenuClient == null) {
assert(
false,
'Platform sent onPerformCustomAction when no SystemContextMenuClient was registered.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Maybe add some actionable advice here like this? "ServicesBinding.systemContextMenuCilent shouldn't be cleared unless the menu is hidden."

I'm not sure how helpful that is for app developers, but this assert is such a weird case, not sure if there's a better option. Up to you if you want to include something like this or leave it as-is.

// https://github.com/flutter/flutter/issues/103163
/// An [IOSSystemContextMenuItemData] for custom action buttons defined by the developer.
///
/// Must specify a [title] and [callbackId].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should say title and onPressed.

final VoidCallback onPressed;

/// The unique identifier for this custom action.
String get callbackId => hashCode.toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: It would be possible to cache this to avoid recomputing it each time. Do you think it's worth it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, I think hashCode.toString() is basically free, and each instance's hashCode is already stable throughout its lifetime, not worth the extra code

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on board with that 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Did you think about making callbackId an int instead of a String? I think a String makes sense to me, just wanted to make sure you thought through it.

Comment on lines 434 to 435
final String actionId = args[1] as String;
_systemContextMenuClient!.handleCustomContextMenuAction(actionId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I would call this callbackId here and in the parameter name of handleCustomContextMenuAction. The reason is just to keep the name always the same, because callbackId is used elsewhere in this PR.

await tester.pumpAndSettle();

expect(controller.text, '');
expect(find.text('Text cleared'), findsOneWidget);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also check whether the SystemContextMenu is still visible or not? I assume the answer is no, it hides itself? If not the we should show how to do that in the example, but I think it does hide.

(_) {},
);

expect(customActionCalled, isTrue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also expect SystemContextMenu is hidden here (assuming that's true).

);
expect(customAction2Called, isTrue);

state.hideToolbar();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have to manually call hide or does it automatically hide?

Copy link
Member Author

@jingshao-code jingshao-code Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested on my iPhone - it does auto-close. But I think in tests maybe we need to fake it with hideToolbar().

Comment on lines +1025 to +1120
testWidgets(
'can trigger custom menu action through platform channel message',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this test cover any code paths that the previous test does not?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not you could consider deleting it, but it's up to you if this is valuable or not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept it finally, I think tests mixed items scenario (Cut + custom), which is different from the pure custom test.

},
skip: kIsWeb, // [intended]
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more test I think we should add for completeness: Pump an app with two TextFields, each with their own custom SystemContextMenu item. Open the context menu in one field, expect that only its custom button was shown, tap it, expect that its callback was called. Then do the same for the other field.

@jingshao-code
Copy link
Member Author

jingshao-code commented Aug 11, 2025

Justin, Thanks for the meeting! I've made all the changes:

  1. Fixed naming
  2. Added tests for menu auto-close and two TextFields
  3. Fixed docs to say "title and onPressed"
  4. Added missing periods
  5. Decided against caching (hashCode.toString() is cheap)

Note: Device testing confirmed iOS menus do auto-close after custom actions. Tests simulate this with hideToolbar().

Looking forward to your feedback!

@jingshao-code jingshao-code requested a review from justinmc August 11, 2025 12:35
Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with nits 👍 . Great work!

final VoidCallback onPressed;

/// The unique identifier for this custom action.
String get callbackId => hashCode.toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on board with that 👍

Comment on lines 1087 to 1089
// iOS system menus auto-close after custom actions on real devices,
// but in tests we need to manually call hideToolbar() to simulate this.
state.hideToolbar();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of calling hideToolbar would it be possible to send the ContextMenu.onDismissSystemContextMenu platform channel message in order to make this more realistic?

expect(field2ActionCalled, isTrue);

state2.hideToolbar();
await tester.pump();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Maybe do a final expect that SystemContextMenu is not found. Ending on a pump makes me wonder what was being tested there.

final VoidCallback onPressed;

/// The unique identifier for this custom action.
String get callbackId => hashCode.toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Did you think about making callbackId an int instead of a String? I think a String makes sense to me, just wanted to make sure you thought through it.

@@ -1003,6 +1004,24 @@ - (UIMenu*)editMenuInteraction:(UIEditMenuInteraction*)interaction
selector:@selector(captureTextFromCamera:)
suggestedMenu:suggestedMenu];
}
} else if ([type isEqualToString:@"custom"]) {
NSString* customId = encodedItem[@"id"];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Have you thought about naming this parameter callbackId just so it has the same name as it does in the framework? I'm not sure which name is better, just wanted to call it out.

@jingshao-code jingshao-code marked this pull request as ready for review August 11, 2025 22:19
@jingshao-code jingshao-code requested a review from a team as a code owner August 11, 2025 22:19
[mockPlatformChannel invokeMethod:@"ContextMenu.onPerformCustomAction"
arguments:@[ @(123), @"test-callback-id" ]];
});
}));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't be required. can you revert?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change fixes a CI compilation error:

FlutterTextInputPluginTest.mm:3926:56: error: too many arguments provided to function-like macro invocation ../flutter/third_party/ocmock/Source/OCMock/OCMStubRecorder.h:55:9: note: macro 'andDo' defined here

So I added the parentheses to prevent OCMock's andDo macro from misinterpreting the comma in @[ @(123), @"test-callback-id"] as a macro argument separator.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, this is strange. Searching through the code, we do have both .andDo(( and .andDo(, so andDo(( shouldn't be necessary. Nvm, i guess sometimes the compiler needs extra hint.

Copy link
Member Author

@jingshao-code jingshao-code Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Yes, the comma in array literals specifically triggers this issue.
https://gcc.gnu.org/onlinedocs/cpp/Macro-Arguments.html
This mentions the behavior: "Parentheses within each argument must balance; a comma within such parentheses does not end the argument."

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with nits 👍

I also had a comment about renaming "id" to "callbackId", did you intend to do that or decide to leave it? #171825 (comment)

@@ -889,10 +889,452 @@ void main() {
await tester.tap(find.byType(TextField));
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
expect(state.showToolbar(), true);
await tester.pump();
await tester.pumpAndSettle();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this stay as pump or is it necessary for some reason?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right! I initially used pumpAndSettle() when debugging some test failures, thinking the menu might need more time to appear. I've changed it back to pump(), and the widgets tests passed (I remember before the current infrastructure issues). The earlier failures were likely due to other issues in the test setup that have since been fixed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justinmc The CI is failing with SDK download 404 errors. I've tried reverting to previous passing commits and rebasing to the latest master, but still getting the same infrastructure error. Any suggestions on how to proceed?

@jingshao-code
Copy link
Member Author

LGTM with nits 👍

I also had a comment about renaming "id" to "callbackId", did you intend to do that or decide to leave it? #171825 (comment)

Sorry, I missed replying to this.

I've renamed the variable customId to callbackId to match the framework's property name.

I kept the JSON key as "id", which both framework and engine use for communication. I think this follows the convention where "id" is a standard identifier field, and matches Flutter's existing patterns (like {'id': viewId} in platform views).

That's my thought. Happy to do that if you think it's worth the consistency!

- Add IOSSystemContextMenuItemCustom widget class
- Add IOSSystemContextMenuItemDataCustom data class
- Implement callback registration and management
- Support both API and hardcoded test callbacks
- Instance-based callback management in SystemContextMenuController

- Route custom actions through platform channel via binding.dart

- Clear callbacks on hide() and handleSystemHide()

- Remove getData() side effects, use hashCode for IDs

- Add example (system_context_menu.1.dart) and tests
- Use atomic text editing value updates
- Make callback ID generation internal to IOSSystemContextMenuItemDataCustom
- Simplify JSON output to use single ID field
- Fix hashCode and equality to include onPressed callback
- Add assertion for duplicate callback IDs
- Update tests to use TargetPlatformVariant
- Add platform channel integration test
- Add edge case tests for error scenarios
- Add documentation example for custom menu items
- Verify iOS system menu auto-closes after custom action via device testing
- Change actionId to callbackId throughout for consistency
- Add test for menu auto-close behavior after custom action
- Add test for two TextFields with different custom menus
- Fix documentation to say 'title and onPressed' instead of 'title and callbackId'
- Add AdaptiveTextSelectionToolbar check in example test
- Improve error message in ServicesBinding with actionable advice
- Add missing periods to comments
…age in tests

- Add final expect verification in two TextFields test
- Rename customId to callbackId in engine for consistency
- Rename delegate method to performContextMenuCustomActionWithActionID:textInputClient:
- Add 2 native-side unit tests for custom menu functionality
Add custom type support to systemContextMenuItemDataFromJson with fake callback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: text input Entering text in a text field or keyboard related problems d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos engine flutter/engine repository. See also e: labels. framework flutter/packages/flutter repository. See also f: labels. platform-ios iOS applications specifically team-ios Owned by iOS platform team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants