blob: a5c06a6ea50f795b4ce8b913632fbd06feea430f [file] [log] [blame]
// Copyright 2020 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 "ash/events/accessibility_event_rewriter.h"
#include "ash/accessibility/accessibility_controller_impl.h"
#include "ash/accessibility/point_scan_controller.h"
#include "ash/keyboard/keyboard_util.h"
#include "ash/magnifier/magnification_controller.h"
#include "ash/public/cpp/accessibility_event_rewriter_delegate.h"
#include "ash/shell.h"
#include "ui/chromeos/events/event_rewriter_chromeos.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/events/types/event_type.h"
namespace ash {
namespace {
// Returns a ui::InputDeviceType given a Switch Access string device type.
ui::InputDeviceType GetInputDeviceType(
const std::string& switch_access_device_type) {
if (switch_access_device_type == kSwitchAccessInternalDevice)
return ui::INPUT_DEVICE_INTERNAL;
if (switch_access_device_type == kSwitchAccessUsbDevice)
return ui::INPUT_DEVICE_USB;
if (switch_access_device_type == kSwitchAccessBluetoothDevice)
return ui::INPUT_DEVICE_BLUETOOTH;
NOTREACHED();
return ui::INPUT_DEVICE_UNKNOWN;
}
} // namespace
AccessibilityEventRewriter::AccessibilityEventRewriter(
ui::EventRewriterChromeOS* event_rewriter_chromeos,
AccessibilityEventRewriterDelegate* delegate)
: delegate_(delegate), event_rewriter_chromeos_(event_rewriter_chromeos) {
Shell::Get()->accessibility_controller()->SetAccessibilityEventRewriter(this);
}
AccessibilityEventRewriter::~AccessibilityEventRewriter() {
Shell::Get()->accessibility_controller()->SetAccessibilityEventRewriter(
nullptr);
}
void AccessibilityEventRewriter::OnUnhandledSpokenFeedbackEvent(
std::unique_ptr<ui::Event> event) const {
DCHECK(event->IsKeyEvent()) << "Unexpected unhandled event type";
// Send the event to the continuation for the most recent event rewritten by
// ChromeVox, (that is, through its EventSource). Under the assumption that a
// single AccessibilityEventRewriter is not registered to multiple
// EventSources, this will be the same as this event's original source.
const char* failure_reason = nullptr;
if (chromevox_continuation_) {
ui::EventDispatchDetails details =
SendEvent(chromevox_continuation_, event.get());
if (details.dispatcher_destroyed)
failure_reason = "destroyed dispatcher";
else if (details.target_destroyed)
failure_reason = "destroyed target";
} else if (chromevox_continuation_.WasInvalidated()) {
failure_reason = "destroyed source";
} else {
failure_reason = "no prior rewrite";
}
if (failure_reason) {
VLOG(0) << "Undispatched key " << event->AsKeyEvent()->key_code()
<< " due to " << failure_reason << ".";
}
}
void AccessibilityEventRewriter::SetKeyCodesForSwitchAccessCommand(
const std::map<int, std::set<std::string>>& key_codes,
SwitchAccessCommand command) {
// Remove all keys for the command.
for (auto it = key_code_to_switch_access_command_.begin();
it != key_code_to_switch_access_command_.end();) {
if (it->second == command) {
switch_access_key_codes_to_capture_.erase(it->first);
it = key_code_to_switch_access_command_.erase(it);
} else {
it++;
}
}
for (const auto& key_code : key_codes) {
// Remove any preexisting key.
switch_access_key_codes_to_capture_.erase(key_code.first);
key_code_to_switch_access_command_.erase(key_code.first);
// Map device types from Switch Access's internal representation.
std::set<ui::InputDeviceType> device_types;
for (const std::string& switch_access_device : key_code.second)
device_types.insert(GetInputDeviceType(switch_access_device));
switch_access_key_codes_to_capture_.insert({key_code.first, device_types});
key_code_to_switch_access_command_.insert({key_code.first, command});
}
// Conflict resolution occurs up the stack (e.g. in the settings pages for
// Switch Access).
}
bool AccessibilityEventRewriter::RewriteEventForChromeVox(
const ui::Event& event,
const Continuation continuation) {
// Save continuation for |OnUnhandledSpokenFeedbackEvent()|.
chromevox_continuation_ = continuation;
if (!Shell::Get()->accessibility_controller()->spoken_feedback().enabled()) {
return false;
}
if (event.IsKeyEvent()) {
const ui::KeyEvent* key_event = event.AsKeyEvent();
ui::EventRewriterChromeOS::MutableKeyState state(key_event);
event_rewriter_chromeos_->RewriteModifierKeys(*key_event, &state);
// Remove the Search modifier before asking for function keys to be
// rewritten, then restore the flags. This allows ChromeVox to receive keys
// mappings for raw f1-f12 as e.g. back, but also Search+f1-f12 as
// Search+back (rather than just f1-f12).
int original_flags = state.flags;
state.flags = original_flags & ~ui::EF_COMMAND_DOWN;
event_rewriter_chromeos_->RewriteFunctionKeys(*key_event, &state);
state.flags = original_flags;
std::unique_ptr<ui::Event> rewritten_event;
ui::EventRewriterChromeOS::BuildRewrittenKeyEvent(*key_event, state,
&rewritten_event);
const ui::KeyEvent* rewritten_key_event =
rewritten_event.get()->AsKeyEvent();
bool capture = chromevox_capture_all_keys_;
// Always capture the Search key.
capture |= rewritten_key_event->IsCommandDown() ||
rewritten_key_event->key_code() == ui::VKEY_LWIN;
// Don't capture tab as it gets consumed by Blink so never comes back
// unhandled. In third_party/WebKit/Source/core/input/EventHandler.cpp, a
// default tab handler consumes tab even when no focusable nodes are found;
// it sets focus to Chrome and eats the event.
if (rewritten_key_event->GetDomKey() == ui::DomKey::TAB)
capture = false;
delegate_->DispatchKeyEventToChromeVox(
ui::Event::Clone(*rewritten_key_event), capture);
return capture;
}
return false;
}
bool AccessibilityEventRewriter::RewriteEventForSwitchAccess(
const ui::Event& event,
const Continuation continuation) {
if (!event.IsKeyEvent() || suspend_switch_access_key_handling_)
return false;
const ui::KeyEvent* key_event = event.AsKeyEvent();
const auto& key =
switch_access_key_codes_to_capture_.find(key_event->key_code());
if (key == switch_access_key_codes_to_capture_.end())
return false;
int source_device_id = key_event->source_device_id();
ui::InputDeviceType keyboard_type = ui::INPUT_DEVICE_UNKNOWN;
for (const auto& keyboard :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
if (source_device_id == keyboard.id) {
keyboard_type = keyboard.type;
break;
}
}
// An unknown |source_device_id| needs to pass this check as it's set that way
// in tests.
if (source_device_id != ui::ED_UNKNOWN_DEVICE &&
key->second.count(keyboard_type) == 0) {
return false;
}
if (key_event->type() == ui::ET_KEY_PRESSED) {
AccessibilityControllerImpl* accessibility_controller =
Shell::Get()->accessibility_controller();
if (accessibility_controller->IsPointScanEnabled()) {
PointScanController* point_scan_controller =
accessibility_controller->GetPointScanController();
base::Optional<gfx::PointF> point =
point_scan_controller->OnPointSelect();
if (point.has_value()) {
delegate_->SendPointScanPoint(point.value());
}
} else {
SwitchAccessCommand command =
key_code_to_switch_access_command_[key_event->key_code()];
delegate_->SendSwitchAccessCommand(command);
}
}
return true;
}
bool AccessibilityEventRewriter::RewriteEventForMagnifier(
const ui::Event& event,
const Continuation continuation) {
if (!event.IsKeyEvent())
return false;
const ui::KeyEvent* key_event = event.AsKeyEvent();
if (!keyboard_util::IsArrowKeyCode(key_event->key_code()) ||
!key_event->IsControlDown() || !key_event->IsAltDown()) {
return false;
}
if (key_event->type() == ui::ET_KEY_PRESSED) {
// If first time key is pressed (e.g. not repeat), start scrolling.
if (!(key_event->flags() & ui::EF_IS_REPEAT))
OnMagnifierKeyPressed(key_event);
// Either way (first or repeat), capture key press.
return true;
}
if (key_event->type() == ui::ET_KEY_RELEASED) {
OnMagnifierKeyReleased(key_event);
return true;
}
return false;
}
void AccessibilityEventRewriter::OnMagnifierKeyPressed(
const ui::KeyEvent* event) {
MagnificationController* controller =
Shell::Get()->magnification_controller();
switch (event->key_code()) {
case ui::VKEY_UP:
controller->SetScrollDirection(MagnificationController::SCROLL_UP);
delegate_->SendMagnifierCommand(MagnifierCommand::kMoveUp);
break;
case ui::VKEY_DOWN:
controller->SetScrollDirection(MagnificationController::SCROLL_DOWN);
delegate_->SendMagnifierCommand(MagnifierCommand::kMoveDown);
break;
case ui::VKEY_LEFT:
controller->SetScrollDirection(MagnificationController::SCROLL_LEFT);
delegate_->SendMagnifierCommand(MagnifierCommand::kMoveLeft);
break;
case ui::VKEY_RIGHT:
controller->SetScrollDirection(MagnificationController::SCROLL_RIGHT);
delegate_->SendMagnifierCommand(MagnifierCommand::kMoveRight);
break;
default:
NOTREACHED() << "Unexpected keyboard_code:" << event->key_code();
}
}
void AccessibilityEventRewriter::OnMagnifierKeyReleased(
const ui::KeyEvent* event) {
MagnificationController* controller =
Shell::Get()->magnification_controller();
controller->SetScrollDirection(MagnificationController::SCROLL_NONE);
delegate_->SendMagnifierCommand(MagnifierCommand::kMoveStop);
}
void AccessibilityEventRewriter::MaybeSendMouseEvent(const ui::Event& event) {
// Mouse moves are the only pertinent event for accessibility component
// extensions.
if (send_mouse_events_ && event.type() == ui::ET_MOUSE_MOVED &&
(Shell::Get()->magnification_controller()->IsEnabled() ||
Shell::Get()->accessibility_controller()->spoken_feedback().enabled())) {
delegate_->DispatchMouseEvent(ui::Event::Clone(event));
}
}
ui::EventDispatchDetails AccessibilityEventRewriter::RewriteEvent(
const ui::Event& event,
const Continuation continuation) {
bool captured = false;
if (!delegate_)
return SendEvent(continuation, &event);
if (Shell::Get()->accessibility_controller()->IsSwitchAccessRunning()) {
captured = RewriteEventForSwitchAccess(event, continuation);
}
if (!captured && Shell::Get()
->accessibility_controller()
->fullscreen_magnifier()
.enabled()) {
captured = RewriteEventForMagnifier(event, continuation);
}
if (!captured) {
captured = RewriteEventForChromeVox(event, continuation);
}
if (!captured) {
MaybeSendMouseEvent(event);
}
return captured ? DiscardEvent(continuation)
: SendEvent(continuation, &event);
}
} // namespace ash