[macOS] Remove isComposing workaround (flutter/engine#33838)

This commit is contained in:
Matej Knopp 2022-07-29 09:21:12 +02:00 committed by GitHub
parent 9e0811e3c4
commit 5c9df63ade
7 changed files with 200 additions and 66 deletions

View File

@ -196,12 +196,6 @@ typedef _Nullable _NSResponderPtr (^NextResponderProvider)();
}
- (void)performProcessEvent:(NSEvent*)event onFinish:(VoidBlock)onFinish {
if (_viewDelegate.isComposing) {
[self dispatchTextEvent:event];
onFinish();
return;
}
// Having no primary responders require extra logic, but Flutter hard-codes
// all primary responders, so this is a situation that Flutter will never
// encounter.

View File

@ -252,7 +252,6 @@ void clearEvents(std::vector<FlutterKeyEvent>& events) {
@property(nonatomic) FlutterKeyboardManager* manager;
@property(nonatomic) NSResponder* nextResponder;
@property(nonatomic, assign) BOOL isComposing;
#pragma mark - Private
@ -289,7 +288,6 @@ void clearEvents(std::vector<FlutterKeyEvent>& events) {
[self respondChannelCallsWith:FALSE];
[self respondEmbedderCallsWith:FALSE];
[self respondTextInputWith:FALSE];
_isComposing = NO;
_currentLayout = &kUsLayout;
@ -304,7 +302,6 @@ void clearEvents(std::vector<FlutterKeyEvent>& events) {
OCMStub([viewDelegateMock onTextInputKeyEvent:[OCMArg any]])
.andCall(self, @selector(handleTextInputKeyEvent:));
OCMStub([viewDelegateMock getBinaryMessenger]).andReturn(messengerMock);
OCMStub([viewDelegateMock isComposing]).andCall(self, @selector(isComposing));
OCMStub([viewDelegateMock sendKeyEvent:FlutterKeyEvent {} callback:nil userData:nil])
.ignoringNonObjectArgs()
.andCall(self, @selector(handleEmbedderEvent:callback:userData:));
@ -430,7 +427,6 @@ void clearEvents(std::vector<FlutterKeyEvent>& events) {
- (bool)singlePrimaryResponder;
- (bool)doublePrimaryResponder;
- (bool)textInputPlugin;
- (bool)forwardKeyEventsToSystemWhenComposing;
- (bool)emptyNextResponder;
- (bool)racingConditionBetweenKeyAndText;
- (bool)correctLogicalKeyForLayouts;
@ -449,10 +445,6 @@ TEST(FlutterKeyboardManagerUnittests, SingleFinalResponder) {
ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] textInputPlugin]);
}
TEST(FlutterKeyboardManagerUnittests, handlingComposingText) {
ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] forwardKeyEventsToSystemWhenComposing]);
}
TEST(FlutterKeyboardManagerUnittests, EmptyNextResponder) {
ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] emptyNextResponder]);
}
@ -597,31 +589,6 @@ TEST(FlutterKeyboardManagerUnittests, CorrectLogicalKeyForLayouts) {
return true;
}
- (bool)forwardKeyEventsToSystemWhenComposing {
KeyboardTester* tester = OCMPartialMock([[KeyboardTester alloc] init]);
NSMutableArray<FlutterAsyncKeyCallback>* channelCallbacks =
[NSMutableArray<FlutterAsyncKeyCallback> array];
NSMutableArray<FlutterAsyncKeyCallback>* embedderCallbacks =
[NSMutableArray<FlutterAsyncKeyCallback> array];
[tester recordEmbedderCallsTo:embedderCallbacks];
[tester recordChannelCallsTo:channelCallbacks];
// The event shouldn't propagate further even if TextInputPlugin does not
// claim the event.
[tester respondTextInputWith:NO];
tester.isComposing = YES;
// Send a down event with composing == YES.
[tester.manager handleEvent:keyUpEvent(0x50)];
// Nobody gets the event except for the text input plugin.
EXPECT_EQ([channelCallbacks count], 0u);
EXPECT_EQ([embedderCallbacks count], 0u);
OCMVerify(times(1), [tester handleTextInputKeyEvent:checkKeyDownEvent(0x50)]);
return true;
}
- (bool)emptyNextResponder {
KeyboardTester* tester = [[KeyboardTester alloc] init];
tester.nextResponder = nil;

View File

@ -73,16 +73,6 @@ typedef struct {
*/
- (BOOL)onTextInputKeyEvent:(nonnull NSEvent*)event;
/**
* Whether this FlutterKeyboardViewDelegate is actively taking provisional user text input.
*
* This is typically true when a Flutter text field is focused, and the user is entering composing
* text into the text field.
*/
// TODO (LongCatIsLooong): remove this method and implement a long-term fix for
// https://github.com/flutter/flutter/issues/85328.
- (BOOL)isComposing;
/**
* Add a listener that is called whenever the user changes keyboard layout.
*

View File

@ -45,15 +45,6 @@
*/
- (BOOL)isFirstResponder;
/**
* Whether this plugin has composing text.
*
* This is only true when the text input plugin is actively taking user input with composing text.
*/
// TODO (LongCatIsLooong): remove this method and implement a long-term fix for
// https://github.com/flutter/flutter/issues/85328.
- (BOOL)isComposing;
/**
* Handles key down events received from the view controller, responding YES if
* the event was handled.
@ -73,4 +64,5 @@
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange;
- (NSDictionary*)editingState;
@property(readwrite, nonatomic) NSString* customRunLoopMode;
@end

View File

@ -32,6 +32,7 @@ static NSString* const kUpdateEditStateResponseMethod = @"TextInputClient.update
static NSString* const kUpdateEditStateWithDeltasResponseMethod =
@"TextInputClient.updateEditingStateWithDeltas";
static NSString* const kPerformAction = @"TextInputClient.performAction";
static NSString* const kPerformSelectors = @"TextInputClient.performSelectors";
static NSString* const kMultilineInputType = @"TextInputType.multiline";
static NSString* const kTextAffinityDownstream = @"TextAffinity.downstream";
@ -174,6 +175,13 @@ static char markerKey;
*/
@property(nonatomic) BOOL enableDeltaModel;
/**
* Used to gather multiple selectors performed in one run loop turn. These
* will be all sent in one platform channel call so that the framework can process
* them in single microtask.
*/
@property(nonatomic) NSMutableArray* pendingSelectors;
/**
* Handles a Flutter system message on the text input channel.
*/
@ -212,6 +220,11 @@ static char markerKey;
*/
- (NSString*)textAffinityString;
/**
* Allow overriding run loop mode for test.
*/
@property(readwrite, nonatomic) NSString* customRunLoopMode;
@end
@implementation FlutterTextInputPlugin {
@ -504,10 +517,6 @@ static char markerKey;
: kTextAffinityDownstream;
}
- (BOOL)isComposing {
return _activeModel && !_activeModel->composing_range().collapsed();
}
- (BOOL)handleKeyEvent:(NSEvent*)event {
if (event.type == NSEventTypeKeyUp ||
(event.type == NSEventTypeFlagsChanged && event.modifierFlags < _previouslyPressedFlags)) {
@ -615,6 +624,11 @@ static char markerKey;
#pragma mark -
#pragma mark NSTextInputClient
- (void)insertTab:(id)sender {
// Implementing insertTab: makes AppKit send tab as command, instead of
// insertText with '\t'.
}
- (void)insertText:(id)string replacementRange:(NSRange)range {
if (_activeModel == nullptr) {
return;
@ -670,6 +684,36 @@ static char markerKey;
void (*func)(id, SEL, id) = reinterpret_cast<void (*)(id, SEL, id)>(imp);
func(self, selector, nil);
}
if (selector == @selector(insertNewline:)) {
// Already handled through text insertion (multiline) or action.
return;
}
// Group multiple selectors received within a single run loop turn so that
// the framework can process them in single microtask.
NSString* name = NSStringFromSelector(selector);
if (_pendingSelectors == nil) {
_pendingSelectors = [NSMutableArray array];
}
[_pendingSelectors addObject:name];
if (_pendingSelectors.count == 1) {
__weak NSMutableArray* selectors = _pendingSelectors;
__weak FlutterMethodChannel* channel = _channel;
__weak NSNumber* clientID = self.clientID;
CFStringRef runLoopMode = self.customRunLoopMode != nil
? (__bridge CFStringRef)self.customRunLoopMode
: kCFRunLoopCommonModes;
CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{
if (selectors.count > 0) {
[channel invokeMethod:kPerformSelectors arguments:@[ clientID, selectors ]];
[selectors removeAllObjects];
}
});
}
}
- (void)insertNewline:(id)sender {
@ -696,6 +740,19 @@ static char markerKey;
if (!_activeModel->composing()) {
_activeModel->BeginComposing();
}
if (replacementRange.location != NSNotFound) {
// According to the NSTextInputClient documentation replacementRange is
// computed from the beginning of the marked text. That doesn't seem to be
// the case, because in situations where the replacementRange is actually
// specified (i.e. when switching between characters equivalent after long
// key press) the replacementRange is provided while there is no composition.
_activeModel->SetComposingRange(
flutter::TextRange(replacementRange.location,
replacementRange.location + replacementRange.length),
0);
}
flutter::TextRange composingBeforeChange = _activeModel->composing_range();
flutter::TextRange selectionBeforeChange = _activeModel->selection();

View File

@ -173,6 +173,74 @@
return true;
}
- (bool)testSetMarkedTextWithReplacementRange {
id engineMock = OCMClassMock([FlutterEngine class]);
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
[engineMock binaryMessenger])
.andReturn(binaryMessengerMock);
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){
}];
FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
arguments:@{
@"text" : @"1234",
@"selectionBase" : @(3),
@"selectionExtent" : @(3),
@"composingBase" : @(-1),
@"composingExtent" : @(-1),
}];
[plugin handleMethodCall:call
result:^(id){
}];
[plugin setMarkedText:@"marked"
selectedRange:NSMakeRange(1, 0)
replacementRange:NSMakeRange(1, 2)];
NSDictionary* expectedState = @{
@"selectionBase" : @(2),
@"selectionExtent" : @(2),
@"selectionAffinity" : @"TextAffinity.upstream",
@"selectionIsDirectional" : @(NO),
@"composingBase" : @(1),
@"composingExtent" : @(7),
@"text" : @"1marked4",
};
NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance]
encodeMethodCall:[FlutterMethodCall
methodCallWithMethodName:@"TextInputClient.updateEditingState"
arguments:@[ @(1), expectedState ]]];
OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
[binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
@try {
OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
[binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
} @catch (...) {
return false;
}
return true;
}
- (bool)testComposingRegionRemovedByFramework {
id engineMock = OCMClassMock([FlutterEngine class]);
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
@ -1057,6 +1125,68 @@
return localTextAndSelectionUpdated;
}
- (bool)testSelectorsAreForwardedToFramework {
id engineMock = OCMClassMock([FlutterEngine class]);
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
[engineMock binaryMessenger])
.andReturn(binaryMessengerMock);
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",
@"enableDeltaModel" : @"true",
@"inputType" : @{@"name" : @"inputName"},
}
]]
result:^(id){
}];
// Can't run CFRunLoop in default mode because it causes crashes from scheduled
// sources from other tests.
NSString* runLoopMode = @"FlutterTestRunLoopMode";
plugin.customRunLoopMode = runLoopMode;
// Ensure both selectors are grouped in one platform channel call.
[plugin doCommandBySelector:@selector(moveUp:)];
[plugin doCommandBySelector:@selector(moveRightAndModifySelection:)];
__block bool done = false;
CFRunLoopPerformBlock(CFRunLoopGetMain(), (__bridge CFStringRef)runLoopMode, ^{
done = true;
});
while (!done) {
// Each invocation will handle one source.
CFRunLoopRunInMode((__bridge CFStringRef)runLoopMode, 0, true);
}
NSData* performSelectorCall = [[FlutterJSONMethodCodec sharedInstance]
encodeMethodCall:[FlutterMethodCall
methodCallWithMethodName:@"TextInputClient.performSelectors"
arguments:@[
@(1), @[ @"moveUp:", @"moveRightAndModifySelection:" ]
]]];
@try {
OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
[binaryMessengerMock sendOnChannel:@"flutter/textinput" message:performSelectorCall]);
} @catch (...) {
return false;
}
return true;
}
@end
namespace flutter::testing {
@ -1080,6 +1210,10 @@ TEST(FlutterTextInputPluginTest, TestSetMarkedTextWithSelectionChange) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSetMarkedTextWithSelectionChange]);
}
TEST(FlutterTextInputPluginTest, TestSetMarkedTextWithReplacementRange) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSetMarkedTextWithReplacementRange]);
}
TEST(FlutterTextInputPluginTest, TestComposingRegionRemovedByFramework) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingRegionRemovedByFramework]);
}
@ -1120,6 +1254,10 @@ TEST(FlutterTextInputPluginTest, UnhandledKeyEquivalent) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] unhandledKeyEquivalent]);
}
TEST(FlutterTextInputPluginTest, TestSelectorsAreForwardedToFramework) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSelectorsAreForwardedToFramework]);
}
TEST(FlutterTextInputPluginTest, CanWorkWithFlutterTextField) {
FlutterEngine* engine = CreateTestEngine();
NSString* fixtures = @(testing::GetFixturesPath());

View File

@ -688,10 +688,6 @@ static void CommonInit(FlutterViewController* controller) {
#pragma mark - FlutterKeyboardViewDelegate
- (BOOL)isComposing {
return [_textInputPlugin isComposing];
}
- (void)sendKeyEvent:(const FlutterKeyEvent&)event
callback:(nullable FlutterKeyEventCallback)callback
userData:(nullable void*)userData {