blob: ab14f120483c0e94673ecc9567f3e584bf3384c0 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <assert.h>
#include <iostream>
#include <memory>
#include <string>
#include "keyboard_manager_win32.h"
#include "keyboard_win32_common.h"
namespace flutter {
namespace {
// The maximum number of pending events to keep before
// emitting a warning on the console about unhandled events.
static constexpr int kMaxPendingEvents = 1000;
// Returns true if this key is an AltRight key down event.
//
// This is used to resolve an issue where an AltGr press causes CtrlLeft to hang
// when pressed, as reported in https://github.com/flutter/flutter/issues/78005.
//
// When AltGr is pressed (in a supporting layout such as Spanish), Win32 first
// fires a fake CtrlLeft down event, then an AltRight down event.
// This is significant because this fake CtrlLeft down event will not be paired
// with a up event, which is fine until Flutter redispatches the CtrlDown
// event, which Win32 then interprets as a real event, leaving both Win32 and
// the Flutter framework thinking that CtrlLeft is still pressed.
//
// To resolve this, Flutter recognizes this fake CtrlLeft down event using the
// following AltRight down event. Flutter then synthesizes a CtrlLeft key up
// event immediately after the corresponding AltRight key up event.
//
// One catch is that it is impossible to distinguish the fake CtrlLeft down
// from a normal CtrlLeft down (followed by a AltRight down), since they
// contain the exactly same information, including the GetKeyState result.
// Fortunately, this will require the two events to occur *really* close, which
// would be rare, and a misrecognition would only cause a minor consequence
// where the CtrlLeft is released early; the later, real, CtrlLeft up event will
// be ignored.
static bool IsKeyDownAltRight(int action, int virtual_key, bool extended) {
#ifdef WINUWP
return false;
#else
return virtual_key == VK_RMENU && extended &&
(action == WM_KEYDOWN || action == WM_SYSKEYDOWN);
#endif
}
// Returns true if this key is a key up event of AltRight.
//
// This is used to assist a corner case described in |IsKeyDownAltRight|.
static bool IsKeyUpAltRight(int action, int virtual_key, bool extended) {
#ifdef WINUWP
return false;
#else
return virtual_key == VK_RMENU && extended &&
(action == WM_KEYUP || action == WM_SYSKEYUP);
#endif
}
// Returns true if this key is a key down event of CtrlLeft.
//
// This is used to assist a corner case described in |IsKeyDownAltRight|.
static bool IsKeyDownCtrlLeft(int action, int virtual_key) {
#ifdef WINUWP
return false;
#else
return virtual_key == VK_LCONTROL &&
(action == WM_KEYDOWN || action == WM_SYSKEYDOWN);
#endif
}
// Returns true if this key is a key down event of ShiftRight.
//
// This is a temporary solution to
// https://github.com/flutter/flutter/issues/81674, and forces ShiftRight
// KeyDown events to not be redispatched regardless of the framework's response.
//
// If a ShiftRight KeyDown event is not handled by the framework and is
// redispatched, Win32 will not send its following KeyUp event and keeps
// recording ShiftRight as being pressed.
static bool IsKeyDownShiftRight(int virtual_key, bool was_down) {
#ifdef WINUWP
return false;
#else
return virtual_key == VK_RSHIFT && !was_down;
#endif
}
// Returns if a character sent by Win32 is a dead key.
static bool IsDeadKey(uint32_t ch) {
return (ch & kDeadKeyCharMask) != 0;
}
static char32_t CodePointFromSurrogatePair(wchar_t high, wchar_t low) {
return 0x10000 + ((static_cast<char32_t>(high) & 0x000003FF) << 10) +
(low & 0x3FF);
}
static uint16_t ResolveKeyCode(uint16_t original,
bool extended,
uint8_t scancode) {
switch (original) {
case VK_SHIFT:
case VK_LSHIFT:
return MapVirtualKey(scancode, MAPVK_VSC_TO_VK_EX);
case VK_MENU:
case VK_LMENU:
return extended ? VK_RMENU : VK_LMENU;
case VK_CONTROL:
case VK_LCONTROL:
return extended ? VK_RCONTROL : VK_LCONTROL;
default:
return original;
}
}
static bool IsPrintable(uint32_t c) {
constexpr char32_t kMinPrintable = ' ';
constexpr char32_t kDelete = 0x7F;
return c >= kMinPrintable && c != kDelete;
}
} // namespace
KeyboardManagerWin32::KeyboardManagerWin32(WindowDelegate* delegate)
: window_delegate_(delegate),
last_key_is_ctrl_left_down(false),
should_synthesize_ctrl_left_up(false) {}
void KeyboardManagerWin32::RedispatchEvent(
std::unique_ptr<PendingEvent> event) {
for (const Win32Message& message : event->session) {
pending_redispatches_.push_back(message);
UINT result = window_delegate_->Win32DispatchMessage(
message.action, message.wparam, message.lparam);
if (result != 0) {
std::cerr << "Unable to synthesize event for keyboard event."
<< std::endl;
}
}
if (pending_redispatches_.size() > kMaxPendingEvents) {
std::cerr
<< "There are " << pending_redispatches_.size()
<< " keyboard events that have not yet received a response from the "
<< "framework. Are responses being sent?" << std::endl;
}
}
bool KeyboardManagerWin32::RemoveRedispatchedMessage(UINT const action,
WPARAM const wparam,
LPARAM const lparam) {
for (auto iter = pending_redispatches_.begin();
iter != pending_redispatches_.end(); ++iter) {
if (action == iter->action && wparam == iter->wparam) {
pending_redispatches_.erase(iter);
return true;
}
}
return false;
}
void KeyboardManagerWin32::OnKey(std::unique_ptr<PendingEvent> event,
OnKeyCallback callback) {
if (IsKeyDownAltRight(event->action, event->key, event->extended)) {
if (last_key_is_ctrl_left_down) {
should_synthesize_ctrl_left_up = true;
}
}
if (IsKeyDownCtrlLeft(event->action, event->key)) {
last_key_is_ctrl_left_down = true;
ctrl_left_scancode = event->scancode;
should_synthesize_ctrl_left_up = false;
} else {
last_key_is_ctrl_left_down = false;
}
if (IsKeyUpAltRight(event->action, event->key, event->extended)) {
if (should_synthesize_ctrl_left_up) {
should_synthesize_ctrl_left_up = false;
const LPARAM lParam =
(1 /* repeat_count */ << 0) | (ctrl_left_scancode << 16) |
(0 /* extended */ << 24) | (1 /* prev_state */ << 30) |
(1 /* transition */ << 31);
window_delegate_->Win32DispatchMessage(WM_KEYUP, VK_LCONTROL, lParam);
}
}
const PendingEvent clone = *event;
window_delegate_->OnKey(clone.key, clone.scancode, clone.action,
clone.character, clone.extended, clone.was_down,
[this, event = event.release(),
callback = std::move(callback)](bool handled) {
callback(std::unique_ptr<PendingEvent>(event),
handled);
});
}
void KeyboardManagerWin32::DispatchReadyTexts() {
auto front = pending_texts_.begin();
for (; front != pending_texts_.end() && front->ready; ++front) {
window_delegate_->OnText(front->content);
}
pending_texts_.erase(pending_texts_.begin(), front);
}
void KeyboardManagerWin32::HandleOnKeyResult(
std::unique_ptr<PendingEvent> event,
bool handled,
std::list<PendingText>::iterator pending_text) {
// First, patch |handled|, because some key events must always be treated as
// handled.
//
// Redispatching dead keys events makes Win32 ignore the dead key state
// and redispatches a normal character without combining it with the
// next letter key.
//
// Redispatching sys events is impossible due to the limitation of
// |SendInput|.
const bool is_syskey =
event->action == WM_SYSKEYDOWN || event->action == WM_SYSKEYUP;
const bool real_handled = handled || IsDeadKey(event->character) ||
is_syskey ||
IsKeyDownShiftRight(event->key, event->was_down);
// For handled events, that's all.
if (real_handled) {
if (pending_text != pending_texts_.end()) {
pending_texts_.erase(pending_text);
}
return;
}
// For unhandled events, dispatch them to OnText.
if (pending_text != pending_texts_.end()) {
pending_text->ready = true;
DispatchReadyTexts();
}
RedispatchEvent(std::move(event));
}
bool KeyboardManagerWin32::HandleMessage(UINT const action,
WPARAM const wparam,
LPARAM const lparam) {
if (RemoveRedispatchedMessage(action, wparam, lparam)) {
return false;
}
switch (action) {
case WM_DEADCHAR:
case WM_SYSDEADCHAR:
case WM_CHAR:
case WM_SYSCHAR: {
const Win32Message message =
Win32Message{.action = action, .wparam = wparam, .lparam = lparam};
current_session_.push_back(message);
std::u16string text;
char32_t code_point;
if (message.IsHighSurrogate()) {
// A high surrogate is always followed by a low surrogate. Process the
// session later and consider this message as handled.
return true;
} else if (message.IsLowSurrogate()) {
const Win32Message* last_message =
current_session_.size() <= 1
? nullptr
: &current_session_[current_session_.size() - 2];
if (last_message == nullptr || !last_message->IsHighSurrogate()) {
return false;
}
// A low surrogate always follows a high surrogate, marking the end of
// a char session. Process the session after the if clause.
text.push_back(static_cast<wchar_t>(last_message->wparam));
text.push_back(static_cast<wchar_t>(message.wparam));
code_point =
CodePointFromSurrogatePair(last_message->wparam, message.wparam);
} else {
// A non-surrogate character always appears alone. Process the session
// after the if clause.
text.push_back(static_cast<wchar_t>(message.wparam));
code_point = static_cast<wchar_t>(message.wparam);
}
// If this char message is preceded by a key down message, then dispatch
// the key down message as a key down event first, and only dispatch the
// OnText if the key down event is not handled.
if (current_session_.front().IsGeneralKeyDown()) {
const Win32Message first_message = current_session_.front();
const uint8_t scancode = (lparam >> 16) & 0xff;
const uint16_t key_code = first_message.wparam;
const bool extended = ((lparam >> 24) & 0x01) == 0x01;
const bool was_down = lparam & 0x40000000;
// Certain key combinations yield control characters as WM_CHAR's
// lParam. For example, 0x01 for Ctrl-A. Filter these characters. See
// https://docs.microsoft.com/en-us/windows/win32/learnwin32/accelerator-tables
char32_t character;
if (action == WM_DEADCHAR || action == WM_SYSDEADCHAR) {
// Mask the resulting char with kDeadKeyCharMask anyway, because in
// rare cases the bit is *not* set (US INTL Shift-6 circumflex, see
// https://github.com/flutter/flutter/issues/92654 .)
character =
window_delegate_->Win32MapVkToChar(key_code) | kDeadKeyCharMask;
} else {
character = IsPrintable(code_point) ? code_point : 0;
}
auto event = std::make_unique<PendingEvent>(PendingEvent{
.key = key_code,
.scancode = scancode,
.action = static_cast<UINT>(action == WM_SYSCHAR ? WM_SYSKEYDOWN
: WM_KEYDOWN),
.character = character,
.extended = extended,
.was_down = was_down,
.session = std::move(current_session_),
});
// Compute the text that might be dispatched later.
//
// Of the messages handled here, only WM_CHAR should be treated as
// characters. WM_SYS*CHAR are not part of text input, and WM_DEADCHAR
// will be incorporated into a later WM_CHAR with the full character.
//
// Checking `character` filters out non-printable event characters.
std::list<PendingText>::iterator pending_text;
if (action == WM_CHAR && character != 0) {
pending_texts_.push_back(PendingText{
.ready = false,
.content = text,
});
pending_text = std::prev(pending_texts_.end());
} else {
pending_text = pending_texts_.end();
}
// SYS messages must not be handled by `HandleMessage` or be
// redispatched.
const bool is_syskey = action == WM_SYSCHAR || action == WM_SYSDEADCHAR;
OnKey(std::move(event),
[this, pending_text, is_syskey](
std::unique_ptr<PendingEvent> event, bool handled) {
bool real_handled = handled || is_syskey;
HandleOnKeyResult(std::move(event), handled, pending_text);
});
return !is_syskey;
}
// If the charcter session is not preceded by a key down message, dispatch
// the OnText immediately.
// Only WM_CHAR should be treated as characters. WM_SYS*CHAR are not part
// of text input, and WM_DEADCHAR will be incorporated into a later
// WM_CHAR with the full character.
//
// Also filter out ASCII control characters, which are sent as WM_CHAR
// events for all control key shortcuts.
current_session_.clear();
if (action == WM_CHAR && IsPrintable(wparam)) {
pending_texts_.push_back(PendingText{
.ready = true,
.content = text,
});
DispatchReadyTexts();
}
return true;
}
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP: {
current_session_.clear();
current_session_.push_back(
Win32Message{.action = action, .wparam = wparam, .lparam = lparam});
const bool is_keydown_message =
(action == WM_KEYDOWN || action == WM_SYSKEYDOWN);
// Check if this key produces a character. If so, the key press should
// be sent with the character produced at WM_CHAR. Store the produced
// keycode (it's not accessible from WM_CHAR) to be used in WM_CHAR.
//
// Messages with Control or Win modifiers down are never considered as
// character messages. This allows key combinations such as "CTRL + Digit"
// to properly produce key down events even though `MapVirtualKey` returns
// a valid character. See https://github.com/flutter/flutter/issues/85587.
unsigned int character = window_delegate_->Win32MapVkToChar(wparam);
UINT next_key_message = PeekNextMessageType(WM_KEYFIRST, WM_KEYLAST);
bool has_wm_char =
(next_key_message == WM_DEADCHAR ||
next_key_message == WM_SYSDEADCHAR || next_key_message == WM_CHAR ||
next_key_message == WM_SYSCHAR);
if (character > 0 && is_keydown_message && has_wm_char) {
// This key down message has following char events. Process later,
// because the character for the OnKey should be decided by the char
// events. Consider this event as handled.
return true;
}
// Resolve session: A non-char key event.
const uint8_t scancode = (lparam >> 16) & 0xff;
const bool extended = ((lparam >> 24) & 0x01) == 0x01;
// If the key is a modifier, get its side.
const uint16_t key_code = ResolveKeyCode(wparam, extended, scancode);
const bool was_down = lparam & 0x40000000;
auto event = std::make_unique<PendingEvent>(PendingEvent{
.key = key_code,
.scancode = scancode,
.action = action,
.character = 0,
.extended = extended,
.was_down = was_down,
.session = std::move(current_session_),
});
// SYS messages must not be handled by `HandleMessage` or be
// redispatched.
const bool is_syskey = action == WM_SYSKEYDOWN || action == WM_SYSKEYUP;
OnKey(
std::move(event),
[this, is_syskey](std::unique_ptr<PendingEvent> event, bool handled) {
bool real_handled = handled || is_syskey;
HandleOnKeyResult(std::move(event), handled, pending_texts_.end());
});
return !is_syskey;
}
default:
assert(false);
}
return false;
}
UINT KeyboardManagerWin32::PeekNextMessageType(UINT wMsgFilterMin,
UINT wMsgFilterMax) {
MSG next_message;
BOOL has_msg = window_delegate_->Win32PeekMessage(
&next_message, wMsgFilterMin, wMsgFilterMax, PM_NOREMOVE);
if (!has_msg) {
return 0;
}
return next_message.message;
}
uint64_t KeyboardManagerWin32::ComputeEventHash(const PendingEvent& event) {
// Calculate a key event ID based on the scan code of the key pressed,
// and the flags we care about.
return event.scancode | (((event.action == WM_KEYUP ? KEYEVENTF_KEYUP : 0x0) |
(event.extended ? KEYEVENTF_EXTENDEDKEY : 0x0))
<< 16);
}
} // namespace flutter