Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 3f51a94

Browse files
committed
Put FlutterTextInputPlugin in view hierarchy
1 parent 9015062 commit 3f51a94

9 files changed

+59
-188
lines changed

shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.mm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
// first responder.
129129
FlutterTextField* native_text_field = (FlutterTextField*)focused;
130130
if (native_text_field == mac_platform_node_delegate->GetFocus()) {
131-
[native_text_field becomeFirstResponder];
131+
[native_text_field.window makeFirstResponder:native_text_field];
132132
}
133133
break;
134134
}
@@ -172,7 +172,7 @@
172172
(FlutterTextField*)mac_platform_node_delegate->GetNativeViewAccessible();
173173
id focused = mac_platform_node_delegate->GetFocus();
174174
if (!focused || native_text_field == focused) {
175-
[native_text_field becomeFirstResponder];
175+
[native_text_field.window makeFirstResponder:native_text_field];
176176
}
177177
break;
178178
}

shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,13 @@
3939
*/
4040
- (void)handleEvent:(nonnull NSEvent*)event;
4141

42+
/**
43+
* The event currently being redispatched.
44+
*
45+
* In some instances (i.e. emoji shortcut) the event may redelivered by cocoa
46+
* as key equivalent to FlutterTextInput, in which case it shouldn't be
47+
* processed again.
48+
*/
49+
@property(nonatomic, nullable) NSEvent* eventBeingDispatched;
50+
4251
@end

shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ - (void)dispatchTextEvent:(NSEvent*)event {
230230
if (nextResponder == nil) {
231231
return;
232232
}
233+
_eventBeingDispatched = event;
233234
switch (event.type) {
234235
case NSEventTypeKeyDown:
235236
if ([nextResponder respondsToSelector:@selector(keyDown:)]) {
@@ -249,6 +250,7 @@ - (void)dispatchTextEvent:(NSEvent*)event {
249250
default:
250251
NSAssert(false, @"Unexpected key event type (got %lu).", event.type);
251252
}
253+
_eventBeingDispatched = nil;
252254
}
253255

254256
- (void)buildLayout {

shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@
295295
YES);
296296
// The text of TextInputPlugin only starts syncing editing state to the
297297
// native text field when it becomes the first responder.
298-
[native_text_field becomeFirstResponder];
298+
[native_text_field.window makeFirstResponder:native_text_field];
299299
EXPECT_EQ([native_text_field.stringValue isEqualToString:@"textfield"], YES);
300300
}
301301

shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ @interface FlutterTextInputPlugin ()
138138
*/
139139
@property(nonatomic) BOOL enableDeltaModel;
140140

141+
/**
142+
* When plugin becomes first responder it remembers previous responder,
143+
* which will be restored after the plugin is hidden.
144+
*/
145+
@property(nonatomic, weak) NSResponder* previousResponder;
146+
141147
/**
142148
* Handles a Flutter system message on the text input channel.
143149
*/
@@ -262,11 +268,22 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
262268
_activeModel = std::make_unique<flutter::TextInputModel>();
263269
}
264270
} else if ([method isEqualToString:kShowMethod]) {
271+
// Ensure the plugin is in hierarchy.
272+
// When accessibility text field becomes first responder AppKit sometimes
273+
// removes the plugin from hierarchy.
274+
if (_client == nil) {
275+
[_flutterViewController.view addSubview:self];
276+
if (_previousResponder == nil) {
277+
_previousResponder = self.window.firstResponder;
278+
}
279+
[self.window makeFirstResponder:self];
280+
}
265281
_shown = TRUE;
266-
[_textInputContext activate];
267282
} else if ([method isEqualToString:kHideMethod]) {
283+
[self.window makeFirstResponder:_previousResponder];
284+
_previousResponder = nil;
285+
[self removeFromSuperview];
268286
_shown = FALSE;
269-
[_textInputContext deactivate];
270287
} else if ([method isEqualToString:kClearClientMethod]) {
271288
// If there's an active mark region, commit it, end composing, and clear the IME's mark text.
272289
if (_activeModel && _activeModel->composing()) {
@@ -362,7 +379,9 @@ - (void)setEditingState:(NSDictionary*)state {
362379
if (composing_range.collapsed() && wasComposing) {
363380
[_textInputContext discardMarkedText];
364381
}
365-
[_client becomeFirstResponder];
382+
if (_client != nil) {
383+
[self.window makeFirstResponder:_client];
384+
}
366385
[self updateTextAndSelection];
367386
}
368387

@@ -464,12 +483,6 @@ - (BOOL)handleKeyEvent:(NSEvent*)event {
464483
return NO;
465484
}
466485

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

@@ -484,8 +497,18 @@ - (void)keyUp:(NSEvent*)event {
484497
[self.flutterViewController keyUp:event];
485498
}
486499

500+
// Invoked through NSWindow processing of key down event. This can be either
501+
// regular event sent from NSApplication with CMD modifier, in which case the
502+
// event is processed as keyDown, or keyboard manager redispatching the event
503+
// if nextResponder is NSWindow, in which case the event needs to be ignored,
504+
// otherwise it will cause endless loop.
487505
- (BOOL)performKeyEquivalent:(NSEvent*)event {
488-
return [self.flutterViewController performKeyEquivalent:event];
506+
if (_flutterViewController.keyboardManager.eventBeingDispatched == event) {
507+
// This happens with cmd+contorl+space (emoji picker)
508+
return NO;
509+
}
510+
[self.flutterViewController keyDown:event];
511+
return YES;
489512
}
490513

491514
- (void)flagsChanged:(NSEvent*)event {

shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -250,60 +250,6 @@ - (bool)testComposingRegionRemovedByFramework {
250250
return true;
251251
}
252252

253-
- (bool)testInputContextIsKeptActive {
254-
id engineMock = OCMClassMock([FlutterEngine class]);
255-
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
256-
nibName:@""
257-
bundle:nil];
258-
259-
FlutterTextInputPlugin* plugin =
260-
[[FlutterTextInputPlugin alloc] initWithViewController:viewController];
261-
262-
[plugin handleMethodCall:[FlutterMethodCall
263-
methodCallWithMethodName:@"TextInput.setClient"
264-
arguments:@[
265-
@(1), @{
266-
@"inputAction" : @"action",
267-
@"inputType" : @{@"name" : @"inputName"},
268-
}
269-
]]
270-
result:^(id){
271-
}];
272-
273-
[plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
274-
arguments:@{
275-
@"text" : @"",
276-
@"selectionBase" : @(0),
277-
@"selectionExtent" : @(0),
278-
@"composingBase" : @(-1),
279-
@"composingExtent" : @(-1),
280-
}]
281-
result:^(id){
282-
}];
283-
284-
[plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.show"
285-
arguments:@[]]
286-
result:^(id){
287-
}];
288-
289-
[plugin.inputContext deactivate];
290-
EXPECT_EQ(plugin.inputContext.isActive, NO);
291-
NSEvent* keyEvent = [NSEvent keyEventWithType:NSEventTypeKeyDown
292-
location:NSZeroPoint
293-
modifierFlags:0x100
294-
timestamp:0
295-
windowNumber:0
296-
context:nil
297-
characters:@""
298-
charactersIgnoringModifiers:@""
299-
isARepeat:NO
300-
keyCode:0x50];
301-
302-
[plugin handleKeyEvent:keyEvent];
303-
EXPECT_EQ(plugin.inputContext.isActive, YES);
304-
return true;
305-
}
306-
307253
- (bool)testClearClientDuringComposing {
308254
// Set up FlutterTextInputPlugin.
309255
id engineMock = OCMClassMock([FlutterEngine class]);
@@ -1005,10 +951,6 @@ - (bool)testLocalTextAndSelectionUpdateAfterDelta {
1005951
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingRegionRemovedByFramework]);
1006952
}
1007953

