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
3  *
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/
8  *
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.
13  *
14  * The Original Code is Spatial Navigation.
15  *
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.
19  *
20  * Contributor(s):
21  *  Doug Turner <dougt@meer.net>  (Original Author)
22  *
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.
34  *
35  * ***** END LICENSE BLOCK ***** */
37 /**
38  * 
39  * Import this module through
40  *
41  * Components.utils.import("resource://gre/modules/SpatialNavigation.js");
42  *
43  * Usage: (Literal class)
44  *
45  * SpatialNavigation(browser_element, optional_callback);
46  *
47  * optional_callback will be called when a new element is focused.
48  *
49  *    function optional_callback(element) {}
50  *
51  */
54 var EXPORTED_SYMBOLS = ["SpatialNavigation"];
56 var SpatialNavigation = {
58   init: function(browser, callback) {
59     browser.addEventListener("keypress", function (event) { _onInputKeyPress(event, callback) }, true);
60   },
61   
62   uninit: function() {
63   }
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;
127   }
129   if ((target instanceof Ci.nsIDOMHTMLInputElement && (target.type == "text" || target.type == "password")) ||
130       target instanceof Ci.nsIDOMHTMLTextAreaElement ) {
131     
132     // if there is any selection at all, just ignore
133     if (target.selectionEnd - target.selectionStart > 0)
134       return;
135     
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;
143       }
144       else
145       {
146         // we are at the start of the text, okay to move 
147         if (target.selectionStart != 0)
148           return;
149       }
150     }
151   }
153   // Check to see if we are in a select
154   if (target instanceof Ci.nsIDOMHTMLSelectElement)
155   {
156     if (key == PrefObserver['keyCodeDown']) {
157       if (target.selectedIndex + 1 < target.length)
158         return;
159     }
161     if (key == PrefObserver['keyCodeUp']) {
162       if (target.selectedIndex > 0)
163         return;
164     }
165   }
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;
175     }
176     
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;
185     
186     return Ci.nsIDOMNodeFilter.FILTER_SKIP;
187   }
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;
196   
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);
211     
212     if (distance <= distanceToBestElement && distance > 0) {
213       distanceToBestElement = distance;
214       bestElementToFocus = nextNode;
215     }
216   }
217   
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;
229     }
231     if (callback != undefined)
232       callback(bestElementToFocus);
233     
234   } else {
235     // couldn't find anything.  just advance and hope.
236     _focusNextUsingCmdDispatcher(key, callback);
237   }
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();
252     }
254     if (callback != undefined)
255       callback(null);
258 function _isRectInDirection(key, focusedRect, anotherRect)
260   if (key == PrefObserver['keyCodeLeft']) {
261     return (anotherRect.left < focusedRect.left);
262   }
264   if (key == PrefObserver['keyCodeRight']) {
265     return (anotherRect.right > focusedRect.right);
266   }
268   if (key == PrefObserver['keyCodeUp']) {
269     return (anotherRect.top < focusedRect.top);
270   }
272   if (key == PrefObserver['keyCodeDown']) {
273     return (anotherRect.bottom > focusedRect.bottom);
274   }
275     return false;
278 function _inflateRect(rect, value)
280   var newRect = new Object();
281   
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     //  |---|
306     //
307     //  |---|  |---|
308     //  |---|  |---|
309     //
310     //  |---|
311     //  |---|
312     //
313     
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;
320     }
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;       
327     }
328     else {
329       mx = a.left;
330       my = 0;
331       nx = b.right;
332       ny = 0;
333     }
334   } else if (key == PrefObserver['keyCodeRight']) {
336     //         |---|
337     //         |---|
338     //
339     //  |---|  |---|
340     //  |---|  |---|
341     //
342     //         |---|
343     //         |---|
344     //
345     
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;
352     }
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;
364     }
365   } else if (key == PrefObserver['keyCodeUp']) {
367     //  |---|  |---|  |---|
368     //  |---|  |---|  |---|
369     //
370     //         |---|
371     //         |---|
372     //
373     
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;
392     }
393   } else if (key == PrefObserver['keyCodeDown']) {
395     //         |---|
396     //         |---|
397     //
398     //  |---|  |---|  |---|
399     //  |---|  |---|  |---|
400     //
401     
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;
420     }
421   }
422   
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);
430   }
431   else if (key == PrefObserver['keyCodeUp'] ||
432            key == PrefObserver['keyCodeDown']) {
433     scopedRect.top = 0;
434     scopedRect.bottom = Infinity;
435     inlineNavigation = _containsRect(scopedRect, b);
436   }
437   
438   var d = Math.pow((mx-nx), 2) + Math.pow((my-ny), 2);
439   
440   // prefer elements directly aligned with the focused element
441   if (inlineNavigation)
442     d /= gDirectionalBias;
443   
444   return d;
447 // Snav preference observer
449 PrefObserver = {
451   register: function()
452   {
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");
468   },
470   observe: function(aSubject, aTopic, aData)
471   {
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;
483         }
484         break;
485       case "xulContentEnabled":
486         try {
487           this.xulContentEnabled = this._branch.getBoolPref("xulContentEnabled");
488         } catch(e) {
489           this.xulContentEnabled = false;
490         }
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)
503           {
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;
519               }
520             }
521           }
522         } catch(e) {
523             this.keyCodeModifier = kNone;
524         }
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;
531         }
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;
538         }
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;
545         }
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;
552         }
553         break;
554     }
555   },
558 PrefObserver.register();