Skip to content

Commit

Permalink
Put FlutterTextInputPlugin in view hierarchy
Browse files Browse the repository at this point in the history
  • Loading branch information
knopp committed Jun 4, 2022
1 parent 9015062 commit eef9255
Show file tree
Hide file tree
Showing 9 changed files with 57 additions and 188 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
// first responder.
FlutterTextField* native_text_field = (FlutterTextField*)focused;
if (native_text_field == mac_platform_node_delegate->GetFocus()) {
[native_text_field becomeFirstResponder];
[native_text_field.window makeFirstResponder:native_text_field];
}
break;
}
Expand Down Expand Up @@ -172,7 +172,7 @@
(FlutterTextField*)mac_platform_node_delegate->GetNativeViewAccessible();
id focused = mac_platform_node_delegate->GetFocus();
if (!focused || native_text_field == focused) {
[native_text_field becomeFirstResponder];
[native_text_field.window makeFirstResponder:native_text_field];
}
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,13 @@
*/
- (void)handleEvent:(nonnull NSEvent*)event;

/**
* The event currently being redispatched.
*
* In some instances (i.e. emoji shortcut) the event may redelivered by cocoa
* as key equivalent to FlutterTextInput, in which case it shouldn't be
* processed again.
*/
@property(nonatomic, nullable) NSEvent* eventBeingDispatched;

@end
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ - (void)dispatchTextEvent:(NSEvent*)event {
if (nextResponder == nil) {
return;
}
_eventBeingDispatched = event;
switch (event.type) {
case NSEventTypeKeyDown:
if ([nextResponder respondsToSelector:@selector(keyDown:)]) {
Expand All @@ -249,6 +250,7 @@ - (void)dispatchTextEvent:(NSEvent*)event {
default:
NSAssert(false, @"Unexpected key event type (got %lu).", event.type);
}
_eventBeingDispatched = nil;
}

- (void)buildLayout {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@
YES);
// The text of TextInputPlugin only starts syncing editing state to the
// native text field when it becomes the first responder.
[native_text_field becomeFirstResponder];
[native_text_field.window makeFirstResponder:native_text_field];
EXPECT_EQ([native_text_field.stringValue isEqualToString:@"textfield"], YES);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ @interface FlutterTextInputPlugin ()
*/
@property(nonatomic) BOOL enableDeltaModel;

/**
* When plugin becomes first responder it remembers previous responder,
* which will be made first responder after the plugin is hidden.
*/
@property(nonatomic, weak) NSResponder* previousResponder;

/**
* Handles a Flutter system message on the text input channel.
*/
Expand Down Expand Up @@ -262,11 +268,21 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
_activeModel = std::make_unique<flutter::TextInputModel>();
}
} else if ([method isEqualToString:kShowMethod]) {
// Ensure the plugin is in hierarchy.
// When accessibility text field becomes first responder AppKit sometimes
// removes the plugin from hierarchy.
if (_client == nil) {
[_flutterViewController.view addSubview:self];
if (_previousResponder == nil) {
_previousResponder = self.window.firstResponder;
}
[self.window makeFirstResponder:self];
}
_shown = TRUE;
[_textInputContext activate];
} else if ([method isEqualToString:kHideMethod]) {
[self.window makeFirstResponder:_previousResponder];
_previousResponder = nil;
_shown = FALSE;
[_textInputContext deactivate];
} else if ([method isEqualToString:kClearClientMethod]) {
// If there's an active mark region, commit it, end composing, and clear the IME's mark text.
if (_activeModel && _activeModel->composing()) {
Expand Down Expand Up @@ -362,7 +378,9 @@ - (void)setEditingState:(NSDictionary*)state {
if (composing_range.collapsed() && wasComposing) {
[_textInputContext discardMarkedText];
}
[_client becomeFirstResponder];
if (_client != nil) {
[self.window makeFirstResponder:_client];
}
[self updateTextAndSelection];
}

Expand Down Expand Up @@ -464,12 +482,6 @@ - (BOOL)handleKeyEvent:(NSEvent*)event {
return NO;
}

// NSTextInputContext sometimes deactivates itself without calling
// deactivate. One such example is when the composing region is deleted.
// TODO(LongCatIsLooong): put FlutterTextInputPlugin in the view hierarchy and
// request/resign first responder when needed. Activate/deactivate shouldn't
// be called by the application.
[_textInputContext activate];
return [_textInputContext handleEvent:event];
}

Expand All @@ -484,8 +496,17 @@ - (void)keyUp:(NSEvent*)event {
[self.flutterViewController keyUp:event];
}

// Invoked through NSWindow processing of key down event. This can be either
// regular event sent from NSApplication with CMD modifier, in which case the
// event is processed as keyDown, or keyboard manager redispatching the event
// if nextResponder is NSWindow, in which case the event needs to be ignored,
// otherwise it will cause endless loop.
- (BOOL)performKeyEquivalent:(NSEvent*)event {
return [self.flutterViewController performKeyEquivalent:event];
if (_flutterViewController.keyboardManager.eventBeingDispatched == event) {
return NO;
}
[self.flutterViewController keyDown:event];
return YES;
}

- (void)flagsChanged:(NSEvent*)event {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,60 +250,6 @@ - (bool)testComposingRegionRemovedByFramework {
return true;
}

- (bool)testInputContextIsKeptActive {
id engineMock = OCMClassMock([FlutterEngine class]);
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
nibName:@""
bundle:nil];

FlutterTextInputPlugin* plugin =
[[FlutterTextInputPlugin alloc] initWithViewController:viewController];

[plugin handleMethodCall:[FlutterMethodCall
methodCallWithMethodName:@"TextInput.setClient"
arguments:@[
@(1), @{
@"inputAction" : @"action",
@"inputType" : @{@"name" : @"inputName"},
}
]]
result:^(id){
}];

[plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
arguments:@{
@"text" : @"",
@"selectionBase" : @(0),
@"selectionExtent" : @(0),
@"composingBase" : @(-1),
@"composingExtent" : @(-1),
}]
result:^(id){
}];

[plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.show"
arguments:@[]]
result:^(id){
}];

