Skip to content

Commit

Permalink
[macOS - TextInput] Insert new line only when TextInputAction.newline (
Browse files Browse the repository at this point in the history
…#41977)

## Description

This PR updates the macOS text input plugin to avoid adding a new line on a multiline text field when action is not set to `TextInputAction.newline`.

## Related Issue

macOS implementation for flutter/flutter#125879.
(similar to the Linux implementation in #41895).

## Tests

Adds 2 tests.
  • Loading branch information
bleroux committed May 13, 2023
1 parent 8830d9c commit 259daf6
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 1 deletion.
Expand Up @@ -61,6 +61,9 @@
static NSString* const kTextAffinityDownstream = @"TextAffinity.downstream";
static NSString* const kTextAffinityUpstream = @"TextAffinity.upstream";

// TextInputAction types
static NSString* const kInputActionNewline = @"TextInputAction.newline";

#pragma mark - Enums
/**
* The affinity of the current cursor position. If the cursor is at a position representing
Expand Down Expand Up @@ -820,7 +823,8 @@ - (void)insertNewline:(id)sender {
_activeModel->CommitComposing();
_activeModel->EndComposing();
}
if ([self.inputType isEqualToString:kMultilineInputType]) {
if ([self.inputType isEqualToString:kMultilineInputType] &&
[self.inputAction isEqualToString:kInputActionNewline]) {
[self insertText:@"\n" replacementRange:self.selectedRange];
}
[_channel invokeMethod:kPerformAction arguments:@[ self.clientID, self.inputAction ]];
Expand Down
Expand Up @@ -1470,6 +1470,196 @@ - (bool)unhandledKeyEquivalent {
return true;
}

- (bool)testInsertNewLine {
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
[engineMock binaryMessenger])
.andReturn(binaryMessengerMock);
OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
callback:nil
userData:nil]);

FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
nibName:@""
bundle:nil];

FlutterTextInputPlugin* plugin =
[[FlutterTextInputPlugin alloc] initWithViewController:viewController];

NSDictionary* setClientConfig = @{
@"inputType" : @{@"name" : @"TextInputType.multiline"},
@"inputAction" : @"TextInputAction.newline",
};
[plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
arguments:@[ @(1), setClientConfig ]]
result:^(id){
}];

FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
arguments:@{
@"text" : @"Text",
@"selectionBase" : @(4),
@"selectionExtent" : @(4),
@"composingBase" : @(-1),
@"composingExtent" : @(-1),
}];

NSDictionary* expectedState = @{
@"selectionBase" : @(4),
@"selectionExtent" : @(4),
@"selectionAffinity" : @"TextAffinity.upstream",
@"selectionIsDirectional" : @(NO),
@"composingBase" : @(-1),
@"composingExtent" : @(-1),
@"text" : @"Text",
};

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]);

[plugin handleMethodCall:call
result:^(id){
}];

@try {
OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
[binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
} @catch (...) {
return false;
}

[plugin doCommandBySelector:@selector(insertNewline:)];

NSDictionary* updatedState = @{
@"selectionBase" : @(5),
@"selectionExtent" : @(5),
@"selectionAffinity" : @"TextAffinity.upstream",
@"selectionIsDirectional" : @(NO),
@"composingBase" : @(-1),
@"composingExtent" : @(-1),
@"text" : @"Text\n",
};

updateCall = [[FlutterJSONMethodCodec sharedInstance]
encodeMethodCall:[FlutterMethodCall
methodCallWithMethodName:@"TextInputClient.updateEditingState"
arguments:@[ @(1), updatedState ]]];

@try {
OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
[binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
} @catch (...) {
return false;
}

return true;
}

- (bool)testSendActionDoNotInsertNewLine {
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
[engineMock binaryMessenger])
.andReturn(binaryMessengerMock);
OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
callback:nil
userData:nil]);

FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
nibName:@""
bundle:nil];

FlutterTextInputPlugin* plugin =
[[FlutterTextInputPlugin alloc] initWithViewController:viewController];

NSDictionary* setClientConfig = @{
@"inputType" : @{@"name" : @"TextInputType.multiline"},
@"inputAction" : @"TextInputAction.send",
};
[plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
arguments:@[ @(1), setClientConfig ]]
result:^(id){
}];

FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
arguments:@{
@"text" : @"Text",
@"selectionBase" : @(4),
@"selectionExtent" : @(4),
@"composingBase" : @(-1),
@"composingExtent" : @(-1),
}];

NSDictionary* expectedState = @{
@"selectionBase" : @(4),
@"selectionExtent" : @(4),
@"selectionAffinity" : @"TextAffinity.upstream",
@"selectionIsDirectional" : @(NO),
@"composingBase" : @(-1),
@"composingExtent" : @(-1),
@"text" : @"Text",
};

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]);

[plugin handleMethodCall:call
result:^(id){
}];

[plugin doCommandBySelector:@selector(insertNewline:)];

NSData* performActionCall = [[FlutterJSONMethodCodec sharedInstance]
encodeMethodCall:[FlutterMethodCall
methodCallWithMethodName:@"TextInputClient.performAction"
arguments:@[ @(1), @"TextInputAction.send" ]]];

// Input action should be notified.
@try {
OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
[binaryMessengerMock sendOnChannel:@"flutter/textinput" message:performActionCall]);
} @catch (...) {
return false;
}

NSDictionary* updatedState = @{
@"selectionBase" : @(5),
@"selectionExtent" : @(5),
@"selectionAffinity" : @"TextAffinity.upstream",
@"selectionIsDirectional" : @(NO),
@"composingBase" : @(-1),
@"composingExtent" : @(-1),
@"text" : @"Text\n",
};

updateCall = [[FlutterJSONMethodCodec sharedInstance]
encodeMethodCall:[FlutterMethodCall
methodCallWithMethodName:@"TextInputClient.updateEditingState"
arguments:@[ @(1), updatedState ]]];

// Verify that editing state was not be updated.
@try {
OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
[binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
return false;
} @catch (...) {
// Expected.
}

return true;
}

- (bool)testLocalTextAndSelectionUpdateAfterDelta {
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
Expand Down Expand Up @@ -1694,6 +1884,14 @@ - (bool)testSelectorsAreForwardedToFramework {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSelectorsAreForwardedToFramework]);
}

TEST(FlutterTextInputPluginTest, TestInsertNewLine) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testInsertNewLine]);
}

TEST(FlutterTextInputPluginTest, TestSendActionDoNotInsertNewLine) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSendActionDoNotInsertNewLine]);
}

TEST(FlutterTextInputPluginTest, CanWorkWithFlutterTextField) {
FlutterEngine* engine = CreateTestEngine();
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
Expand Down

0 comments on commit 259daf6

Please sign in to comment.