diff --git a/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.mm b/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.mm index a1f3ae9447e3e..1f70690d9d4a1 100644 --- a/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.mm +++ b/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.mm @@ -128,7 +128,7 @@ // first responder. FlutterTextField* native_text_field = (FlutterTextField*)focused; if (native_text_field == mac_platform_node_delegate->GetFocus()) { - [native_text_field becomeFirstResponder]; + [native_text_field startEditing]; } break; } @@ -172,7 +172,7 @@ (FlutterTextField*)mac_platform_node_delegate->GetNativeViewAccessible(); id focused = mac_platform_node_delegate->GetFocus(); if (!focused || native_text_field == focused) { - [native_text_field becomeFirstResponder]; + [native_text_field startEditing]; } break; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h index 02a2565ee4575..c8ba5058aabfb 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h @@ -39,4 +39,13 @@ */ - (void)handleEvent:(nonnull NSEvent*)event; +/** + * Returns yes if is event currently being redispatched. + * + * In some instances (i.e. emoji shortcut) the event may be redelivered by cocoa + * as key equivalent to FlutterTextInput, in which case it shouldn't be + * processed again. + */ +- (BOOL)isDispatchingKeyEvent:(nonnull NSEvent*)event; + @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm index 90ebdf5362865..67dd257cc9c7e 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm @@ -70,6 +70,8 @@ @interface FlutterKeyboardManager () @property(nonatomic) NSMutableDictionary* layoutMap; +@property(nonatomic, nullable) NSEvent* eventBeingDispatched; + /** * Add a primary responder, which asynchronously decides whether to handle an * event. @@ -168,6 +170,10 @@ - (void)handleEvent:(nonnull NSEvent*)event { [self processNextEvent]; } +- (BOOL)isDispatchingKeyEvent:(NSEvent*)event { + return _eventBeingDispatched == event; +} + #pragma mark - Private - (void)processNextEvent { @@ -230,6 +236,8 @@ - (void)dispatchTextEvent:(NSEvent*)event { if (nextResponder == nil) { return; } + NSAssert(_eventBeingDispatched == nil, @"An event is already being dispached."); + _eventBeingDispatched = event; switch (event.type) { case NSEventTypeKeyDown: if ([nextResponder respondsToSelector:@selector(keyDown:)]) { @@ -249,6 +257,8 @@ - (void)dispatchTextEvent:(NSEvent*)event { default: NSAssert(false, @"Unexpected key event type (got %lu).", event.type); } + NSAssert(_eventBeingDispatched != nil, @"_eventBeingDispatched was cleared unexpectedly."); + _eventBeingDispatched = nil; } - (void)buildLayout { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm index b2932ff6f69b4..18b2f21386ea4 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm @@ -293,9 +293,8 @@ EXPECT_EQ(NSEqualRects(native_text_field.frame, NSMakeRect(0, 600 - expectedFrameSize, expectedFrameSize, expectedFrameSize)), YES); - // The text of TextInputPlugin only starts syncing editing state to the - // native text field when it becomes the first responder. - [native_text_field becomeFirstResponder]; + + [native_text_field startEditing]; EXPECT_EQ([native_text_field.stringValue isEqualToString:@"textfield"], YES); } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 1431dc2772f38..9e9f93dad4252 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -237,6 +237,16 @@ - (void)dealloc { #pragma mark - Private +- (void)resignAndRemoveFromSuperview { + if (self.superview != nil) { + // With accessiblity enabled TextInputPlugin is inside _client, so take the + // nextResponder from the _client. + NSResponder* nextResponder = _client != nil ? _client.nextResponder : self.nextResponder; + [self.window makeFirstResponder:nextResponder]; + [self removeFromSuperview]; + } +} + - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { BOOL handled = YES; NSString* method = call.method; @@ -262,12 +272,19 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { _activeModel = std::make_unique(); } } else if ([method isEqualToString:kShowMethod]) { + // Ensure the plugin is in hierarchy. Only do this with accessibility disabled. + // When accessibility is enabled cocoa will reparent the plugin inside + // FlutterTextField in [FlutterTextField startEditing]. + if (_client == nil) { + [_flutterViewController.view addSubview:self]; + } + [self.window makeFirstResponder:self]; _shown = TRUE; - [_textInputContext activate]; } else if ([method isEqualToString:kHideMethod]) { + [self resignAndRemoveFromSuperview]; _shown = FALSE; - [_textInputContext deactivate]; } else if ([method isEqualToString:kClearClientMethod]) { + [self resignAndRemoveFromSuperview]; // If there's an active mark region, commit it, end composing, and clear the IME's mark text. if (_activeModel && _activeModel->composing()) { _activeModel->CommitComposing(); @@ -362,7 +379,8 @@ - (void)setEditingState:(NSDictionary*)state { if (composing_range.collapsed() && wasComposing) { [_textInputContext discardMarkedText]; } - [_client becomeFirstResponder]; + [_client startEditing]; + [self updateTextAndSelection]; } @@ -464,12 +482,6 @@ - (BOOL)handleKeyEvent:(NSEvent*)event { return NO; } - // NSTextInputContext sometimes deactivates itself without calling - // deactivate. One such example is when the composing region is deleted. - // TODO(LongCatIsLooong): put FlutterTextInputPlugin in the view hierarchy and - // request/resign first responder when needed. Activate/deactivate shouldn't - // be called by the application. - [_textInputContext activate]; return [_textInputContext handleEvent:event]; } @@ -485,7 +497,20 @@ - (void)keyUp:(NSEvent*)event { } - (BOOL)performKeyEquivalent:(NSEvent*)event { - return [self.flutterViewController performKeyEquivalent:event]; + if ([_flutterViewController isDispatchingKeyEvent:event]) { + // When NSWindow is nextResponder, keyboard manager will send to it + // unhandled events (through [NSWindow keyDown:]). If event has has both + // control and cmd modifiers set (i.e. cmd+control+space - emoji picker) + // NSWindow will then send this event as performKeyEquivalent: to first + // responder, which is FlutterTextInputPlugin. If that's the case, the + // plugin must not handle the event, otherwise the emoji picker would not + // work (due to first responder returning YES from performKeyEquivalent:) + // and there would be endless loop, because FlutterViewController will + // send the event back to [keyboardManager handleEvent:]. + return NO; + } + [self.flutterViewController keyDown:event]; + return YES; } - (void)flagsChanged:(NSEvent*)event { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index a724b112b7239..2cfded0631969 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -250,60 +250,6 @@ - (bool)testComposingRegionRemovedByFramework { return true; } -- (bool)testInputContextIsKeptActive { - id engineMock = OCMClassMock([FlutterEngine class]); - FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock - nibName:@"" - bundle:nil]; - - FlutterTextInputPlugin* plugin = - [[FlutterTextInputPlugin alloc] initWithViewController:viewController]; - - [plugin handleMethodCall:[FlutterMethodCall - methodCallWithMethodName:@"TextInput.setClient" - arguments:@[ - @(1), @{ - @"inputAction" : @"action", - @"inputType" : @{@"name" : @"inputName"}, - } - ]] - result:^(id){ - }]; - - [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState" - arguments:@{ - @"text" : @"", - @"selectionBase" : @(0), - @"selectionExtent" : @(0), - @"composingBase" : @(-1), - @"composingExtent" : @(-1), - }] - result:^(id){ - }]; - - [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.show" - arguments:@[]] - result:^(id){ - }]; - - [plugin.inputContext deactivate]; - EXPECT_EQ(plugin.inputContext.isActive, NO); - NSEvent* keyEvent = [NSEvent keyEventWithType:NSEventTypeKeyDown - location:NSZeroPoint - modifierFlags:0x100 - timestamp:0 - windowNumber:0 - context:nil - characters:@"" - charactersIgnoringModifiers:@"" - isARepeat:NO - keyCode:0x50]; - - [plugin handleKeyEvent:keyEvent]; - EXPECT_EQ(plugin.inputContext.isActive, YES); - return true; -} - - (bool)testClearClientDuringComposing { // Set up FlutterTextInputPlugin. id engineMock = OCMClassMock([FlutterEngine class]); @@ -917,6 +863,63 @@ - (bool)testComposingWithDeltasWhenSelectionIsActive { return true; } +- (bool)testPerformKeyEquivalent { + __block NSEvent* eventBeingDispatchedByKeyboardManager = nil; + FlutterViewController* viewControllerMock = OCMClassMock([FlutterViewController class]); + OCMStub([viewControllerMock isDispatchingKeyEvent:[OCMArg any]]) + .andDo(^(NSInvocation* invocation) { + NSEvent* event; + [invocation getArgument:(void*)&event atIndex:2]; + BOOL result = event == eventBeingDispatchedByKeyboardManager; + [invocation setReturnValue:&result]; + }); + + NSEvent* event = [NSEvent keyEventWithType:NSEventTypeKeyDown + location:NSZeroPoint + modifierFlags:0x100 + timestamp:0 + windowNumber:0 + context:nil + characters:@"" + charactersIgnoringModifiers:@"" + isARepeat:NO + keyCode:0x50]; + + FlutterTextInputPlugin* plugin = + [[FlutterTextInputPlugin alloc] initWithViewController:viewControllerMock]; + + OCMExpect([viewControllerMock keyDown:event]); + + // Require that event is handled (returns YES) + if (![plugin performKeyEquivalent:event]) { + return false; + }; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [viewControllerMock keyDown:event]); + } @catch (...) { + return false; + } + + // performKeyEquivalent must not forward event if it is being + // dispatched by keyboard manager + eventBeingDispatchedByKeyboardManager = event; + + OCMReject([viewControllerMock keyDown:event]); + @try { + // Require that event is not handled (returns NO) and not + // forwarded to controller + if ([plugin performKeyEquivalent:event]) { + return false; + }; + } @catch (...) { + return false; + } + + return true; +} + - (bool)testLocalTextAndSelectionUpdateAfterDelta { id engineMock = OCMClassMock([FlutterEngine class]); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); @@ -1005,10 +1008,6 @@ - (bool)testLocalTextAndSelectionUpdateAfterDelta { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingRegionRemovedByFramework]); } -TEST(FlutterTextInputPluginTest, TestTextInputContextIsKeptAlive) { - ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testInputContextIsKeptActive]); -} - TEST(FlutterTextInputPluginTest, TestClearClientDuringComposing) { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testClearClientDuringComposing]); } @@ -1037,6 +1036,10 @@ - (bool)testLocalTextAndSelectionUpdateAfterDelta { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testLocalTextAndSelectionUpdateAfterDelta]); } +TEST(FlutterTextInputPluginTest, TestPerformKeyEquivalent) { + ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testPerformKeyEquivalent]); +} + TEST(FlutterTextInputPluginTest, CanWorkWithFlutterTextField) { FlutterEngine* engine = CreateTestEngine(); NSString* fixtures = @(testing::GetFixturesPath()); @@ -1069,7 +1072,7 @@ - (bool)testLocalTextAndSelectionUpdateAfterDelta { [[FlutterTextFieldMock alloc] initWithPlatformNode:&text_platform_node fieldEditor:viewController.textInputPlugin]; [viewController.view addSubview:mockTextField]; - [mockTextField becomeFirstResponder]; + [mockTextField startEditing]; NSDictionary* arguments = @{ @"inputAction" : @"action", @@ -1133,4 +1136,40 @@ - (bool)testLocalTextAndSelectionUpdateAfterDelta { EXPECT_EQ([textField becomeFirstResponder], NO); } +TEST(FlutterTextInputPluginTest, IsAddedAndRemovedFromViewHierarchy) { + FlutterEngine* engine = CreateTestEngine(); + NSString* fixtures = @(testing::GetFixturesPath()); + FlutterDartProject* project = [[FlutterDartProject alloc] + initWithAssetsPath:fixtures + ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + [viewController loadView]; + [engine setViewController:viewController]; + + NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:NO]; + window.contentView = viewController.view; + + ASSERT_EQ(viewController.textInputPlugin.superview, nil); + ASSERT_FALSE(window.firstResponder == viewController.textInputPlugin); + + [viewController.textInputPlugin + handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.show" arguments:@[]] + result:^(id){ + }]; + + ASSERT_EQ(viewController.textInputPlugin.superview, viewController.view); + ASSERT_TRUE(window.firstResponder == viewController.textInputPlugin); + + [viewController.textInputPlugin + handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.hide" arguments:@[]] + result:^(id){ + }]; + + ASSERT_EQ(viewController.textInputPlugin.superview, nil); + ASSERT_FALSE(window.firstResponder == viewController.textInputPlugin); +} + } // namespace flutter::testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h b/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h index 759581889d8d4..e3e4ef4901691 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h @@ -89,4 +89,10 @@ class FlutterTextPlatformNode : public ui::AXPlatformNodeBase { */ - (void)updateString:(NSString*)string withSelection:(NSRange)selection; +/** + * Makes the field editor (plugin) current editor for this TextField, meaning + * that the text field will start getting editing events. + */ +- (void)startEditing; + @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm index 856da8f4d1154..75fecca90163c 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm @@ -103,50 +103,42 @@ - (void)setAccessibilityFocused:(BOOL)isFocused { _node->GetDelegate()->AccessibilityPerformAction(data); } -#pragma mark - NSResponder - -- (BOOL)becomeFirstResponder { +- (void)startEditing { if (!_plugin) { - return NO; + return; } - if (_plugin.client == self && [_plugin isFirstResponder]) { - // This text field is already the first responder. - return YES; + if (self.currentEditor == _plugin) { + return; } - BOOL result = [super becomeFirstResponder]; - if (result) { - _plugin.client = self; - // The default implementation of the becomeFirstResponder will change the - // text editing state. Need to manually set it back. - NSString* textValue = @(_node->GetStringAttribute(ax::mojom::StringAttribute::kValue).data()); - int start = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart); - int end = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd); - NSAssert((start >= 0 && end >= 0) || (start == -1 && end == -1), @"selection is invalid"); - NSRange selection; - if (start >= 0 && end >= 0) { - selection = NSMakeRange(MIN(start, end), ABS(end - start)); - } else { - // The native behavior is to place the cursor at the end of the string if - // there is no selection. - selection = NSMakeRange([self stringValue].length, 0); - } - [self updateString:textValue withSelection:selection]; + // Selecting text seems to be the only way to make the field editor + // current editor. + [self selectText:self]; + NSAssert(self.currentEditor == _plugin, @"Failed to set current editor"); + + _plugin.client = self; + + // Restore previous selection. + NSString* textValue = @(_node->GetStringAttribute(ax::mojom::StringAttribute::kValue).data()); + int start = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart); + int end = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd); + NSAssert((start >= 0 && end >= 0) || (start == -1 && end == -1), @"selection is invalid"); + NSRange selection; + if (start >= 0 && end >= 0) { + selection = NSMakeRange(MIN(start, end), ABS(end - start)); + } else { + // The native behavior is to place the cursor at the end of the string if + // there is no selection. + selection = NSMakeRange([self stringValue].length, 0); } - return result; -} - -- (BOOL)resignFirstResponder { - BOOL result = [super resignFirstResponder]; - if (result && _plugin.client == self) { - _plugin.client = nil; - } - return result; + [self updateString:textValue withSelection:selection]; } #pragma mark - NSObject - (void)dealloc { - [self resignFirstResponder]; + if (_plugin.client == self) { + _plugin.client = nil; + } } @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index f01e3cfd26a00..19c9911e03bcf 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -181,7 +181,7 @@ @interface FlutterViewController () * dispatched to various Flutter key responders, and whether the event is * propagated to the next NSResponder. */ -@property(nonatomic) FlutterKeyboardManager* keyboardManager; +@property(nonatomic, readonly, nonnull) FlutterKeyboardManager* keyboardManager; @property(nonatomic) KeyboardLayoutNotifier keyboardLayoutNotifier; @@ -341,6 +341,10 @@ - (instancetype)initWithEngine:(nonnull FlutterEngine*)engine return self; } +- (BOOL)isDispatchingKeyEvent:(NSEvent*)event { + return [_keyboardManager isDispatchingKeyEvent:event]; +} + - (void)loadView { FlutterView* flutterView; if ([FlutterRenderingBackend renderUsingMetal]) { @@ -431,10 +435,11 @@ - (void)listenForMetaModifiedKeyUpEvents { addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp handler:^NSEvent*(NSEvent* event) { // Intercept keyUp only for events triggered on the current - // view. + // view or textInputPlugin. + NSResponder* firstResponder = [[event window] firstResponder]; if (weakSelf.viewLoaded && weakSelf.flutterView && - ([[event window] firstResponder] == - weakSelf.flutterView) && + (firstResponder == weakSelf.flutterView || + firstResponder == weakSelf.textInputPlugin) && ([event modifierFlags] & NSEventModifierFlagCommand) && ([event type] == NSEventTypeKeyUp)) { [weakSelf keyUp:event]; @@ -481,7 +486,6 @@ - (void)initializeKeyboard { // TODO(goderbauer): Seperate keyboard/textinput stuff into ViewController specific and Engine // global parts. Move the global parts to FlutterEngine. __weak FlutterViewController* weakSelf = self; - _textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:weakSelf]; _keyboardManager = [[FlutterKeyboardManager alloc] initWithViewDelegate:weakSelf]; } @@ -635,16 +639,11 @@ - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase { - (void)onAccessibilityStatusChanged:(BOOL)enabled { if (!enabled && self.viewLoaded && [_textInputPlugin isFirstResponder]) { - // The client (i.e. the FlutterTextField) of the textInputPlugin is a sibling - // of the FlutterView. macOS will pick the ancestor to be the next responder - // when the client is removed from the view hierarchy, which is the result of - // turning off semantics. This will cause the keyboard focus to stick at the - // NSWindow. - // - // Since the view controller creates the illustion that the FlutterTextField is - // below the FlutterView in accessibility (See FlutterViewWrapper), it has to - // manually pick the next responder. - [self.view.window makeFirstResponder:_flutterView]; + // Normally TextInputPlugin, when editing, is child of FlutterViewWrapper. + // When accessiblity is enabled the TextInputPlugin gets added as an indirect + // child to FlutterTextField. When disabling the plugin needs to be reparented + // back. + [self.view addSubview:_textInputPlugin]; } } @@ -740,27 +739,6 @@ - (void)keyUp:(NSEvent*)event { [_keyboardManager handleEvent:event]; } -- (BOOL)performKeyEquivalent:(NSEvent*)event { - [_keyboardManager handleEvent:event]; - if (event.type == NSEventTypeKeyDown) { - // macOS only sends keydown for performKeyEquivalent, but the Flutter framework - // always expects a keyup for every keydown. Synthesizes a key up event so that - // the Flutter framework continues to work. - NSEvent* synthesizedUp = [NSEvent keyEventWithType:NSEventTypeKeyUp - location:event.locationInWindow - modifierFlags:event.modifierFlags - timestamp:event.timestamp - windowNumber:event.windowNumber - context:event.context - characters:event.characters - charactersIgnoringModifiers:event.charactersIgnoringModifiers - isARepeat:event.isARepeat - keyCode:event.keyCode]; - [_keyboardManager handleEvent:synthesizedUp]; - } - return YES; -} - - (void)flagsChanged:(NSEvent*)event { [_keyboardManager handleEvent:event]; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm index 6252453d78283..538c29d8fc3ab 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm @@ -20,7 +20,6 @@ - (bool)testKeyEventsAreSentToFramework; - (bool)testKeyEventsArePropagatedIfNotHandled; - (bool)testKeyEventsAreNotPropagatedIfHandled; - (bool)testFlagsChangedEventsArePropagatedIfNotHandled; -- (bool)testPerformKeyEquivalentSynthesizesKeyUp; - (bool)testKeyboardIsRestartedOnEngineRestart; - (bool)testTrackpadGesturesAreSentToFramework; @@ -72,7 +71,7 @@ + (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event EXPECT_EQ(accessibilityChildren[0], viewControllerMock.flutterView); } -TEST(FlutterViewController, SetsFlutterViewFirstResponderWhenAccessibilityDisabled) { +TEST(FlutterViewController, ReparentsPluginWhenAccessibilityDisabled) { FlutterEngine* engine = CreateTestEngine(); NSString* fixtures = @(testing::GetFixturesPath()); FlutterDartProject* project = [[FlutterDartProject alloc] @@ -87,14 +86,17 @@ + (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event backing:NSBackingStoreBuffered defer:NO]; window.contentView = viewController.view; + NSView* dummyView = [[NSView alloc] initWithFrame:CGRectZero]; + [viewController.view addSubview:dummyView]; // Attaches FlutterTextInputPlugin to the view; - [viewController.view addSubview:viewController.textInputPlugin]; + [dummyView addSubview:viewController.textInputPlugin]; // Makes sure the textInputPlugin can be the first responder. EXPECT_TRUE([window makeFirstResponder:viewController.textInputPlugin]); EXPECT_EQ([window firstResponder], viewController.textInputPlugin); + EXPECT_FALSE(viewController.textInputPlugin.superview == viewController.view); [viewController onAccessibilityStatusChanged:NO]; - // FlutterView becomes the first responder. - EXPECT_EQ([window firstResponder], viewController.flutterView); + // FlutterView becomes child of view controller + EXPECT_TRUE(viewController.textInputPlugin.superview == viewController.view); } TEST(FlutterViewController, CanSetMouseTrackingModeBeforeViewLoaded) { @@ -124,10 +126,6 @@ + (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event [[FlutterViewControllerTestObjC alloc] testFlagsChangedEventsArePropagatedIfNotHandled]); } -TEST(FlutterViewControllerTest, TestPerformKeyEquivalentSynthesizesKeyUp) { - ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testPerformKeyEquivalentSynthesizesKeyUp]); -} - TEST(FlutterViewControllerTest, TestKeyboardIsRestartedOnEngineRestart) { ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testKeyboardIsRestartedOnEngineRestart]); } @@ -341,86 +339,6 @@ - (bool)testKeyEventsAreNotPropagatedIfHandled { return true; } -- (bool)testPerformKeyEquivalentSynthesizesKeyUp { - id engineMock = OCMClassMock([FlutterEngine class]); - 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]) - .andCall([FlutterViewControllerTestObjC class], - @selector(respondFalseForSendEvent:callback:userData:)); - FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock - nibName:@"" - bundle:nil]; - id responderMock = flutter::testing::mockResponder(); - viewController.nextResponder = responderMock; - NSDictionary* expectedKeyDownEvent = @{ - @"keymap" : @"macos", - @"type" : @"keydown", - @"keyCode" : @(65), - @"modifiers" : @(538968064), - @"characters" : @".", - @"charactersIgnoringModifiers" : @".", - }; - NSData* encodedKeyDownEvent = - [[FlutterJSONMessageCodec sharedInstance] encode:expectedKeyDownEvent]; - NSDictionary* expectedKeyUpEvent = @{ - @"keymap" : @"macos", - @"type" : @"keyup", - @"keyCode" : @(65), - @"modifiers" : @(538968064), - @"characters" : @".", - @"charactersIgnoringModifiers" : @".", - }; - NSData* encodedKeyUpEvent = [[FlutterJSONMessageCodec sharedInstance] encode:expectedKeyUpEvent]; - CGEventRef cgEvent = CGEventCreateKeyboardEvent(NULL, 65, TRUE); - NSEvent* event = [NSEvent eventWithCGEvent:cgEvent]; - OCMExpect( // NOLINT(google-objc-avoid-throwing-exception) - [binaryMessengerMock sendOnChannel:@"flutter/keyevent" - message:encodedKeyDownEvent - binaryReply:[OCMArg any]]) - .andDo((^(NSInvocation* invocation) { - FlutterBinaryReply handler; - [invocation getArgument:&handler atIndex:4]; - NSDictionary* reply = @{ - @"handled" : @(true), - }; - NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply]; - handler(encodedReply); - })); - OCMExpect( // NOLINT(google-objc-avoid-throwing-exception) - [binaryMessengerMock sendOnChannel:@"flutter/keyevent" - message:encodedKeyUpEvent - binaryReply:[OCMArg any]]) - .andDo((^(NSInvocation* invocation) { - FlutterBinaryReply handler; - [invocation getArgument:&handler atIndex:4]; - NSDictionary* reply = @{ - @"handled" : @(true), - }; - NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply]; - handler(encodedReply); - })); - [viewController viewWillAppear]; // Initializes the event channel. - [viewController performKeyEquivalent:event]; - @try { - OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) - [binaryMessengerMock sendOnChannel:@"flutter/keyevent" - message:encodedKeyDownEvent - binaryReply:[OCMArg any]]); - OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) - [binaryMessengerMock sendOnChannel:@"flutter/keyevent" - message:encodedKeyUpEvent - binaryReply:[OCMArg any]]); - } @catch (...) { - return false; - } - return true; -} - - (bool)testKeyboardIsRestartedOnEngineRestart { id engineMock = OCMClassMock([FlutterEngine class]); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h index 1da1ab3ecba68..04ed474602cf7 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h @@ -31,6 +31,11 @@ nibName:(nullable NSString*)nibName bundle:(nullable NSBundle*)nibBundle NS_DESIGNATED_INITIALIZER; +/** + * Returns YES if provided event is being currently redispatched by keyboard manager. + */ +- (BOOL)isDispatchingKeyEvent:(nonnull NSEvent*)event; + @end // Private methods made visible for testing