Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

[macOS] Remove isComposing workaround #33838

Merged
merged 20 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,6 @@ - (void)processNextEvent {
}

- (void)performProcessEvent:(NSEvent*)event onFinish:(VoidBlock)onFinish {
if (_viewDelegate.isComposing) {
[self dispatchTextEvent:event];
onFinish();
return;
}

// Having no primary responders require extra logic, but Flutter hard-codes
// all primary responders, so this is a situation that Flutter will never
// encounter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@ - (void)recordCallTypesTo:(nonnull NSMutableArray<NSNumber*>*)typeStorage

@property(nonatomic) FlutterKeyboardManager* manager;
@property(nonatomic) NSResponder* nextResponder;
@property(nonatomic, assign) BOOL isComposing;

#pragma mark - Private

Expand Down Expand Up @@ -289,7 +288,6 @@ - (nonnull instancetype)init {
[self respondChannelCallsWith:FALSE];
[self respondEmbedderCallsWith:FALSE];
[self respondTextInputWith:FALSE];
_isComposing = NO;

_currentLayout = &kUsLayout;

Expand All @@ -304,7 +302,6 @@ - (nonnull instancetype)init {
OCMStub([viewDelegateMock onTextInputKeyEvent:[OCMArg any]])
.andCall(self, @selector(handleTextInputKeyEvent:));
OCMStub([viewDelegateMock getBinaryMessenger]).andReturn(messengerMock);
OCMStub([viewDelegateMock isComposing]).andCall(self, @selector(isComposing));
OCMStub([viewDelegateMock sendKeyEvent:FlutterKeyEvent {} callback:nil userData:nil])
.ignoringNonObjectArgs()
.andCall(self, @selector(handleEmbedderEvent:callback:userData:));
Expand Down Expand Up @@ -430,7 +427,6 @@ @interface FlutterKeyboardManagerUnittestsObjC : NSObject
- (bool)singlePrimaryResponder;
- (bool)doublePrimaryResponder;
- (bool)textInputPlugin;
- (bool)forwardKeyEventsToSystemWhenComposing;
- (bool)emptyNextResponder;
- (bool)racingConditionBetweenKeyAndText;
- (bool)correctLogicalKeyForLayouts;
Expand All @@ -449,10 +445,6 @@ - (bool)correctLogicalKeyForLayouts;
ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] textInputPlugin]);
}

TEST(FlutterKeyboardManagerUnittests, handlingComposingText) {
ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] forwardKeyEventsToSystemWhenComposing]);
}

TEST(FlutterKeyboardManagerUnittests, EmptyNextResponder) {
ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] emptyNextResponder]);
}
Expand Down Expand Up @@ -597,31 +589,6 @@ - (bool)textInputPlugin {
return true;
}

- (bool)forwardKeyEventsToSystemWhenComposing {
KeyboardTester* tester = OCMPartialMock([[KeyboardTester alloc] init]);

NSMutableArray<FlutterAsyncKeyCallback>* channelCallbacks =
[NSMutableArray<FlutterAsyncKeyCallback> array];
NSMutableArray<FlutterAsyncKeyCallback>* embedderCallbacks =
[NSMutableArray<FlutterAsyncKeyCallback> array];
[tester recordEmbedderCallsTo:embedderCallbacks];
[tester recordChannelCallsTo:channelCallbacks];
// The event shouldn't propagate further even if TextInputPlugin does not
// claim the event.
[tester respondTextInputWith:NO];

tester.isComposing = YES;
// Send a down event with composing == YES.
[tester.manager handleEvent:keyUpEvent(0x50)];

// Nobody gets the event except for the text input plugin.
EXPECT_EQ([channelCallbacks count], 0u);
EXPECT_EQ([embedderCallbacks count], 0u);
OCMVerify(times(1), [tester handleTextInputKeyEvent:checkKeyDownEvent(0x50)]);

return true;
}

