blob: 042e295679cbb77dc3e87d8cd55bb39bd9c63c45 [file] [log] [blame]
// Copyright 2018 The Chromium 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 "ui/events/win/keyboard_hook_win.h"
#include <utility>
#include "base/logging.h"
#include "base/macros.h"
#include "base/optional.h"
#include "base/threading/thread_checker.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
#include "ui/events/win/events_win_utils.h"
#include "ui/gfx/native_widget_types.h"
namespace ui {
namespace {
// The Windows KeyboardHook implementation uses a low-level hook in order to
// intercept system key combinations such as Alt + Tab. A standard hook or
// other window message filtering mechanisms will allow us to observe key events
// but we would be unable to block them or the corresponding system action.
// There are downsides to this approach as described in the following blurbs.
// Low-level hooks are not given a repeat state for each key event. This is
// because the events are intercepted prior to the OS handling them and tracking
// their states the usual way. We solve this by tracking the last key seen and
// modifying the event manually to indicate it is a repeat. This works for
// every key except escape which is used to exit fullscreen and tear down the
// hook. The quirk is that after the hook is torn down, the first escape key
// event passed to the window will appear like an initial keydown. This is
// because it *is* an initial keydown from Windows' perspective as it was being
// intercepted before then.
// Intercepting modifier keys (Ctrl, Shift, Win, Etc.) within a low-level
// keyboard hook will result in an incorrect modifier state reported by the OS
// for that key. This is because the hook intercepts the event before the OS
// has a chance to observe the event and update its internal state. This
// trade-off is necessary as we also want to intercept and prevent specific key
// combos from taking effect.
// A related side-effect occurs when intercepting printable characters. Since
// the key event is intercepted before the OS handles it, no char events are
// produced. So if we intercept scan codes which should generate a printable
// character, the result is that the key up/down events are sent correctly but
// no WM_CHAR message is generated. This is unacceptable for some usages of the
// hook as the behavior is very different between locked and unlocked states.
// This is a fair number of downsides however the ability to block system key
// combos is a hard requirement so we need to work around them.
// The solution we have adopted is:
// - Only intercept modifiers and allow all other keys to pass through
// Note: Shift is not included as otherwise it is not applied by the OS and
// the printable characters generated by the key event will be wrong.
// - Track the repeat state ourselves
// - Update the per-thread key state for the tracked modifiers using
// SetKeyboardState().
// In practice this works well as intercepting the modifiers allows us to block
// system keyboard combos and all other keys generate the proper events and
// printable chars.
// Using SetKeyboardState() allows us to tell Windows the current state for the
// modifiers we intercept. This state only works for the current thread,
// meaning that other applications / threads which check the key state for the
// modifiers will not receive an accurate value. One constraint for the
// KeyboardHook is that it is only engaged when our window is fullscreen and
// focused so we don't need to worry too much about other apps.
// Represents a VK_LCONTROL scancode with the 0x02 flag in the high word.
// The 0x02 flag does not seem to be documented on MSDN or in the public Windows
// headers. I suspect it is related to the KF_ family of constants which skips
// from 0x0100 to 0x0800 with 0x0200 and 0x0400 undocumented (likely reserved).
constexpr DWORD kSynthesizedControlScanCodeForAltGr = 0x021D;
// {Get|Set}KeyboardState() receives an array with 256 elements. This is
// described in MSDN but no constant exists for this value. Function reference:
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getkeyboardstate
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-setkeyboardstate
constexpr int kKeyboardStateArraySize = 256;
// Used for setting and testing the bits in the KeyboardState array.
constexpr BYTE kKeyDown = 0x80;
constexpr BYTE kKeyUp = 0x00;
bool IsAltKey(DWORD vk) {
return vk == VK_MENU || vk == VK_LMENU || vk == VK_RMENU;
}
bool IsControlKey(DWORD vk) {
return vk == VK_CONTROL || vk == VK_LCONTROL || vk == VK_RCONTROL;
}
bool IsWindowsKey(DWORD vk) {
return vk == VK_LWIN || vk == VK_RWIN;
}
bool IsModifierKey(DWORD vk) {
// We don't track the state of the shift key as we want to allow the OS to
// know about its state so it is correctly applied to printable characters.
return IsAltKey(vk) || IsControlKey(vk) || IsWindowsKey(vk);
}
class KeyboardHookWinImpl : public KeyboardHookWin {
public:
KeyboardHookWinImpl(base::Optional<base::flat_set<DomCode>> dom_codes,
KeyEventCallback callback,
bool enable_hook_registration);
~KeyboardHookWinImpl() override;
// KeyboardHookWin implementation.
bool ProcessKeyEventMessage(WPARAM w_param,
DWORD vk,
DWORD scan_code,
DWORD time_stamp) override;
bool Register();
private:
static LRESULT CALLBACK ProcessKeyEvent(int code,
WPARAM w_param,
LPARAM l_param);
void UpdateModifierState(DWORD vk, bool key_down);
void ClearModifierStates();
static KeyboardHookWinImpl* instance_;
THREAD_CHECKER(thread_checker_);
HHOOK hook_ = nullptr;
// Tracks the last non-located key down seen in order to determine if the
// current key event should be marked as a repeated key press.
DWORD last_key_down_ = 0;
// Tracks the number of AltGr key sequences seen since the start of the most
// recent AltGr key down. When the AltGr key is pressed, Windows injects a
// synthesized left control key event followed by the right alt key event.
// This sequence occurs on the initial keypress and every repeat.
int altgr_sequence_count_ = 0;
const bool enable_hook_registration_ = true;
DISALLOW_COPY_AND_ASSIGN(KeyboardHookWinImpl);
};
// static
KeyboardHookWinImpl* KeyboardHookWinImpl::instance_ = nullptr;
KeyboardHookWinImpl::KeyboardHookWinImpl(
base::Optional<base::flat_set<DomCode>> dom_codes,
KeyEventCallback callback,
bool enable_hook_registration)
: KeyboardHookWin(std::move(dom_codes), std::move(callback)),
enable_hook_registration_(enable_hook_registration) {}
KeyboardHookWinImpl::~KeyboardHookWinImpl() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
ClearModifierStates();
if (!enable_hook_registration_)
return;
DCHECK_EQ(instance_, this);
instance_ = nullptr;
if (!UnhookWindowsHookEx(hook_))
DPLOG(ERROR) << "UnhookWindowsHookEx failed";
}
bool KeyboardHookWinImpl::Register() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// If the hook was created for testing, |Register()| should not be called.
DCHECK(enable_hook_registration_);
// Only one instance of this class can be registered at a time.
DCHECK(!instance_);
instance_ = this;
// Per MSDN this Hook procedure will be called in the context of the thread
// which installed it.
hook_ = SetWindowsHookEx(
WH_KEYBOARD_LL,
reinterpret_cast<HOOKPROC>(&KeyboardHookWinImpl::ProcessKeyEvent),
/*hMod=*/nullptr,
/*dwThreadId=*/0);
DPLOG_IF(ERROR, !hook_) << "SetWindowsHookEx failed";
return hook_ != nullptr;
}
void KeyboardHookWinImpl::ClearModifierStates() {
BYTE keyboard_state[kKeyboardStateArraySize] = {0};
if (!GetKeyboardState(keyboard_state)) {
DPLOG(ERROR) << "GetKeyboardState() failed: ";
return;
}
// Reset each modifier position.
keyboard_state[VK_CONTROL] = kKeyUp;
keyboard_state[VK_LCONTROL] = kKeyUp;
keyboard_state[VK_RCONTROL] = kKeyUp;
keyboard_state[VK_MENU] = kKeyUp;
keyboard_state[VK_LMENU] = kKeyUp;
keyboard_state[VK_RMENU] = kKeyUp;
keyboard_state[VK_LWIN] = kKeyUp;
keyboard_state[VK_RWIN] = kKeyUp;
if (!SetKeyboardState(keyboard_state))
DPLOG(ERROR) << "SetKeyboardState() failed: ";
}
bool KeyboardHookWinImpl::ProcessKeyEventMessage(WPARAM w_param,
DWORD vk,
DWORD scan_code,
DWORD time_stamp) {
// The |vk| delivered to the low-level hook includes a location which is
// needed to track individual keystates such as when both left and right
// control keys are pressed. Make sure that location information was retained
// when the vkey was passed into this method.
DCHECK_NE(vk, static_cast<DWORD>(VK_CONTROL));
DCHECK_NE(vk, static_cast<DWORD>(VK_MENU));
if (!IsModifierKey(vk))
return false;
// Windows synthesizes an additional key event when AltGr is pressed. The
// event has the left control scan code in the low word and a 0x02 flag set in
// the high word. The VK_RMENU event will be sent once this key is processed.
bool is_altgr_sequence = false;
if (scan_code == kSynthesizedControlScanCodeForAltGr) {
is_altgr_sequence = true;
scan_code = KeycodeConverter::DomCodeToNativeKeycode(DomCode::CONTROL_LEFT);
}
// If the caller has only requested that Alt be captured, then we don't want
// to bail early on the injected control event.
DomCode dom_code = DomCode::NONE;
if (is_altgr_sequence)
dom_code = DomCode::ALT_RIGHT;
else
dom_code = KeycodeConverter::NativeKeycodeToDomCode(scan_code);
if (!ShouldCaptureKeyEvent(dom_code))
return false;
if (is_altgr_sequence)
altgr_sequence_count_++;
else if (vk != VK_RMENU)
altgr_sequence_count_ = 0;
// The Windows key is always located, the other modifiers are not.
DWORD non_located_vk =
IsWindowsKey(vk)
? vk
: LocatedToNonLocatedKeyboardCode(static_cast<KeyboardCode>(vk));
bool is_repeat = false;
MSG msg = {nullptr, w_param, non_located_vk, GetLParamFromScanCode(scan_code),
time_stamp};
EventType event_type = EventTypeFromMSG(msg);
if (event_type == ET_KEY_PRESSED) {
UpdateModifierState(vk, /*key_down=*/true);
// We use the non-located vkey to determine whether a key event is a repeat
// or not. The exception is for AltGr which has a two key sequence which
// alternates.
is_repeat = (last_key_down_ == non_located_vk) || altgr_sequence_count_ > 1;
last_key_down_ = non_located_vk;
} else {
DCHECK_EQ(event_type, ET_KEY_RELEASED);
UpdateModifierState(vk, /*key_down=*/false);
altgr_sequence_count_ = 0;
last_key_down_ = 0;
}
std::unique_ptr<KeyEvent> key_event =
std::make_unique<KeyEvent>(KeyEventFromMSG(msg));
if (is_repeat)
key_event->set_flags(key_event->flags() | EF_IS_REPEAT);
ForwardCapturedKeyEvent(std::move(key_event));
return true;
}
void KeyboardHookWinImpl::UpdateModifierState(DWORD vk, bool is_key_down) {
BYTE keyboard_state[kKeyboardStateArraySize] = {0};
if (!GetKeyboardState(keyboard_state)) {
DPLOG(ERROR) << "GetKeyboardState() failed: ";
return;
}
// Update the located virtual key first.
keyboard_state[vk] = is_key_down ? kKeyDown : kKeyUp;
// Now update the non-located virtual key.
keyboard_state[VK_CONTROL] = (keyboard_state[VK_LCONTROL] == kKeyDown ||
keyboard_state[VK_RCONTROL] == kKeyDown)
? kKeyDown
: kKeyUp;
keyboard_state[VK_MENU] = (keyboard_state[VK_LMENU] == kKeyDown ||
keyboard_state[VK_RMENU] == kKeyDown)
? kKeyDown
: kKeyUp;
if (!SetKeyboardState(keyboard_state))
PLOG(ERROR) << "SetKeyboardState() failed: ";
}
// static
LRESULT CALLBACK KeyboardHookWinImpl::ProcessKeyEvent(int code,
WPARAM w_param,
LPARAM l_param) {
// If there is an error unhooking, this method could be called with a null
// |instance_|. Ensure we have a valid instance and that |code| is correct
// before proceeding.
if (!instance_ || code != HC_ACTION)
return CallNextHookEx(nullptr, code, w_param, l_param);
DCHECK_CALLED_ON_VALID_THREAD(instance_->thread_checker_);
KBDLLHOOKSTRUCT* ll_hooks = reinterpret_cast<KBDLLHOOKSTRUCT*>(l_param);
// This vkey represents both a vkey and a location on the keyboard such as
// VK_LCONTROL or VK_RCONTROL.
DWORD vk = ll_hooks->vkCode;
// Apply the extended flag prior to passing |scan_code| since |instance_| does
// not have access to the low-level hook flags.
DWORD scan_code = ll_hooks->scanCode;
if (ll_hooks->flags & LLKHF_EXTENDED)
scan_code |= 0xE000;
if (instance_->ProcessKeyEventMessage(w_param, vk, scan_code, ll_hooks->time))
return 1;
return CallNextHookEx(nullptr, code, w_param, l_param);
}
} // namespace
// static
std::unique_ptr<KeyboardHook> KeyboardHook::Create(
base::Optional<base::flat_set<DomCode>> dom_codes,
gfx::AcceleratedWidget accelerated_widget,
KeyEventCallback callback) {
std::unique_ptr<KeyboardHookWinImpl> keyboard_hook =
std::make_unique<KeyboardHookWinImpl>(std::move(dom_codes),
std::move(callback),
/*enable_hook_registration=*/true);
if (!keyboard_hook->Register())
return nullptr;
return keyboard_hook;
}
std::unique_ptr<KeyboardHookWin> KeyboardHookWin::CreateForTesting(
base::Optional<base::flat_set<DomCode>> dom_codes,
KeyEventCallback callback) {
return std::make_unique<KeyboardHookWinImpl>(
std::move(dom_codes), std::move(callback),
/*enable_hook_registration=*/false);
}
KeyboardHookWin::KeyboardHookWin(
base::Optional<base::flat_set<DomCode>> dom_codes,
KeyEventCallback callback)
: KeyboardHookBase(std::move(dom_codes), std::move(callback)) {}
KeyboardHookWin::~KeyboardHookWin() = default;
} // namespace ui