blob: 99294970d0db8b6e1b1b44385518351c58664a9e [file] [log] [blame]
// 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/FlutterTextInputSemanticsObject.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
#include "flutter/third_party/accessibility/ax/ax_action_data.h"
#include "flutter/third_party/accessibility/gfx/geometry/rect_conversions.h"
#include "flutter/third_party/accessibility/gfx/mac/coordinate_conversion.h"
#pragma mark - FlutterTextFieldCell
/**
* A convenient class that can be used to set a custom field editor for an
* NSTextField.
*
* The FlutterTextField uses this class set the FlutterTextInputPlugin as
* its field editor.
*/
@interface FlutterTextFieldCell : NSTextFieldCell
/**
* Initializes the NSCell for the input NSTextField.
*/
- (instancetype)initWithTextField:(NSTextField*)textField fieldEditor:(NSTextView*)editor;
@end
@implementation FlutterTextFieldCell {
NSTextView* _editor;
}
#pragma mark - Private
- (instancetype)initWithTextField:(NSTextField*)textField fieldEditor:(NSTextView*)editor {
self = [super initTextCell:textField.stringValue];
if (self) {
_editor = editor;
[self setControlView:textField];
// Read-only text fields are sent to the mac embedding as static
// text. This text field must be editable and selectable at this
// point.
self.editable = YES;
self.selectable = YES;
}
return self;
}
#pragma mark - NSCell
- (NSTextView*)fieldEditorForView:(NSView*)controlView {
return _editor;
}
@end
#pragma mark - FlutterTextField
@implementation FlutterTextField {
flutter::FlutterTextPlatformNode* _node;
FlutterTextInputPlugin* _plugin;
}
#pragma mark - Public
- (instancetype)initWithPlatformNode:(flutter::FlutterTextPlatformNode*)node
fieldEditor:(FlutterTextInputPlugin*)plugin {
self = [super initWithFrame:NSZeroRect];
if (self) {
_node = node;
_plugin = plugin;
[self setCell:[[FlutterTextFieldCell alloc] initWithTextField:self fieldEditor:plugin]];
}
return self;
}
- (void)updateString:(NSString*)string withSelection:(NSRange)selection {
NSAssert(_plugin.client == self,
@"Can't update FlutterTextField when it is not the first responder");
if (![[self stringValue] isEqualToString:string]) {
[self setStringValue:string];
}
if (!NSEqualRanges(_plugin.selectedRange, selection)) {
[_plugin setSelectedRange:selection];
}
}
#pragma mark - NSView
- (NSRect)frame {
return _node->GetFrame();
}
#pragma mark - NSAccessibilityProtocol
- (void)setAccessibilityFocused:(BOOL)isFocused {
[super setAccessibilityFocused:isFocused];
ui::AXActionData data;
data.action = isFocused ? ax::mojom::Action::kFocus : ax::mojom::Action::kBlur;
_node->GetDelegate()->AccessibilityPerformAction(data);
}
#pragma mark - NSResponder
- (BOOL)becomeFirstResponder {
if (!_plugin) {
return NO;
}
if (_plugin.client == self && [_plugin isFirstResponder]) {
// This text field is already the first responder.
return YES;
}
BOOL result = [super becomeFirstResponder];
if (result) {
_plugin.client = self;
// The default implementation of the becomeFirstResponder will change the
// text editing state. Need to manually set it back.
NSString* textValue = @(_node->GetStringAttribute(ax::mojom::StringAttribute::kValue).data());
int start = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart);
int end = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd);
NSAssert((start >= 0 && end >= 0) || (start == -1 && end == -1), @"selection is invalid");
NSRange selection;
if (start >= 0 && end >= 0) {
selection = NSMakeRange(MIN(start, end), ABS(end - start));
} else {
// The native behavior is to place the cursor at the end of the string if
// there is no selection.
selection = NSMakeRange([self stringValue].length, 0);
}
[self updateString:textValue withSelection:selection];
}
return result;
}
- (BOOL)resignFirstResponder {
BOOL result = [super resignFirstResponder];
if (result && _plugin.client == self) {
_plugin.client = nil;
}
return result;
}
#pragma mark - NSObject
- (void)dealloc {
[self resignFirstResponder];
}
@end
namespace flutter {
FlutterTextPlatformNode::FlutterTextPlatformNode(FlutterPlatformNodeDelegate* delegate,
__weak FlutterViewController* view_controller) {
Init(delegate);
view_controller_ = view_controller;
appkit_text_field_ =
[[FlutterTextField alloc] initWithPlatformNode:this
fieldEditor:view_controller.textInputPlugin];
appkit_text_field_.bezeled = NO;
appkit_text_field_.drawsBackground = NO;
appkit_text_field_.bordered = NO;
appkit_text_field_.focusRingType = NSFocusRingTypeNone;
}
FlutterTextPlatformNode::~FlutterTextPlatformNode() {
EnsureDetachedFromView();
}
gfx::NativeViewAccessible FlutterTextPlatformNode::GetNativeViewAccessible() {
if (EnsureAttachedToView()) {
return appkit_text_field_;
}
return nil;
}
NSRect FlutterTextPlatformNode::GetFrame() {
if (!view_controller_.viewLoaded) {
return NSZeroRect;
}
FlutterPlatformNodeDelegate* delegate = static_cast<FlutterPlatformNodeDelegate*>(GetDelegate());
bool offscreen;
auto bridge_ptr = delegate->GetOwnerBridge().lock();
gfx::RectF bounds = bridge_ptr->RelativeToGlobalBounds(delegate->GetAXNode(), offscreen, true);
// Converts to NSRect to use NSView rect conversion.
NSRect ns_local_bounds = NSMakeRect(bounds.x(), bounds.y(), bounds.width(), bounds.height());
// The macOS XY coordinates start at bottom-left and increase toward top-right,
// which is different from the Flutter's XY coordinates that start at top-left
// increasing to bottom-right. Flip the y coordinate to convert from Flutter
// coordinates to macOS coordinates.
ns_local_bounds.origin.y = -ns_local_bounds.origin.y - ns_local_bounds.size.height;
NSRect ns_view_bounds = [view_controller_.flutterView convertRectFromBacking:ns_local_bounds];
return [view_controller_.flutterView convertRect:ns_view_bounds toView:nil];
}
bool FlutterTextPlatformNode::EnsureAttachedToView() {
if (!view_controller_.viewLoaded) {
return false;
}
if ([appkit_text_field_ isDescendantOf:view_controller_.view]) {
return true;
}
[view_controller_.view addSubview:appkit_text_field_
positioned:NSWindowBelow
relativeTo:view_controller_.flutterView];
return true;
}
void FlutterTextPlatformNode::EnsureDetachedFromView() {
[appkit_text_field_ removeFromSuperview];
}
}