Files
Greg Spencer 572e3d16c0 Fix editing selection and deletion on macOS (#13702)
This fixes the selection and deletion during text editing on macOS so that it can handle selections that have a base (selection start) that is after the extent (selection end) in the string.

Apparently the insertText:replacementRange on macOS can supply an NSRange object that has negative values for the length (even though the length is an unsigned NSUInteger). This change checks for that case, and fixes the range of characters replaced in the text to have the right bounds.

Also, I added an additional updateEditState after the state is set from the framework, since there is a timing problem: when the delete key is pressed, it sends an insertText message, which updates the framework state after the framework has already sent its state update to delete the selected region. This additional updateEditState puts the engine and framework back in sync again. Ideally, we would just avoid sending the insertText message, but there's really no way for the engine to know if the pressed key is part of an edit sequence or not.
2019-11-05 19:17:52 -08:00

311 lines
11 KiB
Plaintext

// 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/FlutterTextInputPlugin.h"
#import <objc/message.h>
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputModel.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
static NSString* const kTextInputChannel = @"flutter/textinput";
// See https://docs.flutter.io/flutter/services/SystemChannels/textInput-constant.html
static NSString* const kSetClientMethod = @"TextInput.setClient";
static NSString* const kShowMethod = @"TextInput.show";
static NSString* const kHideMethod = @"TextInput.hide";
static NSString* const kClearClientMethod = @"TextInput.clearClient";
static NSString* const kSetEditingStateMethod = @"TextInput.setEditingState";
static NSString* const kUpdateEditStateResponseMethod = @"TextInputClient.updateEditingState";
static NSString* const kPerformAction = @"TextInputClient.performAction";
static NSString* const kMultilineInputType = @"TextInputType.multiline";
/**
* Private properties of FlutterTextInputPlugin.
*/
@interface FlutterTextInputPlugin () <NSTextInputClient>
/**
* A text input context, representing a connection to the Cocoa text input system.
*/
@property(nonatomic) NSTextInputContext* textInputContext;
/**
* The currently active text input model.
*/
@property(nonatomic, nullable) FlutterTextInputModel* activeModel;
/**
* The channel used to communicate with Flutter.
*/
@property(nonatomic) FlutterMethodChannel* channel;
/**
* The FlutterViewController to manage input for.
*/
@property(nonatomic, weak) FlutterViewController* flutterViewController;
/**
* Handles a Flutter system message on the text input channel.
*/
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
@end
@implementation FlutterTextInputPlugin
- (instancetype)initWithViewController:(FlutterViewController*)viewController {
self = [super init];
if (self != nil) {
_flutterViewController = viewController;
_channel = [FlutterMethodChannel methodChannelWithName:kTextInputChannel
binaryMessenger:viewController.engine.binaryMessenger
codec:[FlutterJSONMethodCodec sharedInstance]];
__weak FlutterTextInputPlugin* weakSelf = self;
[_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
[weakSelf handleMethodCall:call result:result];
}];
_textInputContext = [[NSTextInputContext alloc] initWithClient:self];
}
return self;
}
#pragma mark - Private
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
BOOL handled = YES;
NSString* method = call.method;
if ([method isEqualToString:kSetClientMethod]) {
if (!call.arguments[0] || !call.arguments[1]) {
result([FlutterError
errorWithCode:@"error"
message:@"Missing arguments"
details:@"Missing arguments while trying to set a text input client"]);
return;
}
NSNumber* clientID = call.arguments[0];
if (clientID != nil) {
self.activeModel = [[FlutterTextInputModel alloc] initWithClientID:clientID
configuration:call.arguments[1]];
if (!self.activeModel) {
result([FlutterError errorWithCode:@"error"
message:@"Failed to create an input model"
details:@"Configuration arguments might be missing"]);
return;
}
}
} else if ([method isEqualToString:kShowMethod]) {
[self.flutterViewController addKeyResponder:self];
[_textInputContext activate];
} else if ([method isEqualToString:kHideMethod]) {
[self.flutterViewController removeKeyResponder:self];
[_textInputContext deactivate];
} else if ([method isEqualToString:kClearClientMethod]) {
self.activeModel = nil;
} else if ([method isEqualToString:kSetEditingStateMethod]) {
NSDictionary* state = call.arguments;
self.activeModel.state = state;
// Close the loop, since the framework state could have been updated by the
// engine since it sent this update, and needs to now be made to match the
// engine's version of the state.
[self updateEditState];
} else {
handled = NO;
NSLog(@"Unhandled text input method '%@'", method);
}
result(handled ? nil : FlutterMethodNotImplemented);
}
/**
* Informs the Flutter framework of changes to the text input model's state.
*/
- (void)updateEditState {
if (self.activeModel == nil) {
return;
}
[_channel invokeMethod:kUpdateEditStateResponseMethod
arguments:@[ self.activeModel.clientID, self.activeModel.state ]];
}
#pragma mark -
#pragma mark NSResponder
/**
* 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.
*/
- (void)keyDown:(NSEvent*)event {
[_textInputContext handleEvent:event];
}
#pragma mark -
#pragma mark NSStandardKeyBindingMethods
/**
* Note, experimentation indicates that moveRight and moveLeft are called rather
* than the supposedly more RTL-friendly moveForward and moveBackward.
*/
- (void)moveLeft:(nullable id)sender {
NSRange selection = self.activeModel.selectedRange;
if (selection.length == 0) {
if (selection.location > 0) {
// Move to previous location
self.activeModel.selectedRange = NSMakeRange(selection.location - 1, 0);
[self updateEditState];
}
} else {
// Collapse current selection
self.activeModel.selectedRange = NSMakeRange(selection.location, 0);
[self updateEditState];
}
}
- (void)moveRight:(nullable id)sender {
NSRange selection = self.activeModel.selectedRange;
if (selection.length == 0) {
if (selection.location < self.activeModel.text.length) {
// Move to next location
self.activeModel.selectedRange = NSMakeRange(selection.location + 1, 0);
[self updateEditState];
}
} else {
// Collapse current selection
self.activeModel.selectedRange = NSMakeRange(selection.location + selection.length, 0);
[self updateEditState];
}
}
- (void)deleteBackward:(id)sender {
NSRange selection = self.activeModel.selectedRange;
if (selection.location == 0)
return;
NSRange range = selection;
if (selection.length == 0) {
NSUInteger location = (selection.location == NSNotFound) ? self.activeModel.text.length - 1
: selection.location - 1;
range = NSMakeRange(location, 1);
}
self.activeModel.selectedRange = NSMakeRange(range.location, 0);
[self insertText:@"" replacementRange:range]; // Updates edit state
}
#pragma mark -
#pragma mark NSTextInputClient
- (void)insertText:(id)string replacementRange:(NSRange)range {
if (self.activeModel != nil) {
if (range.location == NSNotFound && range.length == 0) {
// Use selection
range = self.activeModel.selectedRange;
}
// The selected range can actually have negative numbers, since it can start
// at the end of the range if the user selected the text going backwards.
// NSRange uses NSUIntegers, however, so we have to cast them to know if the
// selection is reversed or not.
long signedLength = static_cast<long>(range.length);
NSUInteger length;
NSUInteger location;
if (signedLength >= 0) {
location = range.location;
length = range.length;
} else {
location = range.location + range.length;
length = ABS(signedLength);
}
if (location > self.activeModel.text.length)
location = self.activeModel.text.length;
if (length > (self.activeModel.text.length - location))
length = self.activeModel.text.length - location;
[self.activeModel.text replaceCharactersInRange:NSMakeRange(location, length)
withString:string];
self.activeModel.selectedRange = NSMakeRange(location + ((NSString*)string).length, 0);
[self updateEditState];
}
}
- (void)doCommandBySelector:(SEL)selector {
if ([self respondsToSelector:selector]) {
// Note: The more obvious [self performSelector...] doesn't give ARC enough information to
// handle retain semantics properly. See https://stackoverflow.com/questions/7017281/ for more
// information.
IMP imp = [self methodForSelector:selector];
void (*func)(id, SEL, id) = reinterpret_cast<void (*)(id, SEL, id)>(imp);
func(self, selector, nil);
}
}
- (void)insertNewline:(id)sender {
if (self.activeModel != nil) {
if ([self.activeModel.inputType isEqualToString:kMultilineInputType]) {
[self insertText:@"\n" replacementRange:self.activeModel.selectedRange];
}
[_channel invokeMethod:kPerformAction
arguments:@[ self.activeModel.clientID, self.activeModel.inputAction ]];
}
}
- (void)setMarkedText:(id)string
selectedRange:(NSRange)selectedRange
replacementRange:(NSRange)replacementRange {
if (self.activeModel != nil) {
[self.activeModel.text replaceCharactersInRange:replacementRange withString:string];
self.activeModel.selectedRange = selectedRange;
[self updateEditState];
}
}
- (void)unmarkText {
if (self.activeModel != nil) {
self.activeModel.markedRange = NSMakeRange(NSNotFound, 0);
[self updateEditState];
}
}
- (NSRange)selectedRange {
return (self.activeModel == nil) ? NSMakeRange(NSNotFound, 0) : self.activeModel.selectedRange;
}
- (NSRange)markedRange {
return (self.activeModel == nil) ? NSMakeRange(NSNotFound, 0) : self.activeModel.markedRange;
}
- (BOOL)hasMarkedText {
return (self.activeModel == nil) ? NO : self.activeModel.markedRange.location != NSNotFound;
}
- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
actualRange:(NSRangePointer)actualRange {
if (self.activeModel) {
if (actualRange != nil)
*actualRange = range;
NSString* substring = [self.activeModel.text substringWithRange:range];
return [[NSAttributedString alloc] initWithString:substring attributes:nil];
} else {
return nil;
}
}
- (NSArray<NSString*>*)validAttributesForMarkedText {
return @[];
}
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
// TODO: Implement.
// Note: This function can't easily be implemented under the system-message architecture.
return CGRectZero;
}
- (NSUInteger)characterIndexForPoint:(NSPoint)point {
// TODO: Implement.
// Note: This function can't easily be implemented under the system-message architecture.
return 0;
}
@end