blob: cdac91a46148777403cdd389d27f557cfc326295 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/events/peripheral_customization_event_rewriter.h"
#include <linux/input.h>
#include <iterator>
#include <memory>
#include "ash/accelerators/accelerator_controller_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/accelerators_util.h"
#include "ash/public/mojom/input_device_settings.mojom-forward.h"
#include "ash/public/mojom/input_device_settings.mojom-shared.h"
#include "ash/public/mojom/input_device_settings.mojom.h"
#include "ash/shell.h"
#include "ash/system/input_device_settings/input_device_settings_controller_impl.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/span.h"
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "ui/base/ui_base_features.h"
#include "ui/display/screen.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/event_dispatcher.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/dom_key.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/ozone/evdev/mouse_button_property.h"
#include "ui/events/ozone/layout/keyboard_layout_engine.h"
#include "ui/events/ozone/layout/keyboard_layout_engine_manager.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point_f.h"
namespace ash {
namespace {
using RemappingActionResult =
PeripheralCustomizationEventRewriter::RemappingActionResult;
constexpr int kMouseRemappableFlags = ui::EF_BACK_MOUSE_BUTTON |
ui::EF_FORWARD_MOUSE_BUTTON |
ui::EF_MIDDLE_MOUSE_BUTTON;
constexpr int kGraphicsTabletRemappableFlags =
ui::EF_RIGHT_MOUSE_BUTTON | ui::EF_BACK_MOUSE_BUTTON |
ui::EF_FORWARD_MOUSE_BUTTON | ui::EF_MIDDLE_MOUSE_BUTTON;
constexpr auto kStaticActionToMouseButtonFlag =
base::MakeFixedFlatMap<mojom::StaticShortcutAction, ui::EventFlags>({
{mojom::StaticShortcutAction::kLeftClick, ui::EF_LEFT_MOUSE_BUTTON},
{mojom::StaticShortcutAction::kRightClick, ui::EF_RIGHT_MOUSE_BUTTON},
{mojom::StaticShortcutAction::kMiddleClick, ui::EF_MIDDLE_MOUSE_BUTTON},
});
constexpr std::string_view ToMetricsString(
PeripheralCustomizationEventRewriter::PeripheralCustomizationMetricsType
peripheral_kind) {
switch (peripheral_kind) {
case PeripheralCustomizationEventRewriter::
PeripheralCustomizationMetricsType::kMouse:
return "Mouse";
case PeripheralCustomizationEventRewriter::
PeripheralCustomizationMetricsType::kGraphicsTablet:
return "GraphicsTablet";
case PeripheralCustomizationEventRewriter::
PeripheralCustomizationMetricsType::kGraphicsTabletPen:
return "GraphicsTabletPen";
}
}
mojom::KeyEvent GetStaticShortcutAction(mojom::StaticShortcutAction action) {
mojom::KeyEvent key_event;
switch (action) {
case mojom::StaticShortcutAction::kDisable:
case mojom::StaticShortcutAction::kLeftClick:
case mojom::StaticShortcutAction::kRightClick:
case mojom::StaticShortcutAction::kMiddleClick:
NOTREACHED_NORETURN();
case mojom::StaticShortcutAction::kCopy:
key_event = mojom::KeyEvent(
ui::VKEY_C, static_cast<int>(ui::DomCode::US_C),
static_cast<int>(ui::DomKey::Constant<'c'>::Character),
ui::EF_CONTROL_DOWN, /*key_display=*/"");
break;
case mojom::StaticShortcutAction::kPaste:
key_event = mojom::KeyEvent(
ui::VKEY_V, static_cast<int>(ui::DomCode::US_V),
static_cast<int>(ui::DomKey::Constant<'v'>::Character),
ui::EF_CONTROL_DOWN, /*key_display=*/"");
break;
case mojom::StaticShortcutAction::kUndo:
key_event = mojom::KeyEvent(
ui::VKEY_Z, static_cast<int>(ui::DomCode::US_Z),
static_cast<int>(ui::DomKey::Constant<'z'>::Character),
ui::EF_CONTROL_DOWN, /*key_display=*/"");
break;
case mojom::StaticShortcutAction::kRedo:
key_event = mojom::KeyEvent(
ui::VKEY_Z, static_cast<int>(ui::DomCode::US_Z),
static_cast<int>(ui::DomKey::Constant<'z'>::Character),
ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN, /*key_display=*/"");
break;
case mojom::StaticShortcutAction::kZoomIn:
key_event = mojom::KeyEvent(
ui::VKEY_OEM_PLUS, static_cast<int>(ui::DomCode::EQUAL),
static_cast<int>(ui::DomKey::Constant<'='>::Character),
ui::EF_CONTROL_DOWN, /*key_display=*/"");
break;
case mojom::StaticShortcutAction::kZoomOut:
key_event = mojom::KeyEvent(
ui::VKEY_OEM_MINUS, static_cast<int>(ui::DomCode::MINUS),
static_cast<int>(ui::DomKey::Constant<'-'>::Character),
ui::EF_CONTROL_DOWN, /*key_display=*/"");
break;
case mojom::StaticShortcutAction::kPreviousPage:
key_event = mojom::KeyEvent(ui::VKEY_BROWSER_BACK,
static_cast<int>(ui::DomCode::BROWSER_BACK),
static_cast<int>(ui::DomKey::BROWSER_BACK),
ui::EF_NONE, /*key_display=*/"");
break;
case mojom::StaticShortcutAction::kNextPage:
key_event =
mojom::KeyEvent(ui::VKEY_BROWSER_FORWARD,
static_cast<int>(ui::DomCode::BROWSER_FORWARD),
static_cast<int>(ui::DomKey::BROWSER_FORWARD),
ui::EF_NONE, /*key_display=*/"");
break;
}
return key_event;
}
int ConvertKeyCodeToFlags(ui::KeyboardCode key_code) {
switch (key_code) {
case ui::VKEY_LWIN:
case ui::VKEY_RWIN:
return ui::EF_COMMAND_DOWN;
case ui::VKEY_CONTROL:
case ui::VKEY_RCONTROL:
return ui::EF_CONTROL_DOWN;
case ui::VKEY_SHIFT:
case ui::VKEY_LSHIFT:
case ui::VKEY_RSHIFT:
return ui::EF_SHIFT_DOWN;
case ui::VKEY_MENU:
case ui::VKEY_RMENU:
return ui::EF_ALT_DOWN;
default:
return ui::EF_NONE;
}
}
bool AreScrollWheelEventRewritesAllowed(
mojom::CustomizationRestriction customization_restriction) {
switch (customization_restriction) {
case mojom::CustomizationRestriction::kDisallowCustomizations:
case mojom::CustomizationRestriction::kDisableKeyEventRewrites:
case mojom::CustomizationRestriction::kAllowAlphabetKeyEventRewrites:
case mojom::CustomizationRestriction::
kAllowAlphabetOrNumberKeyEventRewrites:
case mojom::CustomizationRestriction::kAllowTabEventRewrites:
return false;
case mojom::CustomizationRestriction::kAllowHorizontalScrollWheelRewrites:
case mojom::CustomizationRestriction::kAllowCustomizations:
return true;
}
}
template <typename Iterator>
std::vector<std::unique_ptr<ui::Event>> RewriteModifiers(
const ui::Event& event,
uint32_t modifiers_to_press,
uint32_t modifiers_already_pressed,
bool pressed,
Iterator begin,
Iterator end) {
std::vector<std::unique_ptr<ui::Event>> rewritten_events;
// Keeps track of the modifier flags that must be applied to the next
// rewritten event. For key presses, this should start at
// `0` and go to `modifiers_to_press`. For releases, it should do the opposite
// as we start will all modifiers down.
uint32_t modifiers_pressed = pressed ? 0u : modifiers_to_press;
for (auto iter = begin; iter != end; iter++) {
if (!(iter->flag & modifiers_to_press)) {
continue;
}
if (pressed) {
modifiers_pressed += iter->flag;
} else {
modifiers_pressed -= iter->flag;
}
const ui::EventType pressed_or_released_type =
pressed ? ui::ET_KEY_PRESSED : ui::ET_KEY_RELEASED;
auto rewritten_modifier_event = std::make_unique<ui::KeyEvent>(
pressed_or_released_type, iter->key_code, iter->dom_code,
modifiers_pressed | modifiers_already_pressed, event.time_stamp());
rewritten_modifier_event->set_source_device_id(event.source_device_id());
rewritten_events.push_back(std::move(rewritten_modifier_event));
}
// Verify our modifier rewriting worked as expected.
if (pressed) {
CHECK_EQ(modifiers_to_press, modifiers_pressed);
} else {
CHECK_EQ(0u, modifiers_pressed);
}
return rewritten_events;
}
std::vector<std::unique_ptr<ui::Event>> GenerateFullKeyEventSequence(
const ui::Event& event,
uint32_t modifiers_to_press,
uint32_t modifiers_already_pressed,
bool pressed,
std::unique_ptr<ui::Event> rewritten_event) {
static constexpr struct {
ui::KeyboardCode key_code;
ui::DomCode dom_code;
ui::EventFlags flag;
} kModifiers[] = {
{ui::VKEY_LWIN, ui::DomCode::META_LEFT, ui::EF_COMMAND_DOWN},
{ui::VKEY_CONTROL, ui::DomCode::CONTROL_LEFT, ui::EF_CONTROL_DOWN},
{ui::VKEY_MENU, ui::DomCode::ALT_LEFT, ui::EF_ALT_DOWN},
{ui::VKEY_SHIFT, ui::DomCode::SHIFT_LEFT, ui::EF_SHIFT_DOWN},
};
static constexpr auto kModifierSpan = base::make_span(kModifiers);
CHECK(rewritten_event);
std::vector<std::unique_ptr<ui::Event>> rewritten_events;
if (pressed) {
// If it is a key press, we rewrite the modifiers in forward order and then
// add the main rewritten event after the press events.
rewritten_events =
RewriteModifiers(event, modifiers_to_press, modifiers_already_pressed,
pressed, kModifierSpan.begin(), kModifierSpan.end());
rewritten_events.push_back(std::move(rewritten_event));
} else {
// For key releases, we add the main rewritten event first and then append
// the rewritten modifiers after. The modifiers must be rewritten in reverse
// order from the `kModifiers` array.
rewritten_events.push_back(std::move(rewritten_event));
auto modifier_events =
RewriteModifiers(event, modifiers_to_press, modifiers_already_pressed,
pressed, kModifierSpan.rbegin(), kModifierSpan.rend());
rewritten_events.insert(rewritten_events.end(),
std::make_move_iterator(modifier_events.begin()),
std::make_move_iterator(modifier_events.end()));
}
return rewritten_events;
}
std::vector<std::unique_ptr<ui::Event>> RewriteEventToKeyEvents(
const ui::Event& event,
const mojom::KeyEvent& key_event,
bool key_press) {
const ui::EventType event_type =
key_press ? ui::ET_KEY_PRESSED : ui::ET_KEY_RELEASED;
const uint32_t modifier_key_flag = ConvertKeyCodeToFlags(key_event.vkey);
// `other_modifiers_to_apply` symbolizes the flags that are not handled by
// `modifier_key_flag` or flags that are already included in the event. The
// flags remaining in `other_modifiers_to_apply` are the modifiers we must
// write events for in addition to the main key event.
const uint32_t other_modifiers_to_apply =
key_event.modifiers & ~event.flags() & ~modifier_key_flag;
uint32_t applied_modifier_key_flag = modifier_key_flag;
// Do not apply modifier flags when the key is a modifier and it is a release
// event. Modifier keys do not apply their flag on release.
if (event_type == ui::ET_KEY_RELEASED &&
key_event.modifiers == modifier_key_flag) {
applied_modifier_key_flag = ui::EF_NONE;
}
auto rewritten_event = std::make_unique<ui::KeyEvent>(
event_type, key_event.vkey, static_cast<ui::DomCode>(key_event.dom_code),
applied_modifier_key_flag | other_modifiers_to_apply | event.flags(),
static_cast<ui::DomKey>(key_event.dom_key), event.time_stamp());
rewritten_event->set_source_device_id(event.source_device_id());
return GenerateFullKeyEventSequence(
event, other_modifiers_to_apply, event.flags(),
/*pressed=*/event_type == ui::ET_KEY_PRESSED, std::move(rewritten_event));
}
std::vector<std::unique_ptr<ui::Event>> RewriteEventToKeyEvents(
const ui::Event& event,
const mojom::KeyEvent& key_event) {
// If the original event is a mouse scroll event, we must generate both a
// press and release from the single event.
const bool should_press_and_release = event.type() == ui::ET_MOUSEWHEEL;
const bool key_press = should_press_and_release ||
event.type() == ui::ET_MOUSE_PRESSED ||
event.type() == ui::ET_KEY_PRESSED;
std::vector<std::unique_ptr<ui::Event>> rewritten_events =
RewriteEventToKeyEvents(event, key_event, key_press);
if (should_press_and_release) {
std::vector<std::unique_ptr<ui::Event>> release_rewritten_events =
RewriteEventToKeyEvents(event, key_event, /*key_press=*/false);
rewritten_events.reserve(rewritten_events.size() +
release_rewritten_events.size());
rewritten_events.insert(
rewritten_events.end(),
std::make_move_iterator(release_rewritten_events.begin()),
std::make_move_iterator(release_rewritten_events.end()));
}
return rewritten_events;
}
std::vector<std::unique_ptr<ui::Event>> RewriteEventToMouseButtonEvents(
const ui::Event& event,
mojom::StaticShortcutAction action) {
// If the original event is a mouse scroll event, we must generate both a
// press and release from the single event.
const bool should_press_and_release = event.type() == ui::ET_MOUSEWHEEL;
std::vector<std::unique_ptr<ui::Event>> rewritten_events;
auto* flag_iter = kStaticActionToMouseButtonFlag.find(action);
CHECK(flag_iter != kStaticActionToMouseButtonFlag.end());
const int characteristic_flag = flag_iter->second;
auto* screen = display::Screen::GetScreen();
CHECK(screen);
auto display = screen->GetDisplayNearestPoint(screen->GetCursorScreenPoint());
const gfx::PointF location =
gfx::ScalePoint(gfx::PointF(screen->GetCursorScreenPoint() -
display.bounds().origin().OffsetFromOrigin()),
display.device_scale_factor());
const ui::EventType type =
(should_press_and_release || event.type() == ui::ET_MOUSE_PRESSED ||
event.type() == ui::ET_KEY_PRESSED)
? ui::ET_MOUSE_PRESSED
: ui::ET_MOUSE_RELEASED;
rewritten_events.push_back(std::make_unique<ui::MouseEvent>(
type, location, location, event.time_stamp(),
event.flags() | characteristic_flag, characteristic_flag));
rewritten_events.back()->set_source_device_id(event.source_device_id());
if (should_press_and_release) {
rewritten_events.push_back(std::make_unique<ui::MouseEvent>(
ui::ET_MOUSE_RELEASED, location, location, event.time_stamp(),
event.flags() | characteristic_flag, characteristic_flag));
rewritten_events.back()->set_source_device_id(event.source_device_id());
}
return rewritten_events;
}
bool IsMouseButtonEvent(const ui::MouseEvent& mouse_event) {
return mouse_event.type() == ui::ET_MOUSE_PRESSED ||
mouse_event.type() == ui::ET_MOUSE_RELEASED;
}
bool IsMouseRemappableButton(int flags) {
return (flags & kMouseRemappableFlags) != 0;
}
bool IsGraphicsTabletRemappableButton(int flags) {
return (flags & kGraphicsTabletRemappableFlags) != 0;
}
int GetRemappableMouseEventFlags(
PeripheralCustomizationEventRewriter::DeviceType device_type) {
switch (device_type) {
case PeripheralCustomizationEventRewriter::DeviceType::kMouse:
return kMouseRemappableFlags;
case PeripheralCustomizationEventRewriter::DeviceType::kGraphicsTablet:
return kGraphicsTabletRemappableFlags;
}
}
mojom::ButtonPtr GetButtonFromMouseEvent(const ui::MouseEvent& mouse_event) {
switch (mouse_event.changed_button_flags()) {
case ui::EF_LEFT_MOUSE_BUTTON:
return mojom::Button::NewCustomizableButton(
mojom::CustomizableButton::kLeft);
case ui::EF_RIGHT_MOUSE_BUTTON:
return mojom::Button::NewCustomizableButton(
mojom::CustomizableButton::kRight);
case ui::EF_MIDDLE_MOUSE_BUTTON:
return mojom::Button::NewCustomizableButton(
mojom::CustomizableButton::kMiddle);
case ui::EF_FORWARD_MOUSE_BUTTON:
case ui::EF_BACK_MOUSE_BUTTON:
break;
}
CHECK(mouse_event.changed_button_flags() == ui::EF_FORWARD_MOUSE_BUTTON ||
mouse_event.changed_button_flags() == ui::EF_BACK_MOUSE_BUTTON);
auto linux_key_code = ui::GetForwardBackMouseButtonProperty(mouse_event);
if (!linux_key_code) {
return (mouse_event.changed_button_flags() == ui::EF_FORWARD_MOUSE_BUTTON)
? mojom::Button::NewCustomizableButton(
mojom::CustomizableButton::kForward)
: mojom::Button::NewCustomizableButton(
mojom::CustomizableButton::kBack);
}
switch (*linux_key_code) {
case BTN_FORWARD:
return mojom::Button::NewCustomizableButton(
mojom::CustomizableButton::kForward);
case BTN_BACK:
return mojom::Button::NewCustomizableButton(
mojom::CustomizableButton::kBack);
case BTN_SIDE:
return mojom::Button::NewCustomizableButton(
mojom::CustomizableButton::kSide);
case BTN_EXTRA:
return mojom::Button::NewCustomizableButton(
mojom::CustomizableButton::kExtra);
}
NOTREACHED_NORETURN();
}
// Returns the customizable button for the scroll wheel event. Will return null
// if the scroll event is not a horizontal scroll event.
mojom::ButtonPtr GetButtonFromMouseWheelEvent(
const ui::MouseWheelEvent& mouse_wheel_event) {
if (mouse_wheel_event.x_offset() == 0) {
return nullptr;
}
if (mouse_wheel_event.x_offset() > 0) {
return mojom::Button::NewCustomizableButton(
mojom::CustomizableButton::kScrollRight);
}
return mojom::Button::NewCustomizableButton(
mojom::CustomizableButton::kScrollLeft);
}
int ConvertButtonToFlags(const mojom::Button& button) {
if (button.is_customizable_button()) {
switch (button.get_customizable_button()) {
case mojom::CustomizableButton::kLeft:
return ui::EF_LEFT_MOUSE_BUTTON;
case mojom::CustomizableButton::kRight:
return ui::EF_RIGHT_MOUSE_BUTTON;
case mojom::CustomizableButton::kMiddle:
return ui::EF_MIDDLE_MOUSE_BUTTON;
case mojom::CustomizableButton::kForward:
case mojom::CustomizableButton::kExtra:
return ui::EF_FORWARD_MOUSE_BUTTON;
case mojom::CustomizableButton::kBack:
case mojom::CustomizableButton::kSide:
return ui::EF_BACK_MOUSE_BUTTON;
case mojom::CustomizableButton::kScrollLeft:
case mojom::CustomizableButton::kScrollRight:
return ui::EF_NONE;
}
}
if (button.is_vkey()) {
return ConvertKeyCodeToFlags(button.get_vkey());
}
return ui::EF_NONE;
}
std::optional<PeripheralCustomizationEventRewriter::RemappingActionResult>
GetRemappingActionFromMouseSettings(const mojom::Button& button,
const mojom::MouseSettings& settings) {
const auto button_remapping_iter = base::ranges::find(
settings.button_remappings, button,
[](const mojom::ButtonRemappingPtr& entry) { return *entry->button; });
if (button_remapping_iter != settings.button_remappings.end()) {
const mojom::ButtonRemapping& button_remapping = *(*button_remapping_iter);
if (!button_remapping.remapping_action) {
return std::nullopt;
}
auto result = PeripheralCustomizationEventRewriter::RemappingActionResult(
*button_remapping.remapping_action,
PeripheralCustomizationEventRewriter::
PeripheralCustomizationMetricsType::kMouse);
return result;
}
return std::nullopt;
}
std::optional<PeripheralCustomizationEventRewriter::RemappingActionResult>
GetRemappingActionFromGraphicsTabletSettings(
const mojom::Button& button,
const mojom::GraphicsTabletSettings& settings) {
const auto pen_button_remapping_iter = base::ranges::find(
settings.pen_button_remappings, button,
[](const mojom::ButtonRemappingPtr& entry) { return *entry->button; });
if (pen_button_remapping_iter != settings.pen_button_remappings.end()) {
const mojom::ButtonRemapping& button_remapping =
*(*pen_button_remapping_iter);
if (!button_remapping.remapping_action) {
return std::nullopt;
}
auto pen_action =
PeripheralCustomizationEventRewriter::RemappingActionResult(
*button_remapping.remapping_action,
PeripheralCustomizationEventRewriter::
PeripheralCustomizationMetricsType::kGraphicsTabletPen);
return std::move(pen_action);
}
const auto tablet_button_remapping_iter = base::ranges::find(
settings.tablet_button_remappings, button,
[](const mojom::ButtonRemappingPtr& entry) { return *entry->button; });
if (tablet_button_remapping_iter != settings.tablet_button_remappings.end()) {
const mojom::ButtonRemapping& button_remapping =
*(*tablet_button_remapping_iter);
if (!button_remapping.remapping_action) {
return std::nullopt;
}
auto tablet_action =
PeripheralCustomizationEventRewriter::RemappingActionResult(
*button_remapping.remapping_action,
PeripheralCustomizationEventRewriter::
PeripheralCustomizationMetricsType::kGraphicsTablet);
return std::move(tablet_action);
}
return std::nullopt;
}
int GetRemappedModifiersFromMouseSettings(
const mojom::MouseSettings& settings) {
int modifiers = 0;
for (const auto& button_remapping : settings.button_remappings) {
if (button_remapping->remapping_action) {
modifiers |= ConvertButtonToFlags(*button_remapping->button);
}
}
return modifiers;
}
int GetRemappedModifiersFromGraphicsTabletSettings(
const mojom::GraphicsTabletSettings& settings) {
int modifiers = 0;
for (const auto& button_remapping : settings.pen_button_remappings) {
modifiers |= ConvertButtonToFlags(*button_remapping->button);
}
for (const auto& button_remapping : settings.tablet_button_remappings) {
modifiers |= ConvertButtonToFlags(*button_remapping->button);
}
return modifiers;
}
// Verify if the keyboard code is an alphabet letter.
bool IsAlphaKeyboardCode(ui::KeyboardCode key_code) {
return key_code >= ui::VKEY_A && key_code <= ui::VKEY_Z;
}
// Verify if the keyboard code is a number.
bool IsNumberKeyboardCode(ui::KeyboardCode key_code) {
return key_code >= ui::VKEY_0 && key_code <= ui::VKEY_9;
}
} // namespace
// Compares the `DeviceIdButton` struct based on first the device id, and then
// the button stored within the `DeviceIdButton` struct.
bool operator<(
const PeripheralCustomizationEventRewriter::DeviceIdButton& left,
const PeripheralCustomizationEventRewriter::DeviceIdButton& right) {
if (right.device_id != left.device_id) {
return left.device_id < right.device_id;
}
// If both are VKeys, compare them.
if (left.button->is_vkey() && right.button->is_vkey()) {
return left.button->get_vkey() < right.button->get_vkey();
}
// If both are customizable buttons, compare them.
if (left.button->is_customizable_button() &&
right.button->is_customizable_button()) {
return left.button->get_customizable_button() <
right.button->get_customizable_button();
}
// Otherwise, return true if the lhs is a VKey as they mismatch and VKeys
// should be considered less than customizable buttons.
return left.button->is_vkey();
}
PeripheralCustomizationEventRewriter::DeviceIdButton::DeviceIdButton(
int device_id,
mojom::ButtonPtr button)
: device_id(device_id), button(std::move(button)) {}
PeripheralCustomizationEventRewriter::DeviceIdButton::DeviceIdButton(
DeviceIdButton&& device_id_button)
: device_id(device_id_button.device_id),
button(std::move(device_id_button.button)) {}
PeripheralCustomizationEventRewriter::DeviceIdButton::~DeviceIdButton() =
default;
PeripheralCustomizationEventRewriter::DeviceIdButton&
PeripheralCustomizationEventRewriter::DeviceIdButton::operator=(
PeripheralCustomizationEventRewriter::DeviceIdButton&& device_id_button) =
default;
PeripheralCustomizationEventRewriter::RemappingActionResult::
RemappingActionResult(mojom::RemappingAction& remapping_action,
PeripheralCustomizationMetricsType peripheral_kind)
: remapping_action(remapping_action), peripheral_kind(peripheral_kind) {}
PeripheralCustomizationEventRewriter::RemappingActionResult::
RemappingActionResult(RemappingActionResult&& result)
: remapping_action(std::move(result.remapping_action)),
peripheral_kind(result.peripheral_kind) {}
PeripheralCustomizationEventRewriter::RemappingActionResult::
~RemappingActionResult() = default;
PeripheralCustomizationEventRewriter::PeripheralCustomizationEventRewriter(
InputDeviceSettingsController* input_device_settings_controller)
: input_device_settings_controller_(input_device_settings_controller) {
metrics_manager_ = std::make_unique<InputDeviceSettingsMetricsManager>();
}
PeripheralCustomizationEventRewriter::~PeripheralCustomizationEventRewriter() =
default;
std::optional<PeripheralCustomizationEventRewriter::DeviceType>
PeripheralCustomizationEventRewriter::GetDeviceTypeToObserve(int device_id) {
if (mice_to_observe_.contains(device_id)) {
return DeviceType::kMouse;
}
if (graphics_tablets_to_observe_.contains(device_id)) {
return DeviceType::kGraphicsTablet;
}
return std::nullopt;
}
void PeripheralCustomizationEventRewriter::StartObservingMouse(
int device_id,
mojom::CustomizationRestriction customization_restriction) {
mice_to_observe_.insert_or_assign(device_id, customization_restriction);
}
void PeripheralCustomizationEventRewriter::StartObservingGraphicsTablet(
int device_id,
mojom::CustomizationRestriction customization_restriction) {
graphics_tablets_to_observe_.insert_or_assign(device_id,
customization_restriction);
}
void PeripheralCustomizationEventRewriter::StopObserving() {
graphics_tablets_to_observe_.clear();
mice_to_observe_.clear();
}
bool PeripheralCustomizationEventRewriter::NotifyMouseWheelEventObserving(
const ui::MouseWheelEvent& mouse_wheel_event,
DeviceType device_type) {
const auto customization_restriction_iter =
mice_to_observe_.find(mouse_wheel_event.source_device_id());
if (customization_restriction_iter == mice_to_observe_.end()) {
return false;
}
auto customization_restriction = customization_restriction_iter->second;
if (!AreScrollWheelEventRewritesAllowed(customization_restriction)) {
return false;
}
const mojom::ButtonPtr button =
GetButtonFromMouseWheelEvent(mouse_wheel_event);
if (!button) {
return false;
}
switch (device_type) {
case DeviceType::kMouse:
input_device_settings_controller_->OnMouseButtonPressed(
mouse_wheel_event.source_device_id(), *button);
break;
case DeviceType::kGraphicsTablet:
input_device_settings_controller_->OnGraphicsTabletButtonPressed(
mouse_wheel_event.source_device_id(), *button);
break;
}
return true;
}
bool PeripheralCustomizationEventRewriter::NotifyMouseEventObserving(
const ui::MouseEvent& mouse_event,
DeviceType device_type) {
if (!IsMouseButtonEvent(mouse_event)) {
return false;
}
// Make sure the button is remappable for the current `device_type`.
switch (device_type) {
case DeviceType::kMouse:
if (!IsMouseRemappableButton(mouse_event.changed_button_flags())) {
return false;
}
break;
case DeviceType::kGraphicsTablet:
if (!IsGraphicsTabletRemappableButton(
mouse_event.changed_button_flags())) {
return false;
}
break;
}
if (mouse_event.type() != ui::ET_MOUSE_PRESSED) {
return true;
}
const auto button = GetButtonFromMouseEvent(mouse_event);
switch (device_type) {
case DeviceType::kMouse:
input_device_settings_controller_->OnMouseButtonPressed(
mouse_event.source_device_id(), *button);
break;
case DeviceType::kGraphicsTablet:
input_device_settings_controller_->OnGraphicsTabletButtonPressed(
mouse_event.source_device_id(), *button);
break;
}
return true;
}
bool PeripheralCustomizationEventRewriter::IsButtonCustomizable(
const ui::KeyEvent& key_event) {
const auto iter = mice_to_observe_.find(key_event.source_device_id());
if (iter == mice_to_observe().end()) {
return false;
}
const auto customization_restriction = iter->second;
// There are several cases for the customization restriction:
// 1. If restriction is kAllowCustomizations, mice are allowed to observe
// key events.
// 2. If restriction is kAllowAlphabetKeyEventRewrites, mice are allowed to
// observe only alphabet letters key event.
// 3. If restriction is kAllowAlphabetOrNumberKeyEventRewrites, mice are
// allowed to observe alphabet letters or number key event.
// 4. Mice are not allowed to observe key event in other cases.
switch (customization_restriction) {
case mojom::CustomizationRestriction::kAllowCustomizations:
return true;
case mojom::CustomizationRestriction::kAllowAlphabetKeyEventRewrites:
return IsAlphaKeyboardCode(key_event.key_code());
case mojom::CustomizationRestriction::
kAllowAlphabetOrNumberKeyEventRewrites:
return IsAlphaKeyboardCode(key_event.key_code()) ||
IsNumberKeyboardCode(key_event.key_code());
case mojom::CustomizationRestriction::kAllowTabEventRewrites:
return key_event.key_code() == ui::VKEY_TAB;
case mojom::CustomizationRestriction::kDisallowCustomizations:
case mojom::CustomizationRestriction::kDisableKeyEventRewrites:
case mojom::CustomizationRestriction::kAllowHorizontalScrollWheelRewrites:
return false;
}
}
bool PeripheralCustomizationEventRewriter::NotifyKeyEventObserving(
const ui::KeyEvent& key_event,
DeviceType device_type) {
if (device_type == DeviceType::kMouse && !IsButtonCustomizable(key_event)) {
return false;
}
// Observers should only be notified on key presses.
if (key_event.type() != ui::ET_KEY_PRESSED) {
return true;
}
const auto button = mojom::Button::NewVkey(key_event.key_code());
switch (device_type) {
case DeviceType::kMouse:
input_device_settings_controller_->OnMouseButtonPressed(
key_event.source_device_id(), *button);
break;
case DeviceType::kGraphicsTablet:
input_device_settings_controller_->OnGraphicsTabletButtonPressed(
key_event.source_device_id(), *button);
break;
}
return true;
}
bool PeripheralCustomizationEventRewriter::RewriteEventFromButton(
const ui::Event& event,
const mojom::Button& button,
std::vector<std::unique_ptr<ui::Event>>& rewritten_events) {
absl::optional<RemappingActionResult> remapping_action_result =
GetRemappingAction(event.source_device_id(), button);
if (!remapping_action_result) {
return false;
}
auto remapping_action = remapping_action_result->remapping_action;
if (event.type() == ui::ET_KEY_PRESSED ||
event.type() == ui::ET_MOUSE_PRESSED) {
metrics_manager_->RecordRemappingActionWhenButtonPressed(
*remapping_action,
ToMetricsString(remapping_action_result->peripheral_kind).data());
}
if (remapping_action->is_accelerator_action()) {
if (event.type() == ui::ET_KEY_PRESSED ||
event.type() == ui::ET_MOUSE_PRESSED ||
event.type() == ui::ET_MOUSEWHEEL) {
// Every accelerator supported by peripheral customization is not impacted
// by the accelerator passed. Therefore, passing an empty accelerator will
// cause no issues.
Shell::Get()->accelerator_controller()->PerformActionIfEnabled(
remapping_action->get_accelerator_action(), /*accelerator=*/{});
}
return true;
}
if (remapping_action->is_key_event()) {
const auto& key_event = remapping_action->get_key_event();
auto entry = FindKeyCodeEntry(key_event->vkey);
// If no entry can be found, use the stored key_event struct.
if (!entry) {
rewritten_events = RewriteEventToKeyEvents(event, *key_event);
} else {
rewritten_events = RewriteEventToKeyEvents(
event, mojom::KeyEvent(entry->resulting_key_code,
static_cast<int>(entry->dom_code),
static_cast<int>(entry->dom_key),
key_event->modifiers, ""));
}
}
if (remapping_action->is_static_shortcut_action()) {
const auto static_action = remapping_action->get_static_shortcut_action();
if (static_action == mojom::StaticShortcutAction::kDisable) {
// Return true to discard the event.
return true;
}
if (kStaticActionToMouseButtonFlag.contains(static_action)) {
rewritten_events = RewriteEventToMouseButtonEvents(event, static_action);
} else {
rewritten_events = RewriteEventToKeyEvents(
event, GetStaticShortcutAction(
remapping_action->get_static_shortcut_action()));
}
}
return false;
}
ui::EventDispatchDetails PeripheralCustomizationEventRewriter::RewriteKeyEvent(
const ui::KeyEvent& key_event,
const Continuation continuation) {
auto device_type_to_observe =
GetDeviceTypeToObserve(key_event.source_device_id());
if (device_type_to_observe) {
if (NotifyKeyEventObserving(key_event, *device_type_to_observe)) {
return DiscardEvent(continuation);
}
}
// Clone event and remove the already remapped modifiers and use this as the
// "source" key event for the rest of the rewriting.
std::unique_ptr<ui::Event> original_event_with_modifiers_removed =
CloneEvent(key_event);
RemoveRemappedModifiers(*original_event_with_modifiers_removed);
std::vector<std::unique_ptr<ui::Event>> rewritten_events;
mojom::ButtonPtr button = mojom::Button::NewVkey(key_event.key_code());
bool updated_button_map = false;
if (RewriteEventFromButton(*original_event_with_modifiers_removed, *button,
rewritten_events)) {
return DiscardEvent(continuation);
}
const bool event_rewritten = !rewritten_events.empty();
// Discard all "button" type events usually from graphics tablets.
if (!event_rewritten && key_event.key_code() >= ui::VKEY_BUTTON_0 &&
key_event.key_code() <= ui::VKEY_BUTTON_Z) {
return DiscardEvent(continuation);
}
// Add an event to our list to rewrite based on other pressed buttons.
if (rewritten_events.empty()) {
rewritten_events.push_back(
std::move(original_event_with_modifiers_removed));
}
// If the button was released, the pressed button map must be updated before
// applying remapped modifiers.
const ui::Event& last_rewritten_event = *rewritten_events.back();
if (event_rewritten &&
(last_rewritten_event.type() == ui::ET_MOUSE_RELEASED ||
last_rewritten_event.type() == ui::ET_KEY_RELEASED)) {
updated_button_map = true;
UpdatePressedButtonMap(std::move(button), key_event, rewritten_events);
}
for (const auto& rewritten_event : rewritten_events) {
ApplyRemappedModifiers(*rewritten_event);
}
if (event_rewritten && !updated_button_map) {
UpdatePressedButtonMap(std::move(button), key_event, rewritten_events);
}
ui::EventDispatchDetails details;
for (const auto& rewritten_event : rewritten_events) {
details = SendEvent(continuation, rewritten_event.get());
}
return details;
}
void PeripheralCustomizationEventRewriter::UpdatePressedButtonMap(
mojom::ButtonPtr button,
const ui::Event& original_event,
const std::vector<std::unique_ptr<ui::Event>>& rewritten_events) {
// Scroll wheel events cannot affect other events modifiers since they do a
// full press/release sequence with the one event.
if (original_event.type() == ui::ET_MOUSEWHEEL) {
return;
}
DeviceIdButton device_id_button_key =
DeviceIdButton{original_event.source_device_id(), std::move(button)};
// If the button is released, the entry must be removed from the map.
if (original_event.type() == ui::ET_MOUSE_RELEASED ||
original_event.type() == ui::ET_KEY_RELEASED) {
device_button_to_flags_.erase(std::move(device_id_button_key));
return;
}
// For each rewritten event, combine the flags that need to be applied to
// correctly handle the newly pressed event. This matters when pressing a
// modifier or a key with a combo of modifiers or when holding a rewritten
// mouse button.
ui::EventFlags event_flags = 0;
for (const auto& rewritten_event : rewritten_events) {
if (!rewritten_event) {
continue;
}
if (rewritten_event->IsKeyEvent()) {
const auto& key_event = *rewritten_event->AsKeyEvent();
event_flags |= ConvertKeyCodeToFlags(key_event.key_code());
continue;
}
if (rewritten_event->IsMouseEvent()) {
const auto& mouse_event = *rewritten_event->AsMouseEvent();
event_flags |= mouse_event.changed_button_flags();
continue;
}
}
if (!event_flags) {
return;
}
// Add the entry to the map with the flags that must be applied to other
// events.
device_button_to_flags_.insert_or_assign(std::move(device_id_button_key),
event_flags);
}
ui::EventDispatchDetails
PeripheralCustomizationEventRewriter::RewriteMouseWheelEvent(
const ui::MouseWheelEvent& mouse_wheel_event,
const Continuation continuation) {
auto device_type_to_observe =
GetDeviceTypeToObserve(mouse_wheel_event.source_device_id());
if (device_type_to_observe) {
if (NotifyMouseWheelEventObserving(mouse_wheel_event,
*device_type_to_observe)) {
return DiscardEvent(continuation);
}
// Otherwise, the flags must be cleared for the remappable buttons so they
// do not affect the application while the mouse is meant to be observed.
std::unique_ptr<ui::Event> rewritten_event = CloneEvent(mouse_wheel_event);
const int remappable_flags =
GetRemappableMouseEventFlags(*device_type_to_observe);
rewritten_event->SetFlags(rewritten_event->flags() & ~remappable_flags);
if (rewritten_event->IsMouseEvent()) {
auto& rewritten_mouse_event = *rewritten_event->AsMouseEvent();
rewritten_mouse_event.set_changed_button_flags(
rewritten_mouse_event.changed_button_flags() & ~remappable_flags);
}
return SendEvent(continuation, rewritten_event.get());
}
// Clone event and remove the already remapped modifiers and use this as the
// "source" key event for the rest of the rewriting.
std::unique_ptr<ui::Event> original_event_with_modifiers_removed =
CloneEvent(mouse_wheel_event);
RemoveRemappedModifiers(*original_event_with_modifiers_removed);
std::vector<std::unique_ptr<ui::Event>> rewritten_events;
mojom::ButtonPtr button = GetButtonFromMouseWheelEvent(mouse_wheel_event);
bool updated_button_map = false;
if (!button.is_null()) {
if (RewriteEventFromButton(*original_event_with_modifiers_removed, *button,
rewritten_events)) {
return DiscardEvent(continuation);
}
}
const bool event_rewritten = !rewritten_events.empty();
// Add an event to our list to rewrite based on other pressed buttons.
if (!event_rewritten) {
rewritten_events.push_back(
std::move(original_event_with_modifiers_removed));
}
// If the button was released, the pressed button map must be updated before
// applying remapped modifiers.
const ui::Event& last_rewritten_event = *rewritten_events.back();
if (event_rewritten &&
(last_rewritten_event.type() == ui::ET_MOUSE_RELEASED ||
last_rewritten_event.type() == ui::ET_KEY_RELEASED)) {
updated_button_map = true;
UpdatePressedButtonMap(std::move(button), mouse_wheel_event,
rewritten_events);
}
for (const auto& rewritten_event : rewritten_events) {
ApplyRemappedModifiers(*rewritten_event);
}
if (event_rewritten && !updated_button_map) {
UpdatePressedButtonMap(std::move(button), mouse_wheel_event,
rewritten_events);
}
ui::EventDispatchDetails details;
for (const auto& rewritten_event : rewritten_events) {
details = SendEvent(continuation, rewritten_event.get());
}
return details;
}
ui::EventDispatchDetails
PeripheralCustomizationEventRewriter::RewriteMouseEvent(
const ui::MouseEvent& mouse_event,
const Continuation continuation) {
auto device_type_to_observe =
GetDeviceTypeToObserve(mouse_event.source_device_id());
if (device_type_to_observe) {
if (NotifyMouseEventObserving(mouse_event, *device_type_to_observe)) {
return DiscardEvent(continuation);
}
// Otherwise, the flags must be cleared for the remappable buttons so they
// do not affect the application while the mouse is meant to be observed.
std::unique_ptr<ui::Event> rewritten_event = CloneEvent(mouse_event);
const int remappable_flags =
GetRemappableMouseEventFlags(*device_type_to_observe);
rewritten_event->SetFlags(rewritten_event->flags() & ~remappable_flags);
if (rewritten_event->IsMouseEvent()) {
auto& rewritten_mouse_event = *rewritten_event->AsMouseEvent();
rewritten_mouse_event.set_changed_button_flags(
rewritten_mouse_event.changed_button_flags() & ~remappable_flags);
}
return SendEvent(continuation, rewritten_event.get());
}
// Clone event and remove the already remapped modifiers and use this as the
// "source" key event for the rest of the rewriting.
std::unique_ptr<ui::Event> original_event_with_modifiers_removed =
CloneEvent(mouse_event);
RemoveRemappedModifiers(*original_event_with_modifiers_removed);
std::vector<std::unique_ptr<ui::Event>> rewritten_events;
mojom::ButtonPtr button;
bool updated_button_map = false;
if (IsMouseButtonEvent(mouse_event) && mouse_event.changed_button_flags()) {
button = GetButtonFromMouseEvent(mouse_event);
if (RewriteEventFromButton(*original_event_with_modifiers_removed, *button,
rewritten_events)) {
return DiscardEvent(continuation);
}
}
const bool event_rewritten = !rewritten_events.empty();
// Add an event to our list to rewrite based on other pressed buttons.
if (!event_rewritten) {
rewritten_events.push_back(
std::move(original_event_with_modifiers_removed));
}
// If the button was released, the pressed button map must be updated before
// applying remapped modifiers.
const ui::Event& last_rewritten_event = *rewritten_events.back();
if (event_rewritten &&
(last_rewritten_event.type() == ui::ET_MOUSE_RELEASED ||
last_rewritten_event.type() == ui::ET_KEY_RELEASED)) {
updated_button_map = true;
UpdatePressedButtonMap(std::move(button), mouse_event, rewritten_events);
}
for (const auto& rewritten_event : rewritten_events) {
ApplyRemappedModifiers(*rewritten_event);
}
if (event_rewritten && !updated_button_map) {
UpdatePressedButtonMap(std::move(button), mouse_event, rewritten_events);
}
ui::EventDispatchDetails details;
for (const auto& rewritten_event : rewritten_events) {
details = SendEvent(continuation, rewritten_event.get());
}
return details;
}
ui::EventDispatchDetails PeripheralCustomizationEventRewriter::RewriteEvent(
const ui::Event& event,
const Continuation continuation) {
DCHECK(features::IsPeripheralCustomizationEnabled() ||
::features::IsShortcutCustomizationEnabled());
if (event.IsMouseWheelEvent()) {
return RewriteMouseWheelEvent(*event.AsMouseWheelEvent(), continuation);
}
if (event.IsMouseEvent()) {
return RewriteMouseEvent(*event.AsMouseEvent(), continuation);
}
if (event.IsKeyEvent()) {
return RewriteKeyEvent(*event.AsKeyEvent(), continuation);
}
return SendEvent(continuation, &event);
}
std::optional<PeripheralCustomizationEventRewriter::RemappingActionResult>
PeripheralCustomizationEventRewriter::GetRemappingAction(
int device_id,
const mojom::Button& button) {
const auto* mouse_settings =
input_device_settings_controller_->GetMouseSettings(device_id);
if (mouse_settings) {
return GetRemappingActionFromMouseSettings(button, *mouse_settings);
}
const auto* graphics_tablet_settings =
input_device_settings_controller_->GetGraphicsTabletSettings(device_id);
if (graphics_tablet_settings) {
return GetRemappingActionFromGraphicsTabletSettings(
button, *graphics_tablet_settings);
}
return absl::nullopt;
}
void PeripheralCustomizationEventRewriter::RemoveRemappedModifiers(
ui::Event& event) {
int modifier_flags = 0;
if (const auto* mouse_settings =
input_device_settings_controller_->GetMouseSettings(
event.source_device_id());
mouse_settings) {
modifier_flags = GetRemappedModifiersFromMouseSettings(*mouse_settings);
} else if (const auto* graphics_tablet_settings =
input_device_settings_controller_->GetGraphicsTabletSettings(
event.source_device_id());
graphics_tablet_settings) {
modifier_flags = GetRemappedModifiersFromGraphicsTabletSettings(
*graphics_tablet_settings);
}
// TODO(dpad): This logic isn't quite correct. If a second devices is holding
// "Ctrl" and the original device has a button that is "Ctrl" that is
// remapped, this will behave incorrectly as it will remove "Ctrl". Instead,
// this needs to track what keys are being pressed by the device that have
// modifiers attached to them. For now, this is close enough to being correct.
event.SetFlags(event.flags() & ~modifier_flags);
}
void PeripheralCustomizationEventRewriter::ApplyRemappedModifiers(
ui::Event& event) {
int flags = 0;
for (const auto& [_, flag] : device_button_to_flags_) {
flags |= flag;
}
event.SetFlags(event.flags() | flags);
}
std::unique_ptr<ui::Event> PeripheralCustomizationEventRewriter::CloneEvent(
const ui::Event& event) {
std::unique_ptr<ui::Event> cloned_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 `mouse_event`.
SetNativeEvent(*cloned_event, event.native_event());
return cloned_event;
}
} // namespace ash