[macOS, Keyboard] Refactor: Clean up keyboard initialization, connection, and unit test framework (flutter/engine#31940)

This commit is contained in:
Tong Mu
2022-03-10 22:35:13 -08:00
committed by GitHub
parent d3517e43a9
commit f6f38145a4
11 changed files with 381 additions and 257 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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 <Cocoa/Cocoa.h>
/**
* 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

View File

@@ -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 <Cocoa/Cocoa.h>
#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.
* Processes keyboard events and cooperate with |TextInputPlugin|.
*
* This class manages one or more primary responders, as well as zero or more
* secondary responders.
* 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 received by |handleEvent| is first dispatched to *all*
* primary responders. Each primary responder responds *asynchronously* with a
* boolean, indicating whether it handles the event.
*
* 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<FlutterKeyboardViewDelegate>)viewDelegate;
/**
* Add a primary responder, which asynchronously decides whether to handle an
* event.
*/
- (void)addPrimaryResponder:(nonnull id<FlutterKeyPrimaryResponder>)responder;
/**
* Add a secondary responder, which synchronously decides whether to handle an
* event in order if no earlier responders handle.
*/
- (void)addSecondaryResponder:(nonnull id<FlutterKeySecondaryResponder>)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;

View File

@@ -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<FlutterKeyboardViewDelegate> viewDelegate;
/**
* The primary responders added by addPrimaryResponder.
@@ -17,22 +22,41 @@
@property(nonatomic) NSMutableArray<id<FlutterKeyPrimaryResponder>>* primaryResponders;
/**
* The secondary responders added by addSecondaryResponder.
* Add a primary responder, which asynchronously decides whether to handle an
* event.
*/
@property(nonatomic) NSMutableArray<id<FlutterKeySecondaryResponder>>* secondaryResponders;
- (void)addPrimaryResponder:(nonnull id<FlutterKeyPrimaryResponder>)responder;
- (void)dispatchToSecondaryResponders:(NSEvent*)event;
@end
@implementation FlutterKeyboardManager
@implementation FlutterKeyboardManager {
NextResponderProvider _getNextResponder;
}
- (nonnull instancetype)initWithOwner:(NSResponder*)weakOwner {
- (nonnull instancetype)initWithViewDelegate:(nonnull id<FlutterKeyboardViewDelegate>)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 @@
[_primaryResponders addObject:responder];
}
- (void)addSecondaryResponder:(nonnull id<FlutterKeySecondaryResponder>)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 @@
#pragma mark - Private
- (void)dispatchToSecondaryResponders:(NSEvent*)event {
for (id<FlutterKeySecondaryResponder> 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:

View File

@@ -5,22 +5,18 @@
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.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"
#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 @@ NSResponder* mockOwnerWithDownOnlyNext() {
return owner;
}
typedef void (^KeyCallbackSetter)(FlutterAsyncKeyCallback callback);
typedef BOOL (^BoolGetter)();
id<FlutterKeyPrimaryResponder> mockPrimaryResponder(KeyCallbackSetter callbackSetter) {
id<FlutterKeyPrimaryResponder> 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;
}
id<FlutterKeySecondaryResponder> mockSecondaryResponder(BoolGetter resultGetter) {
id<FlutterKeySecondaryResponder> mock =
OCMStrictProtocolMock(@protocol(FlutterKeySecondaryResponder));
OCMStub([mock handleKeyEvent:[OCMArg any]]).andDo((^(NSInvocation* invocation) {
BOOL result = resultGetter();
[invocation setReturnValue:&result];
}));
return mock;
}
} // namespace
@interface KeyboardTester : NSObject
- (nonnull instancetype)init;
- (void)respondEmbedderCallsWith:(BOOL)response;
- (void)recordEmbedderCallsTo:(nonnull NSMutableArray<FlutterAsyncKeyCallback>*)storage;
- (void)respondChannelCallsWith:(BOOL)response;
- (void)recordChannelCallsTo:(nonnull NSMutableArray<FlutterAsyncKeyCallback>*)storage;
@property(nonatomic) FlutterKeyboardManager* manager;
@property(nonatomic) NSResponder* nextResponder;
#pragma mark - Private
- (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;
}
- (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;
}
- (void)respondEmbedderCallsWith:(BOOL)response {
_embedderHandler = ^(AsyncKeyCallback callback) {
callback(response);
};
}
- (void)recordEmbedderCallsTo:(nonnull NSMutableArray<FlutterAsyncKeyCallback>*)storage {
_embedderHandler = ^(AsyncKeyCallback callback) {
[storage addObject:callback];
};
}
- (void)respondChannelCallsWith:(BOOL)response {
_channelHandler = ^(AsyncKeyCallback callback) {
callback(response);
};
}
- (void)recordChannelCallsTo:(nonnull NSMutableArray<FlutterAsyncKeyCallback>*)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 @@ TEST(FlutterKeyboardManagerUnittests, DoublePrimaryResponder) {
}
TEST(FlutterKeyboardManagerUnittests, SingleFinalResponder) {
ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] singleSecondaryResponder]);
ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] textInputPlugin]);
}
TEST(FlutterKeyboardManagerUnittests, EmptyNextResponder) {
@@ -122,9 +219,9 @@ TEST(FlutterKeyboardManagerUnittests, EmptyNextResponder) {
// 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 @@ TEST(FlutterKeyboardManagerUnittests, EmptyNextResponder) {
}
- (bool)singlePrimaryResponder {
NSResponder* owner = flutter::testing::mockOwnerWithDownOnlyNext();
FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] initWithOwner:owner];
__block NSMutableArray<FlutterAsyncKeyCallback>* callbacks =
KeyboardTester* tester = [[KeyboardTester alloc] init];
NSMutableArray<FlutterAsyncKeyCallback>* embedderCallbacks =
[NSMutableArray<FlutterAsyncKeyCallback> 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<FlutterAsyncKeyCallback>* callbacks1 =
// Send a down event first so we can send an up event later.
[tester respondEmbedderCallsWith:false];
[tester respondChannelCallsWith:false];
[tester.manager handleEvent:keyDownEvent(0x50)];
NSMutableArray<FlutterAsyncKeyCallback>* embedderCallbacks =
[NSMutableArray<FlutterAsyncKeyCallback> array];
[manager addPrimaryResponder:flutter::testing::mockPrimaryResponder(
^(FlutterAsyncKeyCallback callback) {
[callbacks1 addObject:callback];
})];
__block NSMutableArray<FlutterAsyncKeyCallback>* callbacks2 =
NSMutableArray<FlutterAsyncKeyCallback>* channelCallbacks =
[NSMutableArray<FlutterAsyncKeyCallback> array];
[manager addPrimaryResponder:flutter::testing::mockPrimaryResponder(
^(FlutterAsyncKeyCallback callback) {
[callbacks2 addObject:callback];
})];
[tester recordEmbedderCallsTo:embedderCallbacks];
[tester recordChannelCallsTo:channelCallbacks];
// 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];
// 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<FlutterAsyncKeyCallback>* callbacks =
// Send a down event first so we can send an up event later.
[tester respondEmbedderCallsWith:false];
[tester respondChannelCallsWith:false];
[tester.manager handleEvent:keyDownEvent(0x50)];
NSMutableArray<FlutterAsyncKeyCallback>* callbacks =
[NSMutableArray<FlutterAsyncKeyCallback> array];
[manager addPrimaryResponder:flutter::testing::mockPrimaryResponder(
^(FlutterAsyncKeyCallback callback) {
[callbacks addObject:callback];
})];
__block BOOL nextResponse;
[manager addSecondaryResponder:flutter::testing::mockSecondaryResponder(^() {
return nextResponse;
})];
[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;
}

View File

@@ -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 <Cocoa/Cocoa.h>
#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<FlutterBinaryMessenger>)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

View File

@@ -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 <FlutterKeySecondaryResponder>
@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

View File

@@ -461,19 +461,6 @@ static flutter::TextRange RangeFromBaseExtent(NSNumber* base,
: 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)) {

View File

@@ -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 @@ static void CommonInit(FlutterViewController* controller) {
- (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 @@ static void CommonInit(FlutterViewController* controller) {
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<FlutterBinaryMessenger>)getBinaryMessenger {
return _engine.binaryMessenger;
}
- (BOOL)onTextInputKeyEvent:(nonnull NSEvent*)event {
return [_textInputPlugin handleKeyEvent:event];
}
#pragma mark - NSResponder
- (BOOL)acceptsFirstResponder {

View File

@@ -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 () <FlutterKeyboardViewDelegate>
// The FlutterView for this view controller.
@property(nonatomic, readonly, nullable) FlutterView* flutterView;