Index: chrome/browser/resources/chromeos/select_to_speak/select_to_speak.js |
diff --git a/chrome/browser/resources/chromeos/select_to_speak/select_to_speak.js b/chrome/browser/resources/chromeos/select_to_speak/select_to_speak.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..65cf06bacce01c096079728ffa2a3543eef6b7f2 |
--- /dev/null |
+++ b/chrome/browser/resources/chromeos/select_to_speak/select_to_speak.js |
@@ -0,0 +1,212 @@ |
+// Copyright 2016 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+var AutomationEvent = chrome.automation.AutomationEvent; |
+var AutomationNode = chrome.automation.AutomationNode; |
+var EventType = chrome.automation.EventType; |
+var RoleType = chrome.automation.RoleType; |
+ |
+/** |
+ * Return the rect that encloses two points. |
+ * @param {number} x1 The first x coordinate. |
+ * @param {number} y1 The first y coordinate. |
+ * @param {number} x2 The second x coordinate. |
+ * @param {number} y2 The second x coordinate. |
+ * @return {{left: number, top: number, width: number, height: number}} |
+ */ |
+function rectFromPoints(x1, y1, x2, y2) { |
+ var left = Math.min(x1, x2); |
+ var right = Math.max(x1, x2); |
+ var top = Math.min(y1, y2); |
+ var bottom = Math.max(y1, y2); |
+ return {left: left, |
+ top: top, |
+ width: right - left, |
+ height: bottom - top}; |
+} |
+ |
+/** |
+ * Returns true if |rect1| and |rect2| overlap. The rects must define |
+ * left, top, width, and height. |
+ * @param {{left: number, top: number, width: number, height: number}} rect1 |
+ * @param {{left: number, top: number, width: number, height: number}} rect2 |
+ * @return {boolean} True if the rects overlap. |
+ */ |
+function overlaps(rect1, rect2) { |
+ var l1 = rect1.left; |
+ var r1 = rect1.left + rect1.width; |
+ var t1 = rect1.top; |
+ var b1 = rect1.top + rect1.height; |
+ var l2 = rect2.left; |
+ var r2 = rect2.left + rect2.width; |
+ var t2 = rect2.top; |
+ var b2 = rect2.top + rect2.height; |
+ return (l1 < r2 && r1 > l2 && t1 < b2 && b1 > t2); |
+} |
+ |
+/** |
+ * @constructor |
+ */ |
+var SelectToSpeak = function() { |
+ /** @type { AutomationNode } @private */ |
+ this.node_ = null; |
+ |
+ /** @type { boolean } @private */ |
+ this.down_ = false; |
+ |
+ /** @type {{x: number, y: number}} @private */ |
+ this.mouseStart_ = {x: 0, y: 0}; |
+ |
+ chrome.automation.getDesktop(function(desktop) { |
+ desktop.addEventListener( |
+ EventType.mousePressed, this.onMousePressed_.bind(this), true); |
+ desktop.addEventListener( |
+ EventType.mouseDragged, this.onMouseDragged_.bind(this), true); |
+ desktop.addEventListener( |
+ EventType.mouseReleased, this.onMouseReleased_.bind(this), true); |
+ desktop.addEventListener( |
+ EventType.mouseCanceled, this.onMouseCanceled_.bind(this), true); |
+ }.bind(this)); |
+}; |
+ |
+SelectToSpeak.prototype = { |
+ /** |
+ * Called when the mouse is pressed and the user is in a mode where |
+ * select-to-speak is capturing mouse events (for example holding down |
+ * Search). |
+ * |
+ * @param {!AutomationEvent} evt |
+ */ |
+ onMousePressed_: function(evt) { |
+ this.down_ = true; |
+ this.mouseStart_ = {x: evt.mouseX, y: evt.mouseY}; |
+ this.startNode_ = evt.target; |
+ chrome.tts.stop(); |
+ this.onMouseDragged_(evt); |
+ }, |
+ |
+ /** |
+ * Called when the mouse is moved or dragged and the user is in a |
+ * mode where select-to-speak is capturing mouse events (for example |
+ * holding down Search). |
+ * |
+ * @param {!AutomationEvent} evt |
+ */ |
+ onMouseDragged_: function(evt) { |
+ if (!this.down_) |
+ return; |
+ |
+ var rect = rectFromPoints( |
+ this.mouseStart_.x, this.mouseStart_.y, |
+ evt.mouseX, evt.mouseY); |
+ chrome.accessibilityPrivate.setFocusRing([rect]); |
+ }, |
+ |
+ /** |
+ * Called when the mouse is released and the user is in a |
+ * mode where select-to-speak is capturing mouse events (for example |
+ * holding down Search). |
+ * |
+ * @param {!AutomationEvent} evt |
+ */ |
+ onMouseReleased_: function(evt) { |
+ this.onMouseDragged_(evt); |
+ this.down_ = false; |
+ |
+ chrome.accessibilityPrivate.setFocusRing([]); |
+ |
+ // Walk up to the nearest window, web area, or dialog that the |
+ // hit node is contained inside. Only speak objects within that |
+ // container. In the future we might include other container-like |
+ // roles here. |
+ var root = this.startNode_; |
+ while (root.parent && |
+ root.role != RoleType.window && |
+ root.role != RoleType.rootWebArea && |
+ root.role != RoleType.desktop && |
+ root.role != RoleType.dialog) { |
+ root = root.parent; |
+ } |
+ |
+ var rect = rectFromPoints( |
+ this.mouseStart_.x, this.mouseStart_.y, |
+ evt.mouseX, evt.mouseY); |
+ var nodes = []; |
+ this.findAllMatching_(root, rect, nodes); |
+ this.startSpeechQueue_(nodes); |
+ }, |
+ |
+ /** |
+ * Called when the user cancels select-to-speak's capturing of mouse |
+ * events (for example by releasing Search while the mouse is still down). |
+ * |
+ * @param {!AutomationEvent} evt |
+ */ |
+ onMouseCanceled_: function(evt) { |
+ this.down_ = false; |
+ chrome.accessibilityPrivate.setFocusRing([]); |
+ chrome.tts.stop(); |
+ }, |
+ |
+ /** |
+ * Finds all nodes within the subtree rooted at |node| that overlap |
+ * a given rectangle. |
+ * @param {AutomationNode} node The starting node. |
+ * @param {{left: number, top: number, width: number, height: number}} rect |
+ * The bounding box to search. |
+ * @param {Array<AutomationNode>} nodes The matching node array to be |
+ * populated. |
+ * @return {boolean} True if any matches are found. |
+ */ |
+ findAllMatching_: function(node, rect, nodes) { |
+ var found = false; |
+ for (var c = node.firstChild; c; c = c.nextSibling) { |
+ if (this.findAllMatching_(c, rect, nodes)) |
+ found = true; |
+ } |
+ |
+ if (found) |
+ return true; |
+ |
+ if (!node.name || !node.location) |
+ return false; |
+ |
+ if (overlaps(node.location, rect)) { |
+ nodes.push(node); |
+ return true; |
+ } |
+ |
+ return false; |
+ }, |
+ |
+ /** |
+ * Enqueue speech commands for all of the given nodes. |
+ * @param {Array<AutomationNode>} nodes The nodes to speak. |
+ */ |
+ startSpeechQueue_: function(nodes) { |
+ chrome.tts.stop(); |
+ for (var i = 0; i < nodes.length; i++) { |
+ var node = nodes[i]; |
+ var isLast = (i == nodes.length - 1); |
+ chrome.tts.speak(node.name, { |
+ lang: 'en-US', |
+ 'enqueue': true, |
+ onEvent: (function(node, isLast, event) { |
+ if (event.type == 'start') { |
+ chrome.accessibilityPrivate.setFocusRing([node.location]); |
+ } else if (event.type == 'interrupted' || |
+ event.type == 'cancelled') { |
+ chrome.accessibilityPrivate.setFocusRing([]); |
+ } else if (event.type == 'end') { |
+ if (isLast) { |
+ chrome.accessibilityPrivate.setFocusRing([]); |
+ } |
+ } |
+ }).bind(this, node, isLast) |
+ }); |
+ } |
+ } |
+}; |
+ |
+new SelectToSpeak(); |