1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/modules/SpatialNavigation.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,527 @@ 1.4 +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +/** 1.10 + * Import this module through 1.11 + * 1.12 + * Components.utils.import("resource://gre/modules/SpatialNavigation.jsm"); 1.13 + * 1.14 + * Usage: (Literal class) 1.15 + * 1.16 + * SpatialNavigation.init(browser_element, optional_callback); 1.17 + * 1.18 + * optional_callback will be called when a new element is focused. 1.19 + * 1.20 + * function optional_callback(element) {} 1.21 + */ 1.22 + 1.23 +"use strict"; 1.24 + 1.25 +this.EXPORTED_SYMBOLS = ["SpatialNavigation"]; 1.26 + 1.27 +var SpatialNavigation = { 1.28 + init: function(browser, callback) { 1.29 + browser.addEventListener("keydown", function (event) { 1.30 + _onInputKeyPress(event, callback); 1.31 + }, true); 1.32 + } 1.33 +}; 1.34 + 1.35 +// Private stuff 1.36 + 1.37 +const Cc = Components.classes; 1.38 +const Ci = Components.interfaces; 1.39 +const Cu = Components.utils; 1.40 + 1.41 +Cu["import"]("resource://gre/modules/Services.jsm", this); 1.42 + 1.43 +let eventListenerService = Cc["@mozilla.org/eventlistenerservice;1"] 1.44 + .getService(Ci.nsIEventListenerService); 1.45 +let focusManager = Cc["@mozilla.org/focus-manager;1"] 1.46 + .getService(Ci.nsIFocusManager); 1.47 +let windowMediator = Cc['@mozilla.org/appshell/window-mediator;1'] 1.48 + .getService(Ci.nsIWindowMediator); 1.49 + 1.50 +// Debug helpers: 1.51 +function dump(a) { 1.52 + Services.console.logStringMessage("SpatialNavigation: " + a); 1.53 +} 1.54 + 1.55 +function dumpRect(desc, rect) { 1.56 + dump(desc + " " + Math.round(rect.left) + " " + Math.round(rect.top) + " " + 1.57 + Math.round(rect.right) + " " + Math.round(rect.bottom) + " width:" + 1.58 + Math.round(rect.width) + " height:" + Math.round(rect.height)); 1.59 +} 1.60 + 1.61 +function dumpNodeCoord(desc, node) { 1.62 + let rect = node.getBoundingClientRect(); 1.63 + dump(desc + " " + node.tagName + " x:" + Math.round(rect.left + rect.width/2) + 1.64 + " y:" + Math.round(rect.top + rect.height / 2)); 1.65 +} 1.66 + 1.67 +// modifier values 1.68 + 1.69 +const kAlt = "alt"; 1.70 +const kShift = "shift"; 1.71 +const kCtrl = "ctrl"; 1.72 +const kNone = "none"; 1.73 + 1.74 +function _onInputKeyPress (event, callback) { 1.75 + //If Spatial Navigation isn't enabled, return. 1.76 + if (!PrefObserver['enabled']) { 1.77 + return; 1.78 + } 1.79 + 1.80 + // Use whatever key value is available (either keyCode or charCode). 1.81 + // It might be useful for addons or whoever wants to set different 1.82 + // key to be used here (e.g. "a", "F1", "arrowUp", ...). 1.83 + var key = event.which || event.keyCode; 1.84 + 1.85 + if (key != PrefObserver['keyCodeDown'] && 1.86 + key != PrefObserver['keyCodeRight'] && 1.87 + key != PrefObserver['keyCodeUp'] && 1.88 + key != PrefObserver['keyCodeLeft'] && 1.89 + key != PrefObserver['keyCodeReturn']) { 1.90 + return; 1.91 + } 1.92 + 1.93 + if (key == PrefObserver['keyCodeReturn']) { 1.94 + // We report presses of the action button on a gamepad "A" as the return 1.95 + // key to the DOM. The behaviour of hitting the return key and clicking an 1.96 + // element is the same for some elements, but not all, so we handle the 1.97 + // ones we want (like the Select element) here: 1.98 + if (event.target instanceof Ci.nsIDOMHTMLSelectElement && 1.99 + event.target.click) { 1.100 + event.target.click(); 1.101 + event.stopPropagation(); 1.102 + event.preventDefault(); 1.103 + return; 1.104 + } else { 1.105 + // Leave the action key press to get reported to the DOM as a return 1.106 + // keypress. 1.107 + return; 1.108 + } 1.109 + } 1.110 + 1.111 + // If it is not using the modifiers it should, return. 1.112 + if (!event.altKey && PrefObserver['modifierAlt'] || 1.113 + !event.shiftKey && PrefObserver['modifierShift'] || 1.114 + !event.crtlKey && PrefObserver['modifierCtrl']) { 1.115 + return; 1.116 + } 1.117 + 1.118 + let currentlyFocused = event.target; 1.119 + let currentlyFocusedWindow = currentlyFocused.ownerDocument.defaultView; 1.120 + let bestElementToFocus = null; 1.121 + 1.122 + // If currentlyFocused is an nsIDOMHTMLBodyElement then the page has just been 1.123 + // loaded, and this is the first keypress in the page. 1.124 + if (currentlyFocused instanceof Ci.nsIDOMHTMLBodyElement) { 1.125 + focusManager.moveFocus(currentlyFocusedWindow, null, focusManager.MOVEFOCUS_FIRST, 0); 1.126 + event.stopPropagation(); 1.127 + event.preventDefault(); 1.128 + return; 1.129 + } 1.130 + 1.131 + if ((currentlyFocused instanceof Ci.nsIDOMHTMLInputElement && 1.132 + currentlyFocused.mozIsTextField(false)) || 1.133 + currentlyFocused instanceof Ci.nsIDOMHTMLTextAreaElement) { 1.134 + // If there is a text selection, remain in the element. 1.135 + if (currentlyFocused.selectionEnd - currentlyFocused.selectionStart != 0) { 1.136 + return; 1.137 + } 1.138 + 1.139 + // If there is no text, there is nothing special to do. 1.140 + if (currentlyFocused.textLength > 0) { 1.141 + if (key == PrefObserver['keyCodeRight'] || 1.142 + key == PrefObserver['keyCodeDown'] ) { 1.143 + // We are moving forward into the document. 1.144 + if (currentlyFocused.textLength != currentlyFocused.selectionEnd) { 1.145 + return; 1.146 + } 1.147 + } else if (currentlyFocused.selectionStart != 0) { 1.148 + return; 1.149 + } 1.150 + } 1.151 + } 1.152 + 1.153 + let windowUtils = currentlyFocusedWindow.QueryInterface(Ci.nsIInterfaceRequestor) 1.154 + .getInterface(Ci.nsIDOMWindowUtils); 1.155 + let cssPageRect = _getRootBounds(windowUtils); 1.156 + let searchRect = _getSearchRect(currentlyFocused, key, cssPageRect); 1.157 + 1.158 + let nodes = {}; 1.159 + nodes.length = 0; 1.160 + 1.161 + let searchRectOverflows = false; 1.162 + 1.163 + while (!bestElementToFocus && !searchRectOverflows) { 1.164 + switch (key) { 1.165 + case PrefObserver['keyCodeLeft']: 1.166 + case PrefObserver['keyCodeRight']: { 1.167 + if (searchRect.top < cssPageRect.top && 1.168 + searchRect.bottom > cssPageRect.bottom) { 1.169 + searchRectOverflows = true; 1.170 + } 1.171 + break; 1.172 + } 1.173 + case PrefObserver['keyCodeUp']: 1.174 + case PrefObserver['keyCodeDown']: { 1.175 + if (searchRect.left < cssPageRect.left && 1.176 + searchRect.right > cssPageRect.right) { 1.177 + searchRectOverflows = true; 1.178 + } 1.179 + break; 1.180 + } 1.181 + } 1.182 + 1.183 + nodes = windowUtils.nodesFromRect(searchRect.left, searchRect.top, 1.184 + 0, searchRect.width, searchRect.height, 0, 1.185 + true, false); 1.186 + // Make the search rectangle "wider": double it's size in the direction 1.187 + // that is not the keypress. 1.188 + switch (key) { 1.189 + case PrefObserver['keyCodeLeft']: 1.190 + case PrefObserver['keyCodeRight']: { 1.191 + searchRect.top = searchRect.top - (searchRect.height / 2); 1.192 + searchRect.bottom = searchRect.top + (searchRect.height * 2); 1.193 + searchRect.height = searchRect.height * 2; 1.194 + break; 1.195 + } 1.196 + case PrefObserver['keyCodeUp']: 1.197 + case PrefObserver['keyCodeDown']: { 1.198 + searchRect.left = searchRect.left - (searchRect.width / 2); 1.199 + searchRect.right = searchRect.left + (searchRect.width * 2); 1.200 + searchRect.width = searchRect.width * 2; 1.201 + break; 1.202 + } 1.203 + } 1.204 + bestElementToFocus = _getBestToFocus(nodes, key, currentlyFocused); 1.205 + } 1.206 + 1.207 + 1.208 + if (bestElementToFocus === null) { 1.209 + // Couldn't find an element to focus. 1.210 + return; 1.211 + } 1.212 + 1.213 + focusManager.setFocus(bestElementToFocus, focusManager.FLAG_SHOWRING); 1.214 + 1.215 + //if it is a text element, select all. 1.216 + if ((bestElementToFocus instanceof Ci.nsIDOMHTMLInputElement && 1.217 + bestElementToFocus.mozIsTextField(false)) || 1.218 + bestElementToFocus instanceof Ci.nsIDOMHTMLTextAreaElement) { 1.219 + bestElementToFocus.selectionStart = 0; 1.220 + bestElementToFocus.selectionEnd = bestElementToFocus.textLength; 1.221 + } 1.222 + 1.223 + if (callback != undefined) { 1.224 + callback(bestElementToFocus); 1.225 + } 1.226 + 1.227 + event.preventDefault(); 1.228 + event.stopPropagation(); 1.229 +} 1.230 + 1.231 +// Returns the bounds of the page relative to the viewport. 1.232 +function _getRootBounds(windowUtils) { 1.233 + let cssPageRect = windowUtils.getRootBounds(); 1.234 + 1.235 + let scrollX = {}; 1.236 + let scrollY = {}; 1.237 + windowUtils.getScrollXY(false, scrollX, scrollY); 1.238 + 1.239 + let cssPageRectCopy = {}; 1.240 + 1.241 + cssPageRectCopy.right = cssPageRect.right - scrollX.value; 1.242 + cssPageRectCopy.left = cssPageRect.left - scrollX.value; 1.243 + cssPageRectCopy.top = cssPageRect.top - scrollY.value; 1.244 + cssPageRectCopy.bottom = cssPageRect.bottom - scrollY.value; 1.245 + cssPageRectCopy.width = cssPageRect.width; 1.246 + cssPageRectCopy.height = cssPageRect.height; 1.247 + 1.248 + return cssPageRectCopy; 1.249 +} 1.250 + 1.251 +// Returns the best node to focus from the list of nodes returned by the hit 1.252 +// test. 1.253 +function _getBestToFocus(nodes, key, currentlyFocused) { 1.254 + let best = null; 1.255 + let bestDist; 1.256 + let bestMid; 1.257 + let nodeMid; 1.258 + let currentlyFocusedMid = _getMidpoint(currentlyFocused); 1.259 + let currentlyFocusedRect = currentlyFocused.getBoundingClientRect(); 1.260 + 1.261 + for (let i = 0; i < nodes.length; i++) { 1.262 + // Reject the currentlyFocused, and all node types we can't focus 1.263 + if (!_canFocus(nodes[i]) || nodes[i] === currentlyFocused) { 1.264 + continue; 1.265 + } 1.266 + 1.267 + // Reject all nodes that aren't "far enough" in the direction of the 1.268 + // keypress 1.269 + nodeMid = _getMidpoint(nodes[i]); 1.270 + switch (key) { 1.271 + case PrefObserver['keyCodeLeft']: 1.272 + if (nodeMid.x >= (currentlyFocusedMid.x - currentlyFocusedRect.width / 2)) { 1.273 + continue; 1.274 + } 1.275 + break; 1.276 + case PrefObserver['keyCodeRight']: 1.277 + if (nodeMid.x <= (currentlyFocusedMid.x + currentlyFocusedRect.width / 2)) { 1.278 + continue; 1.279 + } 1.280 + break; 1.281 + 1.282 + case PrefObserver['keyCodeUp']: 1.283 + if (nodeMid.y >= (currentlyFocusedMid.y - currentlyFocusedRect.height / 2)) { 1.284 + continue; 1.285 + } 1.286 + break; 1.287 + case PrefObserver['keyCodeDown']: 1.288 + if (nodeMid.y <= (currentlyFocusedMid.y + currentlyFocusedRect.height / 2)) { 1.289 + continue; 1.290 + } 1.291 + break; 1.292 + } 1.293 + 1.294 + // Initialize best to the first viable value: 1.295 + if (!best) { 1.296 + best = nodes[i]; 1.297 + bestDist = _spatialDistance(best, currentlyFocused); 1.298 + continue; 1.299 + } 1.300 + 1.301 + // Of the remaining nodes, pick the one closest to the currently focused 1.302 + // node. 1.303 + let curDist = _spatialDistance(nodes[i], currentlyFocused); 1.304 + if (curDist > bestDist) { 1.305 + continue; 1.306 + } 1.307 + 1.308 + bestMid = _getMidpoint(best); 1.309 + switch (key) { 1.310 + case PrefObserver['keyCodeLeft']: 1.311 + if (nodeMid.x > bestMid.x) { 1.312 + best = nodes[i]; 1.313 + bestDist = curDist; 1.314 + } 1.315 + break; 1.316 + case PrefObserver['keyCodeRight']: 1.317 + if (nodeMid.x < bestMid.x) { 1.318 + best = nodes[i]; 1.319 + bestDist = curDist; 1.320 + } 1.321 + break; 1.322 + case PrefObserver['keyCodeUp']: 1.323 + if (nodeMid.y > bestMid.y) { 1.324 + best = nodes[i]; 1.325 + bestDist = curDist; 1.326 + } 1.327 + break; 1.328 + case PrefObserver['keyCodeDown']: 1.329 + if (nodeMid.y < bestMid.y) { 1.330 + best = nodes[i]; 1.331 + bestDist = curDist; 1.332 + } 1.333 + break; 1.334 + } 1.335 + } 1.336 + return best; 1.337 +} 1.338 + 1.339 +// Returns the midpoint of a node. 1.340 +function _getMidpoint(node) { 1.341 + let mid = {}; 1.342 + let box = node.getBoundingClientRect(); 1.343 + mid.x = box.left + (box.width / 2); 1.344 + mid.y = box.top + (box.height / 2); 1.345 + 1.346 + return mid; 1.347 +} 1.348 + 1.349 +// Returns true if the node is a type that we want to focus, false otherwise. 1.350 +function _canFocus(node) { 1.351 + if (node instanceof Ci.nsIDOMHTMLLinkElement || 1.352 + node instanceof Ci.nsIDOMHTMLAnchorElement) { 1.353 + return true; 1.354 + } 1.355 + if ((node instanceof Ci.nsIDOMHTMLButtonElement || 1.356 + node instanceof Ci.nsIDOMHTMLInputElement || 1.357 + node instanceof Ci.nsIDOMHTMLLinkElement || 1.358 + node instanceof Ci.nsIDOMHTMLOptGroupElement || 1.359 + node instanceof Ci.nsIDOMHTMLSelectElement || 1.360 + node instanceof Ci.nsIDOMHTMLTextAreaElement) && 1.361 + node.disabled === false) { 1.362 + return true; 1.363 + } 1.364 + return false; 1.365 +} 1.366 + 1.367 +// Returns a rectangle that extends to the end of the screen in the direction that 1.368 +// the key is pressed. 1.369 +function _getSearchRect(currentlyFocused, key, cssPageRect) { 1.370 + let currentlyFocusedRect = currentlyFocused.getBoundingClientRect(); 1.371 + 1.372 + let newRect = {}; 1.373 + newRect.left = currentlyFocusedRect.left; 1.374 + newRect.top = currentlyFocusedRect.top; 1.375 + newRect.right = currentlyFocusedRect.right; 1.376 + newRect.bottom = currentlyFocusedRect.bottom; 1.377 + newRect.width = currentlyFocusedRect.width; 1.378 + newRect.height = currentlyFocusedRect.height; 1.379 + 1.380 + switch (key) { 1.381 + case PrefObserver['keyCodeLeft']: 1.382 + newRect.left = cssPageRect.left; 1.383 + newRect.width = newRect.right - newRect.left; 1.384 + break; 1.385 + 1.386 + case PrefObserver['keyCodeRight']: 1.387 + newRect.right = cssPageRect.right; 1.388 + newRect.width = newRect.right - newRect.left; 1.389 + break; 1.390 + 1.391 + case PrefObserver['keyCodeUp']: 1.392 + newRect.top = cssPageRect.top; 1.393 + newRect.height = newRect.bottom - newRect.top; 1.394 + break; 1.395 + 1.396 + case PrefObserver['keyCodeDown']: 1.397 + newRect.bottom = cssPageRect.bottom; 1.398 + newRect.height = newRect.bottom - newRect.top; 1.399 + break; 1.400 + } 1.401 + return newRect; 1.402 +} 1.403 + 1.404 +// Gets the distance between two points a and b. 1.405 +function _spatialDistance(a, b) { 1.406 + let mida = _getMidpoint(a); 1.407 + let midb = _getMidpoint(b); 1.408 + 1.409 + return Math.round(Math.pow(mida.x - midb.x, 2) + 1.410 + Math.pow(mida.y - midb.y, 2)); 1.411 +} 1.412 + 1.413 +// Snav preference observer 1.414 +var PrefObserver = { 1.415 + register: function() { 1.416 + this.prefService = Cc["@mozilla.org/preferences-service;1"] 1.417 + .getService(Ci.nsIPrefService); 1.418 + 1.419 + this._branch = this.prefService.getBranch("snav."); 1.420 + this._branch.QueryInterface(Ci.nsIPrefBranch2); 1.421 + this._branch.addObserver("", this, false); 1.422 + 1.423 + // set current or default pref values 1.424 + this.observe(null, "nsPref:changed", "enabled"); 1.425 + this.observe(null, "nsPref:changed", "xulContentEnabled"); 1.426 + this.observe(null, "nsPref:changed", "keyCode.modifier"); 1.427 + this.observe(null, "nsPref:changed", "keyCode.right"); 1.428 + this.observe(null, "nsPref:changed", "keyCode.up"); 1.429 + this.observe(null, "nsPref:changed", "keyCode.down"); 1.430 + this.observe(null, "nsPref:changed", "keyCode.left"); 1.431 + this.observe(null, "nsPref:changed", "keyCode.return"); 1.432 + }, 1.433 + 1.434 + observe: function(aSubject, aTopic, aData) { 1.435 + if (aTopic != "nsPref:changed") { 1.436 + return; 1.437 + } 1.438 + 1.439 + // aSubject is the nsIPrefBranch we're observing (after appropriate QI) 1.440 + // aData is the name of the pref that's been changed (relative to aSubject) 1.441 + switch (aData) { 1.442 + case "enabled": 1.443 + try { 1.444 + this.enabled = this._branch.getBoolPref("enabled"); 1.445 + } catch(e) { 1.446 + this.enabled = false; 1.447 + } 1.448 + break; 1.449 + 1.450 + case "xulContentEnabled": 1.451 + try { 1.452 + this.xulContentEnabled = this._branch.getBoolPref("xulContentEnabled"); 1.453 + } catch(e) { 1.454 + this.xulContentEnabled = false; 1.455 + } 1.456 + break; 1.457 + 1.458 + case "keyCode.modifier": { 1.459 + let keyCodeModifier; 1.460 + try { 1.461 + keyCodeModifier = this._branch.getCharPref("keyCode.modifier"); 1.462 + 1.463 + // resetting modifiers 1.464 + this.modifierAlt = false; 1.465 + this.modifierShift = false; 1.466 + this.modifierCtrl = false; 1.467 + 1.468 + if (keyCodeModifier != this.kNone) { 1.469 + // we are using '+' as a separator in about:config. 1.470 + let mods = keyCodeModifier.split(/\++/); 1.471 + for (let i = 0; i < mods.length; i++) { 1.472 + let mod = mods[i].toLowerCase(); 1.473 + if (mod === "") 1.474 + continue; 1.475 + else if (mod == kAlt) 1.476 + this.modifierAlt = true; 1.477 + else if (mod == kShift) 1.478 + this.modifierShift = true; 1.479 + else if (mod == kCtrl) 1.480 + this.modifierCtrl = true; 1.481 + else { 1.482 + keyCodeModifier = kNone; 1.483 + break; 1.484 + } 1.485 + } 1.486 + } 1.487 + } catch(e) { } 1.488 + break; 1.489 + } 1.490 + 1.491 + case "keyCode.up": 1.492 + try { 1.493 + this.keyCodeUp = this._branch.getIntPref("keyCode.up"); 1.494 + } catch(e) { 1.495 + this.keyCodeUp = Ci.nsIDOMKeyEvent.DOM_VK_UP; 1.496 + } 1.497 + break; 1.498 + case "keyCode.down": 1.499 + try { 1.500 + this.keyCodeDown = this._branch.getIntPref("keyCode.down"); 1.501 + } catch(e) { 1.502 + this.keyCodeDown = Ci.nsIDOMKeyEvent.DOM_VK_DOWN; 1.503 + } 1.504 + break; 1.505 + case "keyCode.left": 1.506 + try { 1.507 + this.keyCodeLeft = this._branch.getIntPref("keyCode.left"); 1.508 + } catch(e) { 1.509 + this.keyCodeLeft = Ci.nsIDOMKeyEvent.DOM_VK_LEFT; 1.510 + } 1.511 + break; 1.512 + case "keyCode.right": 1.513 + try { 1.514 + this.keyCodeRight = this._branch.getIntPref("keyCode.right"); 1.515 + } catch(e) { 1.516 + this.keyCodeRight = Ci.nsIDOMKeyEvent.DOM_VK_RIGHT; 1.517 + } 1.518 + break; 1.519 + case "keyCode.return": 1.520 + try { 1.521 + this.keyCodeReturn = this._branch.getIntPref("keyCode.return"); 1.522 + } catch(e) { 1.523 + this.keyCodeReturn = Ci.nsIDOMKeyEvent.DOM_VK_RETURN; 1.524 + } 1.525 + break; 1.526 + } 1.527 + } 1.528 +}; 1.529 + 1.530 +PrefObserver.register();