blob: 7273f4b6ac9192603bf1680b918a98dcceb25587 [file] [log] [blame]
// Copyright 2017 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 "chrome/browser/chromeos/accessibility/select_to_speak_event_rewriter.h"
#include <memory>
#include <string>
#include <utility>
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "chrome/browser/chromeos/accessibility/accessibility_manager.h"
#include "chrome/browser/chromeos/accessibility/event_handler_common.h"
#include "chrome/common/extensions/extension_constants.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_host.h"
#include "third_party/WebKit/public/platform/WebMouseEvent.h"
#include "ui/aura/client/screen_position_client.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/content_accelerators/accelerator_util.h"
#include "ui/display/display.h"
#include "ui/events/blink/web_input_event.h"
#include "ui/events/event.h"
#include "ui/events/event_sink.h"
namespace {
gfx::PointF GetScreenLocationFromEvent(const ui::LocatedEvent& event) {
aura::Window* root =
static_cast<aura::Window*>(event.target())->GetRootWindow();
aura::client::ScreenPositionClient* spc =
aura::client::GetScreenPositionClient(root);
if (!spc)
return event.root_location_f();
gfx::PointF screen_location(event.root_location_f());
spc->ConvertPointToScreen(root, &screen_location);
return screen_location;
}
} // namespace
const ui::KeyboardCode kSpeakSelectionKey = ui::VKEY_S;
SelectToSpeakEventRewriter::SelectToSpeakEventRewriter(
aura::Window* root_window)
: root_window_(root_window) {
DCHECK(root_window_);
}
SelectToSpeakEventRewriter::~SelectToSpeakEventRewriter() = default;
void SelectToSpeakEventRewriter::CaptureForwardedEventsForTesting(
SelectToSpeakEventDelegateForTesting* delegate) {
event_delegate_for_testing_ = delegate;
}
bool SelectToSpeakEventRewriter::IsSelectToSpeakEnabled() {
if (event_delegate_for_testing_)
return true;
return chromeos::AccessibilityManager::Get()->IsSelectToSpeakEnabled();
}
bool SelectToSpeakEventRewriter::OnKeyEvent(const ui::KeyEvent* event) {
DCHECK(event);
// We can only call TtsController on the UI thread, make sure we
// don't ever try to run this code on some other thread.
CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
ui::KeyboardCode key_code = event->key_code();
bool cancel_event = false;
// Update the state when pressing and releasing the Search key (VKEY_LWIN).
if (key_code == ui::VKEY_LWIN) {
if (event->type() == ui::ET_KEY_PRESSED && state_ == INACTIVE) {
state_ = SEARCH_DOWN;
} else if (event->type() == ui::ET_KEY_RELEASED) {
if (state_ == CAPTURING_MOUSE) {
cancel_event = true;
state_ = WAIT_FOR_MOUSE_RELEASE;
} else if (state_ == MOUSE_RELEASED) {
cancel_event = true;
state_ = INACTIVE;
} else if (state_ == CAPTURING_SPEAK_SELECTION_KEY) {
cancel_event = true;
state_ = WAIT_FOR_SPEAK_SELECTION_KEY_RELEASE;
} else if (state_ == SPEAK_SELECTION_KEY_RELEASED) {
cancel_event = true;
state_ = INACTIVE;
} else if (state_ == SEARCH_DOWN) {
// They just tapped the search key without clicking the mouse.
// Don't cancel this event -- the search key may still be used
// by another part of Chrome, and we didn't use it here.
state_ = INACTIVE;
}
}
} else if (key_code == kSpeakSelectionKey) {
if (event->type() == ui::ET_KEY_PRESSED &&
(state_ == SEARCH_DOWN || state_ == SPEAK_SELECTION_KEY_RELEASED)) {
// They pressed the S key while search was down.
// It's possible to press the selection key multiple times to read
// the same region over and over, so state S_RELEASED can become state
// CAPTURING_SPEAK_SELECTION_KEY if the search key is not lifted.
cancel_event = true;
state_ = CAPTURING_SPEAK_SELECTION_KEY;
} else if (event->type() == ui::ET_KEY_RELEASED) {
if (state_ == CAPTURING_SPEAK_SELECTION_KEY) {
// They released the speak selection key while it was being captured.
cancel_event = true;
state_ = SPEAK_SELECTION_KEY_RELEASED;
} else if (state_ == WAIT_FOR_SPEAK_SELECTION_KEY_RELEASE) {
// They have already released the search key
cancel_event = true;
state_ = INACTIVE;
}
}
} else if (state_ == SEARCH_DOWN) {
state_ = INACTIVE;
}
// Forward the key to the extension.
extensions::ExtensionHost* host = chromeos::GetAccessibilityExtensionHost(
extension_misc::kSelectToSpeakExtensionId);
if (host)
chromeos::ForwardKeyToExtension(*event, host);
return cancel_event;
}
bool SelectToSpeakEventRewriter::OnMouseEvent(const ui::MouseEvent* event) {
DCHECK(event);
if (state_ == INACTIVE)
return false;
if ((state_ == SEARCH_DOWN || state_ == MOUSE_RELEASED) &&
event->type() == ui::ET_MOUSE_PRESSED) {
state_ = CAPTURING_MOUSE;
}
if (state_ == WAIT_FOR_MOUSE_RELEASE &&
event->type() == ui::ET_MOUSE_RELEASED) {
state_ = INACTIVE;
return false;
}
if (state_ != CAPTURING_MOUSE)
return false;
if (event->type() == ui::ET_MOUSE_RELEASED)
state_ = MOUSE_RELEASED;
ui::MouseEvent mutable_event(*event);
ConvertMouseEventToDIPs(&mutable_event);
// If we're in the capturing mouse state, forward the mouse event to
// select-to-speak.
if (event_delegate_for_testing_) {
event_delegate_for_testing_->OnForwardEventToSelectToSpeakExtension(
mutable_event);
} else {
extensions::ExtensionHost* host = chromeos::GetAccessibilityExtensionHost(
extension_misc::kSelectToSpeakExtensionId);
if (!host)
return false;
content::RenderViewHost* rvh = host->render_view_host();
if (!rvh)
return false;
const blink::WebMouseEvent web_event = ui::MakeWebMouseEvent(
mutable_event, base::Bind(&GetScreenLocationFromEvent));
rvh->GetWidget()->ForwardMouseEvent(web_event);
}
return true;
}
void SelectToSpeakEventRewriter::ConvertMouseEventToDIPs(
ui::MouseEvent* mouse_event) {
// The event is in Pixels, and needs to be scaled to DIPs.
gfx::Point location = mouse_event->location();
gfx::Point root_location = mouse_event->root_location();
root_window_->GetHost()->ConvertPixelsToDIP(&location);
root_window_->GetHost()->ConvertPixelsToDIP(&root_location);
mouse_event->set_location(location);
mouse_event->set_root_location(root_location);
}
ui::EventRewriteStatus SelectToSpeakEventRewriter::RewriteEvent(
const ui::Event& event,
std::unique_ptr<ui::Event>* new_event) {
if (!IsSelectToSpeakEnabled())
return ui::EVENT_REWRITE_CONTINUE;
if (event.type() == ui::ET_KEY_PRESSED ||
event.type() == ui::ET_KEY_RELEASED) {
const ui::KeyEvent key_event = static_cast<const ui::KeyEvent&>(event);
if (OnKeyEvent(&key_event))
return ui::EVENT_REWRITE_DISCARD;
}
if (event.type() == ui::ET_MOUSE_PRESSED ||
event.type() == ui::ET_MOUSE_DRAGGED ||
event.type() == ui::ET_MOUSE_RELEASED ||
event.type() == ui::ET_MOUSE_MOVED) {
const ui::MouseEvent mouse_event =
static_cast<const ui::MouseEvent&>(event);
if (OnMouseEvent(&mouse_event) && (event.type() == ui::ET_MOUSE_PRESSED ||
event.type() == ui::ET_MOUSE_RELEASED)) {
// Cancel only click events if they were consumed by Select-to-Speak.
// Mouse move and drag should still happen or the mouse cursor may
// not be drawn in the right place.
return ui::EVENT_REWRITE_DISCARD;
}
}
return ui::EVENT_REWRITE_CONTINUE;
}
ui::EventRewriteStatus SelectToSpeakEventRewriter::NextDispatchEvent(
const ui::Event& last_event,
std::unique_ptr<ui::Event>* new_event) {
return ui::EVENT_REWRITE_CONTINUE;
}