diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 77cd371f9113d..a07b05d5bd2ca 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1582,10 +1582,10 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterGLCom FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h -FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterMacOSExternalTexture.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterMetalCompositor.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterMetalCompositor.mm diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index 302c9d0287816..2ddb5babbc1d5 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -82,9 +82,9 @@ source_set("flutter_framework_source") { "framework/Source/FlutterIOSurfaceHolder.h", "framework/Source/FlutterIOSurfaceHolder.mm", "framework/Source/FlutterKeyPrimaryResponder.h", - "framework/Source/FlutterKeySecondaryResponder.h", "framework/Source/FlutterKeyboardManager.h", "framework/Source/FlutterKeyboardManager.mm", + "framework/Source/FlutterKeyboardViewDelegate.h", "framework/Source/FlutterMacOSExternalTexture.h", "framework/Source/FlutterMacOSExternalTexture.h", "framework/Source/FlutterMetalCompositor.h", diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h b/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h deleted file mode 100644 index e0d33522d2342..0000000000000 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -/** - * An interface for a responder that can process a key event and decides whether - * to handle an event synchronously. - * - * To use this class, add it to a |FlutterKeyboardManager| with - * |addSecondaryResponder|. - */ -@protocol FlutterKeySecondaryResponder -/** - * Informs the receiver that the user has interacted with a key. - * - * The return value indicates whether it has handled the given event. - * - * Default implementation returns NO. - */ -@required -- (BOOL)handleKeyEvent:(nonnull NSEvent*)event; -@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h index fc83771d2dd66..af723a7d2b837 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h @@ -2,68 +2,50 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h" - #import -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h" +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h" + +namespace { +// Someohow this pointer type must be defined as a single type for the compiler +// to compile the function pointer type (due to _Nullable). +typedef NSResponder* _NSResponderPtr; +} + +typedef _Nullable _NSResponderPtr (^NextResponderProvider)(); /** - * A hub that manages how key events are dispatched to various Flutter key - * responders, and whether the event is propagated to the next NSResponder. - * - * This class manages one or more primary responders, as well as zero or more - * secondary responders. + * Processes keyboard events and cooperate with |TextInputPlugin|. * - * An event that is received by |handleEvent| is first dispatched to *all* - * primary responders. Each primary responder responds *asynchronously* with a - * boolean, indicating whether it handles the event. + * A keyboard event goes through a few sections, each can choose to handled the + * event, and only unhandled events can move to the next section: * - * An event that is not handled by any primary responders is then passed to to - * the first secondary responder (in the chronological order of addition), - * which responds *synchronously* with a boolean, indicating whether it handles - * the event. If not, the event is passed to the next secondary responder, and - * so on. - * - * If no responders handle the event, the event is then handed over to the - * owner's |nextResponder| if not nil, dispatching to method |keyDown|, - * |keyUp|, or |flagsChanged| depending on the event's type. If the - * |nextResponder| is nil, then the event will be propagated no further. - * - * Preventing primary responders from receiving events is not supported, - * because in reality this class will only support 2 hardcoded ones (channel - * and embedder), where the only purpose of supporting two is to support the - * legacy API (channel) during the deprecation window, after which the channel - * responder should be removed. + * - Pre-filtering: Events during IME are sent to the system immediately + * (to be implemented). + * - Keyboard: Dispatch to the embedder responder and the channel responder + * simultaneously. After both responders have responded (asynchronously), the + * event is considered handled if either responder handles. + * - Text input: Events are sent to |TextInputPlugin| and are handled + * synchronously. + * - Next responder: Events are sent to the next responder as specified by + * |viewDelegate|. */ @interface FlutterKeyboardManager : NSObject /** - * Create a manager by specifying the owner. + * Create a keyboard manager. * - * The owner should be an object that handles the lifecycle of this instance. - * The |owner.nextResponder| can be nil, but if it isn't, it will be where the - * key events are propagated to if no responders handle the event. The owner - * is typically a |FlutterViewController|. + * The |viewDelegate| is a weak reference, typically implemented by + * |FlutterViewController|. */ -- (nonnull instancetype)initWithOwner:(nonnull NSResponder*)weakOwner; +- (nonnull instancetype)initWithViewDelegate:(nonnull id)viewDelegate; /** - * Add a primary responder, which asynchronously decides whether to handle an - * event. - */ -- (void)addPrimaryResponder:(nonnull id)responder; - -/** - * Add a secondary responder, which synchronously decides whether to handle an - * event in order if no earlier responders handle. - */ -- (void)addSecondaryResponder:(nonnull id)responder; - -/** - * Dispatch a key event to all responders, and possibly the next |NSResponder| - * afterwards. + * Processes a key event. + * + * Unhandled events will be dispatched to the text input system, and possibly + * the next responder afterwards. */ - (void)handleEvent:(nonnull NSEvent*)event; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm index 63e6487368270..a7c3eb8657739 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm @@ -4,12 +4,17 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h" + @interface FlutterKeyboardManager () /** - * The owner set by initWithOwner. + * The text input plugin set by initialization. */ -@property(nonatomic, weak) NSResponder* owner; +@property(nonatomic) id viewDelegate; /** * The primary responders added by addPrimaryResponder. @@ -17,22 +22,41 @@ @interface FlutterKeyboardManager () @property(nonatomic) NSMutableArray>* primaryResponders; /** - * The secondary responders added by addSecondaryResponder. + * Add a primary responder, which asynchronously decides whether to handle an + * event. */ -@property(nonatomic) NSMutableArray>* secondaryResponders; +- (void)addPrimaryResponder:(nonnull id)responder; - (void)dispatchToSecondaryResponders:(NSEvent*)event; @end -@implementation FlutterKeyboardManager +@implementation FlutterKeyboardManager { + NextResponderProvider _getNextResponder; +} -- (nonnull instancetype)initWithOwner:(NSResponder*)weakOwner { +- (nonnull instancetype)initWithViewDelegate:(nonnull id)viewDelegate { self = [super init]; if (self != nil) { - _owner = weakOwner; + _viewDelegate = viewDelegate; + _primaryResponders = [[NSMutableArray alloc] init]; - _secondaryResponders = [[NSMutableArray alloc] init]; + [self addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, + FlutterKeyEventCallback callback, + void* userData) { + [_viewDelegate sendKeyEvent:event + callback:callback + userData:userData]; + }]]; + [self + addPrimaryResponder:[[FlutterChannelKeyResponder alloc] + initWithChannel:[FlutterBasicMessageChannel + messageChannelWithName:@"flutter/keyevent" + binaryMessenger:[_viewDelegate + getBinaryMessenger] + codec:[FlutterJSONMessageCodec + sharedInstance]]]]; } return self; } @@ -41,10 +65,6 @@ - (void)addPrimaryResponder:(nonnull id)responder { [_primaryResponders addObject:responder]; } -- (void)addSecondaryResponder:(nonnull id)responder { - [_secondaryResponders addObject:responder]; -} - - (void)handleEvent:(nonnull NSEvent*)event { // Be sure to add a handling method in propagateKeyEvent when allowing more // event types here. @@ -77,25 +97,27 @@ - (void)handleEvent:(nonnull NSEvent*)event { #pragma mark - Private - (void)dispatchToSecondaryResponders:(NSEvent*)event { - for (id responder in _secondaryResponders) { - if ([responder handleKeyEvent:event]) { - return; - } + if ([_viewDelegate onTextInputKeyEvent:event]) { + return; + } + NSResponder* nextResponder = _viewDelegate.nextResponder; + if (nextResponder == nil) { + return; } switch (event.type) { case NSEventTypeKeyDown: - if ([_owner.nextResponder respondsToSelector:@selector(keyDown:)]) { - [_owner.nextResponder keyDown:event]; + if ([nextResponder respondsToSelector:@selector(keyDown:)]) { + [nextResponder keyDown:event]; } break; case NSEventTypeKeyUp: - if ([_owner.nextResponder respondsToSelector:@selector(keyUp:)]) { - [_owner.nextResponder keyUp:event]; + if ([nextResponder respondsToSelector:@selector(keyUp:)]) { + [nextResponder keyUp:event]; } break; case NSEventTypeFlagsChanged: - if ([_owner.nextResponder respondsToSelector:@selector(flagsChanged:)]) { - [_owner.nextResponder flagsChanged:event]; + if ([nextResponder respondsToSelector:@selector(flagsChanged:)]) { + [nextResponder flagsChanged:event]; } break; default: diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm index 3d0589dd1c4ff..2430da1257eb5 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm @@ -5,22 +5,18 @@ #import #import +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h" #import "flutter/testing/testing.h" #include "third_party/googletest/googletest/include/gtest/gtest.h" -@interface FlutterKeyboardManagerUnittestsObjC : NSObject -- (bool)nextResponderShouldThrowOnKeyUp; -- (bool)singlePrimaryResponder; -- (bool)doublePrimaryResponder; -- (bool)singleSecondaryResponder; -- (bool)emptyNextResponder; -@end - -namespace flutter::testing { - namespace { +typedef BOOL (^BoolGetter)(); +typedef void (^AsyncKeyCallback)(BOOL handled); +typedef void (^AsyncKeyCallbackHandler)(AsyncKeyCallback callback); + NSEvent* keyDownEvent(unsigned short keyCode) { return [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSZeroPoint @@ -68,33 +64,134 @@ id checkKeyDownEvent(unsigned short keyCode) { return owner; } -typedef void (^KeyCallbackSetter)(FlutterAsyncKeyCallback callback); -typedef BOOL (^BoolGetter)(); +} // namespace + +@interface KeyboardTester : NSObject +- (nonnull instancetype)init; +- (void)respondEmbedderCallsWith:(BOOL)response; +- (void)recordEmbedderCallsTo:(nonnull NSMutableArray*)storage; +- (void)respondChannelCallsWith:(BOOL)response; +- (void)recordChannelCallsTo:(nonnull NSMutableArray*)storage; + +@property(nonatomic) FlutterKeyboardManager* manager; +@property(nonatomic) NSResponder* nextResponder; + +#pragma mark - Private -id mockPrimaryResponder(KeyCallbackSetter callbackSetter) { - id mock = - OCMStrictProtocolMock(@protocol(FlutterKeyPrimaryResponder)); - OCMStub([mock handleEvent:[OCMArg any] callback:[OCMArg any]]) - .andDo((^(NSInvocation* invocation) { - FlutterAsyncKeyCallback callback; - [invocation getArgument:&callback atIndex:3]; - callbackSetter(callback); - })); - return mock; +- (void)handleEmbedderEvent:(const FlutterKeyEvent&)event + callback:(nullable FlutterKeyEventCallback)callback + userData:(nullable void*)userData; + +- (void)handleChannelMessage:(NSString*)channel + message:(NSData* _Nullable)message + binaryReply:(FlutterBinaryReply _Nullable)callback; + +- (BOOL)handleTextInputKeyEvent:(NSEvent*)event; +@end + +@implementation KeyboardTester { + AsyncKeyCallbackHandler _embedderHandler; + AsyncKeyCallbackHandler _channelHandler; + BOOL _textInputResponse; } -id mockSecondaryResponder(BoolGetter resultGetter) { - id mock = - OCMStrictProtocolMock(@protocol(FlutterKeySecondaryResponder)); - OCMStub([mock handleKeyEvent:[OCMArg any]]).andDo((^(NSInvocation* invocation) { - BOOL result = resultGetter(); - [invocation setReturnValue:&result]; - })); - return mock; +- (nonnull instancetype)init { + self = [super init]; + if (self == nil) { + return nil; + } + + _nextResponder = OCMClassMock([NSResponder class]); + [self respondChannelCallsWith:FALSE]; + [self respondEmbedderCallsWith:FALSE]; + [self respondTextInputWith:FALSE]; + + id messengerMock = OCMStrictProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub([messengerMock sendOnChannel:@"flutter/keyevent" + message:[OCMArg any] + binaryReply:[OCMArg any]]) + .andCall(self, @selector(handleChannelMessage:message:binaryReply:)); + + id viewDelegateMock = OCMStrictProtocolMock(@protocol(FlutterKeyboardViewDelegate)); + OCMStub([viewDelegateMock nextResponder]).andReturn(_nextResponder); + OCMStub([viewDelegateMock onTextInputKeyEvent:[OCMArg any]]) + .andCall(self, @selector(handleTextInputKeyEvent:)); + OCMStub([viewDelegateMock getBinaryMessenger]).andReturn(messengerMock); + OCMStub([viewDelegateMock sendKeyEvent:FlutterKeyEvent {} callback:nil userData:nil]) + .ignoringNonObjectArgs() + .andCall(self, @selector(handleEmbedderEvent:callback:userData:)); + + _manager = [[FlutterKeyboardManager alloc] initWithViewDelegate:viewDelegateMock]; + return self; } -} // namespace +- (void)respondEmbedderCallsWith:(BOOL)response { + _embedderHandler = ^(AsyncKeyCallback callback) { + callback(response); + }; +} + +- (void)recordEmbedderCallsTo:(nonnull NSMutableArray*)storage { + _embedderHandler = ^(AsyncKeyCallback callback) { + [storage addObject:callback]; + }; +} + +- (void)respondChannelCallsWith:(BOOL)response { + _channelHandler = ^(AsyncKeyCallback callback) { + callback(response); + }; +} + +- (void)recordChannelCallsTo:(nonnull NSMutableArray*)storage { + _channelHandler = ^(AsyncKeyCallback callback) { + [storage addObject:callback]; + }; +} + +- (void)respondTextInputWith:(BOOL)response { + _textInputResponse = response; +} +#pragma mark - Private + +- (void)handleEmbedderEvent:(const FlutterKeyEvent&)event + callback:(nullable FlutterKeyEventCallback)callback + userData:(nullable void*)userData { + if (callback != nullptr) { + _embedderHandler(^(BOOL handled) { + callback(handled, userData); + }); + } +} + +- (void)handleChannelMessage:(NSString*)channel + message:(NSData* _Nullable)message + binaryReply:(FlutterBinaryReply _Nullable)callback { + _channelHandler(^(BOOL handled) { + NSDictionary* result = @{ + @"handled" : @(handled), + }; + NSData* encodedKeyEvent = [[FlutterJSONMessageCodec sharedInstance] encode:result]; + callback(encodedKeyEvent); + }); +} + +- (BOOL)handleTextInputKeyEvent:(NSEvent*)event { + return _textInputResponse; +} + +@end + +@interface FlutterKeyboardManagerUnittestsObjC : NSObject +- (bool)nextResponderShouldThrowOnKeyUp; +- (bool)singlePrimaryResponder; +- (bool)doublePrimaryResponder; +- (bool)textInputPlugin; +- (bool)emptyNextResponder; +@end + +namespace flutter::testing { TEST(FlutterKeyboardManagerUnittests, NextResponderShouldThrowOnKeyUp) { ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] nextResponderShouldThrowOnKeyUp]); } @@ -108,7 +205,7 @@ id checkKeyDownEvent(unsigned short keyCode) { } TEST(FlutterKeyboardManagerUnittests, SingleFinalResponder) { - ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] singleSecondaryResponder]); + ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] textInputPlugin]); } TEST(FlutterKeyboardManagerUnittests, EmptyNextResponder) { @@ -122,9 +219,9 @@ @implementation FlutterKeyboardManagerUnittestsObjC // Verify that the nextResponder returned from mockOwnerWithDownOnlyNext() // throws exception when keyUp is called. - (bool)nextResponderShouldThrowOnKeyUp { - NSResponder* owner = flutter::testing::mockOwnerWithDownOnlyNext(); + NSResponder* owner = mockOwnerWithDownOnlyNext(); @try { - [owner.nextResponder keyUp:flutter::testing::keyUpEvent(0x50)]; + [owner.nextResponder keyUp:keyUpEvent(0x50)]; return false; } @catch (...) { return true; @@ -132,144 +229,142 @@ - (bool)nextResponderShouldThrowOnKeyUp { } - (bool)singlePrimaryResponder { - NSResponder* owner = flutter::testing::mockOwnerWithDownOnlyNext(); - FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] initWithOwner:owner]; - - __block NSMutableArray* callbacks = + KeyboardTester* tester = [[KeyboardTester alloc] init]; + NSMutableArray* embedderCallbacks = [NSMutableArray array]; - [manager addPrimaryResponder:flutter::testing::mockPrimaryResponder( - ^(FlutterAsyncKeyCallback callback) { - [callbacks addObject:callback]; - })]; + [tester recordEmbedderCallsTo:embedderCallbacks]; // Case: The responder reports FALSE - [manager handleEvent:flutter::testing::keyDownEvent(0x50)]; - EXPECT_EQ([callbacks count], 1u); - callbacks[0](FALSE); - OCMVerify([owner.nextResponder keyDown:flutter::testing::checkKeyDownEvent(0x50)]); - [callbacks removeAllObjects]; + [tester.manager handleEvent:keyDownEvent(0x50)]; + EXPECT_EQ([embedderCallbacks count], 1u); + embedderCallbacks[0](FALSE); + OCMVerify([tester.nextResponder keyDown:checkKeyDownEvent(0x50)]); + [embedderCallbacks removeAllObjects]; // Case: The responder reports TRUE - [manager handleEvent:flutter::testing::keyUpEvent(0x50)]; - EXPECT_EQ([callbacks count], 1u); - callbacks[0](TRUE); + [tester.manager handleEvent:keyUpEvent(0x50)]; + EXPECT_EQ([embedderCallbacks count], 1u); + embedderCallbacks[0](TRUE); // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown. return true; } - (bool)doublePrimaryResponder { - NSResponder* owner = flutter::testing::mockOwnerWithDownOnlyNext(); - FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] initWithOwner:owner]; + KeyboardTester* tester = [[KeyboardTester alloc] init]; - __block NSMutableArray* callbacks1 = - [NSMutableArray array]; - [manager addPrimaryResponder:flutter::testing::mockPrimaryResponder( - ^(FlutterAsyncKeyCallback callback) { - [callbacks1 addObject:callback]; - })]; + // Send a down event first so we can send an up event later. + [tester respondEmbedderCallsWith:false]; + [tester respondChannelCallsWith:false]; + [tester.manager handleEvent:keyDownEvent(0x50)]; - __block NSMutableArray* callbacks2 = + NSMutableArray* embedderCallbacks = [NSMutableArray array]; - [manager addPrimaryResponder:flutter::testing::mockPrimaryResponder( - ^(FlutterAsyncKeyCallback callback) { - [callbacks2 addObject:callback]; - })]; - - // Case: Both responder report TRUE. - [manager handleEvent:flutter::testing::keyUpEvent(0x50)]; - EXPECT_EQ([callbacks1 count], 1u); - EXPECT_EQ([callbacks2 count], 1u); - callbacks1[0](TRUE); - callbacks2[0](TRUE); - EXPECT_EQ([callbacks1 count], 1u); - EXPECT_EQ([callbacks2 count], 1u); - // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown. - [callbacks1 removeAllObjects]; - [callbacks2 removeAllObjects]; + NSMutableArray* channelCallbacks = + [NSMutableArray array]; + [tester recordEmbedderCallsTo:embedderCallbacks]; + [tester recordChannelCallsTo:channelCallbacks]; + + // Case: Both responders report TRUE. + [tester.manager handleEvent:keyUpEvent(0x50)]; + EXPECT_EQ([embedderCallbacks count], 1u); + EXPECT_EQ([channelCallbacks count], 1u); + embedderCallbacks[0](TRUE); + channelCallbacks[0](TRUE); + EXPECT_EQ([embedderCallbacks count], 1u); + EXPECT_EQ([channelCallbacks count], 1u); + // [tester.nextResponder keyUp:] should not be called, otherwise an error will be thrown. + [embedderCallbacks removeAllObjects]; + [channelCallbacks removeAllObjects]; // Case: One responder reports TRUE. - [manager handleEvent:flutter::testing::keyUpEvent(0x50)]; - EXPECT_EQ([callbacks1 count], 1u); - EXPECT_EQ([callbacks2 count], 1u); - callbacks1[0](FALSE); - callbacks2[0](TRUE); - EXPECT_EQ([callbacks1 count], 1u); - EXPECT_EQ([callbacks2 count], 1u); - // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown. - [callbacks1 removeAllObjects]; - [callbacks2 removeAllObjects]; + [tester respondEmbedderCallsWith:false]; + [tester respondChannelCallsWith:false]; + [tester.manager handleEvent:keyDownEvent(0x50)]; + + [tester recordEmbedderCallsTo:embedderCallbacks]; + [tester recordChannelCallsTo:channelCallbacks]; + [tester.manager handleEvent:keyUpEvent(0x50)]; + EXPECT_EQ([embedderCallbacks count], 1u); + EXPECT_EQ([channelCallbacks count], 1u); + embedderCallbacks[0](FALSE); + channelCallbacks[0](TRUE); + EXPECT_EQ([embedderCallbacks count], 1u); + EXPECT_EQ([channelCallbacks count], 1u); + // [tester.nextResponder keyUp:] should not be called, otherwise an error will be thrown. + [embedderCallbacks removeAllObjects]; + [channelCallbacks removeAllObjects]; // Case: Both responders report FALSE. - [manager handleEvent:flutter::testing::keyDownEvent(0x50)]; - EXPECT_EQ([callbacks1 count], 1u); - EXPECT_EQ([callbacks2 count], 1u); - callbacks1[0](FALSE); - callbacks2[0](FALSE); - EXPECT_EQ([callbacks1 count], 1u); - EXPECT_EQ([callbacks2 count], 1u); - OCMVerify([owner.nextResponder keyDown:flutter::testing::checkKeyDownEvent(0x50)]); - [callbacks1 removeAllObjects]; - [callbacks2 removeAllObjects]; + [tester.manager handleEvent:keyDownEvent(0x53)]; + EXPECT_EQ([embedderCallbacks count], 1u); + EXPECT_EQ([channelCallbacks count], 1u); + embedderCallbacks[0](FALSE); + channelCallbacks[0](FALSE); + EXPECT_EQ([embedderCallbacks count], 1u); + EXPECT_EQ([channelCallbacks count], 1u); + OCMVerify([tester.nextResponder keyDown:checkKeyDownEvent(0x53)]); + [embedderCallbacks removeAllObjects]; + [channelCallbacks removeAllObjects]; return true; } -- (bool)singleSecondaryResponder { - NSResponder* owner = flutter::testing::mockOwnerWithDownOnlyNext(); - FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] initWithOwner:owner]; +- (bool)textInputPlugin { + KeyboardTester* tester = [[KeyboardTester alloc] init]; - __block NSMutableArray* callbacks = - [NSMutableArray array]; - [manager addPrimaryResponder:flutter::testing::mockPrimaryResponder( - ^(FlutterAsyncKeyCallback callback) { - [callbacks addObject:callback]; - })]; + // Send a down event first so we can send an up event later. + [tester respondEmbedderCallsWith:false]; + [tester respondChannelCallsWith:false]; + [tester.manager handleEvent:keyDownEvent(0x50)]; - __block BOOL nextResponse; - [manager addSecondaryResponder:flutter::testing::mockSecondaryResponder(^() { - return nextResponse; - })]; + NSMutableArray* callbacks = + [NSMutableArray array]; + [tester recordEmbedderCallsTo:callbacks]; // Case: Primary responder responds TRUE. The event shouldn't be handled by // the secondary responder. - nextResponse = FALSE; - [manager handleEvent:flutter::testing::keyUpEvent(0x50)]; + [tester respondTextInputWith:FALSE]; + [tester.manager handleEvent:keyUpEvent(0x50)]; EXPECT_EQ([callbacks count], 1u); callbacks[0](TRUE); // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown. [callbacks removeAllObjects]; + // Send a down event first so we can send an up event later. + [tester respondEmbedderCallsWith:false]; + [tester.manager handleEvent:keyDownEvent(0x50)]; + // Case: Primary responder responds FALSE. The secondary responder returns // TRUE. - nextResponse = TRUE; - [manager handleEvent:flutter::testing::keyUpEvent(0x50)]; + [tester recordEmbedderCallsTo:callbacks]; + [tester respondTextInputWith:TRUE]; + [tester.manager handleEvent:keyUpEvent(0x50)]; EXPECT_EQ([callbacks count], 1u); callbacks[0](FALSE); // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown. [callbacks removeAllObjects]; // Case: Primary responder responds FALSE. The secondary responder returns FALSE. - nextResponse = FALSE; - [manager handleEvent:flutter::testing::keyDownEvent(0x50)]; + [tester respondTextInputWith:FALSE]; + [tester.manager handleEvent:keyDownEvent(0x50)]; EXPECT_EQ([callbacks count], 1u); callbacks[0](FALSE); - OCMVerify([owner.nextResponder keyDown:flutter::testing::checkKeyDownEvent(0x50)]); + OCMVerify([tester.nextResponder keyDown:checkKeyDownEvent(0x50)]); [callbacks removeAllObjects]; return true; } - (bool)emptyNextResponder { - NSResponder* owner = OCMStrictClassMock([NSResponder class]); - OCMStub([owner nextResponder]).andReturn(nil); + KeyboardTester* tester = [[KeyboardTester alloc] init]; + tester.nextResponder = nil; - FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] initWithOwner:owner]; + [tester respondEmbedderCallsWith:false]; + [tester respondChannelCallsWith:false]; + [tester respondTextInputWith:false]; + [tester.manager handleEvent:keyDownEvent(0x50)]; - [manager addPrimaryResponder:flutter::testing::mockPrimaryResponder( - ^(FlutterAsyncKeyCallback callback) { - callback(FALSE); - })]; // Passes if no error is thrown. return true; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h new file mode 100644 index 0000000000000..580fe045b419a --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h @@ -0,0 +1,53 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#import "flutter/shell/platform/embedder/embedder.h" + +/** + * An interface for a class that can provides |FlutterKeyboardManager| with + * platform-related features. + * + * This protocol is typically implemented by |FlutterViewController|. + */ +@protocol FlutterKeyboardViewDelegate + +@required + +/** + * Get the next responder to dispatch events that the keyboard system + * (including text input) do not handle. + * + * If the |nextResponder| is null, then those events will be discarded. + */ +@property(nonatomic, readonly, nullable) NSResponder* nextResponder; + +/** + * Dispatch events to the framework to be processed by |HardwareKeyboard|. + * + * This method typically forwards events to + * |FlutterEngine.sendKeyEvent:callback:userData:|. + */ +- (void)sendKeyEvent:(const FlutterKeyEvent&)event + callback:(nullable FlutterKeyEventCallback)callback + userData:(nullable void*)userData; + +/** + * Get a binary messenger to send channel messages with. + * + * This method is used to create the key data channel and typically + * forwards to |FlutterEngine.binaryMessenger|. + */ +- (nonnull id)getBinaryMessenger; + +/** + * Dispatch events that are not handled by the keyboard event handlers + * to the text input handler. + * + * This method typically forwards events to |TextInputPlugin.handleKeyEvent|. + */ +- (BOOL)onTextInputKeyEvent:(nonnull NSEvent*)event; + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h index b5b8b545d4b6e..57a5aec0e532b 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h @@ -6,7 +6,6 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h" @class FlutterTextField; @@ -22,7 +21,7 @@ * When accessibility is on, accessibility bridge creates a NSTextField, i.e. FlutterTextField, * for every text field in the Flutter. This plugin acts as a field editor for those NSTextField[s]. */ -@interface FlutterTextInputPlugin : NSTextView +@interface FlutterTextInputPlugin : NSTextView /** * The NSTextField that currently has this plugin as its field editor. @@ -46,6 +45,18 @@ */ - (BOOL)isFirstResponder; +/** + * Handles key down events received from the view controller, responding YES if + * the event was handled. + * + * Note, the Apple docs suggest that clients should override essentially all the + * mouse and keyboard event-handling methods of NSResponder. However, experimentation + * indicates that only key events are processed by the native layer; Flutter processes + * mouse events. Additionally, processing both keyUp and keyDown results in duplicate + * processing of the same keys. + */ +- (BOOL)handleKeyEvent:(NSEvent*)event; + @end // Private methods made visible for testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 55f7064b46770..a9485ece992b0 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -461,19 +461,6 @@ - (NSString*)textAffinityString { : kTextAffinityDownstream; } -#pragma mark - -#pragma mark FlutterKeySecondaryResponder - -/** - * Handles key down events received from the view controller, responding YES if - * the event was handled. - * - * Note, the Apple docs suggest that clients should override essentially all the - * mouse and keyboard event-handling methods of NSResponder. However, experimentation - * indicates that only key events are processed by the native layer; Flutter processes - * mouse events. Additionally, processing both keyUp and keyDown results in duplicate - * processing of the same keys. - */ - (BOOL)handleKeyEvent:(NSEvent*)event { if (event.type == NSEventTypeKeyUp || (event.type == NSEventTypeFlagsChanged && event.modifierFlags < _previouslyPressedFlags)) { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index 2bd004cc373f4..b7caab0758c2e 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -9,8 +9,6 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h" @@ -448,23 +446,7 @@ - (void)configureTrackingArea { - (void)initializeKeyboard { __weak FlutterViewController* weakSelf = self; _textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:weakSelf]; - _keyboardManager = [[FlutterKeyboardManager alloc] initWithOwner:weakSelf]; - [_keyboardManager addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc] - initWithSendEvent:^(const FlutterKeyEvent& event, - FlutterKeyEventCallback callback, - void* userData) { - [weakSelf.engine sendKeyEvent:event - callback:callback - userData:userData]; - }]]; - [_keyboardManager - addPrimaryResponder:[[FlutterChannelKeyResponder alloc] - initWithChannel:[FlutterBasicMessageChannel - messageChannelWithName:@"flutter/keyevent" - binaryMessenger:_engine.binaryMessenger - codec:[FlutterJSONMessageCodec - sharedInstance]]]]; - [_keyboardManager addSecondaryResponder:_textInputPlugin]; + _keyboardManager = [[FlutterKeyboardManager alloc] initWithViewDelegate:weakSelf]; } - (void)addInternalPlugins { @@ -677,6 +659,22 @@ - (void)viewDidReshape:(NSView*)view { return [_engine registrarForPlugin:pluginName]; } +#pragma mark - FlutterKeyboardViewDelegate + +- (void)sendKeyEvent:(const FlutterKeyEvent&)event + callback:(nullable FlutterKeyEventCallback)callback + userData:(nullable void*)userData { + [_engine sendKeyEvent:event callback:callback userData:userData]; +} + +- (id)getBinaryMessenger { + return _engine.binaryMessenger; +} + +- (BOOL)onTextInputKeyEvent:(nonnull NSEvent*)event { + return [_textInputPlugin handleKeyEvent:event]; +} + #pragma mark - NSResponder - (BOOL)acceptsFirstResponder { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h index f6dd076d112dd..2607242cea4ce 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h @@ -4,11 +4,11 @@ #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" -@interface FlutterViewController () +@interface FlutterViewController () // The FlutterView for this view controller. @property(nonatomic, readonly, nullable) FlutterView* flutterView;