- (bool)emptyNextResponder {
KeyboardTester* tester = [[KeyboardTester alloc] init];
tester.nextResponder = nil;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,6 @@ typedef struct {
*/
- (BOOL)onTextInputKeyEvent:(nonnull NSEvent*)event;

/**
* Whether this FlutterKeyboardViewDelegate is actively taking provisional user text input.
*
* This is typically true when a Flutter text field is focused, and the user is entering composing
* text into the text field.
*/
// TODO (LongCatIsLooong): remove this method and implement a long-term fix for
// https://github.com/flutter/flutter/issues/85328.
- (BOOL)isComposing;

/**
* Add a listener that is called whenever the user changes keyboard layout.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,6 @@
*/
- (BOOL)isFirstResponder;

/**
* Whether this plugin has composing text.
*
* This is only true when the text input plugin is actively taking user input with composing text.
*/
// TODO (LongCatIsLooong): remove this method and implement a long-term fix for
// https://github.com/flutter/flutter/issues/85328.
- (BOOL)isComposing;

/**
* Handles key down events received from the view controller, responding YES if
* the event was handled.
Expand All @@ -73,4 +64,5 @@
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange;
- (NSDictionary*)editingState;
@property(readwrite, nonatomic) NSString* customRunLoopMode;
@end
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
static NSString* const kUpdateEditStateWithDeltasResponseMethod =
@"TextInputClient.updateEditingStateWithDeltas";
static NSString* const kPerformAction = @"TextInputClient.performAction";
static NSString* const kPerformSelectors = @"TextInputClient.performSelectors";
static NSString* const kMultilineInputType = @"TextInputType.multiline";

static NSString* const kTextAffinityDownstream = @"TextAffinity.downstream";
Expand Down Expand Up @@ -174,6 +175,13 @@ @interface FlutterTextInputPlugin ()
*/
@property(nonatomic) BOOL enableDeltaModel;

/**
* Used to gather multiple selectors performed in one run loop turn. These
* will be all sent in one platform channel call so that the framework can process
* them in single microtask.
*/
@property(nonatomic) NSMutableArray* pendingSelectors;

/**
* Handles a Flutter system message on the text input channel.
*/
Expand Down Expand Up @@ -212,6 +220,11 @@ - (void)updateTextAndSelection;
*/
- (NSString*)textAffinityString;

/**
* Allow overriding run loop mode for test.
*/
@property(readwrite, nonatomic) NSString* customRunLoopMode;

@end

@implementation FlutterTextInputPlugin {
Expand Down Expand Up @@ -504,10 +517,6 @@ - (NSString*)textAffinityString {
: kTextAffinityDownstream;
}

- (BOOL)isComposing {
return _activeModel && !_activeModel->composing_range().collapsed();
}

- (BOOL)handleKeyEvent:(NSEvent*)event {
if (event.type == NSEventTypeKeyUp ||
(event.type == NSEventTypeFlagsChanged && event.modifierFlags < _previouslyPressedFlags)) {
Expand Down Expand Up @@ -615,6 +624,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'.
Copy link
Member

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.

Copy link
Member Author

@knopp knopp Jun 23, 2022

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

Copy link
Contributor

@dkwingsmt dkwingsmt Jun 30, 2022

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?

Copy link
Member Author

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?

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?

}

- (void)insertText:(id)string replacementRange:(NSRange)range {
if (_activeModel == nullptr) {
return;
Expand Down Expand Up @@ -670,6 +684,36 @@ - (void)doCommandBySelector:(SEL)selector {
void (*func)(id, SEL, id) = reinterpret_cast<void (*)(id, SEL, id)>(imp);
func(self, selector, nil);
}

Copy link
Contributor

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?

Copy link
Member Author

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:

Copy link
Contributor

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.

if (selector == @selector(insertNewline:)) {
// Already handled through text insertion (multiline) or action.
return;
}

// Group multiple selectors received within a single run loop turn so that
// the framework can process them in single microtask.
NSString* name = NSStringFromSelector(selector);
if (_pendingSelectors == nil) {
_pendingSelectors = [NSMutableArray array];
}
[_pendingSelectors addObject:name];

if (_pendingSelectors.count == 1) {
__weak NSMutableArray* selectors = _pendingSelectors;
__weak FlutterMethodChannel* channel = _channel;
__weak NSNumber* clientID = self.clientID;

CFStringRef runLoopMode = self.customRunLoopMode != nil
? (__bridge CFStringRef)self.customRunLoopMode
: kCFRunLoopCommonModes;

CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{
if (selectors.count > 0) {
[channel invokeMethod:kPerformSelectors arguments:@[ clientID, selectors ]];
[selectors removeAllObjects];
}
});
}
}

- (void)insertNewline:(id)sender {
Expand All @@ -696,6 +740,19 @@ - (void)setMarkedText:(id)string
if (!_activeModel->composing()) {
_activeModel->BeginComposing();
}

if (replacementRange.location != NSNotFound) {
// According to the NSTextInputClient documentation replacementRange is
// computed from the beginning of the marked text. That doesn't seem to be
// the case, because in situations where the replacementRange is actually
// specified (i.e. when switching between characters equivalent after long
// key press) the replacementRange is provided while there is no composition.
_activeModel->SetComposingRange(
flutter::TextRange(replacementRange.location,
replacementRange.location + replacementRange.length),
0);
}

flutter::TextRange composingBeforeChange = _activeModel->composing_range();
flutter::TextRange selectionBeforeChange = _activeModel->selection();

Expand Down
Loading