| // 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); |
| } |
| |
| - (void)startEditing { |
| if (!_plugin) { |
| return; |
| } |
| if (self.currentEditor == _plugin) { |
| return; |
| } |
| // Selecting text seems to be the only way to make the field editor |
| // current editor. |
| [self selectText:self]; |
| NSAssert(self.currentEditor == _plugin, @"Failed to set current editor"); |
| |
| _plugin.client = self; |
| |
| // Restore previous selection. |
| 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]; |
| } |
| |
| #pragma mark - NSObject |
| |
| - (void)dealloc { |
| if (_plugin.client == self) { |
| _plugin.client = nil; |
| } |
| } |
| |
| @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]; |
| } |
| |
| } // namespace flutter |