[macOS] Reset keyboard states on engine restart (flutter/engine#29283)
With this PR, the state of the keyboard system will be reset when the engine is reset (typically during a hot restart.)
This commit is contained in:
@@ -169,7 +169,7 @@ public class FlutterNativeView implements BinaryMessenger {
|
||||
}
|
||||
|
||||
private final class EngineLifecycleListenerImpl implements EngineLifecycleListener {
|
||||
// Called by native to notify when the engine is restarted (cold reload).
|
||||
// Called by native to notify right before the engine is restarted (cold reload).
|
||||
@SuppressWarnings("unused")
|
||||
public void onPreEngineRestart() {
|
||||
if (mFlutterView != null) {
|
||||
|
||||
@@ -53,4 +53,12 @@ FLUTTER_DARWIN_EXPORT
|
||||
NS_DESIGNATED_INITIALIZER;
|
||||
- (nonnull instancetype)initWithCoder:(nonnull NSCoder*)nibNameOrNil NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/**
|
||||
* Invoked by the engine right before the engine is restarted.
|
||||
*
|
||||
* This should reset states to as if the application has just started. It
|
||||
* usually indicates a hot restart (Shift-R in Flutter CLI.)
|
||||
*/
|
||||
- (void)onPreEngineRestart;
|
||||
|
||||
@end
|
||||
|
||||
@@ -75,6 +75,14 @@ static FlutterLocale FlutterLocaleFromNSLocale(NSLocale* locale) {
|
||||
*/
|
||||
- (void)engineCallbackOnPlatformMessage:(const FlutterPlatformMessage*)message;
|
||||
|
||||
/**
|
||||
* Invoked right before the engine is restarted.
|
||||
*
|
||||
* This should reset states to as if the application has just started. It
|
||||
* usually indicates a hot restart (Shift-R in Flutter CLI.)
|
||||
*/
|
||||
- (void)engineCallbackOnPreEngineRestart;
|
||||
|
||||
/**
|
||||
* Requests that the task be posted back the to the Flutter engine at the target time. The target
|
||||
* time is in the clock used by the Flutter engine.
|
||||
@@ -296,6 +304,11 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
|
||||
|
||||
flutterArguments.compositor = [self createFlutterCompositor];
|
||||
|
||||
flutterArguments.on_pre_engine_restart_callback = [](void* user_data) {
|
||||
FlutterEngine* engine = (__bridge FlutterEngine*)user_data;
|
||||
[engine engineCallbackOnPreEngineRestart];
|
||||
};
|
||||
|
||||
FlutterRendererConfig rendererConfig = [_renderer createRendererConfig];
|
||||
FlutterEngineResult result = _embedderAPI.Initialize(
|
||||
FLUTTER_ENGINE_VERSION, &rendererConfig, &flutterArguments, (__bridge void*)(self), &_engine);
|
||||
@@ -584,6 +597,12 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
|
||||
}
|
||||
}
|
||||
|
||||
- (void)engineCallbackOnPreEngineRestart {
|
||||
if (_viewController) {
|
||||
[_viewController onPreEngineRestart];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Called from dealloc. Should not use accessors or other methods.
|
||||
*/
|
||||
|
||||
@@ -186,7 +186,7 @@ static flutter::TextRange RangeFromBaseExtent(NSNumber* base,
|
||||
[_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
|
||||
[unsafeSelf handleMethodCall:call result:result];
|
||||
}];
|
||||
_textInputContext = [[NSTextInputContext alloc] initWithClient:self];
|
||||
_textInputContext = [[NSTextInputContext alloc] initWithClient:unsafeSelf];
|
||||
_previouslyPressedFlags = 0;
|
||||
|
||||
_flutterViewController = viewController;
|
||||
@@ -208,6 +208,11 @@ static flutter::TextRange RangeFromBaseExtent(NSNumber* base,
|
||||
|
||||
- (void)dealloc {
|
||||
[_channel setMethodCallHandler:nil];
|
||||
if (_textInputContext) {
|
||||
[_textInputContext deactivate];
|
||||
[_textInputContext discardMarkedText];
|
||||
_textInputContext = nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
@@ -138,6 +138,11 @@ struct MouseState {
|
||||
*/
|
||||
- (void)addInternalPlugins;
|
||||
|
||||
/**
|
||||
* Creates and registers keyboard related components.
|
||||
*/
|
||||
- (void)initializeKeyboard;
|
||||
|
||||
/**
|
||||
* Calls dispatchMouseEvent:phase: with a phase determined by self.mouseState.
|
||||
*
|
||||
@@ -358,6 +363,10 @@ static void CommonInit(FlutterViewController* controller) {
|
||||
[self configureTrackingArea];
|
||||
}
|
||||
|
||||
- (void)onPreEngineRestart {
|
||||
[self initializeKeyboard];
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
|
||||
- (BOOL)launchEngine {
|
||||
@@ -431,9 +440,9 @@ static void CommonInit(FlutterViewController* controller) {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addInternalPlugins {
|
||||
- (void)initializeKeyboard {
|
||||
__weak FlutterViewController* weakSelf = self;
|
||||
[FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"]];
|
||||
_textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:weakSelf];
|
||||
_keyboardManager = [[FlutterKeyboardManager alloc] initWithOwner:weakSelf];
|
||||
[_keyboardManager addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc]
|
||||
initWithSendEvent:^(const FlutterKeyEvent& event,
|
||||
@@ -451,6 +460,12 @@ static void CommonInit(FlutterViewController* controller) {
|
||||
codec:[FlutterJSONMessageCodec
|
||||
sharedInstance]]]];
|
||||
[_keyboardManager addSecondaryResponder:_textInputPlugin];
|
||||
}
|
||||
|
||||
- (void)addInternalPlugins {
|
||||
__weak FlutterViewController* weakSelf = self;
|
||||
[FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"]];
|
||||
[self initializeKeyboard];
|
||||
_settingsChannel =
|
||||
[FlutterBasicMessageChannel messageChannelWithName:@"flutter/settings"
|
||||
binaryMessenger:_engine.binaryMessenger
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
- (bool)testKeyEventsAreNotPropagatedIfHandled;
|
||||
- (bool)testFlagsChangedEventsArePropagatedIfNotHandled;
|
||||
- (bool)testPerformKeyEquivalentSynthesizesKeyUp;
|
||||
- (bool)testKeyboardIsRestartedOnEngineRestart;
|
||||
|
||||
+ (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event
|
||||
callback:(nullable FlutterKeyEventCallback)callback
|
||||
@@ -171,6 +172,10 @@ TEST(FlutterViewControllerTest, TestPerformKeyEquivalentSynthesizesKeyUp) {
|
||||
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testPerformKeyEquivalentSynthesizesKeyUp]);
|
||||
}
|
||||
|
||||
TEST(FlutterViewControllerTest, TestKeyboardIsRestartedOnEngineRestart) {
|
||||
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testKeyboardIsRestartedOnEngineRestart]);
|
||||
}
|
||||
|
||||
} // namespace flutter::testing
|
||||
|
||||
@implementation FlutterViewControllerTestObjC
|
||||
@@ -456,6 +461,60 @@ TEST(FlutterViewControllerTest, TestPerformKeyEquivalentSynthesizesKeyUp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
- (bool)testKeyboardIsRestartedOnEngineRestart {
|
||||
id engineMock = OCMClassMock([FlutterEngine class]);
|
||||
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
|
||||
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
|
||||
[engineMock binaryMessenger])
|
||||
.andReturn(binaryMessengerMock);
|
||||
__block bool called = false;
|
||||
__block FlutterKeyEvent last_event;
|
||||
OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
|
||||
callback:nil
|
||||
userData:nil])
|
||||
.andDo((^(NSInvocation* invocation) {
|
||||
FlutterKeyEvent* event;
|
||||
[invocation getArgument:&event atIndex:2];
|
||||
called = true;
|
||||
last_event = *event;
|
||||
}));
|
||||
|
||||
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
|
||||
nibName:@""
|
||||
bundle:nil];
|
||||
[viewController viewWillAppear];
|
||||
NSEvent* keyADown = [NSEvent keyEventWithType:NSEventTypeKeyDown
|
||||
location:NSZeroPoint
|
||||
modifierFlags:0x100
|
||||
timestamp:0
|
||||
windowNumber:0
|
||||
context:nil
|
||||
characters:@"a"
|
||||
charactersIgnoringModifiers:@"a"
|
||||
isARepeat:FALSE
|
||||
keyCode:0];
|
||||
const uint64_t kPhysicalKeyA = 0x70004;
|
||||
|
||||
// Send KeyA key down event twice. Without restarting the keyboard during
|
||||
// onPreEngineRestart, the second event received will be an empty event with
|
||||
// physical key 0x0 because duplicate key down events are ignored.
|
||||
|
||||
called = false;
|
||||
[viewController keyDown:keyADown];
|
||||
EXPECT_TRUE(called);
|
||||
EXPECT_EQ(last_event.type, kFlutterKeyEventTypeDown);
|
||||
EXPECT_EQ(last_event.physical, kPhysicalKeyA);
|
||||
|
||||
[viewController onPreEngineRestart];
|
||||
|
||||
called = false;
|
||||
[viewController keyDown:keyADown];
|
||||
EXPECT_TRUE(called);
|
||||
EXPECT_EQ(last_event.type, kFlutterKeyEventTypeDown);
|
||||
EXPECT_EQ(last_event.physical, kPhysicalKeyA);
|
||||
return true;
|
||||
}
|
||||
|
||||
+ (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event
|
||||
callback:(nullable FlutterKeyEventCallback)callback
|
||||
userData:(nullable void*)userData {
|
||||
|
||||
@@ -1580,7 +1580,7 @@ typedef struct {
|
||||
// callbacks on `log_message_callback`. Defaults to "flutter" if unspecified.
|
||||
const char* log_tag;
|
||||
|
||||
// A callback that is invoked when the engine is restarted.
|
||||
// A callback that is invoked right before the engine is restarted.
|
||||
//
|
||||
// This optional callback is typically used to reset states to as if the
|
||||
// engine has just been started, and usually indicates the user has requested
|
||||
|
||||
@@ -48,7 +48,7 @@ struct _FlEngine {
|
||||
gpointer update_semantics_node_handler_data;
|
||||
GDestroyNotify update_semantics_node_handler_destroy_notify;
|
||||
|
||||
// Function to call when the engine is restarted.
|
||||
// Function to call right before the engine is restarted.
|
||||
FlEngineOnPreEngineRestartHandler on_pre_engine_restart_handler;
|
||||
gpointer on_pre_engine_restart_handler_data;
|
||||
GDestroyNotify on_pre_engine_restart_handler_destroy_notify;
|
||||
@@ -284,7 +284,7 @@ static void fl_engine_update_semantics_node_cb(const FlutterSemanticsNode* node,
|
||||
}
|
||||
}
|
||||
|
||||
// Called when the engine is restarted.
|
||||
// Called right before the engine is restarted.
|
||||
//
|
||||
// This method should reset states to as if the engine has just been started,
|
||||
// which usually indicates the user has requested a hot restart (Shift-R in the
|
||||
|
||||
@@ -133,7 +133,7 @@ void fl_engine_set_update_semantics_node_handler(
|
||||
* @destroy_notify: (allow-none): a function which gets called to free
|
||||
* @user_data, or %NULL.
|
||||
*
|
||||
* Registers the function called when the engine is restarted.
|
||||
* Registers the function called right before the engine is restarted.
|
||||
*/
|
||||
void fl_engine_set_on_pre_engine_restart_handler(
|
||||
FlEngine* engine,
|
||||
|
||||
@@ -102,7 +102,7 @@ static void fl_view_init_keyboard(FlView* self) {
|
||||
FL_KEY_RESPONDER(fl_key_channel_responder_new(messenger)));
|
||||
}
|
||||
|
||||
// Called when the engine is restarted.
|
||||
// Invoked by the engine right before the engine is restarted.
|
||||
//
|
||||
// This method should reset states to be as if the engine had just been started,
|
||||
// which usually indicates the user has requested a hot restart (Shift-R in the
|
||||
|
||||
@@ -90,7 +90,7 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate,
|
||||
// Returns the frame buffer id for the engine to render to.
|
||||
uint32_t GetFrameBufferId(size_t width, size_t height);
|
||||
|
||||
// Called when the engine is restarted.
|
||||
// Invoked by the engine right before the engine is restarted.
|
||||
//
|
||||
// This should reset necessary states to as if the view has just been
|
||||
// created. This is typically caused by a hot restart (Shift-R in CLI.)
|
||||
|
||||
Reference in New Issue
Block a user