| // Copyright 2020 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/accessibility_event_rewriter.h" |
| |
| #include "ash/accessibility/accessibility_controller.h" |
| #include "ash/accessibility/magnifier/docked_magnifier_controller.h" |
| #include "ash/accessibility/magnifier/fullscreen_magnifier_controller.h" |
| #include "ash/accessibility/switch_access/point_scan_controller.h" |
| #include "ash/constants/ash_constants.h" |
| #include "ash/keyboard/keyboard_util.h" |
| #include "ash/public/cpp/accessibility_event_rewriter_delegate.h" |
| #include "ash/shell.h" |
| #include "base/system/sys_info.h" |
| #include "ui/events/ash/event_rewriter_ash.h" |
| #include "ui/events/devices/device_data_manager.h" |
| #include "ui/events/event.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/events/keycodes/dom/keycode_converter.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; |
| // On Chrome OS emulated on Linux, the keyboard is always "UNKNOWN". |
| if (base::SysInfo::IsRunningOnChromeOS()) |
| NOTREACHED(); |
| return ui::INPUT_DEVICE_UNKNOWN; |
| } |
| } // namespace |
| |
| AccessibilityEventRewriter::AccessibilityEventRewriter( |
| ui::EventRewriterAsh* event_rewriter_ash, |
| AccessibilityEventRewriterDelegate* delegate) |
| : delegate_(delegate), event_rewriter_ash_(event_rewriter_ash) { |
| Shell::Get()->accessibility_controller()->SetAccessibilityEventRewriter(this); |
| observation_.Observe(input_method::InputMethodManager::Get()); |
| // InputMethodManagerImpl::AddObserver calls our InputMethodChanged, so no |
| // further initialization needed. |
| } |
| |
| 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::EventRewriterAsh::MutableKeyState state(key_event); |
| |
| // On new rewriter sequence, modifiers are already rewritten before |
| // this rewriter. |
| if (!features::IsKeyboardRewriterFixEnabled()) { |
| event_rewriter_ash_->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_ash_->RewriteFunctionKeys(*key_event, &state); |
| state.flags = original_flags; |
| |
| std::unique_ptr<ui::Event> rewritten_event; |
| ui::EventRewriterAsh::BuildRewrittenKeyEvent(*key_event, state, |
| &rewritten_event); |
| ui::KeyEvent* rewritten_key_event = rewritten_event.get()->AsKeyEvent(); |
| |
| // Account for positional keys which we want to remap. |
| if (try_rewriting_positional_keys_for_chromevox_) { |
| const ui::KeyboardCode remapped_key_code = |
| ui::KeycodeConverter::MapPositionalDomCodeToUSShortcutKey( |
| key_event->code(), key_event->key_code()); |
| if (remapped_key_code != ui::VKEY_UNKNOWN) |
| rewritten_key_event->set_key_code(remapped_key_code); |
| } |
| |
| bool capture = chromevox_capture_all_keys_; |
| |
| // Always capture the Search key. |
| capture |= rewritten_key_event->IsCommandDown() || |
| rewritten_key_event->key_code() == ui::VKEY_LWIN || |
| rewritten_key_event->key_code() == ui::VKEY_RWIN; |
| |
| // 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(rewritten_key_event->Clone(), |
| 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(); |
| ui::EventRewriterAsh::MutableKeyState state(key_event); |
| // On new rewriter sequence, modifiers are already rewritten before |
| // this rewriter. |
| if (!features::IsKeyboardRewriterFixEnabled()) { |
| event_rewriter_ash_->RewriteModifierKeys(*key_event, &state); |
| } |
| event_rewriter_ash_->RewriteFunctionKeys(*key_event, &state); |
| |
| std::unique_ptr<ui::Event> rewritten_event; |
| ui::EventRewriterAsh::BuildRewrittenKeyEvent(*key_event, state, |
| &rewritten_event); |
| ui::KeyEvent* rewritten_key_event = rewritten_event.get()->AsKeyEvent(); |
| |
| const auto& key = |
| switch_access_key_codes_to_capture_.find(rewritten_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) { |
| AccessibilityController* accessibility_controller = |
| Shell::Get()->accessibility_controller(); |
| |
| if (accessibility_controller->IsPointScanEnabled()) { |
| PointScanController* point_scan_controller = |
| accessibility_controller->GetPointScanController(); |
| std::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_[rewritten_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) { |
| FullscreenMagnifierController* controller = |
| Shell::Get()->fullscreen_magnifier_controller(); |
| switch (event->key_code()) { |
| case ui::VKEY_UP: |
| controller->SetScrollDirection(FullscreenMagnifierController::SCROLL_UP); |
| delegate_->SendMagnifierCommand(MagnifierCommand::kMoveUp); |
| break; |
| case ui::VKEY_DOWN: |
| controller->SetScrollDirection( |
| FullscreenMagnifierController::SCROLL_DOWN); |
| delegate_->SendMagnifierCommand(MagnifierCommand::kMoveDown); |
| break; |
| case ui::VKEY_LEFT: |
| controller->SetScrollDirection( |
| FullscreenMagnifierController::SCROLL_LEFT); |
| delegate_->SendMagnifierCommand(MagnifierCommand::kMoveLeft); |
| break; |
| case ui::VKEY_RIGHT: |
| controller->SetScrollDirection( |
| FullscreenMagnifierController::SCROLL_RIGHT); |
| delegate_->SendMagnifierCommand(MagnifierCommand::kMoveRight); |
| break; |
| default: |
| NOTREACHED() << "Unexpected keyboard_code:" << event->key_code(); |
| } |
| } |
| |
| void AccessibilityEventRewriter::OnMagnifierKeyReleased( |
| const ui::KeyEvent* event) { |
| FullscreenMagnifierController* controller = |
| Shell::Get()->fullscreen_magnifier_controller(); |
| controller->SetScrollDirection(FullscreenMagnifierController::SCROLL_NONE); |
| delegate_->SendMagnifierCommand(MagnifierCommand::kMoveStop); |
| } |
| |
| void AccessibilityEventRewriter::MaybeSendMouseEvent(const ui::Event& event) { |
| // Mouse moves are the only pertinent event for accessibility component |
| // extensions. |
| AccessibilityController* accessibility_controller = |
| Shell::Get()->accessibility_controller(); |
| if (send_mouse_events_ && |
| (event.type() == ui::ET_MOUSE_MOVED || |
| event.type() == ui::ET_MOUSE_DRAGGED) && |
| (accessibility_controller->fullscreen_magnifier().enabled() || |
| accessibility_controller->docked_magnifier().enabled() || |
| accessibility_controller->spoken_feedback().enabled() || |
| accessibility_controller->face_gaze().enabled())) { |
| delegate_->DispatchMouseEvent(event.Clone()); |
| } |
| } |
| |
| 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); |
| } |
| |
| void AccessibilityEventRewriter::InputMethodChanged( |
| input_method::InputMethodManager* manager, |
| Profile* profile, |
| bool show_message) { |
| try_rewriting_positional_keys_for_chromevox_ = |
| manager->ArePositionalShortcutsUsedByCurrentInputMethod(); |
| } |
| |
| } // namespace ash |