// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/events/ash/caps_lock_event_rewriter.h"
#include "ash/constants/ash_features.h"
#include "base/check.h"
#include "ui/base/accelerators/ash/right_alt_event_property.h"
#include "ui/base/ui_base_features.h"
#include "ui/events/ash/event_property.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/dom_key.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
namespace ui {
CapsLockEventRewriter::CapsLockEventRewriter(
KeyboardLayoutEngine* keyboard_layout_engine,
KeyboardCapability* keyboard_capability,
ash::input_method::ImeKeyboard* ime_keyboard)
: keyboard_layout_engine_(keyboard_layout_engine),
keyboard_capability_(keyboard_capability),
ime_keyboard_(ime_keyboard) {}
CapsLockEventRewriter::~CapsLockEventRewriter() = default;
EventDispatchDetails CapsLockEventRewriter::RewriteEvent(
const Event& event,
const Continuation continuation) {
std::unique_ptr<Event> rewritten_event;
switch (event.type()) {
case EventType::kKeyPressed: {
rewritten_event = RewritePressKeyEvent(*event.AsKeyEvent());
break;
}
case EventType::kKeyReleased: {
rewritten_event = RewriteReleaseKeyEvent(*event.AsKeyEvent());
break;
}
default: {
// Update flags by reconstructing them from the modifier key status.
const int flags = event.flags();
const int rewritten_flags = RewriteModifierFlags(event.flags());
if (flags != rewritten_flags) {
rewritten_event = event.Clone();
// SetNativeEvent must be called explicitly as native events are not
// copied on ChromeOS by default. This is because `PlatformEvent` is a
// pointer by default, so its lifetime can not be guaranteed in general.
// In this case, the lifetime of `rewritten_event` is guaranteed to be
// less than the original `event`.
SetNativeEvent(*rewritten_event, event.native_event());
// Note: this updates DomKey to reflect the new flags.
rewritten_event->SetFlags(rewritten_flags);
}
break;
}
}
return SendEvent(continuation,
rewritten_event ? rewritten_event.get() : &event);
}
std::unique_ptr<KeyEvent> CapsLockEventRewriter::RewritePressKeyEvent(
const KeyEvent& key_event) {
internal::PhysicalKey physical_key{key_event.code(),
GetKeyboardDeviceIdProperty(key_event)};
// If this is a repeat event, remap it the same as the original press.
if (auto it = remapped_keys_.find(physical_key); it != remapped_keys_.end()) {
return BuildRewrittenEvent(key_event, it->second);
}
RemappedKey remapped_key = {key_event.code(), key_event.GetDomKey(),
key_event.key_code()};
const bool is_right_alt_key = key_event.code() == DomCode::LAUNCH_ASSISTANT &&
HasRightAltProperty(key_event);
const bool is_function_down = (key_event.flags() & EF_FUNCTION_DOWN) != 0;
if (is_right_alt_key && is_function_down) {
// Update DomKey and KeyboardCode respecting the current keyboard layout.
const DomCode remapped_dom_code = DomCode::CAPS_LOCK;
DomKey dom_key;
KeyboardCode key_code;
if (!keyboard_layout_engine_->Lookup(remapped_dom_code, key_event.flags(),
&dom_key, &key_code)) {
LOG(ERROR) << "Failed to look up keyboard layout";
return nullptr;
}
remapped_key = {remapped_dom_code, dom_key, key_code,
/*should_remove_function_modifier=*/true};
remapped_keys_.insert_or_assign(physical_key, remapped_key);
}
// When the keyboard rewriter fix is enabled, the `CapsLockEventRewriter`
// is responsible for toggling CapsLock when CapsLock DomKeys are
// encountered in the input stream. These can originate from other event
// rewriters as well as physical CapsLock keys.
if (remapped_key.key == DomKey::CAPS_LOCK) {
if (pressed_modifier_keys_.insert_or_assign(physical_key, EF_MOD3_DOWN)
.second) {
ime_keyboard_->SetCapsLockEnabled(!ime_keyboard_->IsCapsLockEnabled());
}
}
return BuildRewrittenEvent(key_event, remapped_key);
}
std::unique_ptr<KeyEvent> CapsLockEventRewriter::RewriteReleaseKeyEvent(
const KeyEvent& event) {
int device_id = GetKeyboardDeviceIdProperty(event);
internal::PhysicalKey physical_key{event.code(), device_id};
pressed_modifier_keys_.erase(physical_key);
// Instead of looking up the remap rule again here, we'll just reuse the remap
// data on the pressed event, so that this release event is remapped in
// the same way with the pressed event.
std::optional<RemappedKey> remapped;
if (auto it = remapped_keys_.find(physical_key); it != remapped_keys_.end()) {
remapped = it->second;
remapped_keys_.erase(it);
}
return BuildRewrittenEvent(
event, remapped.value_or(RemappedKey{event.code(), event.GetDomKey(),
event.key_code()}));
}
std::unique_ptr<KeyEvent> CapsLockEventRewriter::BuildRewrittenEvent(
const KeyEvent& event,
const RemappedKey& remapped) {
// Get rewritten flags removing the function modifier if it should be removed.
const EventFlags flags = RewriteModifierFlags(
event.flags() &
~(remapped.should_remove_function_modifier ? EF_FUNCTION_DOWN : EF_NONE));
if (remapped.code == event.code() && remapped.key == event.GetDomKey() &&
remapped.key_code == event.key_code() && flags == event.flags()) {
// Nothing is rewritten.
return nullptr;
}
auto rewritten_event =
std::make_unique<KeyEvent>(event.type(), remapped.key_code, remapped.code,
flags, remapped.key, event.time_stamp());
rewritten_event->set_scan_code(event.scan_code());
rewritten_event->set_source_device_id(event.source_device_id());
return rewritten_event;
}
EventFlags CapsLockEventRewriter::RewriteModifierFlags(EventFlags flags) const {
// Only the capslock modifier flag can be removed based on the set of
// currently pressed keys. Will be readded based on the current capslock
// state.
constexpr EventFlags kTargetModifierFlags = EF_MOD3_DOWN | EF_CAPS_LOCK_ON;
flags &= ~kTargetModifierFlags;
// Recalculate modifier flags from the currently pressed keys.
for (const auto& [_, modifier] : pressed_modifier_keys_) {
flags |= modifier;
}
// Update CapsLock.
if (ime_keyboard_->IsCapsLockEnabled()) {
flags |= EF_CAPS_LOCK_ON;
}
return flags;
}
} // namespace ui