From 12805484b55401580d4320ddb99a7754bf57ab28 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Thu, 21 Sep 2023 13:55:43 +0200 Subject: [PATCH 1/2] [macOS] TextInputPlugin should mark navigation events in IME popover has handled --- .../macos/framework/Source/FlutterTextInputPlugin.mm | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index f6460f2309f37..ee0d2cc8f9b95 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -620,7 +620,15 @@ - (BOOL)handleKeyEvent:(NSEvent*)event { // text command (indicated by calling doCommandBySelector) or might not (for example, Cmd+Q). In // the latter case, this command somehow has not been executed yet and Flutter must dispatch it to // the next responder. See https://github.com/flutter/flutter/issues/106354 . - if (event.isKeyEquivalent && !_eventProducedOutput) { + // The event is also not redispatched if there is IME composition active, because it might be + // handled by the IME. See https://github.com/flutter/flutter/issues/134699 + + // both NSEventModifierFlagNumericPad and NSEventModifierFlagFunction are set for arrow keys. + bool is_navigation = event.modifierFlags & NSEventModifierFlagFunction && + event.modifierFlags & NSEventModifierFlagNumericPad; + bool is_navigation_in_ime = is_navigation && self.hasMarkedText; + + if (event.isKeyEquivalent && !is_navigation_in_ime && !_eventProducedOutput) { return NO; } return res; From 9c5b1302b339e9bf50a5c135e287f0a67b928fb1 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Thu, 21 Sep 2023 14:45:25 +0200 Subject: [PATCH 2/2] Add test --- .../framework/Source/FlutterTextInputPlugin.h | 1 + .../Source/FlutterTextInputPluginTest.mm | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h index dedfc661884f6..d5e23c8d5aadc 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h @@ -64,5 +64,6 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange; - (NSDictionary*)editingState; +@property(nonatomic) NSTextInputContext* textInputContext; @property(readwrite, nonatomic) NSString* customRunLoopMode; @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index 00bbb68dc69ae..32591147103ac 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -1361,6 +1361,89 @@ - (bool)testPerformKeyEquivalent { return true; } +- (bool)handleArrowKeyWhenImePopoverIsActive { + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [engineMock binaryMessenger]) + .andReturn(binaryMessengerMock); + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + callback:nil + userData:nil]); + + NSTextInputContext* textInputContext = OCMClassMock([NSTextInputContext class]); + OCMStub([textInputContext handleEvent:[OCMArg any]]).andReturn(YES); + + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock + nibName:@"" + bundle:nil]; + + FlutterTextInputPlugin* plugin = + [[FlutterTextInputPlugin alloc] initWithViewController:viewController]; + + plugin.textInputContext = textInputContext; + + NSDictionary* setClientConfig = @{ + @"inputAction" : @"action", + @"enableDeltaModel" : @"true", + @"inputType" : @{@"name" : @"inputName"}, + }; + [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ @(1), setClientConfig ]] + result:^(id){ + }]; + + [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.show" + arguments:@[]] + result:^(id){ + }]; + + // Set marked text, simulate active IME popover. + [plugin setMarkedText:@"m" + selectedRange:NSMakeRange(0, 1) + replacementRange:NSMakeRange(NSNotFound, 0)]; + + // Right arrow key. This, unlike the key below should be handled by the plugin. + NSEvent* event = [NSEvent keyEventWithType:NSEventTypeKeyDown + location:NSZeroPoint + modifierFlags:0xa00100 + timestamp:0 + windowNumber:0 + context:nil + characters:@"\uF702" + charactersIgnoringModifiers:@"\uF702" + isARepeat:NO + keyCode:0x4]; + + // Plugin should mark the event as key equivalent. + [plugin performKeyEquivalent:event]; + + if ([plugin handleKeyEvent:event] != true) { + return false; + } + + // CTRL+H (delete backwards) + event = [NSEvent keyEventWithType:NSEventTypeKeyDown + location:NSZeroPoint + modifierFlags:0x40101 + timestamp:0 + windowNumber:0 + context:nil + characters:@"\uF702" + charactersIgnoringModifiers:@"\uF702" + isARepeat:NO + keyCode:0x4]; + + // Plugin should mark the event as key equivalent. + [plugin performKeyEquivalent:event]; + + if ([plugin handleKeyEvent:event] != false) { + return false; + } + + return true; +} + - (bool)unhandledKeyEquivalent { id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); @@ -1814,6 +1897,10 @@ - (bool)testSelectorsAreForwardedToFramework { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testPerformKeyEquivalent]); } +TEST(FlutterTextInputPluginTest, HandleArrowKeyWhenImePopoverIsActive) { + ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] handleArrowKeyWhenImePopoverIsActive]); +} + TEST(FlutterTextInputPluginTest, UnhandledKeyEquivalent) { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] unhandledKeyEquivalent]); }