-
Notifications
You must be signed in to change notification settings - Fork 6k
[macOS] Remove isComposing workaround #33838
Conversation
b8750e6
to
f7641eb
Compare
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 looks straightforward to me. Here's my framework PR review: flutter/flutter#105407 (review)
If we're going to move forward with this we should test that kPerformIntent is invoked.
cc @justinmc Are you able to review this? |
46cb18b
to
4de9b63
Compare
This comment was marked as outdated.
This comment was marked as outdated.
e210cc7
to
593ba19
Compare
@@ -620,6 +634,28 @@ - (void)doCommandBySelector:(SEL)selector { | |||
void (*func)(id, SEL, id) = reinterpret_cast<void (*)(id, SEL, id)>(imp); | |||
func(self, selector, 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.
If the selector is implemented by FlutterTextInputPlugin
I think we don't want to forward the selector to the framework? Otherwise the same selector could be handled twice?
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 selectors are implemented by NSTextView
(they just are mostly empty implementation). However this means that [self respondsToSelector:selector]
always returns true.
We only seem to be handling insertNewline:
in text input plugin (I'll double check) translating it into text, so perhaps we could just not forward this one.
We could also check with objc runtime to see whether particular selector is overridden, which seems a bit of overkill, but that will need special exception for insertTab:
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.
There's a noop:
selector (or maybe noop
) when a shortcut isn't mapped to anything.
? (__bridge CFStringRef)self.customRunLoopMode | ||
: kCFRunLoopCommonModes; | ||
|
||
CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{ |
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 I understand this correctly, this is trying to ensure when a shortcut/key equivalent is mapped to a sequence of selectors (e.g., Alt+↑ => moveBackward:
then moveToBeginningOfParagraph:
), these selectors are sent to the framework in the same channel message as a list?
Will this cause some events to reorder, for example, if a key is bound to 3 selectors [moveBackward
, insert: a
, moveForward
], since insert:a
will be handled by the input plugin, this essentially becomes [insert: a
, moveBackward
, moveForward
], from the framework's perspective?
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.
Maybe use dispatch_async
instead of CFRunLoopPerformBlock
? Then you don't have to set the mode?
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.
nit: maybe don't schedule the block when it's already scheduled (e.g., selectors.length > 1
)?
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.
Maybe use dispatch_async instead of CFRunLoopPerformBlock? Then you don't have to set the mode?
We can't pump the dispatch queue manually in unit tests. We can however run the run loop in custom mode so that only these blocks are executed.
nit: maybe don't schedule the block when it's already scheduled (e.g., selectors.length > 1)?
Done.
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.
Will this cause some events to reorder, for example, if a key is bound to 3 selectors [moveBackward, insert: a, moveForward], since insert:a will be handled by the input plugin, this essentially becomes [insert: a, moveBackward, moveForward], from the framework's perspective?
So it will indeed become that. However even if we flush buffered selectors and it will become [moveBackward, insert:a, moveForward] it still won't give expected result, because when TextInputPlugin
processes insert:a
the selection hasn't been updated by the framework yet so that first [moveBackward:] will only update selection after text is already inserted.
I don't think we can handle compound actions that involve inserting text correctly at this point. It may required some rather significant changes to get this working.
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.
Something still needs to pump the main dispatch queue. I don't know any other way to do this except running the CFRunLoop in common modes. However running the CFRunLoop
in default mode in tests crashes because of various scheduled NSView
related tasks from other tests.
Dispatch barrier is unlikely to help here. Doing anything blocking on main thread (dispatch_sync
, dispatch_barrier_sync
, dispatch_barrier_async_and_wait
) will crash with "LIBDISPATCH: dispatch_sync called on queue already owned by current thread". And async barrier will do not anything useful (it still needs the queue to be pumped).
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 test passes (didn't need a barrier since the main queue is not concurrent):
func testExample() throws {
var x = 0
let expectation = XCTestExpectation()
DispatchQueue.main.async {
print("execute")
x += 1
}
DispatchQueue.main.async {
XCTAssertEqual(x, 1)
expectation.fulfill()
}
print("wait")
wait(for: [expectation], timeout: 10)
}
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.
xctwaiter.wait
calls _dispatch_main_queue_drain()
, so the main queue is pumped in a sense.
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.
_dispatch_main_queue_drain()
doesn't seem to be exported from libdispatch. Looking at XCTest it seems that waitFor
actually runs the run loop, which would have same sideeffect as calling CFRunLoopRunInMode
with default modes manually, as in it would crash flutter_desktop_darwin_unittests
. Now why this crash happens is another issue (perhaps we could drain the run loop in each test while the appropriate cocoa objects still exist), but that should be tracked as another issue.
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.
Created flutter/flutter#106509.
593ba19
to
f53a793
Compare
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.
Just jumping in with a few nits here as I read through this PR.
shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm
Outdated
Show resolved
Hide resolved
@@ -568,6 +577,11 @@ - (NSTextInputContext*)inputContext { | |||
#pragma mark - | |||
#pragma mark NSTextInputClient | |||
|
|||
- (void)insertTab:(id)sender { | |||
// Implementing insertTab: makes AppKit send tab as command, instead of | |||
// insertText with '\t'. |
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.
/cc @loic-sharma who's looking at tab handling on Windows.
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 flutter text doesn't define tab shortcut and thus it's always handled as focus, when I wanted to insert tab I simply used this Action
class InsertTabAction extends Action<NextFocusIntent> {
final TextEditingController controller;
InsertTabAction(this.controller);
void _sendIntent(Intent intent) {
final BuildContext? primaryContext = primaryFocus?.context;
if (primaryContext != null) {
Actions.invoke(primaryContext, intent);
}
}
@override
Object? invoke(NextFocusIntent intent) {
_sendIntent(ReplaceTextIntent(controller.value, '', controller.selection,
SelectionChangedCause.keyboard));
_sendIntent(ReplaceTextIntent(controller.value, '\t', controller.selection,
SelectionChangedCause.keyboard));
return null;
}
}
and bound it to NextFocusIntent
above text editor. It's not the most obvious solution as it requires binding to NextFocusIntent
instead of using Shortcut and custom intent, but it works even with Mac IME input after this PR.
/cc @loic-sharma
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.
Can you illustrate why using SingleActivator(LogicalKeyboardKey.tab)
and some InsertTabIntent
in a Shortcuts
above the text editor wouldn't work?
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.
Can you illustrate why using
SingleActivator(LogicalKeyboardKey.tab)
and someInsertTabIntent
in aShortcuts
above the text editor wouldn't work?
It wouldn't. That's why I said it requires binding to NextFocusIntent
. Tab has to be unconditionally passed to the IME (because we have no way of knowing whether the candidates popup is shown), and when we receive insertTab:
from TextInputContext we send NextFocusIntent
to current focus node. This is not ideal, but I don't think there's any way currently to synthesize key event and activate shortcuts, is there?
LGTM modulo filtering out selectors already handled by the |
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 and +1 on a small comment to explain the setMarkedText behaviour; the Apple docs lack a lot of detail.
@knopp is this good to land? |
Sorry I'm still taking a look, trying to understand the data flow after the change. |
I'd prefer landing #34250 first and then rebasing this one.
As far as engine is concerned the only change is that editing events are not forwarded to |
After talking with @LongCatIsLooong I think I understand the flow better. Let me phrase it and see if it's correct. After this and flutter/flutter#105407, certain control keys (tab, arrows, etc.) will by rejected by the framework when a text input is being focused (formally, these keys are mapped to These events are sent back to the AppKit platform. Now, I assume, is when the platform gets to dispatch them to the IME (emoji picker.) If the emoji picker doesn't handle them, the platform dispatches them to These string-actions are sent to Framework's Since these string-actions now always dispatch fixed intents, for example, a Tab key always dispatches If so, I have a few questions: First, the framework change only rejects control keys without any modifiers. For example, it rejects ArrowLeft, but not Alt-ArrowLeft, but Alt-ArrowLeft is still removed from Second, while I'm still concerned with forcing the developer with override a NextFocusIntent with an InsertTabAction, I can't think of a better solution. However, this requires much more education, since overriding stuff is more confusing. We need a way to tell the developers to override the intent-to-action map, instead of the activator-to-intent map (which is understandably more intuitive), and the breakage otherwise will only be noticed in the corner case of emoji picker. Moreover, they will have to remember the enumerated intents to override, which basically means they're associating I'm wondering if it will be a better choice to create a And also, I definitely prefer documenting the data flow above somewhere in the engine. |
It rejects ArrowLeft, because it is mapped to Alt-ArrowLeft on the other hand is not mapped to anything in framework. So framework does not handle the event, the engine will pass it to TextInputContext, which will produce the
Agreed. Ideally the framework should invoke whatever intent is mapped to Esc key above the editor. Users would expect that Shortcut with
Which selectors would this be for? If I understand correctly you suggesting mapping
|
6f6ee70
to
5f0b88a
Compare
@chinmaygarde, this should be good to go, after the tests pass. I needs to be merged together with the framework PR, not sure how to do that. |
I think merging the engine PR first shouldn't break anything. It adds the functionality that unhandled key presses will be converted to selectors, but since these text shortcuts are still handled, this functionality should not be triggered. |
Fixes flutter/flutter#85328
Framework PR: flutter/flutter#105407
If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.
Pre-launch Checklist
writing and running engine tests.
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.