-
Notifications
You must be signed in to change notification settings - Fork 6k
Support text editing voiceover feedback in macOS #25600
Conversation
c3cd2ed
to
1fbef2b
Compare
Hi @dnfield @cbracken Can you take a look at this PR? I have a design doc the outline the entire approach go/macos-text-editing-feedback also cc @gaaclarke for that weird __unsafe_unretained issue for NSTextView |
maybe @dkwingsmt for keyboard changes and @stuartmorgan for generic macos change |
Sorry, I lost track of this, but can work on reviewing it. It would probably be good for @stuartmorgan or @cbracken to look as well if they're available. |
(mainly because my head ends up stuck in MRC mode, and I'm less certain about how things end up working with ARC :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't look super closely at the test files, and I may be making some MRC minded mistakes, but overall:
- We should be cautious about accessing the view without checking that it's loaded, otherwise we could unintentionally force it to load when it shouldn't.
- I'm curious about assuming the type of the app delegate. I think if we can avoid doing that it will be better, even if we're not already supporting it elsewhere.
- The unsafe unassigned seems fishy to me.
//------------------------------------------------------------------------------ | ||
/// @brief Gets the owner of this platform node delegate. This is useful | ||
/// when you want to get the information surround this platform | ||
/// node delegate, e.g. the global rect of this platform node | ||
/// delegate. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please mention which task runner this is safe to use on. I assume Platform.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not too familiar with this part, what makes some thing safe to run on platform thread vs the other. Also The accessibility birdge class should only be used in embedding code, can embedding code run things on UI thread?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fml weaks are only safe to use on the thread they were created on. The embedder can access the other task runners.
[strong_engine.viewController.view convertRectFromBacking:ns_local_bounds]; | ||
NSRect ns_window_bounds = [strong_engine.viewController.view convertRect:ns_view_bounds | ||
toView:nil]; | ||
[strong_engine.viewController.flutterView convertRectFromBacking:ns_local_bounds]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here as well: we should either be asserting that the view is loaded, or fizzling out/returning a zero rect if it's not.
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" | ||
|
||
@interface FlutterViewController () | ||
|
||
// The FlutterView for this view controller. | ||
@property(nonatomic, readonly, nullable) FlutterView* flutterView; | ||
@property(nonatomic, strong, nullable) FlutterView* flutterView; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is the internal header, but I think we probably still want this ot be readonly
in the header. We can redefine it as readwrite in the implementation file instead.
/** | ||
* The text input plugin that handles text editing state for text fields. | ||
*/ | ||
@property(nonatomic, strong, nonnull) FlutterTextInputPlugin* textInputPlugin; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not readonly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a first pass; I haven't gone over everything in detail (especially once I hit the window delegate issue described below).
@@ -29,6 +29,7 @@ AccessibilityBridge::AccessibilityBridge( | |||
AccessibilityBridge::~AccessibilityBridge() { | |||
event_generator_.ReleaseTree(); | |||
tree_.RemoveObserver(static_cast<ui::AXTreeObserver*>(this)); | |||
id_wrapper_map_.clear(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need to clear an ivar in the destructor? Is this enforcing a specific ordering? If so, it absolutely needs a comment explaining that so someone doesn't just remove it.
/** | ||
* A plugin to handle text input. | ||
* | ||
* Responsible for bridging the native macOS text input system with the Flutter framework text | ||
* editing classes, via system channels. | ||
* | ||
* This is not an FlutterPlugin since it needs access to FlutterViewController internals, so needs | ||
* to be managed differently. | ||
* to be managed differently. The FlutterViewController has the ownership of this plugin. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason for adding this comment?
I am generally very skeptical of comments that make assertions about code above this class. If someone refactors FVC at some point and this is no longer the case, what are the chances they will find and update this comment?
Sometimes the benefits outweigh the risk of the comments becoming wrong, but I'm not clear what the benefits are here.
shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h
Outdated
Show resolved
Hide resolved
@interface FlutterTextInputPlugin : NSTextView <FlutterKeySecondaryResponder> | ||
|
||
/** | ||
* The native text field this backed by this plugin as its field editor. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not able to parse this sentence (and I'm not sure what it's intended to say).
|
||
/** | ||
* Initializes a text input plugin that coordinates key event handling with |viewController|. | ||
*/ | ||
- (instancetype)initWithViewController:(FlutterViewController*)viewController; | ||
|
||
/** | ||
* Where this plugin is the first responder of this NSWindow. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whether
@end | ||
|
||
/** | ||
* Private interface declaration for FlutterNativeTextFieldHidder. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hider
Although really this whole line should be removed; comments should not restate obvious aspects of the language itself; anyone familiar with ObjC knows that this is an interface declaration. Just like we don't write comments like:
// This is declaring an integer variable:
int x;
shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
/** | ||
* Private interface declaration for FlutterNativeTextFieldHidder. | ||
* | ||
* This NSView is the content view of the FlutterViewController. This view |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/This NSView is the/The/ (see rationale above)
} | ||
|
||
- (void)viewDidLoad { | ||
FlutterAppDelegate* appDelegate = (FlutterAppDelegate*)[NSApp delegate]; | ||
self.windowDelegate = [[FlutterWindowDelegate alloc] initWithController:self]; | ||
appDelegate.mainFlutterWindow.delegate = _windowDelegate; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a non-starter; just as we don't own the application, we don't own the window. This could stomp a developer's window delegate, or they could stomp this.
If a window-level hook is the only way to make this workaround function, then you'll need to wire up an explicit way for an application to delegate to Flutter (likely via FVC) and then make the template set up a skeleton window delegate that explicitly does that forwarding, with a comment explaining why, and what to do it they want to have a different delegate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing this out. There may be a way to inject field editor without windows delegate if i can create the custom NSTextFieldCell. I have to test it out.
|
||
- (id)windowWillReturnFieldEditor:(NSWindow*)sender toObject:(id)client { | ||
if ([client isKindOfClass:[FlutterTextField class]]) { | ||
return _controller.textInputPlugin; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if there is more than one Flutter view in the window?
9ffa362
to
c8d40c2
Compare
@stuartmorgan @dnfield Thanks for the review, I have addressed all the comments, the changes are in the second commit. |
A friendly bump |
/// @brief Gets the owner of this platform node delegate. This is useful | ||
/// when you want to get the information surround this platform | ||
/// node delegate, e.g. the global rect of this platform node | ||
/// delegate. This pointer is only safe in the platform thread. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is only safe to use on the Platform thread, we should probably use a fml::WeakPtr
, which will actually do assertions around usage of this, rather than std::weak_ptr
which is meant to be thread safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are we allowed to depend on fml in this common library? I remember there is a plan to make fml into its own standalone library, but i am not sure whether that is done yet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhh you're right sorry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're close to being able to use it, but not quite. Can you add a TODO here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All that's left for me is using the fml::WeakPtr
instead fo std::weak_ptr
for a non-thread-safe weak pointer - as long as @stuartmorgan 's concerns are also addressed.
@@ -451,15 +454,17 @@ - (void)setSemanticsEnabled:(BOOL)enabled { | |||
return; | |||
} | |||
_semanticsEnabled = enabled; | |||
// We need to remove the accessibility children from flutter view |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Who is "we" here? (go/avoidwe)
// | ||
// Since we will nil the handler in dealloc. the weakSelf should | ||
// be valid if the handler is ever called. | ||
__unsafe_unretained FlutterTextInputPlugin* weakSelf = self; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't call an unsafe pointer weakSelf
; this is the idiomatic name for a weak pointer to self, not an unsafe pointer to self.
// NSTextView does not support _weak reference, so we have to | ||
// use __unsafe_unretained and manage the reference ourselves. | ||
// | ||
// Since we will nil the handler in dealloc. the weakSelf should |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the stray .
here please. Also, as above, use something clearer than "we" (e.g., you can just say "|dealloc| removes the handler, so the pointer should be valid if the handler is ever called.").
@@ -176,6 +199,17 @@ - (instancetype)initWithViewController:(FlutterViewController*)viewController { | |||
return self; | |||
} | |||
|
|||
- (BOOL)isFirstResponder { | |||
if (!self.flutterViewController.viewLoaded || !self.flutterViewController.view.window) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second half of this check isn't doing anything useful; if self.flutterViewController.view.window
is nil, then [self.flutterViewController.view.window firstResponder]
is also nil, and thus can't be equal to self
.
shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
} | ||
|
||
- (instancetype)initWithFlutterView:(FlutterView*)view { | ||
self = [super initWithFrame:view.frame]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Taking the frame from its child seems confusing. Why are you structuring it this way instead of following the more normal pattern of setting the outermost view's frame and letting subviews resize accordingly?
@@ -278,7 +321,11 @@ - (void)loadView { | |||
} | |||
flutterView = [[FlutterView alloc] initWithMainContext:mainContext reshapeListener:self]; | |||
} | |||
self.view = flutterView; | |||
FlutterViewWrapper* nativeTextFieldHidder = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wrapperView
self.view = flutterView; | ||
FlutterViewWrapper* nativeTextFieldHidder = | ||
[[FlutterViewWrapper alloc] initWithFlutterView:flutterView]; | ||
[nativeTextFieldHidder addSubview:flutterView]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're going to make the Flutter view an init param, why not do this as part of that? Making it possible (and easy) to set up a wrapper that points to a FlutterView that it doesn't actually wrap seems like an anti-feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are right.
if (event.type == NSEventTypeKeyDown) { | ||
// macOS only sends keydown for performKeyEquivalent. We need | ||
// to synthesize a key up event. This is true for repeat key | ||
// events. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this change related to the PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And why isn't it tested?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops I think i missed the test.
This will be called when user press shortcut, e.g. ctrl+v. If the framework receive the keydown without receive the keyup, it will assume the ctrl+v is held and never released. This cause other shortcut to break
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This wasn't a problem before because the performKeyEquivalent will only be called if the view is a NSControl
c8d40c2
to
1448ba5
Compare
@@ -45,8 +45,8 @@ | |||
* This will invoke [delegate flush:] on raster thread and | |||
* [delegate commit:] on platform thread. The requestCommit call will be blocked | |||
* until this is done. This is necessary to ensure that rasterizer won't start | |||
* rasterizing next frame before we flipped the surface, which must be performed | |||
* on platform thread | |||
* rasterizing next frame before the FlutterSurfaceManager flipped the surface, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I am correct, the FlutterSurfaceManager is in charged of switching context. Let me know if i am wrong
87fea82
to
678480b
Compare
shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h
Outdated
Show resolved
Hide resolved
shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h
Outdated
Show resolved
Hide resolved
} | ||
|
||
FlutterTextPlatformNode::~FlutterTextPlatformNode() { | ||
[native_text_field_ setFieldEditor:nil]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you give an overview of the ownership relation between the various classes involved here? What object ultimately own the FlutterTextPlatformNode
? It's not at all obvious to me what guarantee you have that engine.viewController.textInputPlugin
will outlive this object instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm
Show resolved
Hide resolved
217016e
to
a018092
Compare
shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm
Outdated
Show resolved
Hide resolved
if (start > 0 && end > 0) { | ||
selection = NSMakeRange(start, end - start); | ||
} else { | ||
selection = NSMakeRange([self stringValue].length, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the default to set the cursor at the end of the string?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is the native behavior when accessibility focus on a textfield without any selection. The problem with calling becomeFirstResponder on a textfield manually is that it will select the entire string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setting this will sends the selection back to the framework through the textinputplugin
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sgtm - can you add a comment to that effect?
shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM on the approach taken here; most of the comments are nits or questions, or phrased another way -- requests for comments on the bits that are unclear.
8282fe8
to
5345209
Compare
a65f9af
to
3dac97c
Compare
3dac97c
to
95abace
Compare
Fixes flutter/flutter#77834
design doc go/macos-text-editing-feedback
Pre-launch Checklist
writing and running engine tests.
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.