OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2016 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 var AutomationEvent = chrome.automation.AutomationEvent; | |
6 var AutomationNode = chrome.automation.AutomationNode; | |
7 var EventType = chrome.automation.EventType; | |
8 var RoleType = chrome.automation.RoleType; | |
9 | |
10 /** | |
11 * Return the rect that encloses two points. | |
12 * @param {number} x1 The first x coordinate. | |
13 * @param {number} y1 The first y coordinate. | |
14 * @param {number} x2 The second x coordinate. | |
15 * @param {number} y2 The second x coordinate. | |
16 * @return {{left: number, top: number, width: number, height: number}} | |
17 */ | |
18 function rectFromPoints(x1, y1, x2, y2) { | |
19 var left = Math.min(x1, x2); | |
20 var right = Math.max(x1, x2); | |
21 var top = Math.min(y1, y2); | |
22 var bottom = Math.max(y1, y2); | |
23 return {left: left, | |
24 top: top, | |
25 width: right - left, | |
26 height: bottom - top}; | |
27 } | |
28 | |
29 /** | |
30 * Returns true if |rect1| and |rect2| overlap. The rects must define | |
31 * left, top, width, and height. | |
32 * @param {{left: number, top: number, width: number, height: number}} rect1 | |
33 * @param {{left: number, top: number, width: number, height: number}} rect2 | |
34 * @return {boolean} True if the rects overlap. | |
35 */ | |
36 function overlaps(rect1, rect2) { | |
37 var l1 = rect1.left; | |
38 var r1 = rect1.left + rect1.width; | |
39 var t1 = rect1.top; | |
40 var b1 = rect1.top + rect1.height; | |
41 var l2 = rect2.left; | |
42 var r2 = rect2.left + rect2.width; | |
43 var t2 = rect2.top; | |
44 var b2 = rect2.top + rect2.height; | |
45 return (l1 < r2 && r1 > l2 && t1 < b2 && b1 > t2); | |
46 } | |
47 | |
48 /** | |
49 * @constructor | |
50 */ | |
51 var SelectToSpeak = function() { | |
52 /** @type { AutomationNode } @private */ | |
53 this.node_ = null; | |
54 | |
55 /** @type { boolean } @prinivate */ | |
Dan Beam
2016/11/30 23:58:41
@private {boolean}
dmazzoni
2016/12/01 04:10:32
Thanks, didn't know about that shorthand.
| |
56 this.down_ = false; | |
57 | |
58 /** @type {{x: number, y: number}} @private */ | |
59 this.mouseStart_ = {x: 0, y: 0}; | |
60 | |
61 chrome.automation.getDesktop(function(desktop) { | |
62 desktop.addEventListener( | |
63 EventType.mousePressed, this.onMousePressed_.bind(this), true); | |
64 desktop.addEventListener( | |
65 EventType.mouseDragged, this.onMouseDragged_.bind(this), true); | |
66 desktop.addEventListener( | |
67 EventType.mouseReleased, this.onMouseReleased_.bind(this), true); | |
68 desktop.addEventListener( | |
69 EventType.mouseCanceled, this.onMouseCanceled_.bind(this), true); | |
70 }.bind(this)); | |
71 }; | |
72 | |
73 SelectToSpeak.prototype = { | |
74 /** | |
75 * Called when the mouse is pressed and the user is in a mode where | |
76 * select-to-speak is capturing mouse events (for example holding down | |
77 * Search). | |
78 * | |
79 * @param {!AutomationEvent} evt | |
80 */ | |
81 onMousePressed_: function(evt) { | |
82 this.down_ = true; | |
83 this.mouseStart_ = {x: evt.mouseX, y: evt.mouseY}; | |
84 this.startNode_ = evt.target; | |
85 chrome.tts.stop(); | |
86 this.onMouseDragged_(evt); | |
87 }, | |
88 | |
89 /** | |
90 * Called when the mouse is moved or dragged and the user is in a | |
91 * mode where select-to-speak is capturing mouse events (for example | |
92 * holding down Search). | |
93 * | |
94 * @param {!AutomationEvent} evt | |
95 */ | |
96 onMouseDragged_: function(evt) { | |
97 if (!this.down_) | |
98 return; | |
99 | |
100 var rect = rectFromPoints( | |
101 this.mouseStart_.x, this.mouseStart_.y, | |
102 evt.mouseX, evt.mouseY); | |
103 chrome.accessibilityPrivate.setFocusRing([rect]); | |
104 }, | |
105 | |
106 /** | |
107 * Called when the mouse is released and the user is in a | |
108 * mode where select-to-speak is capturing mouse events (for example | |
109 * holding down Search). | |
110 * | |
111 * @param {!AutomationEvent} evt | |
112 */ | |
113 onMouseReleased_: function(evt) { | |
114 this.onMouseDragged_(evt); | |
115 this.down_ = false; | |
116 | |
117 chrome.accessibilityPrivate.setFocusRing([]); | |
118 | |
119 // Walk up to the nearest window, web area, or dialog that the | |
120 // hit node is contained inside. Only speak objects within that | |
121 // container. In the future we might include other container-like | |
122 // roles here. | |
123 var root = this.startNode_; | |
124 while (root.parent && | |
125 root.role != RoleType.window && | |
126 root.role != RoleType.rootWebArea && | |
127 root.role != RoleType.desktop && | |
128 root.role != RoleType.dialog) { | |
129 root = root.parent; | |
130 } | |
131 | |
132 var rect = rectFromPoints( | |
133 this.mouseStart_.x, this.mouseStart_.y, | |
134 evt.mouseX, evt.mouseY); | |
135 var nodes = []; | |
136 this.findAllMatching_(root, rect, nodes); | |
137 this.startSpeechQueue_(nodes); | |
138 }, | |
139 | |
140 /** | |
141 * Called when the user cancels select-to-speak's capturing of mouse | |
142 * events (for example by releasing Search while the mouse is still down). | |
143 * | |
144 * @param {!AutomationEvent} evt | |
145 */ | |
146 onMouseCanceled_: function(evt) { | |
147 this.down_ = false; | |
148 chrome.accessibilityPrivate.setFocusRing([]); | |
149 chrome.tts.stop(); | |
150 }, | |
151 | |
152 /** | |
153 * Finds all nodes within the subtree rooted at |node| that overlap | |
154 * a given rectangle. | |
155 * @param {AutomationNode} node The starting node. | |
156 * @param {{left: number, top: number, width: number, height: number}} rect | |
157 * The bounding box to search. | |
158 * @param {Array<AutomationNode>} nodes The matching node array to be | |
159 * populated. | |
160 * @return {boolean} True if any matches are found. | |
161 */ | |
162 findAllMatching_: function(node, rect, nodes) { | |
163 var found = false; | |
164 for (var c = node.firstChild; c; c = c.nextSibling) { | |
165 if (this.findAllMatching_(c, rect, nodes)) | |
166 found = true; | |
167 } | |
168 | |
169 if (found) | |
170 return true; | |
171 | |
172 if (!node.name || !node.location) | |
173 return false; | |
174 | |
175 if (overlaps(node.location, rect)) { | |
176 nodes.push(node); | |
177 return true; | |
178 } | |
179 | |
180 return false; | |
181 }, | |
182 | |
183 /** | |
184 * Enqueue speech commands for all of the given nodes. | |
185 * @param {Array<AutomationNode>} nodes The nodes to speak. | |
186 */ | |
187 startSpeechQueue_: function(nodes) { | |
188 chrome.tts.stop(); | |
189 for (var i = 0; i < nodes.length; i++) { | |
190 var node = nodes[i]; | |
191 var isLast = (i == nodes.length - 1); | |
192 chrome.tts.speak(node.name, { | |
193 lang: 'en-US', | |
194 'enqueue': true, | |
195 onEvent: (function(node, isLast, event) { | |
196 if (event.type == 'start') { | |
197 chrome.accessibilityPrivate.setFocusRing([node.location]); | |
198 } else if (event.type == 'interrupted' || | |
199 event.type == 'cancelled') { | |
200 chrome.accessibilityPrivate.setFocusRing([]); | |
201 } else if (event.type == 'end') { | |
202 if (isLast) { | |
203 chrome.accessibilityPrivate.setFocusRing([]); | |
204 } | |
205 } | |
206 }).bind(this, node, isLast) | |
207 }); | |
208 } | |
209 } | |
210 }; | |
211 | |
212 new SelectToSpeak(); | |
OLD | NEW |