Flutter iOS Embedder
FlutterChannelKeyResponder.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
6 
10 
12 
13 namespace {
14 // An enumeration of the modifier values that the framework expects. These are
15 // largely the same values as the OS (UIKeyModifierShift, etc.), but because the
16 // framework code expects certain values, and has additional values (like the
17 // sided modifier values below), we translate the iOS values to the framework
18 // values, and add a mask for all the possible values.
19 typedef NS_OPTIONS(NSInteger, kKeyboardModifier) {
20  kKeyboardModifierAlphaShift = 0x10000,
21  kKeyboardModifierShift = 0x20000,
22  kKeyboardModifierLeftShift = 0x02,
23  kKeyboardModifierRightShift = 0x04,
24  kKeyboardModifierControl = 0x40000,
25  kKeyboardModifierLeftControl = 0x01,
26  kKeyboardModifierRightControl = 0x2000,
27  kKeyboardModifierOption = 0x80000,
28  kKeyboardModifierLeftOption = 0x20,
29  kKeyboardModifierRightOption = 0x40,
30  kKeyboardModifierCommand = 0x100000,
31  kKeyboardModifierLeftCommand = 0x08,
32  kKeyboardModifierRightCommand = 0x10,
33  kKeyboardModifierNumericPad = 0x200000,
34  kKeyboardModifierMask = kKeyboardModifierAlphaShift | //
35  kKeyboardModifierShift | //
36  kKeyboardModifierLeftShift | //
37  kKeyboardModifierRightShift | //
38  kKeyboardModifierControl | //
39  kKeyboardModifierLeftControl | //
40  kKeyboardModifierRightControl | //
41  kKeyboardModifierOption | //
42  kKeyboardModifierLeftOption | //
43  kKeyboardModifierRightOption | //
44  kKeyboardModifierCommand | //
45  kKeyboardModifierLeftCommand | //
46  kKeyboardModifierRightCommand | //
47  kKeyboardModifierNumericPad,
48 };
49 
50 /**
51  * Filters out some special cases in the characters field on UIKey.
52  */
53 static NSString* getEventCharacters(NSString* characters, UIKeyboardHIDUsage keyCode)
54  API_AVAILABLE(ios(13.4)) {
55  if (characters == nil) {
56  return nil;
57  }
58  if ([characters length] == 0) {
59  return nil;
60  }
61  if (@available(iOS 13.4, *)) {
62  // On iOS, function keys return the UTF8 string "\^P" (with a literal '/',
63  // '^' and a 'P', not escaped ctrl-P) as their "characters" field. This
64  // isn't a valid (single) UTF8 character. Looking at the only UTF16
65  // character for a function key yields a value of "16", which is a Unicode
66  // "SHIFT IN" character, which is just odd. UTF8 conversion of that string
67  // is what generates the three characters "\^P".
68  //
69  // Anyhow, we strip them all out and replace them with empty strings, since
70  // function keys shouldn't be printable.
71  if (functionKeyCodes.find(keyCode) != functionKeyCodes.end()) {
72  return nil;
73  }
74  }
75  return characters;
76 }
77 
78 } // namespace
80 
81 /**
82  * The channel used to communicate with Flutter.
83  */
84 @property(nonatomic) FlutterBasicMessageChannel* channel;
85 
86 - (NSInteger)adjustModifiers:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4));
87 - (void)updatePressedModifiers:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4));
88 
89 @property(nonatomic) kKeyboardModifier pressedModifiers;
90 @end
91 
92 @implementation FlutterChannelKeyResponder
93 
94 - (nonnull instancetype)initWithChannel:(nonnull FlutterBasicMessageChannel*)channel {
95  self = [super init];
96  if (self != nil) {
97  _channel = channel;
98  _pressedModifiers = 0;
99  }
100  return self;
101 }
102 
103 - (void)handlePress:(nonnull FlutterUIPressProxy*)press
104  callback:(nonnull FlutterAsyncKeyCallback)callback API_AVAILABLE(ios(13.4)) {
105  if (@available(iOS 13.4, *)) {
106  // no-op
107  } else {
108  return;
109  }
110  NSString* type;
111  switch (press.phase) {
112  case UIPressPhaseBegan:
113  type = @"keydown";
114  break;
115  case UIPressPhaseCancelled:
116  // This event doesn't appear to happen on iOS, at least when switching
117  // apps. Maybe on tvOS? In any case, it's probably best to send a keyup if
118  // we do receive one, since if the event was canceled, it's likely that a
119  // keyup will never be received otherwise.
120  case UIPressPhaseEnded:
121  type = @"keyup";
122  break;
123  case UIPressPhaseChanged:
124  // This only happens for analog devices like joysticks.
125  return;
126  case UIPressPhaseStationary:
127  // The entire volume of documentation of this phase on the Apple site, and
128  // indeed the Internet, is:
129  // "A button was pressed but hasn’t moved since the previous event."
130  // It's unclear what this is actually used for, and we've yet to see it in
131  // the wild.
132  return;
133  }
134 
135  NSString* characters = getEventCharacters(press.key.characters, press.key.keyCode);
136  NSString* charactersIgnoringModifiers =
137  getEventCharacters(press.key.charactersIgnoringModifiers, press.key.keyCode);
138  NSDictionary* keyMessage = @{
139  @"keymap" : @"ios",
140  @"type" : type,
141  @"keyCode" : @(press.key.keyCode),
142  @"modifiers" : @([self adjustModifiers:press]),
143  @"characters" : characters == nil ? @"" : characters,
144  @"charactersIgnoringModifiers" : charactersIgnoringModifiers == nil
145  ? @""
146  : charactersIgnoringModifiers,
147  };
148  [self.channel sendMessage:keyMessage
149  reply:^(id reply) {
150  bool handled = reply ? [[reply valueForKey:@"handled"] boolValue] : true;
151  callback(handled);
152  }];
153 }
154 
155 #pragma mark - Private
156 
157 - (void)updatePressedModifiers:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4)) {
158  if (@available(iOS 13.4, *)) {
159  // no-op
160  } else {
161  return;
162  }
163 
164  bool isKeyDown = false;
165  switch (press.phase) {
166  case UIPressPhaseStationary:
167  case UIPressPhaseChanged:
168  // These kinds of events shouldn't get this far.
169  NSAssert(false, @"Unexpected key event type received in updatePressedModifiers.");
170  return;
171  case UIPressPhaseBegan:
172  isKeyDown = true;
173  break;
174  case UIPressPhaseCancelled:
175  case UIPressPhaseEnded:
176  isKeyDown = false;
177  break;
178  }
179 
180  void (^update)(kKeyboardModifier, bool) = ^(kKeyboardModifier mod, bool isOn) {
181  if (isOn) {
182  _pressedModifiers |= mod;
183  } else {
184  _pressedModifiers &= ~mod;
185  }
186  };
187  switch (press.key.keyCode) {
188  case UIKeyboardHIDUsageKeyboardCapsLock:
189  update(kKeyboardModifierAlphaShift, isKeyDown);
190  break;
191  case UIKeyboardHIDUsageKeypadNumLock:
192  update(kKeyboardModifierNumericPad, isKeyDown);
193  break;
194  case UIKeyboardHIDUsageKeyboardLeftShift:
195  update(kKeyboardModifierLeftShift, isKeyDown);
196  break;
197  case UIKeyboardHIDUsageKeyboardRightShift:
198  update(kKeyboardModifierRightShift, isKeyDown);
199  break;
200  case UIKeyboardHIDUsageKeyboardLeftControl:
201  update(kKeyboardModifierLeftControl, isKeyDown);
202  break;
203  case UIKeyboardHIDUsageKeyboardRightControl:
204  update(kKeyboardModifierRightControl, isKeyDown);
205  break;
206  case UIKeyboardHIDUsageKeyboardLeftAlt:
207  update(kKeyboardModifierLeftOption, isKeyDown);
208  break;
209  case UIKeyboardHIDUsageKeyboardRightAlt:
210  update(kKeyboardModifierRightOption, isKeyDown);
211  break;
212  case UIKeyboardHIDUsageKeyboardLeftGUI:
213  update(kKeyboardModifierLeftCommand, isKeyDown);
214  break;
215  case UIKeyboardHIDUsageKeyboardRightGUI:
216  update(kKeyboardModifierRightCommand, isKeyDown);
217  break;
218  default:
219  // If we didn't update any of the modifiers above, we're done.
220  return;
221  }
222  // Update the non-sided modifier flags to match the content of the sided ones.
223  update(kKeyboardModifierShift,
224  _pressedModifiers & (kKeyboardModifierRightShift | kKeyboardModifierLeftShift));
225  update(kKeyboardModifierControl,
226  _pressedModifiers & (kKeyboardModifierRightControl | kKeyboardModifierLeftControl));
227  update(kKeyboardModifierOption,
228  _pressedModifiers & (kKeyboardModifierRightOption | kKeyboardModifierLeftOption));
229  update(kKeyboardModifierCommand,
230  _pressedModifiers & (kKeyboardModifierRightCommand | kKeyboardModifierLeftCommand));
231 }
232 
233 // Because iOS differs from macOS in that the modifier flags still contain the
234 // flag for the key that is being released on the keyup event, we adjust the
235 // modifiers when the key being released is the matching modifier key itself.
236 - (NSInteger)adjustModifiers:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4)) {
237  if (@available(iOS 13.4, *)) {
238  // no-op
239  } else {
240  return press.key.modifierFlags;
241  }
242 
243  [self updatePressedModifiers:press];
244  // Replace the supplied modifier flags with our computed ones.
245  return _pressedModifiers | (press.key.modifierFlags & ~kKeyboardModifierMask);
246 }
247 
248 @end
FlutterBasicMessageChannel
Definition: FlutterChannels.h:37
API_AVAILABLE
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
FLUTTER_ASSERT_ARC::isKeyDown
static bool isKeyDown(FlutterUIPressProxy *press) API_AVAILABLE(ios(13.4))
Definition: FlutterEmbedderKeyResponder.mm:221
FLUTTER_ASSERT_ARC::getEventCharacters
static NSString * getEventCharacters(NSString *characters, UIKeyboardHIDUsage keyCode) API_AVAILABLE(ios(13.4))
Definition: FlutterChannelKeyResponder.mm:53
FlutterChannelKeyResponder.h
FlutterMacros.h
functionKeyCodes
const std::set< uint32_t > functionKeyCodes
Definition: KeyCodeMap.g.mm:304
FLUTTER_ASSERT_ARC::NS_OPTIONS
typedef NS_OPTIONS(NSInteger, kKeyboardModifier)
Definition: FlutterChannelKeyResponder.mm:19
FlutterAsyncKeyCallback
void(^ FlutterAsyncKeyCallback)(BOOL handled)
Definition: FlutterKeyPrimaryResponder.h:10
FlutterChannelKeyResponder
Definition: FlutterChannelKeyResponder.h:20
FlutterUIPressProxy
Definition: FlutterUIPressProxy.h:17
FlutterUIPressProxy.h
KeyCodeMap_Internal.h
FLUTTER_ASSERT_ARC
Definition: FlutterChannelKeyResponder.mm:13