mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[macOS] Remove isComposing workaround (flutter/engine#33838)
This commit is contained in:
parent
9e0811e3c4
commit
5c9df63ade
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user