diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 19c779ec2a992..6a9c948ab5c08 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -384,10 +384,7 @@ - (void)dealloc { - (void)resignAndRemoveFromSuperview { if (self.superview != nil) { - // With accessiblity enabled TextInputPlugin is inside _client, so take the - // nextResponder from the _client. - NSResponder* nextResponder = _client != nil ? _client.nextResponder : self.nextResponder; - [self.window makeFirstResponder:nextResponder]; + [self.window makeFirstResponder:_flutterViewController.flutterView]; [self removeFromSuperview]; } } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index 4a6afa2e3fccf..165a4b5bb110e 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -2049,6 +2049,41 @@ - (bool)testSelectorsAreForwardedToFramework { ASSERT_FALSE(window.firstResponder == viewController.textInputPlugin); } +TEST(FlutterTextInputPluginTest, FirstResponderIsCorrect) { + FlutterEngine* engine = CreateTestEngine(); + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + [viewController loadView]; + + NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:NO]; + window.contentView = viewController.view; + + ASSERT_TRUE(viewController.flutterView.acceptsFirstResponder); + + [window makeFirstResponder:viewController.flutterView]; + + [viewController.textInputPlugin + handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.show" arguments:@[]] + result:^(id){ + }]; + + ASSERT_TRUE(window.firstResponder == viewController.textInputPlugin); + + ASSERT_FALSE(viewController.flutterView.acceptsFirstResponder); + + [viewController.textInputPlugin + handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.hide" arguments:@[]] + result:^(id){ + }]; + + ASSERT_TRUE(viewController.flutterView.acceptsFirstResponder); + ASSERT_TRUE(window.firstResponder == viewController.flutterView); +} + TEST(FlutterTextInputPluginTest, HasZeroSizeAndClipsToBounds) { id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.h b/shell/platform/darwin/macos/framework/Source/FlutterView.h index 6948b880fcf9b..d82f34c51b0cb 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.h @@ -23,13 +23,19 @@ typedef int64_t FlutterViewId; constexpr FlutterViewId kFlutterImplicitViewId = 0ll; /** - * Listener for view resizing. + * Delegate for FlutterView. */ -@protocol FlutterViewReshapeListener +@protocol FlutterViewDelegate /** * Called when the view's backing store changes size. */ - (void)viewDidReshape:(nonnull NSView*)view; + +/** + * Called to determine whether the view should accept first responder status. + */ +- (BOOL)viewShouldAcceptFirstResponder:(nonnull NSView*)view; + @end /** @@ -43,7 +49,7 @@ constexpr FlutterViewId kFlutterImplicitViewId = 0ll; */ - (nullable instancetype)initWithMTLDevice:(nonnull id)device commandQueue:(nonnull id)commandQueue - reshapeListener:(nonnull id)reshapeListener + delegate:(nonnull id)delegate threadSynchronizer:(nonnull FlutterThreadSynchronizer*)threadSynchronizer viewId:(int64_t)viewId NS_DESIGNATED_INITIALIZER; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.mm b/shell/platform/darwin/macos/framework/Source/FlutterView.mm index 89bbdb9153828..79607d0f759e8 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.mm @@ -11,7 +11,7 @@ @interface FlutterView () { int64_t _viewId; - __weak id _reshapeListener; + __weak id _viewDelegate; FlutterThreadSynchronizer* _threadSynchronizer; FlutterSurfaceManager* _surfaceManager; } @@ -22,7 +22,7 @@ @implementation FlutterView - (instancetype)initWithMTLDevice:(id)device commandQueue:(id)commandQueue - reshapeListener:(id)reshapeListener + delegate:(id)delegate threadSynchronizer:(FlutterThreadSynchronizer*)threadSynchronizer viewId:(int64_t)viewId { self = [super initWithFrame:NSZeroRect]; @@ -31,7 +31,7 @@ - (instancetype)initWithMTLDevice:(id)device [self setBackgroundColor:[NSColor blackColor]]; [self setLayerContentsRedrawPolicy:NSViewLayerContentsRedrawDuringViewResize]; _viewId = viewId; - _reshapeListener = reshapeListener; + _viewDelegate = delegate; _threadSynchronizer = threadSynchronizer; _surfaceManager = [[FlutterSurfaceManager alloc] initWithDevice:device commandQueue:commandQueue @@ -54,7 +54,7 @@ - (void)reshaped { [_threadSynchronizer beginResizeForView:_viewId size:scaledSize notify:^{ - [_reshapeListener viewDidReshape:self]; + [_viewDelegate viewDidReshape:self]; }]; } @@ -89,7 +89,9 @@ - (BOOL)acceptsFirstMouse:(NSEvent*)event { } - (BOOL)acceptsFirstResponder { - return YES; + // This is to ensure that FlutterView does not take first responder status from TextInputPlugin + // on mouse clicks. + return [_viewDelegate viewShouldAcceptFirstResponder:self]; } - (void)cursorUpdate:(NSEvent*)event { @@ -104,7 +106,7 @@ - (void)cursorUpdate:(NSEvent*)event { - (void)viewDidChangeBackingProperties { [super viewDidChangeBackingProperties]; // Force redraw - [_reshapeListener viewDidReshape:self]; + [_viewDelegate viewDidReshape:self]; } - (BOOL)layer:(CALayer*)layer diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index 15d36a777fda0..3ad17b76cbea4 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -169,7 +169,7 @@ - (void)setBackgroundColor:(NSColor*)color; /** * Private interface declaration for FlutterViewController. */ -@interface FlutterViewController () +@interface FlutterViewController () /** * The tracking area used to generate hover events, if enabled. @@ -831,7 +831,7 @@ - (nonnull FlutterView*)createFlutterViewWithMTLDevice:(id)device commandQueue:(id)commandQueue { return [[FlutterView alloc] initWithMTLDevice:device commandQueue:commandQueue - reshapeListener:self + delegate:self threadSynchronizer:_threadSynchronizer viewId:_viewId]; } @@ -851,15 +851,24 @@ - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package { return [FlutterDartProject lookupKeyForAsset:asset fromPackage:package]; } -#pragma mark - FlutterViewReshapeListener +#pragma mark - FlutterViewDelegate /** * Responds to view reshape by notifying the engine of the change in dimensions. */ - (void)viewDidReshape:(NSView*)view { + FML_DCHECK(view == _flutterView); [_engine updateWindowMetricsForViewController:self]; } +- (BOOL)viewShouldAcceptFirstResponder:(NSView*)view { + FML_DCHECK(view == _flutterView); + // Only allow FlutterView to become first responder if TextInputPlugin is + // not active. Otherwise a mouse event inside FlutterView would cause the + // TextInputPlugin to lose first responder status. + return !_textInputPlugin.isFirstResponder; +} + #pragma mark - FlutterPluginRegistry - (id)registrarForPlugin:(NSString*)pluginName { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm index 0d6e7cb550400..65620a53824e4 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm @@ -10,25 +10,29 @@ constexpr int64_t kImplicitViewId = 0ll; -@interface TestReshapeListener : NSObject +@interface TestFlutterViewDelegate : NSObject @end -@implementation TestReshapeListener +@implementation TestFlutterViewDelegate - (void)viewDidReshape:(nonnull NSView*)view { } +- (BOOL)viewShouldAcceptFirstResponder:(NSView*)view { + return YES; +} + @end TEST(FlutterView, ShouldInheritContentsScaleReturnsYes) { id device = MTLCreateSystemDefaultDevice(); id queue = [device newCommandQueue]; - TestReshapeListener* listener = [[TestReshapeListener alloc] init]; + TestFlutterViewDelegate* delegate = [[TestFlutterViewDelegate alloc] init]; FlutterThreadSynchronizer* threadSynchronizer = [[FlutterThreadSynchronizer alloc] init]; FlutterView* view = [[FlutterView alloc] initWithMTLDevice:device commandQueue:queue - reshapeListener:listener + delegate:delegate threadSynchronizer:threadSynchronizer viewId:kImplicitViewId]; EXPECT_EQ([view layer:view.layer shouldInheritContentsScale:3.0 fromWindow:view.window], YES);