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