1008-
TEST(FlutterTextInputPluginTest, TestTextInputContextIsKeptAlive) {
1009-
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testInputContextIsKeptActive]);
1010-
}
1011-
1012954
TEST(FlutterTextInputPluginTest, TestClearClientDuringComposing) {
1013955
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testClearClientDuringComposing]);
1014956
}

shell/platform/darwin/macos/framework/Source/FlutterViewController.mm

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,6 @@ @interface FlutterViewController () <FlutterViewReshapeListener>
176176
*/
177177
@property(nonatomic) id keyUpMonitor;
178178

179-
/**
180-
* Pointer to a keyboard manager, a hub that manages how key events are
181-
* dispatched to various Flutter key responders, and whether the event is
182-
* propagated to the next NSResponder.
183-
*/
184-
@property(nonatomic) FlutterKeyboardManager* keyboardManager;
185-
186179
@property(nonatomic) KeyboardLayoutNotifier keyboardLayoutNotifier;
187180

188181
@property(nonatomic) NSData* keyboardLayoutData;
@@ -431,10 +424,11 @@ - (void)listenForMetaModifiedKeyUpEvents {
431424
addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp
432425
handler:^NSEvent*(NSEvent* event) {
433426
// Intercept keyUp only for events triggered on the current
434-
// view.
427+
// view or textInputPlugin.
428+
NSResponder* firstResponder = [[event window] firstResponder];
435429
if (weakSelf.viewLoaded && weakSelf.flutterView &&
436-
([[event window] firstResponder] ==
437-
weakSelf.flutterView) &&
430+
(firstResponder == weakSelf.flutterView ||
431+
firstResponder == weakSelf.textInputPlugin) &&
438432
([event modifierFlags] & NSEventModifierFlagCommand) &&
439433
([event type] == NSEventTypeKeyUp)) {
440434
[weakSelf keyUp:event];
@@ -481,7 +475,6 @@ - (void)initializeKeyboard {
481475
// TODO(goderbauer): Seperate keyboard/textinput stuff into ViewController specific and Engine
482476
// global parts. Move the global parts to FlutterEngine.
483477
__weak FlutterViewController* weakSelf = self;
484-
_textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:weakSelf];
485478
_keyboardManager = [[FlutterKeyboardManager alloc] initWithViewDelegate:weakSelf];
486479
}
487480

@@ -740,27 +733,6 @@ - (void)keyUp:(NSEvent*)event {
740733
[_keyboardManager handleEvent:event];
741734
}
742735

743-
- (BOOL)performKeyEquivalent:(NSEvent*)event {
744-
[_keyboardManager handleEvent:event];
745-
if (event.type == NSEventTypeKeyDown) {
746-
// macOS only sends keydown for performKeyEquivalent, but the Flutter framework
747-
// always expects a keyup for every keydown. Synthesizes a key up event so that
748-
// the Flutter framework continues to work.
749-
NSEvent* synthesizedUp = [NSEvent keyEventWithType:NSEventTypeKeyUp
750-
location:event.locationInWindow
751-
modifierFlags:event.modifierFlags
752-
timestamp:event.timestamp
753-
windowNumber:event.windowNumber
754-
context:event.context
755-
characters:event.characters
756-
charactersIgnoringModifiers:event.charactersIgnoringModifiers
757-
isARepeat:event.isARepeat
758-
keyCode:event.keyCode];
759-
[_keyboardManager handleEvent:synthesizedUp];
760-
}
761-
return YES;
762-
}
763-
764736
- (void)flagsChanged:(NSEvent*)event {
765737
[_keyboardManager handleEvent:event];
766738
}

shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ - (bool)testKeyEventsAreSentToFramework;
2020
- (bool)testKeyEventsArePropagatedIfNotHandled;
2121
- (bool)testKeyEventsAreNotPropagatedIfHandled;
2222
- (bool)testFlagsChangedEventsArePropagatedIfNotHandled;
23-
- (bool)testPerformKeyEquivalentSynthesizesKeyUp;
2423
- (bool)testKeyboardIsRestartedOnEngineRestart;
2524
- (bool)testTrackpadGesturesAreSentToFramework;
2625

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

127-
TEST(FlutterViewControllerTest, TestPerformKeyEquivalentSynthesizesKeyUp) {
128-
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testPerformKeyEquivalentSynthesizesKeyUp]);
129-
}
130-
131126
TEST(FlutterViewControllerTest, TestKeyboardIsRestartedOnEngineRestart) {
132127
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testKeyboardIsRestartedOnEngineRestart]);
133128
}
@@ -341,86 +336,6 @@ - (bool)testKeyEventsAreNotPropagatedIfHandled {
341336
return true;
342337
}
343338

