toolkit/modules/SpatialNavigation.jsm

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:1027c39f16fc
1 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6 /**
7 * Import this module through
8 *
9 * Components.utils.import("resource://gre/modules/SpatialNavigation.jsm");
10 *
11 * Usage: (Literal class)
12 *
13 * SpatialNavigation.init(browser_element, optional_callback);
14 *
15 * optional_callback will be called when a new element is focused.
16 *
17 * function optional_callback(element) {}
18 */
19
20 "use strict";
21
22 this.EXPORTED_SYMBOLS = ["SpatialNavigation"];
23
24 var SpatialNavigation = {
25 init: function(browser, callback) {
26 browser.addEventListener("keydown", function (event) {
27 _onInputKeyPress(event, callback);
28 }, true);
29 }
30 };
31
32 // Private stuff
33
34 const Cc = Components.classes;
35 const Ci = Components.interfaces;
36 const Cu = Components.utils;
37
38 Cu["import"]("resource://gre/modules/Services.jsm", this);
39
40 let eventListenerService = Cc["@mozilla.org/eventlistenerservice;1"]
41 .getService(Ci.nsIEventListenerService);
42 let focusManager = Cc["@mozilla.org/focus-manager;1"]
43 .getService(Ci.nsIFocusManager);
44 let windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
45 .getService(Ci.nsIWindowMediator);
46
47 // Debug helpers:
48 function dump(a) {
49 Services.console.logStringMessage("SpatialNavigation: " + a);
50 }
51
52 function dumpRect(desc, rect) {
53 dump(desc + " " + Math.round(rect.left) + " " + Math.round(rect.top) + " " +
54 Math.round(rect.right) + " " + Math.round(rect.bottom) + " width:" +
55 Math.round(rect.width) + " height:" + Math.round(rect.height));
56 }
57
58 function dumpNodeCoord(desc, node) {
59 let rect = node.getBoundingClientRect();
60 dump(desc + " " + node.tagName + " x:" + Math.round(rect.left + rect.width/2) +
61 " y:" + Math.round(rect.top + rect.height / 2));
62 }
63
64 // modifier values
65
66 const kAlt = "alt";
67 const kShift = "shift";
68 const kCtrl = "ctrl";
69 const kNone = "none";
70
71 function _onInputKeyPress (event, callback) {
72 //If Spatial Navigation isn't enabled, return.
73 if (!PrefObserver['enabled']) {
74 return;
75 }
76
77 // Use whatever key value is available (either keyCode or charCode).
78 // It might be useful for addons or whoever wants to set different
79 // key to be used here (e.g. "a", "F1", "arrowUp", ...).
80 var key = event.which || event.keyCode;
81
82 if (key != PrefObserver['keyCodeDown'] &&
83 key != PrefObserver['keyCodeRight'] &&
84 key != PrefObserver['keyCodeUp'] &&
85 key != PrefObserver['keyCodeLeft'] &&
86 key != PrefObserver['keyCodeReturn']) {
87 return;
88 }
89
90 if (key == PrefObserver['keyCodeReturn']) {
91 // We report presses of the action button on a gamepad "A" as the return
92 // key to the DOM. The behaviour of hitting the return key and clicking an
93 // element is the same for some elements, but not all, so we handle the
94 // ones we want (like the Select element) here:
95 if (event.target instanceof Ci.nsIDOMHTMLSelectElement &&
96 event.target.click) {
97 event.target.click();
98 event.stopPropagation();
99 event.preventDefault();
100 return;
101 } else {
102 // Leave the action key press to get reported to the DOM as a return
103 // keypress.
104 return;
105 }
106 }
107
108 // If it is not using the modifiers it should, return.
109 if (!event.altKey && PrefObserver['modifierAlt'] ||
110 !event.shiftKey && PrefObserver['modifierShift'] ||
111 !event.crtlKey && PrefObserver['modifierCtrl']) {
112 return;
113 }
114
115 let currentlyFocused = event.target;
116 let currentlyFocusedWindow = currentlyFocused.ownerDocument.defaultView;
117 let bestElementToFocus = null;
118
119 // If currentlyFocused is an nsIDOMHTMLBodyElement then the page has just been
120 // loaded, and this is the first keypress in the page.
121 if (currentlyFocused instanceof Ci.nsIDOMHTMLBodyElement) {
122 focusManager.moveFocus(currentlyFocusedWindow, null, focusManager.MOVEFOCUS_FIRST, 0);
123 event.stopPropagation();
124 event.preventDefault();
125 return;
126 }
127
128 if ((currentlyFocused instanceof Ci.nsIDOMHTMLInputElement &&
129 currentlyFocused.mozIsTextField(false)) ||
130 currentlyFocused instanceof Ci.nsIDOMHTMLTextAreaElement) {
131 // If there is a text selection, remain in the element.
132 if (currentlyFocused.selectionEnd - currentlyFocused.selectionStart != 0) {
133 return;
134 }
135
136 // If there is no text, there is nothing special to do.
137 if (currentlyFocused.textLength > 0) {
138 if (key == PrefObserver['keyCodeRight'] ||
139 key == PrefObserver['keyCodeDown'] ) {
140 // We are moving forward into the document.
141 if (currentlyFocused.textLength != currentlyFocused.selectionEnd) {
142 return;
143 }
144 } else if (currentlyFocused.selectionStart != 0) {
145 return;
146 }
147 }
148 }
149
150 let windowUtils = currentlyFocusedWindow.QueryInterface(Ci.nsIInterfaceRequestor)
151 .getInterface(Ci.nsIDOMWindowUtils);
152 let cssPageRect = _getRootBounds(windowUtils);
153 let searchRect = _getSearchRect(currentlyFocused, key, cssPageRect);
154
155 let nodes = {};
156 nodes.length = 0;
157
158 let searchRectOverflows = false;
159
160 while (!bestElementToFocus && !searchRectOverflows) {
161 switch (key) {
162 case PrefObserver['keyCodeLeft']:
163 case PrefObserver['keyCodeRight']: {
164 if (searchRect.top < cssPageRect.top &&
165 searchRect.bottom > cssPageRect.bottom) {
166 searchRectOverflows = true;
167 }
168 break;
169 }
170 case PrefObserver['keyCodeUp']:
171 case PrefObserver['keyCodeDown']: {
172 if (searchRect.left < cssPageRect.left &&
173 searchRect.right > cssPageRect.right) {
174 searchRectOverflows = true;
175 }
176 break;
177 }
178 }
179
180 nodes = windowUtils.nodesFromRect(searchRect.left, searchRect.top,
181 0, searchRect.width, searchRect.height, 0,
182 true, false);
183 // Make the search rectangle "wider": double it's size in the direction
184 // that is not the keypress.
185 switch (key) {
186 case PrefObserver['keyCodeLeft']:
187 case PrefObserver['keyCodeRight']: {
188 searchRect.top = searchRect.top - (searchRect.height / 2);
189 searchRect.bottom = searchRect.top + (searchRect.height * 2);
190 searchRect.height = searchRect.height * 2;
191 break;
192 }
193 case PrefObserver['keyCodeUp']:
194 case PrefObserver['keyCodeDown']: {
195 searchRect.left = searchRect.left - (searchRect.width / 2);
196 searchRect.right = searchRect.left + (searchRect.width * 2);
197 searchRect.width = searchRect.width * 2;
198 break;
199 }
200 }
201 bestElementToFocus = _getBestToFocus(nodes, key, currentlyFocused);
202 }
203
204
205 if (bestElementToFocus === null) {
206 // Couldn't find an element to focus.
207 return;
208 }
209
210 focusManager.setFocus(bestElementToFocus, focusManager.FLAG_SHOWRING);
211
212 //if it is a text element, select all.
213 if ((bestElementToFocus instanceof Ci.nsIDOMHTMLInputElement &&
214 bestElementToFocus.mozIsTextField(false)) ||
215 bestElementToFocus instanceof Ci.nsIDOMHTMLTextAreaElement) {
216 bestElementToFocus.selectionStart = 0;
217 bestElementToFocus.selectionEnd = bestElementToFocus.textLength;
218 }
219
220 if (callback != undefined) {
221 callback(bestElementToFocus);
222 }
223
224 event.preventDefault();
225 event.stopPropagation();
226 }
227
228 // Returns the bounds of the page relative to the viewport.
229 function _getRootBounds(windowUtils) {
230 let cssPageRect = windowUtils.getRootBounds();
231
232 let scrollX = {};
233 let scrollY = {};
234 windowUtils.getScrollXY(false, scrollX, scrollY);
235
236 let cssPageRectCopy = {};
237
238 cssPageRectCopy.right = cssPageRect.right - scrollX.value;
239 cssPageRectCopy.left = cssPageRect.left - scrollX.value;
240 cssPageRectCopy.top = cssPageRect.top - scrollY.value;
241 cssPageRectCopy.bottom = cssPageRect.bottom - scrollY.value;
242 cssPageRectCopy.width = cssPageRect.width;
243 cssPageRectCopy.height = cssPageRect.height;
244
245 return cssPageRectCopy;
246 }
247
248 // Returns the best node to focus from the list of nodes returned by the hit
249 // test.
250 function _getBestToFocus(nodes, key, currentlyFocused) {
251 let best = null;
252 let bestDist;
253 let bestMid;
254 let nodeMid;
255 let currentlyFocusedMid = _getMidpoint(currentlyFocused);
256 let currentlyFocusedRect = currentlyFocused.getBoundingClientRect();
257
258 for (let i = 0; i < nodes.length; i++) {
259 // Reject the currentlyFocused, and all node types we can't focus
260 if (!_canFocus(nodes[i]) || nodes[i] === currentlyFocused) {
261 continue;
262 }
263
264 // Reject all nodes that aren't "far enough" in the direction of the
265 // keypress
266 nodeMid = _getMidpoint(nodes[i]);
267 switch (key) {
268 case PrefObserver['keyCodeLeft']:
269 if (nodeMid.x >= (currentlyFocusedMid.x - currentlyFocusedRect.width / 2)) {
270 continue;
271 }
272 break;
273 case PrefObserver['keyCodeRight']:
274 if (nodeMid.x <= (currentlyFocusedMid.x + currentlyFocusedRect.width / 2)) {
275 continue;
276 }
277 break;
278
279 case PrefObserver['keyCodeUp']:
280 if (nodeMid.y >= (currentlyFocusedMid.y - currentlyFocusedRect.height / 2)) {
281 continue;
282 }
283 break;
284 case PrefObserver['keyCodeDown']:
285 if (nodeMid.y <= (currentlyFocusedMid.y + currentlyFocusedRect.height / 2)) {
286 continue;
287 }
288 break;
289 }
290
291 // Initialize best to the first viable value:
292 if (!best) {
293 best = nodes[i];
294 bestDist = _spatialDistance(best, currentlyFocused);
295 continue;
296 }
297
298 // Of the remaining nodes, pick the one closest to the currently focused
299 // node.
300 let curDist = _spatialDistance(nodes[i], currentlyFocused);
301 if (curDist > bestDist) {
302 continue;
303 }
304
305 bestMid = _getMidpoint(best);
306 switch (key) {
307 case PrefObserver['keyCodeLeft']:
308 if (nodeMid.x > bestMid.x) {
309 best = nodes[i];
310 bestDist = curDist;
311 }
312 break;
313 case PrefObserver['keyCodeRight']:
314 if (nodeMid.x < bestMid.x) {
315 best = nodes[i];
316 bestDist = curDist;
317 }
318 break;
319 case PrefObserver['keyCodeUp']:
320 if (nodeMid.y > bestMid.y) {
321 best = nodes[i];
322 bestDist = curDist;
323 }
324 break;
325 case PrefObserver['keyCodeDown']:
326 if (nodeMid.y < bestMid.y) {
327 best = nodes[i];
328 bestDist = curDist;
329 }
330 break;
331 }
332 }
333 return best;
334 }
335
336 // Returns the midpoint of a node.
337 function _getMidpoint(node) {
338 let mid = {};
339 let box = node.getBoundingClientRect();
340 mid.x = box.left + (box.width / 2);
341 mid.y = box.top + (box.height / 2);
342
343 return mid;
344 }
345
346 // Returns true if the node is a type that we want to focus, false otherwise.
347 function _canFocus(node) {
348 if (node instanceof Ci.nsIDOMHTMLLinkElement ||
349 node instanceof Ci.nsIDOMHTMLAnchorElement) {
350 return true;
351 }
352 if ((node instanceof Ci.nsIDOMHTMLButtonElement ||
353 node instanceof Ci.nsIDOMHTMLInputElement ||
354 node instanceof Ci.nsIDOMHTMLLinkElement ||
355 node instanceof Ci.nsIDOMHTMLOptGroupElement ||
356 node instanceof Ci.nsIDOMHTMLSelectElement ||
357 node instanceof Ci.nsIDOMHTMLTextAreaElement) &&
358 node.disabled === false) {
359 return true;
360 }
361 return false;
362 }
363
364 // Returns a rectangle that extends to the end of the screen in the direction that
365 // the key is pressed.
366 function _getSearchRect(currentlyFocused, key, cssPageRect) {
367 let currentlyFocusedRect = currentlyFocused.getBoundingClientRect();
368
369 let newRect = {};
370 newRect.left = currentlyFocusedRect.left;
371 newRect.top = currentlyFocusedRect.top;
372 newRect.right = currentlyFocusedRect.right;
373 newRect.bottom = currentlyFocusedRect.bottom;
374 newRect.width = currentlyFocusedRect.width;
375 newRect.height = currentlyFocusedRect.height;
376
377 switch (key) {
378 case PrefObserver['keyCodeLeft']:
379 newRect.left = cssPageRect.left;
380 newRect.width = newRect.right - newRect.left;
381 break;
382
383 case PrefObserver['keyCodeRight']:
384 newRect.right = cssPageRect.right;
385 newRect.width = newRect.right - newRect.left;
386 break;
387
388 case PrefObserver['keyCodeUp']:
389 newRect.top = cssPageRect.top;
390 newRect.height = newRect.bottom - newRect.top;
391 break;
392
393 case PrefObserver['keyCodeDown']:
394 newRect.bottom = cssPageRect.bottom;
395 newRect.height = newRect.bottom - newRect.top;
396 break;
397 }
398 return newRect;
399 }
400
401 // Gets the distance between two points a and b.
402 function _spatialDistance(a, b) {
403 let mida = _getMidpoint(a);
404 let midb = _getMidpoint(b);
405
406 return Math.round(Math.pow(mida.x - midb.x, 2) +
407 Math.pow(mida.y - midb.y, 2));
408 }
409
410 // Snav preference observer
411 var PrefObserver = {
412 register: function() {
413 this.prefService = Cc["@mozilla.org/preferences-service;1"]
414 .getService(Ci.nsIPrefService);
415
416 this._branch = this.prefService.getBranch("snav.");
417 this._branch.QueryInterface(Ci.nsIPrefBranch2);
418 this._branch.addObserver("", this, false);
419
420 // set current or default pref values
421 this.observe(null, "nsPref:changed", "enabled");
422 this.observe(null, "nsPref:changed", "xulContentEnabled");
423 this.observe(null, "nsPref:changed", "keyCode.modifier");
424 this.observe(null, "nsPref:changed", "keyCode.right");
425 this.observe(null, "nsPref:changed", "keyCode.up");
426 this.observe(null, "nsPref:changed", "keyCode.down");
427 this.observe(null, "nsPref:changed", "keyCode.left");
428 this.observe(null, "nsPref:changed", "keyCode.return");
429 },
430
431 observe: function(aSubject, aTopic, aData) {
432 if (aTopic != "nsPref:changed") {
433 return;
434 }
435
436 // aSubject is the nsIPrefBranch we're observing (after appropriate QI)
437 // aData is the name of the pref that's been changed (relative to aSubject)
438 switch (aData) {
439 case "enabled":
440 try {
441 this.enabled = this._branch.getBoolPref("enabled");
442 } catch(e) {
443 this.enabled = false;
444 }
445 break;
446
447 case "xulContentEnabled":
448 try {
449 this.xulContentEnabled = this._branch.getBoolPref("xulContentEnabled");
450 } catch(e) {
451 this.xulContentEnabled = false;
452 }
453 break;
454
455 case "keyCode.modifier": {
456 let keyCodeModifier;
457 try {
458 keyCodeModifier = this._branch.getCharPref("keyCode.modifier");
459
460 // resetting modifiers
461 this.modifierAlt = false;
462 this.modifierShift = false;
463 this.modifierCtrl = false;
464
465 if (keyCodeModifier != this.kNone) {
466 // we are using '+' as a separator in about:config.
467 let mods = keyCodeModifier.split(/\++/);
468 for (let i = 0; i < mods.length; i++) {
469 let mod = mods[i].toLowerCase();
470 if (mod === "")
471 continue;
472 else if (mod == kAlt)
473 this.modifierAlt = true;
474 else if (mod == kShift)
475 this.modifierShift = true;
476 else if (mod == kCtrl)
477 this.modifierCtrl = true;
478 else {
479 keyCodeModifier = kNone;
480 break;
481 }
482 }
483 }
484 } catch(e) { }
485 break;
486 }
487
488 case "keyCode.up":
489 try {
490 this.keyCodeUp = this._branch.getIntPref("keyCode.up");
491 } catch(e) {
492 this.keyCodeUp = Ci.nsIDOMKeyEvent.DOM_VK_UP;
493 }
494 break;
495 case "keyCode.down":
496 try {
497 this.keyCodeDown = this._branch.getIntPref("keyCode.down");
498 } catch(e) {
499 this.keyCodeDown = Ci.nsIDOMKeyEvent.DOM_VK_DOWN;
500 }
501 break;
502 case "keyCode.left":
503 try {
504 this.keyCodeLeft = this._branch.getIntPref("keyCode.left");
505 } catch(e) {
506 this.keyCodeLeft = Ci.nsIDOMKeyEvent.DOM_VK_LEFT;
507 }
508 break;
509 case "keyCode.right":
510 try {
511 this.keyCodeRight = this._branch.getIntPref("keyCode.right");
512 } catch(e) {
513 this.keyCodeRight = Ci.nsIDOMKeyEvent.DOM_VK_RIGHT;
514 }
515 break;
516 case "keyCode.return":
517 try {
518 this.keyCodeReturn = this._branch.getIntPref("keyCode.return");
519 } catch(e) {
520 this.keyCodeReturn = Ci.nsIDOMKeyEvent.DOM_VK_RETURN;
521 }
522 break;
523 }
524 }
525 };
526
527 PrefObserver.register();

mercurial