Chromium Code Reviews| 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 |