344-
- (bool)testPerformKeyEquivalentSynthesizesKeyUp {
345-
id engineMock = OCMClassMock([FlutterEngine class]);
346-
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
347-
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
348-
[engineMock binaryMessenger])
349-
.andReturn(binaryMessengerMock);
350-
OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
351-
callback:nil
352-
userData:nil])
353-
.andCall([FlutterViewControllerTestObjC class],
354-
@selector(respondFalseForSendEvent:callback:userData:));
355-
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
356-
nibName:@""
357-
bundle:nil];
358-
id responderMock = flutter::testing::mockResponder();
359-
viewController.nextResponder = responderMock;
360-
NSDictionary* expectedKeyDownEvent = @{
361-
@"keymap" : @"macos",
362-
@"type" : @"keydown",
363-
@"keyCode" : @(65),
364-
@"modifiers" : @(538968064),
365-
@"characters" : @".",
366-
@"charactersIgnoringModifiers" : @".",
367-
};
368-
NSData* encodedKeyDownEvent =
369-
[[FlutterJSONMessageCodec sharedInstance] encode:expectedKeyDownEvent];
370-
NSDictionary* expectedKeyUpEvent = @{
371-
@"keymap" : @"macos",
372-
@"type" : @"keyup",
373-
@"keyCode" : @(65),
374-
@"modifiers" : @(538968064),
375-
@"characters" : @".",
376-
@"charactersIgnoringModifiers" : @".",
377-
};
378-
NSData* encodedKeyUpEvent = [[FlutterJSONMessageCodec sharedInstance] encode:expectedKeyUpEvent];
379-
CGEventRef cgEvent = CGEventCreateKeyboardEvent(NULL, 65, TRUE);
380-
NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
381-
OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
382-
[binaryMessengerMock sendOnChannel:@"flutter/keyevent"
383-
message:encodedKeyDownEvent
384-
binaryReply:[OCMArg any]])
385-
.andDo((^(NSInvocation* invocation) {
386-
FlutterBinaryReply handler;
387-
[invocation getArgument:&handler atIndex:4];
388-
NSDictionary* reply = @{
389-
@"handled" : @(true),
390-
};
391-
NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply];
392-
handler(encodedReply);
393-
}));
394-
OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
395-
[binaryMessengerMock sendOnChannel:@"flutter/keyevent"
396-
message:encodedKeyUpEvent
397-
binaryReply:[OCMArg any]])
398-
.andDo((^(NSInvocation* invocation) {
399-
FlutterBinaryReply handler;
400-
[invocation getArgument:&handler atIndex:4];
401-
NSDictionary* reply = @{
402-
@"handled" : @(true),
403-
};
404-
NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply];
405-
handler(encodedReply);
406-
}));
407-
[viewController viewWillAppear]; // Initializes the event channel.
408-
[viewController performKeyEquivalent:event];
409-
@try {
410-
OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
411-
[binaryMessengerMock sendOnChannel:@"flutter/keyevent"
412-
message:encodedKeyDownEvent
413-
binaryReply:[OCMArg any]]);
414-
OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
415-
[binaryMessengerMock sendOnChannel:@"flutter/keyevent"
416-
message:encodedKeyUpEvent
417-
binaryReply:[OCMArg any]]);
418-
} @catch (...) {
419-
return false;
420-
}
421-
return true;
422-
}
423-
424339
- (bool)testKeyboardIsRestartedOnEngineRestart {
425340
id engineMock = OCMClassMock([FlutterEngine class]);
426341
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));

0 commit comments

Comments
 (0)