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

Commit 25ba16d

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix-ios-escape
2 parents 88642f0 + ca3bd30 commit 25ba16d

File tree

5 files changed

+136
-6
lines changed

5 files changed

+136
-6
lines changed

DEPS

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ vars = {
2727
'skia_git': 'https://skia.googlesource.com',
2828
# OCMock is for testing only so there is no google clone
2929
'ocmock_git': 'https://github.com/erikdoe/ocmock.git',
30-
'skia_revision': '45600771c27b166134b351b44783907061d1f966',
30+
'skia_revision': 'b950ac6d45b04d93ae9d9e10df81d0b08cfd73e2',
3131

3232
# WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY
3333
# See `lib/web_ui/README.md` for how to roll CanvasKit to a new version.
@@ -45,7 +45,7 @@ vars = {
4545
# Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS.
4646
# You can use //tools/dart/create_updated_flutter_deps.py to produce
4747
# updated revision list of existing dependencies.
48-
'dart_revision': '384160690d758ac57ff3a1f024c27027feb3ea1c',
48+
'dart_revision': '29d4cf8934769f9dc21f1a04b4256c86634e524e',
4949

5050
# WARNING: DO NOT EDIT MANUALLY
5151
# The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py
@@ -212,7 +212,7 @@ deps = {
212212
Var('dart_git') + '/dart_style.git@d7b73536a8079331c888b7da539b80e6825270ea',
213213

214214
'src/third_party/dart/third_party/pkg/dartdoc':
215-
Var('dart_git') + '/dartdoc.git@21f9341eb6623a46fc56fc3705e878371991aebb',
215+
Var('dart_git') + '/dartdoc.git@61dea6b8307392ee6dfe8fd952716b07f5243b1d',
216216

217217
'src/third_party/dart/third_party/pkg/ffi':
218218
Var('dart_git') + '/ffi.git@18b2b549d55009ff594600b04705ff6161681e07',

ci/licenses_golden/licenses_skia

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Signature: 0f32c358198338fad7d8ba6dd4da1ad2
1+
Signature: 0eb7a0af1ab30145b26246e7ee92ef4b
22

33
UNUSED LICENSES:
44

ci/licenses_golden/licenses_third_party

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Signature: 820f0af8efdd11587ea6129d4e87aa44
1+
Signature: 08f72272a0292f5602b2dbf25e46fa64
22

33
UNUSED LICENSES:
44

shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,35 @@ typedef NS_ENUM(NSUInteger, FlutterTextAffinity) {
7676
return flutter::TextRange([base unsignedLongValue], [extent unsignedLongValue]);
7777
}
7878

79+
@interface NSEvent (KeyEquivalentMarker)
80+
81+
// Internally marks that the event was received through performKeyEquivalent:.
82+
// When text editing is active, keyboard events that have modifier keys pressed
83+
// are received through performKeyEquivalent: instead of keyDown:. If such event
84+
// is passed to TextInputContext but doesn't result in a text editing action it
85+
// needs to be forwarded by FlutterKeyboardManager to the next responder.
86+
- (void)markAsKeyEquivalent;
87+
88+
// Returns YES if the event is marked as a key equivalent.
89+
- (BOOL)isKeyEquivalent;
90+
91+
@end
92+
93+
@implementation NSEvent (KeyEquivalentMarker)
94+
95+
// This field doesn't need a value because only its address is used as a unique identifier.
96+
static char markerKey;
97+
98+
- (void)markAsKeyEquivalent {
99+
objc_setAssociatedObject(self, &markerKey, @true, OBJC_ASSOCIATION_RETAIN);
100+
}
101+
102+
- (BOOL)isKeyEquivalent {
103+
return [objc_getAssociatedObject(self, &markerKey) boolValue] == YES;
104+
}
105+
106+
@end
107+
79108
/**
80109
* Private properties of FlutterTextInputPlugin.
81110
*/
@@ -130,6 +159,13 @@ @interface FlutterTextInputPlugin ()
130159
*/
131160
@property(nonatomic, nonnull) NSString* inputAction;
132161

162+
/**
163+
* Set to true if the last event fed to the input context produced a text editing command
164+
* or text output. It is reset to false at the beginning of every key event, and is only
165+
* used while processing this event.
166+
*/
167+
@property(nonatomic) BOOL eventProducedOutput;
168+
133169
/**
134170
* Whether to enable the sending of text input updates from the engine to the
135171
* framework as TextEditingDeltas rather than as one TextEditingValue.
@@ -482,7 +518,17 @@ - (BOOL)handleKeyEvent:(NSEvent*)event {
482518
return NO;
483519
}
484520

485-
return [_textInputContext handleEvent:event];
521+
_eventProducedOutput = NO;
522+
BOOL res = [_textInputContext handleEvent:event];
523+
// NSTextInputContext#handleEvent returns YES if the context handles the event. One of the reasons
524+
// the event is handled is because it's a key equivalent. But a key equivalent might produce a
525+
// text command (indicated by calling doCommandBySelector) or might not (for example, Cmd+Q). In
526+
// the latter case, this command somehow has not been executed yet and Flutter must dispatch it to
527+
// the next responder. See https://github.com/flutter/flutter/issues/106354 .
528+
if (event.isKeyEquivalent && !_eventProducedOutput) {
529+
return NO;
530+
}
531+
return res;
486532
}
487533

488534
#pragma mark -
@@ -509,6 +555,7 @@ - (BOOL)performKeyEquivalent:(NSEvent*)event {
509555
// send the event back to [keyboardManager handleEvent:].
510556
return NO;
511557
}
558+
[event markAsKeyEquivalent];
512559
[self.flutterViewController keyDown:event];
513560
return YES;
514561
}
@@ -573,6 +620,8 @@ - (void)insertText:(id)string replacementRange:(NSRange)range {
573620
return;
574621
}
575622

623+
_eventProducedOutput |= true;
624+
576625
if (range.location != NSNotFound) {
577626
// The selected range can actually have negative numbers, since it can start
578627
// at the end of the range if the user selected the text going backwards.
@@ -612,6 +661,7 @@ - (void)insertText:(id)string replacementRange:(NSRange)range {
612661
}
613662

614663
- (void)doCommandBySelector:(SEL)selector {
664+
_eventProducedOutput |= selector != NSSelectorFromString(@"noop:");
615665
if ([self respondsToSelector:selector]) {
616666
// Note: The more obvious [self performSelector...] doesn't give ARC enough information to
617667
// handle retain semantics properly. See https://stackoverflow.com/questions/7017281/ for more

shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,82 @@ - (bool)testPerformKeyEquivalent {
920920
return true;
921921
}
922922

923+
- (bool)unhandledKeyEquivalent {
924+
id engineMock = OCMClassMock([FlutterEngine class]);
925+
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
926+
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
927+
[engineMock binaryMessenger])
928+
.andReturn(binaryMessengerMock);
929+
930+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
931+
nibName:@""
932+
bundle:nil];
933+
934+
FlutterTextInputPlugin* plugin =
935+
[[FlutterTextInputPlugin alloc] initWithViewController:viewController];
936+
937+
[plugin handleMethodCall:[FlutterMethodCall
938+
methodCallWithMethodName:@"TextInput.setClient"
939+
arguments:@[
940+
@(1), @{
941+
@"inputAction" : @"action",
942+
@"enableDeltaModel" : @"true",
943+
@"inputType" : @{@"name" : @"inputName"},
944+
}
945+
]]
946+
result:^(id){
947+
}];
948+
949+
[plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.show"
950+
arguments:@[]]
951+
result:^(id){
952+
}];
953+
954+
// CTRL+H (delete backwards)
955+
NSEvent* event = [NSEvent keyEventWithType:NSEventTypeKeyDown
956+
location:NSZeroPoint
957+
modifierFlags:0x40101
958+
timestamp:0
959+
windowNumber:0
960+
context:nil
961+
characters:@""
962+
charactersIgnoringModifiers:@"h"
963+
isARepeat:NO
964+
keyCode:0x4];
965+
966+
// Plugin should mark the event as key equivalent.
967+
[plugin performKeyEquivalent:event];
968+
969+
// Simulate KeyboardManager sending unhandled event to plugin. This must return
970+
// true because it is a known editing command.
971+
if ([plugin handleKeyEvent:event] != true) {
972+
return false;
973+
}
974+
975+
// CMD+W
976+
event = [NSEvent keyEventWithType:NSEventTypeKeyDown
977+
location:NSZeroPoint
978+
modifierFlags:0x100108
979+
timestamp:0
980+
windowNumber:0
981+
context:nil
982+
characters:@"w"
983+
charactersIgnoringModifiers:@"w"
984+
isARepeat:NO
985+
keyCode:0x13];
986+
987+
// Plugin should mark the event as key equivalent.
988+
[plugin performKeyEquivalent:event];
989+
990+
// This is not a valid editing command, plugin must return false so that
991+
// KeyboardManager sends the event to next responder.
992+
if ([plugin handleKeyEvent:event] != false) {
993+
return false;
994+
}
995+
996+
return true;
997+
}
998+
923999
- (bool)testLocalTextAndSelectionUpdateAfterDelta {
9241000
id engineMock = OCMClassMock([FlutterEngine class]);
9251001
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
@@ -1040,6 +1116,10 @@ - (bool)testLocalTextAndSelectionUpdateAfterDelta {
10401116
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testPerformKeyEquivalent]);
10411117
}
10421118

1119+
TEST(FlutterTextInputPluginTest, UnhandledKeyEquivalent) {
1120+
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] unhandledKeyEquivalent]);
1121+
}
1122+
10431123
TEST(FlutterTextInputPluginTest, CanWorkWithFlutterTextField) {
10441124
FlutterEngine* engine = CreateTestEngine();
10451125
NSString* fixtures = @(testing::GetFixturesPath());

0 commit comments

Comments
 (0)