[plugin.inputContext deactivate];
EXPECT_EQ(plugin.inputContext.isActive, NO);
NSEvent* keyEvent = [NSEvent keyEventWithType:NSEventTypeKeyDown
location:NSZeroPoint
modifierFlags:0x100
timestamp:0
windowNumber:0
context:nil
characters:@""
charactersIgnoringModifiers:@""
isARepeat:NO
keyCode:0x50];

[plugin handleKeyEvent:keyEvent];
EXPECT_EQ(plugin.inputContext.isActive, YES);
return true;
}

- (bool)testClearClientDuringComposing {
// Set up FlutterTextInputPlugin.
id engineMock = OCMClassMock([FlutterEngine class]);
Expand Down Expand Up @@ -1005,10 +951,6 @@ - (bool)testLocalTextAndSelectionUpdateAfterDelta {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingRegionRemovedByFramework]);
}

TEST(FlutterTextInputPluginTest, TestTextInputContextIsKeptAlive) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testInputContextIsKeptActive]);
}

TEST(FlutterTextInputPluginTest, TestClearClientDuringComposing) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testClearClientDuringComposing]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,6 @@ @interface FlutterViewController () <FlutterViewReshapeListener>
*/
@property(nonatomic) id keyUpMonitor;

/**
* Pointer to a keyboard manager, a hub that manages how key events are
* dispatched to various Flutter key responders, and whether the event is
* propagated to the next NSResponder.
*/
@property(nonatomic) FlutterKeyboardManager* keyboardManager;

@property(nonatomic) KeyboardLayoutNotifier keyboardLayoutNotifier;

@property(nonatomic) NSData* keyboardLayoutData;
Expand Down Expand Up @@ -431,10 +424,11 @@ - (void)listenForMetaModifiedKeyUpEvents {
addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp
handler:^NSEvent*(NSEvent* event) {
// Intercept keyUp only for events triggered on the current
// view.
// view or textInputPlugin.
NSResponder* firstResponder = [[event window] firstResponder];
if (weakSelf.viewLoaded && weakSelf.flutterView &&
([[event window] firstResponder] ==
weakSelf.flutterView) &&
(firstResponder == weakSelf.flutterView ||
firstResponder == weakSelf.textInputPlugin) &&
([event modifierFlags] & NSEventModifierFlagCommand) &&
([event type] == NSEventTypeKeyUp)) {
[weakSelf keyUp:event];
Expand Down Expand Up @@ -481,7 +475,6 @@ - (void)initializeKeyboard {
// TODO(goderbauer): Seperate keyboard/textinput stuff into ViewController specific and Engine
// global parts. Move the global parts to FlutterEngine.
__weak FlutterViewController* weakSelf = self;
_textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:weakSelf];
_keyboardManager = [[FlutterKeyboardManager alloc] initWithViewDelegate:weakSelf];
}

Expand Down Expand Up @@ -740,27 +733,6 @@ - (void)keyUp:(NSEvent*)event {
[_keyboardManager handleEvent:event];
}

- (BOOL)performKeyEquivalent:(NSEvent*)event {
[_keyboardManager handleEvent:event];
if (event.type == NSEventTypeKeyDown) {
// macOS only sends keydown for performKeyEquivalent, but the Flutter framework
// always expects a keyup for every keydown. Synthesizes a key up event so that
// the Flutter framework continues to work.
NSEvent* synthesizedUp = [NSEvent keyEventWithType:NSEventTypeKeyUp
location:event.locationInWindow
modifierFlags:event.modifierFlags
timestamp:event.timestamp
windowNumber:event.windowNumber
context:event.context
characters:event.characters
charactersIgnoringModifiers:event.charactersIgnoringModifiers
isARepeat:event.isARepeat
keyCode:event.keyCode];
[_keyboardManager handleEvent:synthesizedUp];
}
return YES;
}

- (void)flagsChanged:(NSEvent*)event {
[_keyboardManager handleEvent:event];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ - (bool)testKeyEventsAreSentToFramework;
- (bool)testKeyEventsArePropagatedIfNotHandled;
- (bool)testKeyEventsAreNotPropagatedIfHandled;
- (bool)testFlagsChangedEventsArePropagatedIfNotHandled;
- (bool)testPerformKeyEquivalentSynthesizesKeyUp;
- (bool)testKeyboardIsRestartedOnEngineRestart;
- (bool)testTrackpadGesturesAreSentToFramework;

Expand Down Expand Up @@ -124,10 +123,6 @@ + (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event
[[FlutterViewControllerTestObjC alloc] testFlagsChangedEventsArePropagatedIfNotHandled]);
}

TEST(FlutterViewControllerTest, TestPerformKeyEquivalentSynthesizesKeyUp) {
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testPerformKeyEquivalentSynthesizesKeyUp]);
}

