Flutter Windows Embedder
keyboard_manager.cc
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 
5 #include <memory>
6 #include <string>
7 
8 #include "flutter/fml/logging.h"
11 
12 namespace flutter {
13 
14 namespace {
15 
16 // The maximum number of pending events to keep before
17 // emitting a warning on the console about unhandled events.
18 static constexpr int kMaxPendingEvents = 1000;
19 
20 // Returns true if this key is an AltRight key down event.
21 //
22 // This is used to resolve an issue where an AltGr press causes CtrlLeft to hang
23 // when pressed, as reported in https://github.com/flutter/flutter/issues/78005.
24 //
25 // When AltGr is pressed (in a supporting layout such as Spanish), Win32 first
26 // fires a fake CtrlLeft down event, then an AltRight down event.
27 // This is significant because this fake CtrlLeft down event will not be paired
28 // with a up event, which is fine until Flutter redispatches the CtrlDown
29 // event, which Win32 then interprets as a real event, leaving both Win32 and
30 // the Flutter framework thinking that CtrlLeft is still pressed.
31 //
32 // To resolve this, Flutter recognizes this fake CtrlLeft down event using the
33 // following AltRight down event. Flutter then forges a CtrlLeft key up event
34 // immediately after the corresponding AltRight key up event.
35 //
36 // One catch is that it is impossible to distinguish the fake CtrlLeft down
37 // from a normal CtrlLeft down (followed by a AltRight down), since they
38 // contain the exactly same information, including the GetKeyState result.
39 // Fortunately, this will require the two events to occur *really* close, which
40 // would be rare, and a misrecognition would only cause a minor consequence
41 // where the CtrlLeft is released early; the later, real, CtrlLeft up event will
42 // be ignored.
43 static bool IsKeyDownAltRight(int action, int virtual_key, bool extended) {
44  return virtual_key == VK_RMENU && extended &&
45  (action == WM_KEYDOWN || action == WM_SYSKEYDOWN);
46 }
47 
48 // Returns true if this key is a key up event of AltRight.
49 //
50 // This is used to assist a corner case described in |IsKeyDownAltRight|.
51 static bool IsKeyUpAltRight(int action, int virtual_key, bool extended) {
52  return virtual_key == VK_RMENU && extended &&
53  (action == WM_KEYUP || action == WM_SYSKEYUP);
54 }
55 
56 // Returns true if this key is a key down event of CtrlLeft.
57 //
58 // This is used to assist a corner case described in |IsKeyDownAltRight|.
59 static bool IsKeyDownCtrlLeft(int action, int virtual_key) {
60  return virtual_key == VK_LCONTROL &&
61  (action == WM_KEYDOWN || action == WM_SYSKEYDOWN);
62 }
63 
64 // Returns if a character sent by Win32 is a dead key.
65 static bool IsDeadKey(uint32_t ch) {
66  return (ch & kDeadKeyCharMask) != 0;
67 }
68 
69 static char32_t CodePointFromSurrogatePair(wchar_t high, wchar_t low) {
70  return 0x10000 + ((static_cast<char32_t>(high) & 0x000003FF) << 10) +
71  (low & 0x3FF);
72 }
73 
74 static uint16_t ResolveKeyCode(uint16_t original,
75  bool extended,
76  uint8_t scancode) {
77  switch (original) {
78  case VK_SHIFT:
79  case VK_LSHIFT:
80  return MapVirtualKey(scancode, MAPVK_VSC_TO_VK_EX);
81  case VK_MENU:
82  case VK_LMENU:
83  return extended ? VK_RMENU : VK_LMENU;
84  case VK_CONTROL:
85  case VK_LCONTROL:
86  return extended ? VK_RCONTROL : VK_LCONTROL;
87  default:
88  return original;
89  }
90 }
91 
92 static bool IsPrintable(uint32_t c) {
93  constexpr char32_t kMinPrintable = ' ';
94  constexpr char32_t kDelete = 0x7F;
95  return c >= kMinPrintable && c != kDelete;
96 }
97 
98 static bool IsSysAction(UINT action) {
99  return action == WM_SYSKEYDOWN || action == WM_SYSKEYUP ||
100  action == WM_SYSCHAR || action == WM_SYSDEADCHAR;
101 }
102 
103 } // namespace
104 
106  : window_delegate_(delegate),
107  last_key_is_ctrl_left_down(false),
108  should_synthesize_ctrl_left_up(false),
109  processing_event_(false) {}
110 
111 void KeyboardManager::RedispatchEvent(std::unique_ptr<PendingEvent> event) {
112  for (const Win32Message& message : event->session) {
113  // Never redispatch sys keys, because their original messages have been
114  // passed to the system default processor.
115  if (IsSysAction(message.action)) {
116  continue;
117  }
118  pending_redispatches_.push_back(message);
119  UINT result = window_delegate_->Win32DispatchMessage(
121  if (result != 0) {
122  FML_LOG(ERROR) << "Unable to synthesize event for keyboard event.";
123  }
124  }
125  if (pending_redispatches_.size() > kMaxPendingEvents) {
126  FML_LOG(ERROR)
127  << "There are " << pending_redispatches_.size()
128  << " keyboard events that have not yet received a response from the "
129  << "framework. Are responses being sent?";
130  }
131 }
132 
133 bool KeyboardManager::RemoveRedispatchedMessage(UINT const action,
134  WPARAM const wparam,
135  LPARAM const lparam) {
136  for (auto iter = pending_redispatches_.begin();
137  iter != pending_redispatches_.end(); ++iter) {
138  if (action == iter->action && wparam == iter->wparam) {
139  pending_redispatches_.erase(iter);
140  return true;
141  }
142  }
143  return false;
144 }
145 
147  WPARAM const wparam,
148  LPARAM const lparam) {
149  if (RemoveRedispatchedMessage(action, wparam, lparam)) {
150  return false;
151  }
152  switch (action) {
153  case WM_DEADCHAR:
154  case WM_SYSDEADCHAR:
155  case WM_CHAR:
156  case WM_SYSCHAR: {
157  const Win32Message message =
158  Win32Message{.action = action, .wparam = wparam, .lparam = lparam};
159  current_session_.push_back(message);
160 
161  char32_t code_point;
162  if (message.IsHighSurrogate()) {
163  // A high surrogate is always followed by a low surrogate. Process the
164  // session later and consider this message as handled.
165  return true;
166  } else if (message.IsLowSurrogate()) {
167  const Win32Message* last_message =
168  current_session_.size() <= 1
169  ? nullptr
170  : &current_session_[current_session_.size() - 2];
171  if (last_message == nullptr || !last_message->IsHighSurrogate()) {
172  return false;
173  }
174  // A low surrogate always follows a high surrogate, marking the end of
175  // a char session. Process the session after the if clause.
176  code_point =
177  CodePointFromSurrogatePair(last_message->wparam, message.wparam);
178  } else {
179  // A non-surrogate character always appears alone. Process the session
180  // after the if clause.
181  code_point = static_cast<wchar_t>(message.wparam);
182  }
183 
184  // If this char message is preceded by a key down message, then dispatch
185  // the key down message as a key down event first, and only dispatch the
186  // OnText if the key down event is not handled.
187  if (current_session_.front().IsGeneralKeyDown()) {
188  const Win32Message first_message = current_session_.front();
189  const uint8_t scancode = (lparam >> 16) & 0xff;
190  const uint16_t key_code = first_message.wparam;
191  const bool extended = ((lparam >> 24) & 0x01) == 0x01;
192  const bool was_down = lparam & 0x40000000;
193  // Certain key combinations yield control characters as WM_CHAR's
194  // lParam. For example, 0x01 for Ctrl-A. Filter these characters. See
195  // https://docs.microsoft.com/en-us/windows/win32/learnwin32/accelerator-tables
196  char32_t character;
197  if (action == WM_DEADCHAR || action == WM_SYSDEADCHAR) {
198  // Mask the resulting char with kDeadKeyCharMask anyway, because in
199  // rare cases the bit is *not* set (US INTL Shift-6 circumflex, see
200  // https://github.com/flutter/flutter/issues/92654 .)
201  character =
202  window_delegate_->Win32MapVkToChar(key_code) | kDeadKeyCharMask;
203  } else {
204  character = IsPrintable(code_point) ? code_point : 0;
205  }
206  auto event = std::make_unique<PendingEvent>(PendingEvent{
207  .key = key_code,
208  .scancode = scancode,
209  .action = static_cast<UINT>(action == WM_SYSCHAR ? WM_SYSKEYDOWN
210  : WM_KEYDOWN),
211  .character = character,
212  .extended = extended,
213  .was_down = was_down,
214  .session = std::move(current_session_),
215  });
216 
217  pending_events_.push_back(std::move(event));
218  ProcessNextEvent();
219 
220  // SYS messages must not be consumed by `HandleMessage` so that they are
221  // forwarded to the system.
222  return !IsSysAction(action);
223  }
224 
225  // If the charcter session is not preceded by a key down message,
226  // mark PendingEvent::action as WM_CHAR, informing |PerformProcessEvent|
227  // to dispatch the text content immediately.
228  //
229  // Only WM_CHAR should be treated as characters. WM_SYS*CHAR are not part
230  // of text input, and WM_DEADCHAR will be incorporated into a later
231  // WM_CHAR with the full character.
232  if (action == WM_CHAR) {
233  auto event = std::make_unique<PendingEvent>(PendingEvent{
234  .action = WM_CHAR,
235  .character = code_point,
236  .session = std::move(current_session_),
237  });
238  pending_events_.push_back(std::move(event));
239  ProcessNextEvent();
240  }
241  return true;
242  }
243 
244  case WM_KEYDOWN:
245  case WM_SYSKEYDOWN:
246  case WM_KEYUP:
247  case WM_SYSKEYUP: {
248  if (wparam == VK_PACKET) {
249  return false;
250  }
251 
252  const uint8_t scancode = (lparam >> 16) & 0xff;
253  const bool extended = ((lparam >> 24) & 0x01) == 0x01;
254  // If the key is a modifier, get its side.
255  const uint16_t key_code = ResolveKeyCode(wparam, extended, scancode);
256  const bool was_down = lparam & 0x40000000;
257 
258  // Detect a pattern of key events in order to forge a CtrlLeft up event.
259  // See |IsKeyDownAltRight| for explanation.
260  if (IsKeyDownAltRight(action, key_code, extended)) {
261  if (last_key_is_ctrl_left_down) {
262  should_synthesize_ctrl_left_up = true;
263  }
264  }
265  if (IsKeyDownCtrlLeft(action, key_code)) {
266  last_key_is_ctrl_left_down = true;
267  ctrl_left_scancode = scancode;
268  should_synthesize_ctrl_left_up = false;
269  } else {
270  last_key_is_ctrl_left_down = false;
271  }
272  if (IsKeyUpAltRight(action, key_code, extended)) {
273  if (should_synthesize_ctrl_left_up) {
274  should_synthesize_ctrl_left_up = false;
275  const LPARAM lParam =
276  (1 /* repeat_count */ << 0) | (ctrl_left_scancode << 16) |
277  (0 /* extended */ << 24) | (1 /* prev_state */ << 30) |
278  (1 /* transition */ << 31);
279  window_delegate_->Win32DispatchMessage(WM_KEYUP, VK_CONTROL, lParam);
280  }
281  }
282 
283  current_session_.clear();
284  current_session_.push_back(
285  Win32Message{.action = action, .wparam = wparam, .lparam = lparam});
286  const bool is_keydown_message =
287  (action == WM_KEYDOWN || action == WM_SYSKEYDOWN);
288  // Check if this key produces a character by peeking if this key down
289  // message has a following char message. Certain key messages are not
290  // followed by char messages even though `MapVirtualKey` returns a valid
291  // character (such as Ctrl + Digit, see
292  // https://github.com/flutter/flutter/issues/85587 ).
293  unsigned int character = window_delegate_->Win32MapVkToChar(wparam);
294  UINT next_key_action = PeekNextMessageType(WM_KEYFIRST, WM_KEYLAST);
295  bool has_char_action =
296  (next_key_action == WM_DEADCHAR ||
297  next_key_action == WM_SYSDEADCHAR || next_key_action == WM_CHAR ||
298  next_key_action == WM_SYSCHAR);
299  if (character > 0 && is_keydown_message && has_char_action) {
300  // This key down message has a following char message. Process this
301  // session in the char message, because the character for the key call
302  // should be decided by the char events. Consider this message as
303  // handled.
304  return true;
305  }
306 
307  // This key down message is not followed by a char message. Conclude this
308  // session.
309  auto event = std::make_unique<PendingEvent>(PendingEvent{
310  .key = key_code,
311  .scancode = scancode,
312  .action = action,
313  .character = 0,
314  .extended = extended,
315  .was_down = was_down,
316  .session = std::move(current_session_),
317  });
318  pending_events_.push_back(std::move(event));
319  ProcessNextEvent();
320  // SYS messages must not be consumed by `HandleMessage` so that they are
321  // forwarded to the system.
322  return !IsSysAction(action);
323  }
324  default:
325  FML_LOG(FATAL) << "No event handler for keyboard event with action "
326  << action;
327  }
328  return false;
329 }
330 
331 void KeyboardManager::ProcessNextEvent() {
332  if (processing_event_ || pending_events_.empty()) {
333  return;
334  }
335  processing_event_ = true;
336  auto pending_event = std::move(pending_events_.front());
337  pending_events_.pop_front();
338  PerformProcessEvent(std::move(pending_event), [this] {
339  FML_DCHECK(processing_event_);
340  processing_event_ = false;
341  ProcessNextEvent();
342  });
343 }
344 
345 void KeyboardManager::PerformProcessEvent(std::unique_ptr<PendingEvent> event,
346  std::function<void()> callback) {
347  // PendingEvent::action being WM_CHAR means this is a char message without
348  // a preceding key message, and should be dispatched immediately.
349  if (event->action == WM_CHAR) {
350  DispatchText(*event);
351  callback();
352  return;
353  }
354 
355  // A unique_ptr can't be sent into a lambda without C++23's
356  // move_only_function. Until then, `event` is sent as a raw pointer, hoping
357  // WindowDelegate::OnKey to correctly call it once and only once.
358  PendingEvent* event_p = event.release();
359  window_delegate_->OnKey(
360  event_p->key, event_p->scancode, event_p->action, event_p->character,
361  event_p->extended, event_p->was_down,
362  [this, event_p, callback = std::move(callback)](bool handled) {
363  HandleOnKeyResult(std::unique_ptr<PendingEvent>(event_p), handled);
364  callback();
365  });
366 }
367 
368 void KeyboardManager::HandleOnKeyResult(std::unique_ptr<PendingEvent> event,
369  bool framework_handled) {
370  const UINT last_action = event->session.back().action;
371  // SYS messages must not be redispached, and their text content is not
372  // dispatched either.
373  bool handled = framework_handled || IsSysAction(last_action);
374 
375  if (handled) {
376  return;
377  }
378 
379  // Only WM_CHAR should be treated as characters. WM_SYS*CHAR are not part of
380  // text input, and WM_DEADCHAR will be incorporated into a later WM_CHAR with
381  // the full character.
382  if (last_action == WM_CHAR) {
383  DispatchText(*event);
384  }
385 
386  RedispatchEvent(std::move(event));
387 }
388 
389 void KeyboardManager::DispatchText(const PendingEvent& event) {
390  // Check if the character is printable based on the last wparam, which works
391  // even if the last wparam is a low surrogate, because the only unprintable
392  // keys defined by `IsPrintable` are certain characters at lower ASCII range.
393  // These ASCII control characters are sent as WM_CHAR events for all control
394  // key shortcuts.
395  FML_DCHECK(!event.session.empty());
396  bool is_printable = IsPrintable(event.session.back().wparam);
397  bool valid = event.character != 0 && is_printable;
398  if (valid) {
399  auto text = EncodeUtf16(event.character);
400  window_delegate_->OnText(text);
401  }
402 }
403 
404 UINT KeyboardManager::PeekNextMessageType(UINT wMsgFilterMin,
405  UINT wMsgFilterMax) {
406  MSG next_message;
407  BOOL has_msg = window_delegate_->Win32PeekMessage(
408  &next_message, wMsgFilterMin, wMsgFilterMax, PM_NOREMOVE);
409  if (!has_msg) {
410  return 0;
411  }
412  return next_message.message;
413 }
414 
415 } // namespace flutter
flutter::KeyboardManager::KeyboardManager
KeyboardManager(WindowDelegate *delegate)
Definition: keyboard_manager.cc:105
scancode
int scancode
Definition: keyboard_key_handler_unittests.cc:115
was_down
bool was_down
Definition: keyboard_key_handler_unittests.cc:119
extended
bool extended
Definition: keyboard_key_handler_unittests.cc:118
flutter::KeyboardManager::WindowDelegate::Win32MapVkToChar
virtual uint32_t Win32MapVkToChar(uint32_t virtual_key)=0
flutter::KeyboardManager::WindowDelegate::OnText
virtual void OnText(const std::u16string &text)=0
character
char32_t character
Definition: keyboard_key_handler_unittests.cc:117
flutter::KeyboardManager::RedispatchEvent
virtual void RedispatchEvent(std::unique_ptr< PendingEvent > event)
Definition: keyboard_manager.cc:111
flutter::EncodeUtf16
std::u16string EncodeUtf16(char32_t character)
Definition: keyboard_utils.cc:11
flutter::KeyboardManager::WindowDelegate
Definition: keyboard_manager.h:52
flutter::KeyboardManager::HandleMessage
bool HandleMessage(UINT const message, WPARAM const wparam, LPARAM const lparam)
Definition: keyboard_manager.cc:146
flutter::KeyboardManager::Win32Message::lparam
LPARAM lparam
Definition: keyboard_manager.h:117
text
std::u16string text
Definition: keyboard_unittests.cc:332
keyboard_utils.h
flutter::KeyboardManager::WindowDelegate::OnKey
virtual void OnKey(int key, int scancode, int action, char32_t character, bool extended, bool was_down, KeyEventCallback callback)=0
flutter::KeyboardManager::PendingEvent::key
WPARAM key
Definition: keyboard_manager.h:129
flutter
Definition: accessibility_bridge_windows.cc:11
flutter::KeyboardManager::Win32Message::IsHighSurrogate
bool IsHighSurrogate() const
Definition: keyboard_manager.h:119
flutter::KeyboardManager::WindowDelegate::Win32PeekMessage
virtual BOOL Win32PeekMessage(LPMSG lpMsg, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg)=0
flutter::KeyboardManager::PendingEvent
Definition: keyboard_manager.h:128
flutter::KeyboardManager::Win32Message::wparam
WPARAM wparam
Definition: keyboard_manager.h:116
flutter::KeyboardManager::Win32Message
Definition: keyboard_manager.h:114
message
Win32Message message
Definition: keyboard_unittests.cc:137
flutter::kDeadKeyCharMask
constexpr int kDeadKeyCharMask
Definition: keyboard_utils.h:30
action
int action
Definition: keyboard_key_handler_unittests.cc:116
keyboard_manager.h
flutter::KeyboardManager::PendingEvent::action
UINT action
Definition: keyboard_manager.h:131
flutter::KeyboardManager::Win32Message::action
UINT action
Definition: keyboard_manager.h:115
flutter::KeyboardManager::Win32Message::IsLowSurrogate
bool IsLowSurrogate() const
Definition: keyboard_manager.h:121
callback
FlutterDesktopBinaryReply callback
Definition: flutter_windows_view_unittests.cc:48
flutter::KeyboardManager::WindowDelegate::Win32DispatchMessage
virtual UINT Win32DispatchMessage(UINT Msg, WPARAM wParam, LPARAM lParam)=0