1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ash/events/select_to_speak_event_handler.h"
6
7 #include "ash/accessibility/accessibility_controller_impl.h"
8 #include "ash/public/cpp/select_to_speak_event_handler_delegate.h"
9 #include "ash/shell.h"
10
11 namespace ash {
12
13 const ui::KeyboardCode kSpeakSelectionKey = ui::VKEY_S;
14
SelectToSpeakEventHandler(SelectToSpeakEventHandlerDelegate * delegate)15 SelectToSpeakEventHandler::SelectToSpeakEventHandler(
16 SelectToSpeakEventHandlerDelegate* delegate)
17 : delegate_(delegate) {
18 DCHECK(delegate_);
19 Shell::Get()->AddPreTargetHandler(this,
20 ui::EventTarget::Priority::kAccessibility);
21 }
22
~SelectToSpeakEventHandler()23 SelectToSpeakEventHandler::~SelectToSpeakEventHandler() {
24 Shell::Get()->RemovePreTargetHandler(this);
25 }
26
IsSelectToSpeakEnabled()27 bool SelectToSpeakEventHandler::IsSelectToSpeakEnabled() {
28 return Shell::Get()->accessibility_controller()->select_to_speak().enabled();
29 }
30
SetSelectToSpeakStateSelecting(bool is_selecting)31 void SelectToSpeakEventHandler::SetSelectToSpeakStateSelecting(
32 bool is_selecting) {
33 if (is_selecting && state_ == INACTIVE) {
34 // The extension has requested that it enter SELECTING state, and we
35 // aren't already in a SELECTING state. Prepare to start capturing events
36 // from stylus, mouse or touch.
37 // If we are already in any state besides INACTIVE then there is no
38 // work that needs to be done.
39 state_ = SELECTION_REQUESTED;
40 } else if (!is_selecting) {
41 // If we were using search + mouse, continue to wait for the search key
42 // up event by not resetting the state to INACTIVE.
43 if (state_ != MOUSE_RELEASED)
44 state_ = INACTIVE;
45 touch_id_ = ui::kPointerIdUnknown;
46 touch_type_ = ui::EventPointerType::kUnknown;
47 }
48 }
49
OnKeyEvent(ui::KeyEvent * event)50 void SelectToSpeakEventHandler::OnKeyEvent(ui::KeyEvent* event) {
51 DCHECK(IsSelectToSpeakEnabled());
52 DCHECK(event);
53
54 ui::KeyboardCode key_code = event->key_code();
55 bool cancel_event = false;
56
57 // Update the state when pressing and releasing the Search key (VKEY_LWIN).
58 if (key_code == ui::VKEY_LWIN) {
59 if (event->type() == ui::ET_KEY_PRESSED && state_ == INACTIVE) {
60 state_ = SEARCH_DOWN;
61 } else if (event->type() == ui::ET_KEY_RELEASED) {
62 if (state_ == CAPTURING_MOUSE) {
63 cancel_event = true;
64 state_ = WAIT_FOR_MOUSE_RELEASE;
65 } else if (state_ == MOUSE_RELEASED) {
66 cancel_event = true;
67 state_ = INACTIVE;
68 } else if (state_ == CAPTURING_SPEAK_SELECTION_KEY) {
69 cancel_event = true;
70 state_ = WAIT_FOR_SPEAK_SELECTION_KEY_RELEASE;
71 } else if (state_ == SPEAK_SELECTION_KEY_RELEASED) {
72 cancel_event = true;
73 state_ = INACTIVE;
74 } else if (state_ == SEARCH_DOWN) {
75 // They just tapped the search key without clicking the mouse.
76 // Don't cancel this event -- the search key may still be used
77 // by another part of Chrome, and we didn't use it here.
78 state_ = INACTIVE;
79 }
80 }
81 } else if (key_code == kSpeakSelectionKey) {
82 if (event->type() == ui::ET_KEY_PRESSED &&
83 (state_ == SEARCH_DOWN || state_ == SPEAK_SELECTION_KEY_RELEASED)) {
84 // They pressed the S key while search was down.
85 // It's possible to press the selection key multiple times to read
86 // the same region over and over, so state S_RELEASED can become state
87 // CAPTURING_SPEAK_SELECTION_KEY if the search key is not lifted.
88 cancel_event = true;
89 state_ = CAPTURING_SPEAK_SELECTION_KEY;
90 } else if (event->type() == ui::ET_KEY_RELEASED) {
91 if (state_ == CAPTURING_SPEAK_SELECTION_KEY) {
92 // They released the speak selection key while it was being captured.
93 cancel_event = true;
94 state_ = SPEAK_SELECTION_KEY_RELEASED;
95 } else if (state_ == WAIT_FOR_SPEAK_SELECTION_KEY_RELEASE) {
96 // They have already released the search key
97 cancel_event = true;
98 state_ = INACTIVE;
99 }
100 }
101 } else if (state_ == SEARCH_DOWN) {
102 state_ = INACTIVE;
103 }
104
105 // Forward the key to the chrome process for the extension.
106 delegate_->DispatchKeyEvent(*event);
107
108 if (cancel_event)
109 CancelEvent(event);
110 }
111
OnMouseEvent(ui::MouseEvent * event)112 void SelectToSpeakEventHandler::OnMouseEvent(ui::MouseEvent* event) {
113 DCHECK(IsSelectToSpeakEnabled());
114 DCHECK(event);
115 if (state_ == INACTIVE)
116 return;
117
118 if (event->type() == ui::ET_MOUSE_PRESSED) {
119 if (state_ == SEARCH_DOWN || state_ == MOUSE_RELEASED)
120 state_ = CAPTURING_MOUSE;
121 else if (state_ == SELECTION_REQUESTED)
122 state_ = CAPTURING_MOUSE_ONLY;
123 }
124
125 if (state_ == WAIT_FOR_MOUSE_RELEASE &&
126 event->type() == ui::ET_MOUSE_RELEASED) {
127 state_ = INACTIVE;
128 return;
129 }
130
131 // Only forward the event to the extension if we are capturing mouse
132 // events.
133 if (state_ != CAPTURING_MOUSE && state_ != CAPTURING_MOUSE_ONLY)
134 return;
135
136 if (event->type() == ui::ET_MOUSE_RELEASED) {
137 if (state_ == CAPTURING_MOUSE)
138 state_ = MOUSE_RELEASED;
139 else if (state_ == CAPTURING_MOUSE_ONLY)
140 state_ = INACTIVE;
141 }
142
143 delegate_->DispatchMouseEvent(*event);
144
145 if (event->type() == ui::ET_MOUSE_PRESSED ||
146 event->type() == ui::ET_MOUSE_RELEASED)
147 CancelEvent(event);
148 }
149
OnTouchEvent(ui::TouchEvent * event)150 void SelectToSpeakEventHandler::OnTouchEvent(ui::TouchEvent* event) {
151 DCHECK(IsSelectToSpeakEnabled());
152 DCHECK(event);
153 // Only capture touch events if selection was requested or we are capturing
154 // touch events already.
155 if (state_ != SELECTION_REQUESTED && state_ != CAPTURING_TOUCH_ONLY)
156 return;
157
158 // On a touch-down event, if selection was requested, we begin capturing
159 // touch events.
160 if (event->type() == ui::ET_TOUCH_PRESSED && state_ == SELECTION_REQUESTED &&
161 touch_id_ == ui::kPointerIdUnknown) {
162 state_ = CAPTURING_TOUCH_ONLY;
163 touch_id_ = event->pointer_details().id;
164 touch_type_ = event->pointer_details().pointer_type;
165 }
166
167 if (touch_id_ != event->pointer_details().id ||
168 touch_type_ != event->pointer_details().pointer_type) {
169 // If this was a different pointer, cancel the event and return early.
170 // We only want to track one touch pointer at a time.
171 CancelEvent(event);
172 return;
173 }
174
175 // On a touch-up event, we go back to inactive state, but still forward the
176 // event to the extension.
177 if (event->type() == ui::ET_TOUCH_RELEASED &&
178 state_ == CAPTURING_TOUCH_ONLY) {
179 state_ = INACTIVE;
180 touch_id_ = ui::kPointerIdUnknown;
181 touch_type_ = ui::EventPointerType::kUnknown;
182 }
183
184 // Create a mouse event to send to the extension, describing the touch.
185 // This is done because there is no RenderWidgetHost::ForwardTouchEvent,
186 // and we already have mouse event plumbing in place for Select-to-Speak.
187 ui::EventType type;
188 switch (event->type()) {
189 case ui::ET_TOUCH_PRESSED:
190 type = ui::ET_MOUSE_PRESSED;
191 break;
192 case ui::ET_TOUCH_RELEASED:
193 case ui::ET_TOUCH_CANCELLED:
194 type = ui::ET_MOUSE_RELEASED;
195 break;
196 case ui::ET_TOUCH_MOVED:
197 type = ui::ET_MOUSE_DRAGGED;
198 break;
199 default:
200 return;
201 }
202 int flags = ui::EF_LEFT_MOUSE_BUTTON;
203 ui::MouseEvent event_to_send(type, event->location(), event->root_location(),
204 event->time_stamp(), flags, flags);
205
206 delegate_->DispatchMouseEvent(event_to_send);
207
208 if (event->type() != ui::ET_TOUCH_MOVED) {
209 // Don't cancel move events in case focus needs to change.
210 CancelEvent(event);
211 }
212 }
213
CancelEvent(ui::Event * event)214 void SelectToSpeakEventHandler::CancelEvent(ui::Event* event) {
215 DCHECK(event);
216 if (event->cancelable()) {
217 event->SetHandled();
218 event->StopPropagation();
219 }
220 }
221
222 } // namespace ash
223