[macOS, Keyboard] Refactor: Clean up keyboard initialization, connection, and unit test framework (flutter/engine#31940)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user