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 /** @private { AutomationNode } */ |
| 53 this.node_ = null; |
| 54 |
| 55 /** @private { boolean } */ |
| 56 this.down_ = false; |
| 57 |
| 58 /** @private {{x: number, y: number}} */ |
| 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 |