diff -r 378495e669f9 toolkit/Makefile.in --- a/toolkit/Makefile.in Sat Jun 28 03:55:30 2008 -0400 +++ b/toolkit/Makefile.in Tue Jul 01 15:36:38 2008 -0400 @@ -49,6 +49,7 @@ DIRS = \ obsolete \ profile \ themes \ + spatial-navigation \ $(NULL) ifneq (,$(filter gtk2,$(MOZ_WIDGET_TOOLKIT))) diff -r 378495e669f9 toolkit/spatial-navigation/Makefile.in --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/spatial-navigation/Makefile.in Tue Jul 01 15:36:38 2008 -0400 @@ -0,0 +1,52 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org build system. +# +# The Initial Developer of the Original Code is Mozilla Corporation +# Portions created by the Initial Developer are Copyright (C) 2008 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Doug Turner +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +# EXTRA_COMPONENTS installs components written JavaScript to +# dist/bin/components +EXTRA_COMPONENTS = spatial-navigation.js + +ifdef ENABLE_TESTS + DIRS += tests +endif + +include $(topsrcdir)/config/rules.mk diff -r 378495e669f9 toolkit/spatial-navigation/spatial-navigation.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/spatial-navigation/spatial-navigation.js Tue Jul 01 15:36:38 2008 -0400 @@ -0,0 +1,373 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Spatial Navigation. + * + * The Initial Developer of the Original Code is Mozilla Corporation + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Doug Turner (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const Cc = Components.classes; +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +//function dump(msg) +//{ +// var console = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService); +// console.logStringMessage("*** SNAV: " + msg); +//} + +var gDirectionalBias = 10; +var gRectFudge = 1; + +function SpatialNavigation() {}; + +SpatialNavigation.prototype = { + classDescription: "Spatial Navigation XPCOM Component", + classID: Components.ID("{0E249DC7-0EAF-438E-A553-55F7DFFBB4E1}"), + contractID: "@mozilla.com/spatialnavigation;1", + _xpcom_categories: [{ category: "app-startup", service: true }], + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), + + observe: function(aSubject, aTopic, aData) { + var observerService = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + switch (aTopic) { + case "app-startup": + observerService.addObserver(this, "profile-after-change", false); + observerService.addObserver(this, "domwindowopened", false); + break; + + case "domwindowopened": + var window = aSubject; + var self = this; + window.addEventListener("load", function() { + self._onWindowOpened(self, window); + }, false); + break; + + case "profile-after-change": + // check to see if our preference is enabled. + // TBD: add other prefs here. + var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + try { + this.mEnabled = prefs.getBoolPref("snav.enabled"); + } catch(e) { + this.mEnabled = false; + } + } + }, + + _onWindowOpened: function(self, aWindow) { + var wType = aWindow.document.documentElement.getAttribute("windowtype"); + if (wType != "navigator:browser") + return; + + var browser = aWindow.document.getElementById("content"); + browser.addEventListener("keypress",self._onInputKeyPress, false); + }, + + _onInputKeyPress: function(event) { + + // TBD: Why not just check this.mEnabled ? + var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + try { + this.mEnabled = prefs.getBoolPref("snav.enabled"); + } catch(e) { + this.mEnabled = false; + } + + if (!this.mEnabled) + return; + + // If it isn't an arrow key, bail. + if (event.keyCode != event.DOM_VK_LEFT && + event.keyCode != event.DOM_VK_RIGHT && + event.keyCode != event.DOM_VK_UP && + event.keyCode != event.DOM_VK_DOWN ) + return; + + function snavfilter(node) { + + if (node instanceof Ci.nsIDOMHTMLLinkElement || + node instanceof Ci.nsIDOMHTMLAnchorElement) { + // if a anchor doesn't have a href, don't target it. + if (node.href == "") + return Ci.nsIDOMNodeFilter.FILTER_SKIP; + return Ci.nsIDOMNodeFilter.FILTER_ACCEPT; + } + + if (node instanceof Ci.nsIDOMHTMLInputElement || + node instanceof Ci.nsIDOMHTMLSelectElement || + node instanceof Ci.nsIDOMHTMLOptionElement) + return Ci.nsIDOMNodeFilter.FILTER_ACCEPT; + return Ci.nsIDOMNodeFilter.FILTER_SKIP; + } + var bestElementToFocus = null; + var distanceToBestElement = Infinity; + var focusedRect = inflateRect(event.target.getBoundingClientRect(), + - gRectFudge); + var doc = event.target.ownerDocument; + + var treeWalker = doc.createTreeWalker(doc, Ci.nsIDOMNodeFilter.SHOW_ELEMENT, snavfilter, false); + var nextNode; + + while ((nextNode = treeWalker.nextNode())) { + + if (nextNode == event.target) + continue; + + var nextRect = inflateRect(nextNode.getBoundingClientRect(), + - gRectFudge); + + if (! isRectInDirection(event, focusedRect, nextRect)) + continue; + + var distance = spatialDistance(event, focusedRect, nextRect); + + if (distance <= distanceToBestElement && distance > 0) { + distanceToBestElement = distance; + bestElementToFocus = nextNode; + } + } + + if (bestElementToFocus != null) { + //dump("focusing element " + bestElementToFocus.nodeName); + bestElementToFocus.focus(); + } else { + // couldn't find anything. just advance and hope. + //dump("advancing focus"); + var windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']. + getService(Ci.nsIWindowMediator); + var window = windowMediator.getMostRecentWindow("navigator:browser"); + window.document.commandDispatcher.advanceFocus(); + } + + event.preventDefault(); + event.stopPropagation(); + }, +}; + +function isRectInDirection(event, focusedRect, anotherRect) +{ + if (event.keyCode == event.DOM_VK_LEFT) { + return (anotherRect.left < focusedRect.left); + } + + if (event.keyCode == event.DOM_VK_RIGHT) { + return (anotherRect.right > focusedRect.right); + } + + if (event.keyCode == event.DOM_VK_UP) { + return (anotherRect.top < focusedRect.top); + } + + if (event.keyCode == event.DOM_VK_DOWN) { + return (anotherRect.bottom > focusedRect.bottom); + } + return false; +} + + +function inflateRect(rect, value) +{ + var newRect = new Object(); + + newRect.left = rect.left - value; + newRect.top = rect.top - value; + newRect.right = rect.right + value; + newRect.bottom = rect.bottom + value; + return newRect; +} + +function Contains(a, b) +{ + return ( (b.left <= a.right) && + (b.right >= a.left) && + (b.top <= a.bottom) && + (b.bottom >= a.top) ); +} + +function spatialDistance(event, a, b) +{ + var inlineNavigation = false; + var mx, my, nx, ny; + + if (event.keyCode == event.DOM_VK_LEFT) { + + // |---| + // |---| + // + // |---| |---| + // |---| |---| + // + // |---| + // |---| + // + + if (a.top > b.bottom) { + // the b rect is above a. + mx = a.left; + my = a.top; + nx = b.right; + ny = b.bottom; + } + else if (a.bottom < b.top) { + // the b rect is below a. + mx = a.left; + my = a.bottom; + nx = b.right; + ny = b.top; + } + else { + mx = a.left; + my = 0; + nx = b.right; + ny = 0; + } + } else if (event.keyCode == event.DOM_VK_RIGHT) { + + // |---| + // |---| + // + // |---| |---| + // |---| |---| + // + // |---| + // |---| + // + + if (a.top > b.bottom) { + // the b rect is above a. + mx = a.right; + my = a.top; + nx = b.left; + ny = b.bottom; + } + else if (a.bottom < b.top) { + // the b rect is below a. + mx = a.right; + my = a.bottom; + nx = b.left; + ny = b.top; + } else { + mx = a.right; + my = 0; + nx = b.left; + ny = 0; + } + } else if (event.keyCode == event.DOM_VK_UP) { + + // |---| |---| |---| + // |---| |---| |---| + // + // |---| + // |---| + // + + if (a.left > b.right) { + // the b rect is to the left of a. + mx = a.left; + my = a.top; + nx = b.right; + ny = b.bottom; + } else if (a.right < b.left) { + // the b rect is to the right of a + mx = a.right; + my = a.top; + nx = b.left; + ny = b.bottom; + } else { + // both b and a share some common x's. + mx = 0; + my = a.top; + nx = 0; + ny = b.bottom; + } + } else if (event.keyCode == event.DOM_VK_DOWN) { + + // |---| + // |---| + // + // |---| |---| |---| + // |---| |---| |---| + // + + if (a.left > b.right) { + // the b rect is to the left of a. + mx = a.left; + my = a.bottom; + nx = b.right; + ny = b.top; + } else if (a.right < b.left) { + // the b rect is to the right of a + mx = a.right; + my = a.bottom; + nx = b.left; + ny = b.top; + } else { + // both b and a share some common x's. + mx = 0; + my = a.bottom; + nx = 0; + ny = b.top; + } + } + + var scopedRect = inflateRect(a, gRectFudge); + + if (event.keyCode == event.DOM_VK_LEFT || + event.keyCode == event.DOM_VK_RIGHT) { + scopedRect.left = 0; + scopedRect.right = Infinity; + inlineNavigation = Contains(scopedRect, b); + } + else if (event.keyCode == event.DOM_VK_UP || + event.keyCode == event.DOM_VK_DOWN) { + scopedRect.top = 0; + scopedRect.bottom = Infinity; + inlineNavigation = Contains(scopedRect, b); + } + + var d = Math.pow((mx-nx), 2) + Math.pow((my-ny), 2); + + // prefer elements directly aligned with the focused element + if (inlineNavigation) + d /= gDirectionalBias; + + return d; +} + +var components = [SpatialNavigation]; + +function NSGetModule(compMgr, fileSpec) { + return XPCOMUtils.generateModule(components); +} diff -r 378495e669f9 toolkit/spatial-navigation/tests/Makefile.in --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/spatial-navigation/tests/Makefile.in Tue Jul 01 15:36:38 2008 -0400 @@ -0,0 +1,55 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org build system. +# +# The Initial Developer of the Original Code is Mozilla Corporation +# Portions created by the Initial Developer are Copyright (C) 2008 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Doug Turner +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ +relativesrcdir = toolkit/spatial-navigation/tests + +include $(DEPTH)/config/autoconf.mk + +MODULE = test_snav + +MOCHI_TESTS = mochitest/test_snav.html \ + $(NULL) + +MOCHI_CONTENT = $(NULL) + +include $(topsrcdir)/config/rules.mk + +libs:: $(MOCHI_TESTS) $(MOCHI_CONTENT) + $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir) diff -r 378495e669f9 toolkit/spatial-navigation/tests/mochitest/test_snav.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/spatial-navigation/tests/mochitest/test_snav.html Tue Jul 01 15:36:38 2008 -0400 @@ -0,0 +1,72 @@ + + + + + Test for Bug 436084 + + + + + + +Mozilla Bug 436084 +

+
+ + + + + + + + + + + + + + + + + + +
testtesttest
testtesttest
testtesttest
+ + +
+
+
+
+ + +