TEST(FlutterViewControllerTest, TestKeyboardIsRestartedOnEngineRestart) {
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testKeyboardIsRestartedOnEngineRestart]);
}
Expand Down Expand Up @@ -341,86 +336,6 @@ - (bool)testKeyEventsAreNotPropagatedIfHandled {
return true;
}

- (bool)testPerformKeyEquivalentSynthesizesKeyUp {
id engineMock = OCMClassMock([FlutterEngine class]);
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
[engineMock binaryMessenger])
.andReturn(binaryMessengerMock);
OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
callback:nil
userData:nil])
.andCall([FlutterViewControllerTestObjC class],
@selector(respondFalseForSendEvent:callback:userData:));
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
nibName:@""
bundle:nil];
id responderMock = flutter::testing::mockResponder();
viewController.nextResponder = responderMock;
NSDictionary* expectedKeyDownEvent = @{
@"keymap" : @"macos",
@"type" : @"keydown",
@"keyCode" : @(65),
@"modifiers" : @(538968064),
@"characters" : @".",
@"charactersIgnoringModifiers" : @".",
};
NSData* encodedKeyDownEvent =
[[FlutterJSONMessageCodec sharedInstance] encode:expectedKeyDownEvent];
NSDictionary* expectedKeyUpEvent = @{
@"keymap" : @"macos",
@"type" : @"keyup",
@"keyCode" : @(65),
@"modifiers" : @(538968064),
@"characters" : @".",
@"charactersIgnoringModifiers" : @".",
};
NSData* encodedKeyUpEvent = [[FlutterJSONMessageCodec sharedInstance] encode:expectedKeyUpEvent];
CGEventRef cgEvent = CGEventCreateKeyboardEvent(NULL, 65, TRUE);
NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
[binaryMessengerMock sendOnChannel:@"flutter/keyevent"
message:encodedKeyDownEvent
binaryReply:[OCMArg any]])
.andDo((^(NSInvocation* invocation) {
FlutterBinaryReply handler;
[invocation getArgument:&handler atIndex:4];
NSDictionary* reply = @{
@"handled" : @(true),
};
NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply];
handler(encodedReply);
}));
OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
[binaryMessengerMock sendOnChannel:@"flutter/keyevent"
message:encodedKeyUpEvent
binaryReply:[OCMArg any]])
.andDo((^(NSInvocation* invocation) {
FlutterBinaryReply handler;
[invocation getArgument:&handler atIndex:4];
NSDictionary* reply = @{
@"handled" : @(true),
};
NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply];
handler(encodedReply);
}));
[viewController viewWillAppear]; // Initializes the event channel.
[viewController performKeyEquivalent:event];
@try {
OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
[binaryMessengerMock sendOnChannel:@"flutter/keyevent"
message:encodedKeyDownEvent
binaryReply:[OCMArg any]]);
OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
[binaryMessengerMock sendOnChannel:@"flutter/keyevent"
message:encodedKeyUpEvent
binaryReply:[OCMArg any]]);
} @catch (...) {
return false;
}
return true;
}

- (bool)testKeyboardIsRestartedOnEngineRestart {
id engineMock = OCMClassMock([FlutterEngine class]);
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
Expand Down
Loading

0 comments on commit eef9255

Please sign in to comment.