toolkit/modules/SpatialNavigation.jsm

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     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/. */
     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  */
    20 "use strict";
    22 this.EXPORTED_SYMBOLS = ["SpatialNavigation"];
    24 var SpatialNavigation = {
    25   init: function(browser, callback) {
    26           browser.addEventListener("keydown", function (event) {
    27             _onInputKeyPress(event, callback);
    28           }, true);
    29   }
    30 };
    32 // Private stuff
    34 const Cc = Components.classes;
    35 const Ci = Components.interfaces;
    36 const Cu = Components.utils;
    38 Cu["import"]("resource://gre/modules/Services.jsm", this);
    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);
    47 // Debug helpers:
    48 function dump(a) {
    49   Services.console.logStringMessage("SpatialNavigation: " + a);
    50 }
    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 }
    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 }
    64 // modifier values
    66 const kAlt   = "alt";
    67 const kShift = "shift";
    68 const kCtrl  = "ctrl";
    69 const kNone  = "none";
    71 function _onInputKeyPress (event, callback) {
    72   //If Spatial Navigation isn't enabled, return.
    73   if (!PrefObserver['enabled']) {
    74     return;
    75   }
    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;
    82   if (key != PrefObserver['keyCodeDown']  &&
    83       key != PrefObserver['keyCodeRight'] &&
    84       key != PrefObserver['keyCodeUp'] &&
    85       key != PrefObserver['keyCodeLeft'] &&
    86       key != PrefObserver['keyCodeReturn']) {
    87     return;
    88   }
    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   }
   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   }
   115   let currentlyFocused = event.target;
   116   let currentlyFocusedWindow = currentlyFocused.ownerDocument.defaultView;
   117   let bestElementToFocus = null;
   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   }
   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     }
   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   }
   150   let windowUtils = currentlyFocusedWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   151                                           .getInterface(Ci.nsIDOMWindowUtils);
   152   let cssPageRect = _getRootBounds(windowUtils);
   153   let searchRect = _getSearchRect(currentlyFocused, key, cssPageRect);
   155   let nodes = {};
   156   nodes.length = 0;
   158   let searchRectOverflows = false;
   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     }
   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   }
   205   if (bestElementToFocus === null) {
   206     // Couldn't find an element to focus.
   207     return;
   208   }
   210   focusManager.setFocus(bestElementToFocus, focusManager.FLAG_SHOWRING);
   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   }
   220   if (callback != undefined) {
   221     callback(bestElementToFocus);
   222   }
   224   event.preventDefault();
   225   event.stopPropagation();
   226 }
   228 // Returns the bounds of the page relative to the viewport.
   229 function _getRootBounds(windowUtils) {
   230   let cssPageRect = windowUtils.getRootBounds();
   232   let scrollX = {};
   233   let scrollY = {};
   234   windowUtils.getScrollXY(false, scrollX, scrollY);
   236   let cssPageRectCopy = {};
   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;
   245   return cssPageRectCopy;
   246 }
   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();
   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     }
   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;
   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     }
   291     // Initialize best to the first viable value:
   292     if (!best) {
   293       best = nodes[i];
   294       bestDist = _spatialDistance(best, currentlyFocused);
   295       continue;
   296     }
   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     }
   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 }
   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);
   343   return mid;
   344 }
   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 }
   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();
   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;
   377   switch (key) {
   378     case PrefObserver['keyCodeLeft']:
   379       newRect.left = cssPageRect.left;
   380       newRect.width = newRect.right - newRect.left;
   381       break;
   383     case PrefObserver['keyCodeRight']:
   384       newRect.right = cssPageRect.right;
   385       newRect.width = newRect.right - newRect.left;
   386       break;
   388     case PrefObserver['keyCodeUp']:
   389       newRect.top = cssPageRect.top;
   390       newRect.height = newRect.bottom - newRect.top;
   391       break;
   393     case PrefObserver['keyCodeDown']:
   394       newRect.bottom = cssPageRect.bottom;
   395       newRect.height = newRect.bottom - newRect.top;
   396       break;
   397   }
   398   return newRect;
   399 }
   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);
   406   return Math.round(Math.pow(mida.x - midb.x, 2) +
   407                     Math.pow(mida.y - midb.y, 2));
   408 }
   410 // Snav preference observer
   411 var PrefObserver = {
   412   register: function() {
   413     this.prefService = Cc["@mozilla.org/preferences-service;1"]
   414                           .getService(Ci.nsIPrefService);
   416     this._branch = this.prefService.getBranch("snav.");
   417     this._branch.QueryInterface(Ci.nsIPrefBranch2);
   418     this._branch.addObserver("", this, false);
   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   },
   431   observe: function(aSubject, aTopic, aData) {
   432     if (aTopic != "nsPref:changed") {
   433       return;
   434     }
   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;
   447       case "xulContentEnabled":
   448         try {
   449           this.xulContentEnabled = this._branch.getBoolPref("xulContentEnabled");
   450         } catch(e) {
   451           this.xulContentEnabled = false;
   452         }
   453         break;
   455       case "keyCode.modifier": {
   456         let keyCodeModifier;
   457         try {
   458           keyCodeModifier = this._branch.getCharPref("keyCode.modifier");
   460           // resetting modifiers
   461           this.modifierAlt = false;
   462           this.modifierShift = false;
   463           this.modifierCtrl = false;
   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       }
   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 };
   527 PrefObserver.register();

mercurial