Skip to content

Commit

Permalink
Enable delayed event delivery for macOS (flutter#21231)
Browse files Browse the repository at this point in the history
This enables delayed event delivery for macOS, so that shortcuts can handle keys that are headed for a text field and intercept them. This fixes the problem where pressing TAB (or other shortcuts) in a text field also inserts a tab character into the text field.
  • Loading branch information
gspencergoog committed Dec 11, 2020
1 parent 4679f7b commit 21691f1
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 34 deletions.
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,8 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterGLCom
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterGLCompositorUnittests.mm
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/FlutterIntermediateKeyResponder.h
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.h
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h
Expand Down
2 changes: 2 additions & 0 deletions shell/platform/darwin/macos/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ source_set("flutter_framework_source") {
"framework/Source/FlutterGLCompositor.mm",
"framework/Source/FlutterIOSurfaceHolder.h",
"framework/Source/FlutterIOSurfaceHolder.mm",
"framework/Source/FlutterIntermediateKeyResponder.h",
"framework/Source/FlutterIntermediateKeyResponder.mm",
"framework/Source/FlutterMouseCursorPlugin.h",
"framework/Source/FlutterMouseCursorPlugin.mm",
"framework/Source/FlutterResizeSynchronizer.h",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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 key responder that can declare itself as the final
* responder of the event, terminating the event propagation.
*
* It differs from an NSResponder in that it returns a boolean from the
* handleKeyUp and handleKeyDown calls, where true means it has handled the
* given event.
*/
@interface FlutterIntermediateKeyResponder : NSObject
/*
* Informs the receiver that the user has released a key.
*
* Default implementation returns NO.
*/
- (BOOL)handleKeyUp:(nonnull NSEvent*)event;
/*
* Informs the receiver that the user has pressed a key.
*
* Default implementation returns NO.
*/
- (BOOL)handleKeyDown:(nonnull NSEvent*)event;
@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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 "flutter/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.h"

@implementation FlutterIntermediateKeyResponder {
}

#pragma mark - Default key handling methods

- (BOOL)handleKeyUp:(NSEvent*)event {
return NO;
}

- (BOOL)handleKeyDown:(NSEvent*)event {
return NO;
}
@end
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#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/FlutterIntermediateKeyResponder.h"

/**
* A plugin to handle text input.
Expand All @@ -16,7 +17,7 @@
* This is not an FlutterPlugin since it needs access to FlutterViewController internals, so needs
* to be managed differently.
*/
@interface FlutterTextInputPlugin : NSResponder
@interface FlutterTextInputPlugin : FlutterIntermediateKeyResponder

/**
* Initializes a text input plugin that coordinates key event handling with |viewController|.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,20 @@ - (void)updateEditState {
}

#pragma mark -
#pragma mark NSResponder
#pragma mark FlutterIntermediateKeyResponder

/**
* Handles key down events received from the view controller, responding TRUE 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. So for now, limit processing to just keyDown.
* processing of the same keys. So for now, limit processing to just handleKeyDown.
*/
- (void)keyDown:(NSEvent*)event {
[_textInputContext handleEvent:event];
- (BOOL)handleKeyDown:(NSEvent*)event {
return [_textInputContext handleEvent:event];
}

#pragma mark -
Expand Down
104 changes: 84 additions & 20 deletions shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,14 @@ void Reset() {
@interface FlutterViewController () <FlutterViewReshapeListener>

/**
* A list of additional responders to keyboard events. Keybord events are forwarded to all of them.
* A list of additional responders to keyboard events.
*
* Keyboard events received by FlutterViewController are first dispatched to
* each additional responder in order. If any of them handle the event (by
* returning true), the event is not dispatched to later additional responders
* or to the nextResponder.
*/
@property(nonatomic) NSMutableOrderedSet<NSResponder*>* additionalKeyResponders;
@property(nonatomic) NSMutableOrderedSet<FlutterIntermediateKeyResponder*>* additionalKeyResponders;

/**
* The tracking area used to generate hover events, if enabled.
Expand Down Expand Up @@ -135,7 +140,15 @@ - (void)dispatchMouseEvent:(nonnull NSEvent*)event;
- (void)dispatchMouseEvent:(nonnull NSEvent*)event phase:(FlutterPointerPhase)phase;

/**
* Converts |event| to a key event channel message, and sends it to the engine.
* Sends |event| to all responders in additionalKeyResponders and then to the
* nextResponder if none of the additional responders handled the event.
*/
- (void)propagateKeyEvent:(NSEvent*)event ofType:(NSString*)type;

/**
* Converts |event| to a key event channel message, and sends it to the engine to
* hand to the framework. Once the framework responds, if the event was not handled,
* propagates the event to any additional key responders.
*/
- (void)dispatchKeyEvent:(NSEvent*)event ofType:(NSString*)type;

Expand Down Expand Up @@ -206,9 +219,11 @@ @implementation FlutterViewController {
* Performs initialization that's common between the different init paths.
*/
static void CommonInit(FlutterViewController* controller) {
controller->_engine = [[FlutterEngine alloc] initWithName:@"io.flutter"
project:controller->_project
allowHeadlessExecution:NO];
if (!controller->_engine) {
controller->_engine = [[FlutterEngine alloc] initWithName:@"io.flutter"
project:controller->_project
allowHeadlessExecution:NO];
}
controller->_additionalKeyResponders = [[NSMutableOrderedSet alloc] init];
controller->_mouseTrackingMode = FlutterMouseTrackingModeInKeyWindow;
}
Expand Down Expand Up @@ -238,6 +253,27 @@ - (instancetype)initWithProject:(nullable FlutterDartProject*)project {
return self;
}

- (instancetype)initWithEngine:(nonnull FlutterEngine*)engine
nibName:(nullable NSString*)nibName
bundle:(nullable NSBundle*)nibBundle {
NSAssert(engine != nil, @"Engine is required");
self = [super initWithNibName:nibName bundle:nibBundle];
if (self) {
if (engine.viewController) {
NSLog(@"The supplied FlutterEngine %@ is already used with FlutterViewController "
"instance %@. One instance of the FlutterEngine can only be attached to one "
"FlutterViewController at a time. Set FlutterEngine.viewController "
"to nil before attaching it to another FlutterViewController.",
[engine description], [engine.viewController description]);
}
_engine = engine;
CommonInit(self);
[engine setViewController:self];
}

return self;
}

- (void)loadView {
NSOpenGLContext* resourceContext = _engine.resourceContext;
if (!resourceContext) {
Expand Down Expand Up @@ -288,11 +324,12 @@ - (FlutterView*)flutterView {
return static_cast<FlutterView*>(self.view);
}

- (void)addKeyResponder:(NSResponder*)responder {
- (void)addKeyResponder:(FlutterIntermediateKeyResponder*)responder {
[self.additionalKeyResponders addObject:responder];
}

- (void)removeKeyResponder:(NSResponder*)responder {
- (void)removeKeyResponder:(FlutterIntermediateKeyResponder*)responder {
[self.additionalKeyResponders removeObject:responder];
}

#pragma mark - Private methods
Expand Down Expand Up @@ -460,19 +497,56 @@ - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase {
}
}

- (void)propagateKeyEvent:(NSEvent*)event ofType:(NSString*)type {
if ([type isEqual:@"keydown"]) {
for (FlutterIntermediateKeyResponder* responder in self.additionalKeyResponders) {
if ([responder handleKeyDown:event]) {
return;
}
}
if ([self.nextResponder respondsToSelector:@selector(keyDown:)]) {
[self.nextResponder keyDown:event];
}
} else if ([type isEqual:@"keyup"]) {
for (FlutterIntermediateKeyResponder* responder in self.additionalKeyResponders) {
if ([responder handleKeyUp:event]) {
return;
}
}
if ([self.nextResponder respondsToSelector:@selector(keyUp:)]) {
[self.nextResponder keyUp:event];
}
}
}

- (void)dispatchKeyEvent:(NSEvent*)event ofType:(NSString*)type {
if (event.type != NSEventTypeKeyDown && event.type != NSEventTypeKeyUp &&
event.type != NSEventTypeFlagsChanged) {
return;
}
NSMutableDictionary* keyMessage = [@{
@"keymap" : @"macos",
@"type" : type,
@"keyCode" : @(event.keyCode),
@"modifiers" : @(event.modifierFlags),
} mutableCopy];
// Calling these methods on any other type of event will raise an exception.
// Calling these methods on any other type of event
// (e.g NSEventTypeFlagsChanged) will raise an exception.
if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp) {
keyMessage[@"characters"] = event.characters;
keyMessage[@"charactersIgnoringModifiers"] = event.charactersIgnoringModifiers;
}
[_keyEventChannel sendMessage:keyMessage];
__weak __typeof__(self) weakSelf = self;
FlutterReply replyHandler = ^(id _Nullable reply) {
if (!reply) {
return;
}
// Only re-dispatch the event to other responders if the framework didn't handle it.
if (![[reply valueForKey:@"handled"] boolValue]) {
[weakSelf propagateKeyEvent:event ofType:type];
}
};
[_keyEventChannel sendMessage:keyMessage reply:replyHandler];
}

- (void)onSettingsChanged:(NSNotification*)notification {
Expand Down Expand Up @@ -571,20 +645,10 @@ - (BOOL)acceptsFirstResponder {

- (void)keyDown:(NSEvent*)event {
[self dispatchKeyEvent:event ofType:@"keydown"];
for (NSResponder* responder in self.additionalKeyResponders) {
if ([responder respondsToSelector:@selector(keyDown:)]) {
[responder keyDown:event];
}
}
}

- (void)keyUp:(NSEvent*)event {
[self dispatchKeyEvent:event ofType:@"keyup"];
for (NSResponder* responder in self.additionalKeyResponders) {
if ([responder respondsToSelector:@selector(keyUp:)]) {
[responder keyUp:event];
}
}
}

- (void)flagsChanged:(NSEvent*)event {
Expand Down

0 comments on commit 21691f1

Please sign in to comment.