forked from firka/flutter
Implement TextInputPlugin on iOS (flutter/engine#3145)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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 <Foundation/Foundation.h>
|
||||
|
||||
@protocol FlutterTextInputDelegate<NSObject>
|
||||
|
||||
- (void)updateEditingClient:(int)client withState:(NSDictionary*)state;
|
||||
|
||||
@end
|
||||
|
||||
#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTDELEGATE_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<FlutterTextInputDelegate> textInputDelegate;
|
||||
|
||||
@end
|
||||
|
||||
#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTPLUGIN_H_
|
||||
@@ -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 <UIKit/UIKit.h>
|
||||
#include <unicode/utf16.h>
|
||||
|
||||
#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<UIKeyInput>
|
||||
|
||||
@property(nonatomic, assign) id<FlutterTextInputDelegate> 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
|
||||
@@ -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 ()<UIAlertViewDelegate>
|
||||
@interface FlutterViewController ()<UIAlertViewDelegate, FlutterTextInputDelegate>
|
||||
@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<shell::PlatformViewIOS> _platformView;
|
||||
base::scoped_nsprotocol<FlutterPlatformPlugin*> _platformPlugin;
|
||||
base::scoped_nsprotocol<FlutterTextInputPlugin*> _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<const char*>(data.bytes);
|
||||
_platformView->DispatchPlatformMessage(
|
||||
ftl::MakeRefCounted<blink::PlatformMessage>(
|
||||
messageName.UTF8String,
|
||||
std::vector<char>(bytes, bytes + data.length),
|
||||
nullptr));
|
||||
}
|
||||
|
||||
- (void)addMessageListener:(NSObject<FlutterMessageListener>*)listener {
|
||||
NSAssert(listener, @"The listener must not be null");
|
||||
NSString* messageName = listener.messageName;
|
||||
|
||||
Reference in New Issue
Block a user