From 981ea3beff6d5effc8c355c3d5cdeb9ad98ad43f Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Wed, 19 Oct 2016 10:14:59 -0700 Subject: [PATCH] Implement TextInputPlugin on iOS (flutter/engine#3145) --- .../shell/platform/darwin/ios/BUILD.gn | 3 + .../framework/Source/FlutterPlatformPlugin.h | 6 +- .../Source/FlutterTextInputDelegate.h | 16 ++ .../framework/Source/FlutterTextInputPlugin.h | 17 ++ .../Source/FlutterTextInputPlugin.mm | 210 ++++++++++++++++++ .../framework/Source/FlutterViewController.mm | 38 +++- 6 files changed, 283 insertions(+), 7 deletions(-) create mode 100644 engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h create mode 100644 engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h create mode 100644 engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index e2e136c912..1e95911246 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -37,6 +37,9 @@ shared_library("flutter_framework_dylib") { "framework/Source/FlutterJSONMessageListener.mm", "framework/Source/FlutterPlatformPlugin.h", "framework/Source/FlutterPlatformPlugin.mm", + "framework/Source/FlutterTextInputDelegate.h", + "framework/Source/FlutterTextInputPlugin.h", + "framework/Source/FlutterTextInputPlugin.mm", "framework/Source/FlutterView.h", "framework/Source/FlutterView.mm", "framework/Source/FlutterViewController.mm", diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h index acd6d92739..9af3eccc43 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORM_PLUGIN_H_ -#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORM_PLUGIN_H_ +#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMPLUGIN_H_ +#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMPLUGIN_H_ #include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterJSONMessageListener.h" @@ -20,4 +20,4 @@ extern const char* const kOverlayStyleUpdateNotificationKey; } // namespace shell -#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORM_PLUGIN_H_ +#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMPLUGIN_H_ diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h new file mode 100644 index 0000000000..3187848991 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h @@ -0,0 +1,16 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTDELEGATE_H_ +#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTDELEGATE_H_ + +#import + +@protocol FlutterTextInputDelegate + +- (void)updateEditingClient:(int)client withState:(NSDictionary*)state; + +@end + +#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTDELEGATE_H_ diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h new file mode 100644 index 0000000000..337c28d952 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h @@ -0,0 +1,17 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTPLUGIN_H_ +#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTPLUGIN_H_ + +#include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterJSONMessageListener.h" +#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h" + +@interface FlutterTextInputPlugin : FlutterJSONMessageListener + +@property(nonatomic, assign) id textInputDelegate; + +@end + +#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTPLUGIN_H_ diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm new file mode 100644 index 0000000000..edd860c340 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -0,0 +1,210 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" + +#include +#include + +#include "base/strings/sys_string_conversions.h" +#include "base/strings/utf_string_conversions.h" + +static const char _kTextAffinityDownstream[] = "TextAffinity.downstream"; +static const char _kTextAffinityUpstream[] = "TextAffinity.upstream"; + +static UIKeyboardType ToUIKeyboardType(NSString* inputType) { + if ([inputType isEqualToString:@"TextInputType.text"]) + return UIKeyboardTypeDefault; + if ([inputType isEqualToString:@"TextInputType.number"]) + return UIKeyboardTypeDecimalPad; + if ([inputType isEqualToString:@"TextInputType.phone"]) + return UIKeyboardTypePhonePad; + return UIKeyboardTypeDefault; +} + +@interface FlutterTextInputView : UIView + +@property(nonatomic, assign) id textInputDelegate; + +@end + +@implementation FlutterTextInputView { + int _textInputClient; + int _selectionBase; + int _selectionExtent; + const char* _selectionAffinity; + base::string16 _text; +} + +@synthesize keyboardType = _keyboardType; + +@synthesize textInputDelegate = _textInputDelegate; + +- (instancetype)init { + self = [super init]; + + if (self) { + _selectionBase = -1; + _selectionExtent = -1; + } + + return self; +} + +- (void)setTextInputClient:(int)client { + _textInputClient = client; +} + +- (void)setTextInputState:(NSDictionary*)state { + _selectionBase = [state[@"selectionBase"] intValue]; + _selectionExtent = [state[@"selectionExtent"] intValue]; + _selectionAffinity = _kTextAffinityDownstream; + if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)]) + _selectionAffinity = _kTextAffinityUpstream; + _text = base::SysNSStringToUTF16(state[@"text"]); +} + +- (UITextAutocorrectionType)autocorrectionType { + return UITextAutocorrectionTypeNo; +} + +#pragma mark - UIResponder Overrides + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +#pragma mark - UIKeyInput Overrides + +- (void)updateEditingState { + [_textInputDelegate updateEditingClient:_textInputClient + withState:@{ + @"selectionBase": @(_selectionBase), + @"selectionExtent": @(_selectionExtent), + @"selectionAffinity": @(_selectionAffinity), + @"selectionIsDirectional": @(false), + @"composingBase": @(0), + @"composingExtent": @(0), + @"text": base::SysUTF16ToNSString(_text), + }]; +} + +- (BOOL)hasText { + return YES; +} + +- (void)insertText:(NSString*)text { + int start = std::max(0, std::min(_selectionBase, _selectionExtent)); + int end = std::max(0, std::max(_selectionBase, _selectionExtent)); + int len = end - start; + _text.replace(start, len, base::SysNSStringToUTF16(text)); + int caret = start + text.length; + _selectionBase = caret; + _selectionExtent = caret; + _selectionAffinity = _kTextAffinityUpstream; + [self updateEditingState]; +} + +- (void)deleteBackward { + int start = std::max(0, std::min(_selectionBase, _selectionExtent)); + int end = std::max(0, std::max(_selectionBase, _selectionExtent)); + int len = end - start; + if (len > 0) { + _text.erase(start, len); + } else if (start > 0) { + start -= 1; + len = 1; + if (start > 0 && + UTF16_IS_LEAD(_text[start - 1]) && + UTF16_IS_TRAIL(_text[start])) { + start -= 1; + len += 1; + } + _text.erase(start, len); + } + _selectionBase = start; + _selectionExtent = start; + _selectionAffinity = _kTextAffinityDownstream; + [self updateEditingState]; +} + +@end + +@implementation FlutterTextInputPlugin { + FlutterTextInputView* _view; +} + +@synthesize textInputDelegate = _textInputDelegate; + +- (instancetype)init { + self = [super init]; + + if (self) { + _view = [[FlutterTextInputView alloc] init]; + } + + return self; +} + +- (void)dealloc { + [self hideTextInput]; + [_view release]; + + [super dealloc]; +} + +- (NSString *)messageName { + return @"flutter/textinput"; +} + +- (NSDictionary*)didReceiveJSON:(NSDictionary*)message { + NSString* method = message[@"method"]; + NSArray* args = message[@"args"]; + if (!args) + return nil; + if ([method isEqualToString:@"TextInput.show"]) { + [self showTextInput]; + } else if ([method isEqualToString:@"TextInput.hide"]) { + [self hideTextInput]; + } else if ([method isEqualToString:@"TextInput.setClient"]) { + [self setTextInputClient:[args[0] intValue] withConfiguration:args[1]]; + } else if ([method isEqualToString:@"TextInput.setEditingState"]) { + [self setTextInputEditingState:args.firstObject]; + } else if ([method isEqualToString:@"TextInput.clearClient"]) { + [self clearTextInputClient]; + } else { + // TODO(abarth): We should signal an error here that gets reported back to + // Dart. + } + return nil; +} + +- (void)showTextInput { + NSAssert([UIApplication sharedApplication].keyWindow != nullptr, + @"The application must have a key window since the keyboard client " + @"must be part of the responder chain to function"); + _view.textInputDelegate = _textInputDelegate; + [[UIApplication sharedApplication].keyWindow addSubview:_view]; + [_view becomeFirstResponder]; +} + +- (void)hideTextInput { + [_view resignFirstResponder]; + [_view removeFromSuperview]; +} + +- (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration { + _view.keyboardType = ToUIKeyboardType(configuration[@"inputType"]); + [_view setTextInputClient:client]; +} + +- (void)setTextInputEditingState:(NSDictionary*)state { + [_view setTextInputState:state]; +} + +- (void)clearTextInputClient { + [_view setTextInputClient:0]; +} + +@end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 98989a0c8f..dd13c728ee 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -16,11 +16,13 @@ #include "flutter/shell/platform/darwin/ios/framework/Source/flutter_touch_mapper.h" #include "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h" #include "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h" +#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h" +#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" #include "flutter/shell/platform/darwin/ios/platform_view_ios.h" #include "lib/ftl/functional/make_copyable.h" #include "lib/ftl/time/time_delta.h" -@interface FlutterViewController () +@interface FlutterViewController () @end void FlutterInit(int argc, const char* argv[]) { @@ -37,6 +39,7 @@ void FlutterInit(int argc, const char* argv[]) { shell::TouchMapper _touchMapper; std::unique_ptr _platformView; base::scoped_nsprotocol _platformPlugin; + base::scoped_nsprotocol _textInputPlugin; BOOL _initialized; } @@ -87,6 +90,11 @@ void FlutterInit(int argc, const char* argv[]) { _platformPlugin.reset([[FlutterPlatformPlugin alloc] init]); [self addMessageListener:_platformPlugin.get()]; + + _textInputPlugin.reset([[FlutterTextInputPlugin alloc] init]); + _textInputPlugin.get().textInputDelegate = self; + [self addMessageListener:_textInputPlugin.get()]; + [self setupNotificationCenterObservers]; [self connectToEngineAndLoad]; @@ -170,14 +178,14 @@ void FlutterInit(int argc, const char* argv[]) { #pragma mark - Loading the view - (void)loadView { - FlutterView* surface = [[FlutterView alloc] init]; + FlutterView* view = [[FlutterView alloc] init]; - self.view = surface; + self.view = view; self.view.multipleTouchEnabled = YES; self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [surface release]; + [view release]; } #pragma mark - Application lifecycle notifications @@ -328,6 +336,15 @@ static inline PointerChangeMapperPhase PointerChangePhaseFromUITouchPhase( _viewportMetrics.Clone()); } +#pragma mark - Text input delegate + +- (void)updateEditingClient:(int)client withState:(NSDictionary*)state { + [self sendJSON:@{ + @"method": @"TextInputClient.updateEditingState", + @"args": @[@(client), state], + } withMessageName:@"flutter/textinputclient"]; +} + #pragma mark - Orientation updates - (void)onOrientationPreferencesUpdated:(NSNotification*)notification { @@ -467,6 +484,19 @@ static inline PointerChangeMapperPhase PointerChangePhaseFromUITouchPhase( }); } +// TODO(abarth): Switch sendString over to using platform messages. +- (void)sendJSON:(NSDictionary*)message withMessageName:(NSString*)messageName { + NSData* data = [NSJSONSerialization dataWithJSONObject:message options:0 error:nil]; + if (!data) + return; + const char* bytes = static_cast(data.bytes); + _platformView->DispatchPlatformMessage( + ftl::MakeRefCounted( + messageName.UTF8String, + std::vector(bytes, bytes + data.length), + nullptr)); +} + - (void)addMessageListener:(NSObject*)listener { NSAssert(listener, @"The listener must not be null"); NSString* messageName = listener.messageName;