summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/SpatialNavigation.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/SpatialNavigation.jsm')
-rw-r--r--toolkit/modules/SpatialNavigation.jsm606
1 files changed, 606 insertions, 0 deletions
diff --git a/toolkit/modules/SpatialNavigation.jsm b/toolkit/modules/SpatialNavigation.jsm
new file mode 100644
index 000000000..c6f18a84f
--- /dev/null
+++ b/toolkit/modules/SpatialNavigation.jsm
@@ -0,0 +1,606 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Import this module through
+ *
+ * Components.utils.import("resource://gre/modules/SpatialNavigation.jsm");
+ *
+ * Usage: (Literal class)
+ *
+ * SpatialNavigation.init(browser_element, optional_callback);
+ *
+ * optional_callback will be called when a new element is focused.
+ *
+ * function optional_callback(element) {}
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["SpatialNavigation"];
+
+var SpatialNavigation = {
+ init: function(browser, callback) {
+ browser.addEventListener("keydown", function (event) {
+ _onInputKeyPress(event, callback);
+ }, true);
+ }
+};
+
+// Private stuff
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu["import"]("resource://gre/modules/Services.jsm", this);
+
+var eventListenerService = Cc["@mozilla.org/eventlistenerservice;1"]
+ .getService(Ci.nsIEventListenerService);
+var focusManager = Cc["@mozilla.org/focus-manager;1"]
+ .getService(Ci.nsIFocusManager);
+var windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
+ .getService(Ci.nsIWindowMediator);
+
+// Debug helpers:
+function dump(a) {
+ Services.console.logStringMessage("SpatialNavigation: " + a);
+}
+
+function dumpRect(desc, rect) {
+ dump(desc + " " + Math.round(rect.left) + " " + Math.round(rect.top) + " " +
+ Math.round(rect.right) + " " + Math.round(rect.bottom) + " width:" +
+ Math.round(rect.width) + " height:" + Math.round(rect.height));
+}
+
+function dumpNodeCoord(desc, node) {
+ let rect = node.getBoundingClientRect();
+ dump(desc + " " + node.tagName + " x:" + Math.round(rect.left + rect.width/2) +
+ " y:" + Math.round(rect.top + rect.height / 2));
+}
+
+// modifier values
+
+const kAlt = "alt";
+const kShift = "shift";
+const kCtrl = "ctrl";
+const kNone = "none";
+
+function _onInputKeyPress (event, callback) {
+ // If Spatial Navigation isn't enabled, return.
+ if (!PrefObserver['enabled']) {
+ return;
+ }
+
+ // Use whatever key value is available (either keyCode or charCode).
+ // It might be useful for addons or whoever wants to set different
+ // key to be used here (e.g. "a", "F1", "arrowUp", ...).
+ var key = event.which || event.keyCode;
+
+ if (key != PrefObserver['keyCodeDown'] &&
+ key != PrefObserver['keyCodeRight'] &&
+ key != PrefObserver['keyCodeUp'] &&
+ key != PrefObserver['keyCodeLeft'] &&
+ key != PrefObserver['keyCodeReturn']) {
+ return;
+ }
+
+ if (key == PrefObserver['keyCodeReturn']) {
+ // We report presses of the action button on a gamepad "A" as the return
+ // key to the DOM. The behaviour of hitting the return key and clicking an
+ // element is the same for some elements, but not all, so we handle the
+ // ones we want (like the Select element) here:
+ if (event.target instanceof Ci.nsIDOMHTMLSelectElement &&
+ event.target.click) {
+ event.target.click();
+ event.stopPropagation();
+ event.preventDefault();
+ return;
+ }
+ // Leave the action key press to get reported to the DOM as a return
+ // keypress.
+ return;
+ }
+
+ // If it is not using the modifiers it should, return.
+ if (!event.altKey && PrefObserver['modifierAlt'] ||
+ !event.shiftKey && PrefObserver['modifierShift'] ||
+ !event.crtlKey && PrefObserver['modifierCtrl']) {
+ return;
+ }
+
+ let currentlyFocused = event.target;
+ let currentlyFocusedWindow = currentlyFocused.ownerDocument.defaultView;
+ let bestElementToFocus = null;
+
+ // If currentlyFocused is an nsIDOMHTMLBodyElement then the page has just been
+ // loaded, and this is the first keypress in the page.
+ if (currentlyFocused instanceof Ci.nsIDOMHTMLBodyElement) {
+ focusManager.moveFocus(currentlyFocusedWindow, null, focusManager.MOVEFOCUS_FIRST, 0);
+ event.stopPropagation();
+ event.preventDefault();
+ return;
+ }
+
+ if ((currentlyFocused instanceof Ci.nsIDOMHTMLInputElement &&
+ currentlyFocused.mozIsTextField(false)) ||
+ currentlyFocused instanceof Ci.nsIDOMHTMLTextAreaElement) {
+ // If there is a text selection, remain in the element.
+ if (currentlyFocused.selectionEnd - currentlyFocused.selectionStart != 0) {
+ return;
+ }
+
+ // If there is no text, there is nothing special to do.
+ if (currentlyFocused.textLength > 0) {
+ if (key == PrefObserver['keyCodeRight'] ||
+ key == PrefObserver['keyCodeDown'] ) {
+ // We are moving forward into the document.
+ if (currentlyFocused.textLength != currentlyFocused.selectionEnd) {
+ return;
+ }
+ } else if (currentlyFocused.selectionStart != 0) {
+ return;
+ }
+ }
+ }
+
+ let windowUtils = currentlyFocusedWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ let cssPageRect = _getRootBounds(windowUtils);
+ let searchRect = _getSearchRect(currentlyFocused, key, cssPageRect);
+
+ let nodes = {};
+ nodes.length = 0;
+
+ let searchRectOverflows = false;
+
+ while (!bestElementToFocus && !searchRectOverflows) {
+ switch (key) {
+ case PrefObserver['keyCodeLeft']:
+ case PrefObserver['keyCodeRight']: {
+ if (searchRect.top < cssPageRect.top &&
+ searchRect.bottom > cssPageRect.bottom) {
+ searchRectOverflows = true;
+ }
+ break;
+ }
+ case PrefObserver['keyCodeUp']:
+ case PrefObserver['keyCodeDown']: {
+ if (searchRect.left < cssPageRect.left &&
+ searchRect.right > cssPageRect.right) {
+ searchRectOverflows = true;
+ }
+ break;
+ }
+ }
+
+ nodes = windowUtils.nodesFromRect(searchRect.left, searchRect.top,
+ 0, searchRect.width, searchRect.height, 0,
+ true, false);
+ // Make the search rectangle "wider": double it's size in the direction
+ // that is not the keypress.
+ switch (key) {
+ case PrefObserver['keyCodeLeft']:
+ case PrefObserver['keyCodeRight']: {
+ searchRect.top = searchRect.top - (searchRect.height / 2);
+ searchRect.bottom = searchRect.top + (searchRect.height * 2);
+ searchRect.height = searchRect.height * 2;
+ break;
+ }
+ case PrefObserver['keyCodeUp']:
+ case PrefObserver['keyCodeDown']: {
+ searchRect.left = searchRect.left - (searchRect.width / 2);
+ searchRect.right = searchRect.left + (searchRect.width * 2);
+ searchRect.width = searchRect.width * 2;
+ break;
+ }
+ }
+ bestElementToFocus = _getBestToFocus(nodes, key, currentlyFocused);
+ }
+
+
+ if (bestElementToFocus === null) {
+ // Couldn't find an element to focus.
+ return;
+ }
+
+ focusManager.setFocus(bestElementToFocus, focusManager.FLAG_SHOWRING);
+
+ // if it is a text element, select all.
+ if ((bestElementToFocus instanceof Ci.nsIDOMHTMLInputElement &&
+ bestElementToFocus.mozIsTextField(false)) ||
+ bestElementToFocus instanceof Ci.nsIDOMHTMLTextAreaElement) {
+ bestElementToFocus.selectionStart = 0;
+ bestElementToFocus.selectionEnd = bestElementToFocus.textLength;
+ }
+
+ if (callback != undefined) {
+ callback(bestElementToFocus);
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+}
+
+// Returns the bounds of the page relative to the viewport.
+function _getRootBounds(windowUtils) {
+ let cssPageRect = windowUtils.getRootBounds();
+
+ let scrollX = {};
+ let scrollY = {};
+ windowUtils.getScrollXY(false, scrollX, scrollY);
+
+ let cssPageRectCopy = {};
+
+ cssPageRectCopy.right = cssPageRect.right - scrollX.value;
+ cssPageRectCopy.left = cssPageRect.left - scrollX.value;
+ cssPageRectCopy.top = cssPageRect.top - scrollY.value;
+ cssPageRectCopy.bottom = cssPageRect.bottom - scrollY.value;
+ cssPageRectCopy.width = cssPageRect.width;
+ cssPageRectCopy.height = cssPageRect.height;
+
+ return cssPageRectCopy;
+}
+
+// Returns the best node to focus from the list of nodes returned by the hit
+// test.
+function _getBestToFocus(nodes, key, currentlyFocused) {
+ let best = null;
+ let bestDist;
+ let bestMid;
+ let nodeMid;
+ let currentlyFocusedMid = _getMidpoint(currentlyFocused);
+ let currentlyFocusedRect = currentlyFocused.getBoundingClientRect();
+
+ for (let i = 0; i < nodes.length; i++) {
+ // Reject the currentlyFocused, and all node types we can't focus
+ if (!_canFocus(nodes[i]) || nodes[i] === currentlyFocused) {
+ continue;
+ }
+
+ // Reject all nodes that aren't "far enough" in the direction of the
+ // keypress
+ nodeMid = _getMidpoint(nodes[i]);
+ switch (key) {
+ case PrefObserver['keyCodeLeft']:
+ if (nodeMid.x >= (currentlyFocusedMid.x - currentlyFocusedRect.width / 2)) {
+ continue;
+ }
+ break;
+ case PrefObserver['keyCodeRight']:
+ if (nodeMid.x <= (currentlyFocusedMid.x + currentlyFocusedRect.width / 2)) {
+ continue;
+ }
+ break;
+
+ case PrefObserver['keyCodeUp']:
+ if (nodeMid.y >= (currentlyFocusedMid.y - currentlyFocusedRect.height / 2)) {
+ continue;
+ }
+ break;
+ case PrefObserver['keyCodeDown']:
+ if (nodeMid.y <= (currentlyFocusedMid.y + currentlyFocusedRect.height / 2)) {
+ continue;
+ }
+ break;
+ }
+
+ // Initialize best to the first viable value:
+ if (!best) {
+ bestDist = _spatialDistanceOfCorner(currentlyFocused, nodes[i], key);
+ if (bestDist >= 0) {
+ best = nodes[i];
+ }
+ continue;
+ }
+
+ // Of the remaining nodes, pick the one closest to the currently focused
+ // node.
+ let curDist = _spatialDistanceOfCorner(currentlyFocused, nodes[i], key);
+ if ((curDist > bestDist) || curDist === -1) {
+ continue;
+ }
+ else if (curDist === bestDist) {
+ let midCurDist = _spatialDistance(currentlyFocused, nodes[i]);
+ let midBestDist = _spatialDistance(currentlyFocused, best);
+ if (midCurDist > midBestDist)
+ continue;
+ }
+
+ best = nodes[i];
+ bestDist = curDist;
+ }
+ return best;
+}
+
+// Returns the midpoint of a node.
+function _getMidpoint(node) {
+ let mid = {};
+ let box = node.getBoundingClientRect();
+ mid.x = box.left + (box.width / 2);
+ mid.y = box.top + (box.height / 2);
+
+ return mid;
+}
+
+// Returns true if the node is a type that we want to focus, false otherwise.
+function _canFocus(node) {
+ if (node instanceof Ci.nsIDOMHTMLLinkElement ||
+ node instanceof Ci.nsIDOMHTMLAnchorElement) {
+ return true;
+ }
+ if ((node instanceof Ci.nsIDOMHTMLButtonElement ||
+ node instanceof Ci.nsIDOMHTMLInputElement ||
+ node instanceof Ci.nsIDOMHTMLLinkElement ||
+ node instanceof Ci.nsIDOMHTMLOptGroupElement ||
+ node instanceof Ci.nsIDOMHTMLSelectElement ||
+ node instanceof Ci.nsIDOMHTMLTextAreaElement) &&
+ node.disabled === false) {
+ return true;
+ }
+ return false;
+}
+
+// Returns a rectangle that extends to the end of the screen in the direction that
+// the key is pressed.
+function _getSearchRect(currentlyFocused, key, cssPageRect) {
+ let currentlyFocusedRect = currentlyFocused.getBoundingClientRect();
+
+ let newRect = {};
+ newRect.left = currentlyFocusedRect.left;
+ newRect.top = currentlyFocusedRect.top;
+ newRect.right = currentlyFocusedRect.right;
+ newRect.bottom = currentlyFocusedRect.bottom;
+ newRect.width = currentlyFocusedRect.width;
+ newRect.height = currentlyFocusedRect.height;
+
+ switch (key) {
+ case PrefObserver['keyCodeLeft']:
+ newRect.right = newRect.left;
+ newRect.left = cssPageRect.left;
+ newRect.width = newRect.right - newRect.left;
+
+ newRect.bottom = cssPageRect.bottom;
+ newRect.top = cssPageRect.top;
+ newRect.height = newRect.bottom - newRect.top;
+ break;
+
+ case PrefObserver['keyCodeRight']:
+ newRect.left = newRect.right;
+ newRect.right = cssPageRect.right;
+ newRect.width = newRect.right - newRect.left;
+
+ newRect.bottom = cssPageRect.bottom;
+ newRect.top = cssPageRect.top;
+ newRect.height = newRect.bottom - newRect.top;
+ break;
+
+ case PrefObserver['keyCodeUp']:
+ newRect.bottom = newRect.top;
+ newRect.top = cssPageRect.top;
+ newRect.height = newRect.bottom - newRect.top;
+
+ newRect.right = cssPageRect.right;
+ newRect.left = cssPageRect.left;
+ newRect.width = newRect.right - newRect.left;
+ break;
+
+ case PrefObserver['keyCodeDown']:
+ newRect.top = newRect.bottom;
+ newRect.bottom = cssPageRect.bottom;
+ newRect.height = newRect.bottom - newRect.top;
+
+ newRect.right = cssPageRect.right;
+ newRect.left = cssPageRect.left;
+ newRect.width = newRect.right - newRect.left;
+ break;
+ }
+ return newRect;
+}
+
+// Gets the distance between two points a and b.
+function _spatialDistance(a, b) {
+ let mida = _getMidpoint(a);
+ let midb = _getMidpoint(b);
+
+ return Math.round(Math.pow(mida.x - midb.x, 2) +
+ Math.pow(mida.y - midb.y, 2));
+}
+
+// Get the distance between the corner of two nodes
+function _spatialDistanceOfCorner(from, to, key) {
+ let fromRect = from.getBoundingClientRect();
+ let toRect = to.getBoundingClientRect();
+ let fromMid = _getMidpoint(from);
+ let toMid = _getMidpoint(to);
+ let hDistance = 0;
+ let vDistance = 0;
+
+ switch (key) {
+ case PrefObserver['keyCodeLeft']:
+ // Make sure the "to" node is really at the left side of "from" node by
+ // 1. Check the mid point
+ // 2. The right border of "to" node must be less than the "from" node
+ if ((fromMid.x - toMid.x) < 0 || toRect.right >= fromRect.right)
+ return -1;
+ hDistance = Math.abs(fromRect.left - toRect.right);
+ if (toRect.bottom <= fromRect.top) {
+ vDistance = fromRect.top - toRect.bottom;
+ }
+ else if (fromRect.bottom <= toRect.top) {
+ vDistance = toRect.top - fromRect.bottom;
+ }
+ else {
+ vDistance = 0;
+ }
+ break;
+
+ case PrefObserver['keyCodeRight']:
+ if ((toMid.x - fromMid.x) < 0 || toRect.left <= fromRect.left)
+ return -1;
+ hDistance = Math.abs(toRect.left - fromRect.right);
+ if (toRect.bottom <= fromRect.top) {
+ vDistance = fromRect.top - toRect.bottom;
+ }
+ else if (fromRect.bottom <= toRect.top) {
+ vDistance = toRect.top - fromRect.bottom;
+ }
+ else {
+ vDistance = 0;
+ }
+ break;
+
+ case PrefObserver['keyCodeUp']:
+ if ((fromMid.y - toMid.y) < 0 || toRect.bottom >= fromRect.bottom)
+ return -1;
+ vDistance = Math.abs(fromRect.top - toRect.bottom);
+ if (fromRect.right <= toRect.left) {
+ hDistance = toRect.left - fromRect.right;
+ }
+ else if (toRect.right <= fromRect.left) {
+ hDistance = fromRect.left - toRect.right;
+ }
+ else {
+ hDistance = 0;
+ }
+ break;
+
+ case PrefObserver['keyCodeDown']:
+ if ((toMid.y - fromMid.y) < 0 || toRect.top <= fromRect.top)
+ return -1;
+ vDistance = Math.abs(toRect.top - fromRect.bottom);
+ if (fromRect.right <= toRect.left) {
+ hDistance = toRect.left - fromRect.right;
+ }
+ else if (toRect.right <= fromRect.left) {
+ hDistance = fromRect.left - toRect.right;
+ }
+ else {
+ hDistance = 0;
+ }
+ break;
+ }
+ return Math.round(Math.pow(hDistance, 2) +
+ Math.pow(vDistance, 2));
+}
+
+// Snav preference observer
+var PrefObserver = {
+ register: function() {
+ this.prefService = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService);
+
+ this._branch = this.prefService.getBranch("snav.");
+ this._branch.QueryInterface(Ci.nsIPrefBranch2);
+ this._branch.addObserver("", this, false);
+
+ // set current or default pref values
+ this.observe(null, "nsPref:changed", "enabled");
+ this.observe(null, "nsPref:changed", "xulContentEnabled");
+ this.observe(null, "nsPref:changed", "keyCode.modifier");
+ this.observe(null, "nsPref:changed", "keyCode.right");
+ this.observe(null, "nsPref:changed", "keyCode.up");
+ this.observe(null, "nsPref:changed", "keyCode.down");
+ this.observe(null, "nsPref:changed", "keyCode.left");
+ this.observe(null, "nsPref:changed", "keyCode.return");
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != "nsPref:changed") {
+ return;
+ }
+
+ // aSubject is the nsIPrefBranch we're observing (after appropriate QI)
+ // aData is the name of the pref that's been changed (relative to aSubject)
+ switch (aData) {
+ case "enabled":
+ try {
+ this.enabled = this._branch.getBoolPref("enabled");
+ } catch (e) {
+ this.enabled = false;
+ }
+ break;
+
+ case "xulContentEnabled":
+ try {
+ this.xulContentEnabled = this._branch.getBoolPref("xulContentEnabled");
+ } catch (e) {
+ this.xulContentEnabled = false;
+ }
+ break;
+
+ case "keyCode.modifier": {
+ let keyCodeModifier;
+ try {
+ keyCodeModifier = this._branch.getCharPref("keyCode.modifier");
+
+ // resetting modifiers
+ this.modifierAlt = false;
+ this.modifierShift = false;
+ this.modifierCtrl = false;
+
+ if (keyCodeModifier != this.kNone) {
+ // we are using '+' as a separator in about:config.
+ let mods = keyCodeModifier.split(/\++/);
+ for (let i = 0; i < mods.length; i++) {
+ let mod = mods[i].toLowerCase();
+ if (mod === "")
+ continue;
+ else if (mod == kAlt)
+ this.modifierAlt = true;
+ else if (mod == kShift)
+ this.modifierShift = true;
+ else if (mod == kCtrl)
+ this.modifierCtrl = true;
+ else {
+ keyCodeModifier = kNone;
+ break;
+ }
+ }
+ }
+ } catch (e) { }
+ break;
+ }
+
+ case "keyCode.up":
+ try {
+ this.keyCodeUp = this._branch.getIntPref("keyCode.up");
+ } catch (e) {
+ this.keyCodeUp = Ci.nsIDOMKeyEvent.DOM_VK_UP;
+ }
+ break;
+ case "keyCode.down":
+ try {
+ this.keyCodeDown = this._branch.getIntPref("keyCode.down");
+ } catch (e) {
+ this.keyCodeDown = Ci.nsIDOMKeyEvent.DOM_VK_DOWN;
+ }
+ break;
+ case "keyCode.left":
+ try {
+ this.keyCodeLeft = this._branch.getIntPref("keyCode.left");
+ } catch (e) {
+ this.keyCodeLeft = Ci.nsIDOMKeyEvent.DOM_VK_LEFT;
+ }
+ break;
+ case "keyCode.right":
+ try {
+ this.keyCodeRight = this._branch.getIntPref("keyCode.right");
+ } catch (e) {
+ this.keyCodeRight = Ci.nsIDOMKeyEvent.DOM_VK_RIGHT;
+ }
+ break;
+ case "keyCode.return":
+ try {
+ this.keyCodeReturn = this._branch.getIntPref("keyCode.return");
+ } catch (e) {
+ this.keyCodeReturn = Ci.nsIDOMKeyEvent.DOM_VK_RETURN;
+ }
+ break;
+ }
+ }
+};
+
+PrefObserver.register();