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