Bug 470455 - test_database_sync_embed_visits.js leaks, r=sdwilsh
[wine-gecko.git] / toolkit / spatial-navigation / SpatialNavigation.js
blob682a0e105124de5672ad8d423314219572d46610
1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 * The contents of this file are subject to the Mozilla Public License Version
5 * 1.1 (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
7 * http://www.mozilla.org/MPL/
9 * Software distributed under the License is distributed on an "AS IS" basis,
10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 * for the specific language governing rights and limitations under the
12 * License.
14 * The Original Code is Spatial Navigation.
16 * The Initial Developer of the Original Code is Mozilla Corporation
17 * Portions created by the Initial Developer are Copyright (C) 2008
18 * the Initial Developer. All Rights Reserved.
20 * Contributor(s):
21 * Doug Turner <dougt@meer.net> (Original Author)
23 * Alternatively, the contents of this file may be used under the terms of
24 * either the GNU General Public License Version 2 or later (the "GPL"), or
25 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26 * in which case the provisions of the GPL or the LGPL are applicable instead
27 * of those above. If you wish to allow use of your version of this file only
28 * under the terms of either the GPL or the LGPL, and not to allow others to
29 * use your version of this file under the terms of the MPL, indicate your
30 * decision by deleting the provisions above and replace them with the notice
31 * and other provisions required by the GPL or the LGPL. If you do not delete
32 * the provisions above, a recipient may use your version of this file under
33 * the terms of any one of the MPL, the GPL or the LGPL.
35 * ***** END LICENSE BLOCK ***** */
37 /**
39 * Import this module through
41 * Components.utils.import("resource://gre/modules/SpatialNavigation.js");
43 * Usage: (Literal class)
45 * SpatialNavigation(browser_element, optional_callback);
47 * optional_callback will be called when a new element is focused.
49 * function optional_callback(element) {}
54 var EXPORTED_SYMBOLS = ["SpatialNavigation"];
56 var SpatialNavigation = {
58 init: function(browser, callback) {
59 browser.addEventListener("keypress", function (event) { _onInputKeyPress(event, callback) }, true);
62 uninit: function() {
67 // Private stuff
69 const Cc = Components.classes;
70 const Ci = Components.interfaces;
72 function dump(msg)
74 var console = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService);
75 console.logStringMessage("*** SNAV: " + msg);
78 var gDirectionalBias = 10;
79 var gRectFudge = 1;
81 // modifier values
82 const kAlt = "alt";
83 const kShift = "shift";
84 const kCtrl = "ctrl";
85 const kNone = "none";
87 function _onInputKeyPress (event, callback) {
89 // Use whatever key value is available (either keyCode or charCode).
90 // It might be useful for addons or whoever wants to set different
91 // key to be used here (e.g. "a", "F1", "arrowUp", ...).
92 var key = event.which || event.keyCode;
94 // If it isn't enabled, bail.
95 if (!PrefObserver['enabled'])
96 return;
98 if (key != PrefObserver['keyCodeDown'] &&
99 key != PrefObserver['keyCodeRight'] &&
100 key != PrefObserver['keyCodeUp'] &&
101 key != PrefObserver['keyCodeLeft'])
102 return;
104 // If it is not using the modifiers it should, bail.
105 if (!event.altKey && PrefObserver['modifierAlt'])
106 return;
108 if (!event.shiftKey && PrefObserver['modifierShift'])
109 return;
111 if (!event.crtlKey && PrefObserver['modifierCtrl'])
112 return;
114 var target = event.target;
116 var doc = target.ownerDocument;
118 // If it is XUL content (e.g. about:config), bail.
119 if (!PrefObserver['xulContentEnabled'] && doc instanceof Ci.nsIDOMXULDocument)
120 return ;
122 // check to see if we are in a textarea or text input element, and if so,
123 // ensure that we let the arrow keys work properly.
124 if (target instanceof Ci.nsIDOMHTMLHtmlElement) {
125 _focusNextUsingCmdDispatcher(key, callback);
126 return;
129 if ((target instanceof Ci.nsIDOMHTMLInputElement && (target.type == "text" || target.type == "password")) ||
130 target instanceof Ci.nsIDOMHTMLTextAreaElement ) {
132 // if there is any selection at all, just ignore
133 if (target.selectionEnd - target.selectionStart > 0)
134 return;
136 // if there is no text, there is nothing special to do.
137 if (target.textLength > 0) {
138 if (key == PrefObserver['keyCodeRight'] ||
139 key == PrefObserver['keyCodeDown'] ) {
140 // we are moving forward into the document
141 if (target.textLength != target.selectionEnd)
142 return;
144 else
146 // we are at the start of the text, okay to move
147 if (target.selectionStart != 0)
148 return;
153 // Check to see if we are in a select
154 if (target instanceof Ci.nsIDOMHTMLSelectElement)
156 if (key == PrefObserver['keyCodeDown']) {
157 if (target.selectedIndex + 1 < target.length)
158 return;
161 if (key == PrefObserver['keyCodeUp']) {
162 if (target.selectedIndex > 0)
163 return;
167 function snavfilter(node) {
169 if (node instanceof Ci.nsIDOMHTMLLinkElement ||
170 node instanceof Ci.nsIDOMHTMLAnchorElement) {
171 // if a anchor doesn't have a href, don't target it.
172 if (node.href == "")
173 return Ci.nsIDOMNodeFilter.FILTER_SKIP;
174 return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
177 if ((node instanceof Ci.nsIDOMHTMLButtonElement ||
178 node instanceof Ci.nsIDOMHTMLInputElement ||
179 node instanceof Ci.nsIDOMHTMLLinkElement ||
180 node instanceof Ci.nsIDOMHTMLOptGroupElement ||
181 node instanceof Ci.nsIDOMHTMLSelectElement ||
182 node instanceof Ci.nsIDOMHTMLTextAreaElement) &&
183 node.disabled == false)
184 return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
186 return Ci.nsIDOMNodeFilter.FILTER_SKIP;
189 var bestElementToFocus = null;
190 var distanceToBestElement = Infinity;
191 var focusedRect = _inflateRect(target.getBoundingClientRect(),
192 - gRectFudge);
194 var treeWalker = doc.createTreeWalker(doc, Ci.nsIDOMNodeFilter.SHOW_ELEMENT, snavfilter, false);
195 var nextNode;
197 while ((nextNode = treeWalker.nextNode())) {
199 if (nextNode == target)
200 continue;
202 var nextRect = _inflateRect(nextNode.getBoundingClientRect(),
203 - gRectFudge);
205 if (! _isRectInDirection(key, focusedRect, nextRect))
206 continue;
208 var distance = _spatialDistance(key, focusedRect, nextRect);
210 //dump("looking at: " + nextNode + " " + distance);
212 if (distance <= distanceToBestElement && distance > 0) {
213 distanceToBestElement = distance;
214 bestElementToFocus = nextNode;
218 if (bestElementToFocus != null) {
219 //dump("focusing element " + bestElementToFocus.nodeName + " " + bestElementToFocus) + "id=" + bestElementToFocus.getAttribute("id");
221 // Wishing we could do element.focus()
222 doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).focus(bestElementToFocus);
224 // if it is a text element, select all.
225 if((bestElementToFocus instanceof Ci.nsIDOMHTMLInputElement && (bestElementToFocus.type == "text" || bestElementToFocus.type == "password")) ||
226 bestElementToFocus instanceof Ci.nsIDOMHTMLTextAreaElement ) {
227 bestElementToFocus.selectionStart = 0;
228 bestElementToFocus.selectionEnd = bestElementToFocus.textLength;
231 if (callback != undefined)
232 callback(bestElementToFocus);
234 } else {
235 // couldn't find anything. just advance and hope.
236 _focusNextUsingCmdDispatcher(key, callback);
239 event.preventDefault();
240 event.stopPropagation();
243 function _focusNextUsingCmdDispatcher(key, callback) {
245 var windowMediator = Cc['@mozilla.org/appshell/window-mediator;1'].getService(Ci.nsIWindowMediator);
246 var window = windowMediator.getMostRecentWindow("navigator:browser");
248 if (key == PrefObserver['keyCodeRight'] || key == PrefObserver['keyCodeDown']) {
249 window.document.commandDispatcher.advanceFocus();
250 } else {
251 window.document.commandDispatcher.rewindFocus();
254 if (callback != undefined)
255 callback(null);
258 function _isRectInDirection(key, focusedRect, anotherRect)
260 if (key == PrefObserver['keyCodeLeft']) {
261 return (anotherRect.left < focusedRect.left);
264 if (key == PrefObserver['keyCodeRight']) {
265 return (anotherRect.right > focusedRect.right);
268 if (key == PrefObserver['keyCodeUp']) {
269 return (anotherRect.top < focusedRect.top);
272 if (key == PrefObserver['keyCodeDown']) {
273 return (anotherRect.bottom > focusedRect.bottom);
275 return false;
278 function _inflateRect(rect, value)
280 var newRect = new Object();
282 newRect.left = rect.left - value;
283 newRect.top = rect.top - value;
284 newRect.right = rect.right + value;
285 newRect.bottom = rect.bottom + value;
286 return newRect;
289 function _containsRect(a, b)
291 return ( (b.left <= a.right) &&
292 (b.right >= a.left) &&
293 (b.top <= a.bottom) &&
294 (b.bottom >= a.top) );
297 function _spatialDistance(key, a, b)
299 var inlineNavigation = false;
300 var mx, my, nx, ny;
302 if (key == PrefObserver['keyCodeLeft']) {
304 // |---|
305 // |---|
307 // |---| |---|
308 // |---| |---|
310 // |---|
311 // |---|
314 if (a.top > b.bottom) {
315 // the b rect is above a.
316 mx = a.left;
317 my = a.top;
318 nx = b.right;
319 ny = b.bottom;
321 else if (a.bottom < b.top) {
322 // the b rect is below a.
323 mx = a.left;
324 my = a.bottom;
325 nx = b.right;
326 ny = b.top;
328 else {
329 mx = a.left;
330 my = 0;
331 nx = b.right;
332 ny = 0;
334 } else if (key == PrefObserver['keyCodeRight']) {
336 // |---|
337 // |---|
339 // |---| |---|
340 // |---| |---|
342 // |---|
343 // |---|
346 if (a.top > b.bottom) {
347 // the b rect is above a.
348 mx = a.right;
349 my = a.top;
350 nx = b.left;
351 ny = b.bottom;
353 else if (a.bottom < b.top) {
354 // the b rect is below a.
355 mx = a.right;
356 my = a.bottom;
357 nx = b.left;
358 ny = b.top;
359 } else {
360 mx = a.right;
361 my = 0;
362 nx = b.left;
363 ny = 0;
365 } else if (key == PrefObserver['keyCodeUp']) {
367 // |---| |---| |---|
368 // |---| |---| |---|
370 // |---|
371 // |---|
374 if (a.left > b.right) {
375 // the b rect is to the left of a.
376 mx = a.left;
377 my = a.top;
378 nx = b.right;
379 ny = b.bottom;
380 } else if (a.right < b.left) {
381 // the b rect is to the right of a
382 mx = a.right;
383 my = a.top;
384 nx = b.left;
385 ny = b.bottom;
386 } else {
387 // both b and a share some common x's.
388 mx = 0;
389 my = a.top;
390 nx = 0;
391 ny = b.bottom;
393 } else if (key == PrefObserver['keyCodeDown']) {
395 // |---|
396 // |---|
398 // |---| |---| |---|
399 // |---| |---| |---|
402 if (a.left > b.right) {
403 // the b rect is to the left of a.
404 mx = a.left;
405 my = a.bottom;
406 nx = b.right;
407 ny = b.top;
408 } else if (a.right < b.left) {
409 // the b rect is to the right of a
410 mx = a.right;
411 my = a.bottom;
412 nx = b.left;
413 ny = b.top;
414 } else {
415 // both b and a share some common x's.
416 mx = 0;
417 my = a.bottom;
418 nx = 0;
419 ny = b.top;
423 var scopedRect = _inflateRect(a, gRectFudge);
425 if (key == PrefObserver['keyCodeLeft'] ||
426 key == PrefObserver['keyCodeRight']) {
427 scopedRect.left = 0;
428 scopedRect.right = Infinity;
429 inlineNavigation = _containsRect(scopedRect, b);
431 else if (key == PrefObserver['keyCodeUp'] ||
432 key == PrefObserver['keyCodeDown']) {
433 scopedRect.top = 0;
434 scopedRect.bottom = Infinity;
435 inlineNavigation = _containsRect(scopedRect, b);
438 var d = Math.pow((mx-nx), 2) + Math.pow((my-ny), 2);
440 // prefer elements directly aligned with the focused element
441 if (inlineNavigation)
442 d /= gDirectionalBias;
444 return d;
447 // Snav preference observer
449 PrefObserver = {
451 register: function()
453 this.prefService = Cc["@mozilla.org/preferences-service;1"]
454 .getService(Ci.nsIPrefService);
456 this._branch = this.prefService.getBranch("snav.");
457 this._branch.QueryInterface(Ci.nsIPrefBranch2);
458 this._branch.addObserver("", this, false);
460 // set current or default pref values
461 this.observe(null, "nsPref:changed", "enabled");
462 this.observe(null, "nsPref:changed", "xulContentEnabled");
463 this.observe(null, "nsPref:changed", "keyCode.modifier");
464 this.observe(null, "nsPref:changed", "keyCode.right");
465 this.observe(null, "nsPref:changed", "keyCode.up");
466 this.observe(null, "nsPref:changed", "keyCode.down");
467 this.observe(null, "nsPref:changed", "keyCode.left");
470 observe: function(aSubject, aTopic, aData)
472 if(aTopic != "nsPref:changed")
473 return;
475 // aSubject is the nsIPrefBranch we're observing (after appropriate QI)
476 // aData is the name of the pref that's been changed (relative to aSubject)
477 switch (aData) {
478 case "enabled":
479 try {
480 this.enabled = this._branch.getBoolPref("enabled");
481 } catch(e) {
482 this.enabled = false;
484 break;
485 case "xulContentEnabled":
486 try {
487 this.xulContentEnabled = this._branch.getBoolPref("xulContentEnabled");
488 } catch(e) {
489 this.xulContentEnabled = false;
491 break;
493 case "keyCode.modifier":
494 try {
495 this.keyCodeModifier = this._branch.getCharPref("keyCode.modifier");
497 // resetting modifiers
498 this.modifierAlt = false;
499 this.modifierShift = false;
500 this.modifierCtrl = false;
502 if (this.keyCodeModifier != this.kNone)
504 // use are using '+' as a separator in about:config.
505 var mods = this.keyCodeModifier.split(/\++/);
506 for (var i = 0; i < mods.length; i++) {
507 var mod = mods[i].toLowerCase();
508 if (mod == "")
509 continue;
510 else if (mod == kAlt)
511 this.modifierAlt = true;
512 else if (mod == kShift)
513 this.modifierShift = true;
514 else if (mod == kCtrl)
515 this.modifierCtrl = true;
516 else {
517 this.keyCodeModifier = kNone;
518 break;
522 } catch(e) {
523 this.keyCodeModifier = kNone;
525 break;
526 case "keyCode.up":
527 try {
528 this.keyCodeUp = this._branch.getIntPref("keyCode.up");
529 } catch(e) {
530 this.keyCodeUp = Ci.nsIDOMKeyEvent.DOM_VK_UP;
532 break;
533 case "keyCode.down":
534 try {
535 this.keyCodeDown = this._branch.getIntPref("keyCode.down");
536 } catch(e) {
537 this.keyCodeDown = Ci.nsIDOMKeyEvent.DOM_VK_DOWN;
539 break;
540 case "keyCode.left":
541 try {
542 this.keyCodeLeft = this._branch.getIntPref("keyCode.left");
543 } catch(e) {
544 this.keyCodeLeft = Ci.nsIDOMKeyEvent.DOM_VK_LEFT;
546 break;
547 case "keyCode.right":
548 try {
549 this.keyCodeRight = this._branch.getIntPref("keyCode.right");
550 } catch(e) {
551 this.keyCodeRight = Ci.nsIDOMKeyEvent.DOM_VK_RIGHT;
553 break;
558 PrefObserver.register();