[Pkg-mozext-commits] [requestpolicy] 242/257: [ref] keyboard shortcut: create class + controller

David Prévot taffit at moszumanska.debian.org
Thu Jan 28 03:20:18 UTC 2016


This is an automated email from the git hooks/post-receive script.

taffit pushed a commit to branch master
in repository requestpolicy.

commit c204fe4a77e2ad2ea6a5edafbc6db01c48b098d6
Author: Martin Kimmerle <dev at 256k.de>
Date:   Fri Dec 25 09:26:00 2015 +0100

    [ref] keyboard shortcut: create class + controller
---
 src/content/controllers/keyboard-shortcuts.jsm     |  57 ++++++
 src/content/lib/classes/keyboard-shortcut.jsm      | 196 +++++++++++++++++++++
 src/content/lib/environment.process.js             |   7 +-
 src/content/lib/utils/javascript.jsm               |  44 +++++
 src/content/lib/utils/xul.jsm                      |  75 ++++++++
 src/content/models/windows.jsm                     |   8 +-
 src/content/ui/xul-trees.js                        |  13 +-
 tests/marionette/rp_puppeteer/tests/test_menu.py   |  15 +-
 tests/marionette/rp_puppeteer/ui/menu.py           |  15 +-
 tests/xpcshell/test_jsutils.js                     |  20 +++
 tests/xpcshell/test_xulutils_keyboard_shortcuts.js |  66 +++++++
 tests/xpcshell/xpcshell.ini                        |   2 +
 12 files changed, 496 insertions(+), 22 deletions(-)

diff --git a/src/content/controllers/keyboard-shortcuts.jsm b/src/content/controllers/keyboard-shortcuts.jsm
new file mode 100644
index 0000000..891fc3a
--- /dev/null
+++ b/src/content/controllers/keyboard-shortcuts.jsm
@@ -0,0 +1,57 @@
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ *
+ * RequestPolicy - A Firefox extension for control over cross-site requests.
+ * Copyright (c) 2015 Martin Kimmerle
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * ***** END LICENSE BLOCK *****
+ */
+
+/* global Components */
+const {utils: Cu} = Components;
+
+/* exported KeyboardShortcuts */
+this.EXPORTED_SYMBOLS = ["KeyboardShortcuts"];
+
+let {ScriptLoader: {importModule}} = Cu.import(
+    "chrome://rpcontinued/content/lib/script-loader.jsm", {});
+let {KeyboardShortcut} = importModule("lib/classes/keyboard-shortcut");
+
+//==============================================================================
+// KeyboardShortcuts
+//==============================================================================
+
+var KeyboardShortcuts = (function() {
+  let self = {};
+
+  let keyboardShortcuts = [];
+
+  self.startup = function() {
+    keyboardShortcuts.push(new KeyboardShortcut("openMenu", "alt shift r",
+        function(window) {
+          window.rpcontinued.overlay.openMenu();
+        }));
+  };
+
+  self.shutdown = function() {
+    keyboardShortcuts.forEach(ks => {
+      ks.destroy();
+    });
+    keyboardShortcuts.length = 0;
+  };
+
+  return self;
+}());
diff --git a/src/content/lib/classes/keyboard-shortcut.jsm b/src/content/lib/classes/keyboard-shortcut.jsm
new file mode 100644
index 0000000..e4c3cab
--- /dev/null
+++ b/src/content/lib/classes/keyboard-shortcut.jsm
@@ -0,0 +1,196 @@
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ *
+ * RequestPolicy - A Firefox extension for control over cross-site requests.
+ * Copyright (c) 2015 Martin Kimmerle
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * ***** END LICENSE BLOCK *****
+ */
+
+/* global Components */
+const {utils: Cu} = Components;
+
+/* exported KeyboardShortcut */
+this.EXPORTED_SYMBOLS = ["KeyboardShortcut"];
+
+let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
+
+let {ScriptLoader: {importModule}} = Cu.import(
+    "chrome://rpcontinued/content/lib/script-loader.jsm", {});
+let {Windows} = importModule("models/windows");
+let {XULUtils} = importModule("lib/utils/xul");
+
+//==============================================================================
+// KeyboardShortcut
+//==============================================================================
+
+/**
+ * Overview of events:
+ *
+ * Init:
+ * - determine attributes
+ * - create element (all windows)          \  load into
+ * - set element attributes (all windows)  /  window
+ *
+ * New window:
+ * - create element (this window)          \  load into
+ * - set element attributes (this window)  /  window
+ *
+ * Pref changed:
+ * - determine attributes
+ * - set element attributes (all windows)
+ */
+
+function KeyboardShortcut(aID, aDefaultCombo, aCallback) {
+  //----------------------------------------------------------------------------
+  // initialize properties
+  //----------------------------------------------------------------------------
+
+  this._id = aID;
+  this._elementID = "rpKey_" + this._id;
+  this._defaultCombo = aDefaultCombo;
+  this._callback = aCallback;
+
+  this._elementAttributes = {
+    disabled: null,
+    modifiers: null,
+    key: null
+  };
+
+  this._listeners = {
+    onWindowLoad: this._loadIntoWindow.bind(this),
+    onWindowUnload: this._unloadFromWindow.bind(this),
+    onKeyCommand: this._onPress.bind(this)
+    // prefChanged: this._onPrefChange.bind(this)
+  };
+
+  //----------------------------------------------------------------------------
+  // initialize
+  //----------------------------------------------------------------------------
+
+  this._determineElementAttributes();
+  Windows.forEachOpenWindow(this._loadIntoWindow.bind(this));
+
+  Windows.addListener("load", this._listeners.onWindowLoad);
+  Windows.addListener("unload", this._listeners.onWindowUnload);
+}
+
+//------------------------------------------------------------------------------
+// main methods
+//------------------------------------------------------------------------------
+
+KeyboardShortcut.prototype.destroy = function() {
+  Windows.forEachOpenWindow(this._unloadFromWindow.bind(this));
+  Windows.removeListener("load", this._listeners.onWindowLoad);
+  Windows.removeListener("unload", this._listeners.onWindowUnload);
+};
+
+KeyboardShortcut.prototype._loadIntoWindow = function(window) {
+  this._createElement(window);
+  this._setElementAttributes(window);
+};
+
+KeyboardShortcut.prototype._unloadFromWindow = function(window) {
+  this._removeElement(window);
+};
+
+KeyboardShortcut.prototype._onPress = function(event) {
+  let window = event.target.ownerDocument.defaultView;
+  this._callback.call(null, window);
+};
+
+// KeyboardShortcut.prototype._onPrefChange = function() {
+//   this._determineElementAttributes();
+//   Windows.forEachOpenWindow(this._loadIntoWindow.bind(this));
+// };
+
+//------------------------------------------------------------------------------
+// assisting methods
+//------------------------------------------------------------------------------
+
+KeyboardShortcut.prototype._createElement = function(window) {
+  let keyset = window.document.getElementById("rpcontinuedKeyset");
+  let element = window.document.createElement("key");
+  element.setAttribute("id", this._elementID);
+  element.setAttribute("disabled", "true");
+  // "oncommand" (or "command") is required! See
+  // https://stackoverflow.com/questions/16779316/how-to-set-an-xul-key-dynamically-and-securely/16786770#16786770
+  element.setAttribute("oncommand", "void(0);");
+  element.addEventListener("command", this._listeners.onKeyCommand, false);
+  keyset.appendChild(element);
+};
+
+KeyboardShortcut.prototype._setElementAttributes = function(window) {
+  let element = window.document.getElementById(this._elementID);
+
+  // disable temporarily
+  element.setAttribute("disabled", "true");
+
+  element.setAttribute("modifiers", this._elementAttributes.modifiers);
+  element.setAttribute("key", this._elementAttributes.key);
+  element.setAttribute("disabled", this._elementAttributes.disabled);
+};
+
+KeyboardShortcut.prototype._removeElement = function(window) {
+  let element = window.document.getElementById(this._elementID);
+  element.removeEventListener("command", this._listeners.onKeyCommand, false);
+  element.remove();
+};
+
+Object.defineProperty(KeyboardShortcut.prototype, "userCombo", {
+  get: function() {
+    // will be changed to pref
+    return "default";
+  }
+});
+
+Object.defineProperty(KeyboardShortcut.prototype, "userEnabled", {
+  get: function() {
+    // will be changed to pref
+    return true;
+  }
+});
+
+const ELEMENT_ATTRIBUTES_WHEN_DISABLED = {
+  disabled: "true",
+  modifiers: "",
+  key: ""
+};
+
+KeyboardShortcut.prototype._determineElementAttributes = function() {
+  if (!this.userEnabled) {
+    this._elementAttributes = ELEMENT_ATTRIBUTES_WHEN_DISABLED;
+    return;
+  }
+
+  let combo = this.userCombo;
+  if (combo === "default") {
+    combo = this._defaultCombo;
+  }
+
+  let rv = XULUtils.keyboardShortcuts.getKeyAttributesFromCombo(combo);
+  if (false === rv.success) {
+    console.error("Error parsing keyboard combination for shortcut \"" +
+        this._id + "\": " + rv.errorMessage);
+    this._elementAttributes = ELEMENT_ATTRIBUTES_WHEN_DISABLED;
+    return;
+  }
+  this._elementAttributes = {
+    disabled: "false",
+    modifiers: rv.modifiers,
+    key: rv.key
+  };
+};
diff --git a/src/content/lib/environment.process.js b/src/content/lib/environment.process.js
index f74ca61..ff795f7 100644
--- a/src/content/lib/environment.process.js
+++ b/src/content/lib/environment.process.js
@@ -107,8 +107,10 @@ var ProcessEnvironment = (function() {
 
   Object.defineProperty(ParentProcessEnvironment.prototype, "controllers", {
     get: function() {
+      let {importModule} = ScriptLoader;
       return [
-        ScriptLoader.importModule("controllers/old-rules").OldRulesController
+        importModule("controllers/keyboard-shortcuts").KeyboardShortcuts,
+        importModule("controllers/old-rules").OldRulesController,
       ];
     }
   });
@@ -142,6 +144,9 @@ var ProcessEnvironment = (function() {
       //       other back end modules are loaded / initialized.
     }
 
+    // TODO: Initialize the "models" first. Then initialize the other
+    // controllers, which is currently "rpService", "ContentPolicy" etc.
+
     // import main modules:
     ScriptLoader.importModules([
       "main/requestpolicy-service",
diff --git a/src/content/lib/utils/javascript.jsm b/src/content/lib/utils/javascript.jsm
new file mode 100644
index 0000000..cd547a8
--- /dev/null
+++ b/src/content/lib/utils/javascript.jsm
@@ -0,0 +1,44 @@
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ *
+ * RequestPolicy - A Firefox extension for control over cross-site requests.
+ * Copyright (c) 2015 Martin Kimmerle
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * ***** END LICENSE BLOCK *****
+ */
+
+/* exported JSUtils */
+this.EXPORTED_SYMBOLS = ["JSUtils"];
+
+//==============================================================================
+// JSUtils
+//==============================================================================
+
+var JSUtils = (function() {
+  "use strict";
+  let self = {};
+
+  self.arrayIncludes = function(array, searchElement) {
+    for (let element of array) {
+      if (element === searchElement) {
+        return true;
+      }
+    }
+    return false;
+  };
+
+  return self;
+}());
diff --git a/src/content/lib/utils/xul.jsm b/src/content/lib/utils/xul.jsm
index b4411c1..74ca999 100644
--- a/src/content/lib/utils/xul.jsm
+++ b/src/content/lib/utils/xul.jsm
@@ -33,6 +33,7 @@ let {ScriptLoader: {importModule}} = Cu.import(
     "chrome://rpcontinued/content/lib/script-loader.jsm", {});
 let {Logger} = importModule("lib/logger");
 let {StringUtils} = importModule("lib/utils/strings");
+let {JSUtils} = importModule("lib/utils/javascript");
 let {C} = importModule("lib/utils/constants");
 
 //==============================================================================
@@ -323,3 +324,77 @@ XULUtils.removeTreeElementsFromWindow = function(aWin, aTreeName) {
     }
   }
 };
+
+XULUtils.keyboardShortcuts = (function() {
+  let self = {};
+
+  // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Attribute/modifiers
+  // See also the SDK Hotkeys API
+  // https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/hotkeys
+  const VALID_MODIFIERS = [
+    "shift",
+    "alt",
+    "meta",
+    "control",
+    "accel"
+  ];
+
+  function error(msg) {
+    return {success: false, errorMessage: msg};
+  }
+
+  function success(returnValue) {
+    returnValue.success = true;
+    return returnValue;
+  }
+
+  function isValidModifier(aModifier) {
+    return JSUtils.arrayIncludes(VALID_MODIFIERS, aModifier);
+  }
+
+  let keyRegEx = /^[a-z]$/;
+
+  function _getKeyAttributesFromCombo(aCombo) {
+    if (typeof aCombo !== "string") {
+      return error("Not a string!");
+    }
+    if (aCombo === "") {
+      return error("The string must not be empty.");
+    }
+
+    let parts = aCombo.split(" ");
+    // Take the last element as the key
+    let key = parts.slice(-1)[0];
+    // Take all elements except the last one as the modifiers.
+    let modifiers = parts.slice(0, -1);
+    // Remove duplicates
+    modifiers = [...new Set(modifiers)];
+
+    for (let modifier of modifiers) {
+      if (false === isValidModifier(modifier)) {
+        return error("Invalid modifier: \"" + modifier + "\"");
+      }
+    }
+
+    if (!keyRegEx.test(key)) {
+      return error("Invalid key: \"" + key + "\"");
+    }
+
+    return success({
+      modifiers: modifiers.join(" "),
+      key: key
+    });
+  }
+
+  /**
+   * Check if the <key modifiers="..."> string is valid.
+   *
+   * @param  {string} aCombo
+   * @return {Object}
+   */
+  self.getKeyAttributesFromCombo = function(aCombo) {
+    return _getKeyAttributesFromCombo(aCombo);
+  };
+
+  return self;
+}());
diff --git a/src/content/models/windows.jsm b/src/content/models/windows.jsm
index 0c04937..9645c48 100644
--- a/src/content/models/windows.jsm
+++ b/src/content/models/windows.jsm
@@ -127,8 +127,12 @@ var Windows = (function() {
 
   function onEvent(eventType, window) {
     if (topicsToListeners.has(eventType)) {
-      let listeners = topicsToListeners.get(eventType);
-      for (let listener of listeners.values()) {
+      let listeners = [...topicsToListeners.get(eventType)];
+      if (eventType === "unload") {
+        // the most recent listeners should be called first
+        listeners.reverse();
+      }
+      for (let listener of listeners) {
         listener.call(null, window);
       }
     }
diff --git a/src/content/ui/xul-trees.js b/src/content/ui/xul-trees.js
index f063791..da2b469 100644
--- a/src/content/ui/xul-trees.js
+++ b/src/content/ui/xul-trees.js
@@ -95,18 +95,7 @@ exports.mainTree = [
     parent: {special: {type: "__window__"}},
 
     tag: "keyset",
-    attributes: {id: "rpcontinuedKeyset"},
-    children: [
-      {
-        tag: "key",
-        // "oncommand" (or "command") is required! See
-        // https://stackoverflow.com/questions/16779316/how-to-set-an-xul-key-dynamically-and-securely/16786770#16786770
-        attributes: {key: "r",
-                     modifiers: "alt shift",
-                     oncommand: "void(0);"},
-        events: {command: ["overlay", "openMenu"]}
-      }
-    ]
+    attributes: {id: "rpcontinuedKeyset"}
   },
 
   {
diff --git a/tests/marionette/rp_puppeteer/tests/test_menu.py b/tests/marionette/rp_puppeteer/tests/test_menu.py
index 78dd100..fe198de 100644
--- a/tests/marionette/rp_puppeteer/tests/test_menu.py
+++ b/tests/marionette/rp_puppeteer/tests/test_menu.py
@@ -14,11 +14,16 @@ class TestMenu(RequestPolicyTestCase):
             super(RequestPolicyTestCase, self).tearDown()
 
     def test_open_close(self):
-        self.assertFalse(self.menu.is_open())
-        self.menu.open()
-        self.assertTrue(self.menu.is_open())
-        self.menu.close()
-        self.assertFalse(self.menu.is_open())
+        def test(trigger):
+            self.assertFalse(self.menu.is_open())
+            self.menu.open(trigger=trigger)
+            self.assertTrue(self.menu.is_open())
+            self.menu.close()
+            self.assertFalse(self.menu.is_open())
+
+        test("api")
+        test("button")
+        test("shortcut")
 
     def test_total_num_requests(self):
         with self.marionette.using_context("content"):
diff --git a/tests/marionette/rp_puppeteer/ui/menu.py b/tests/marionette/rp_puppeteer/ui/menu.py
index f243513..52ec0c7 100644
--- a/tests/marionette/rp_puppeteer/ui/menu.py
+++ b/tests/marionette/rp_puppeteer/ui/menu.py
@@ -3,6 +3,7 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from firefox_puppeteer.base import BaseLib
+from firefox_puppeteer.ui.windows import Windows
 from marionette_driver.wait import Wait
 import re
 
@@ -20,8 +21,18 @@ class Menu(BaseLib):
         match = re.match(r"^(\d+) \((\d+)\+(\d+)\)$", text)
         return int(match.group(1))
 
-    def open(self):
-        self._ensure_popup_state("open")
+    def open(self, trigger="api"):
+        if trigger == "button":
+            self._toolbar_button.click()
+            Wait(self.marionette).until(lambda _: self._popup_state == "open")
+        elif trigger == "shortcut":
+            window = Windows(lambda: self.marionette).current
+            window.send_shortcut("r", alt=True, shift=True)
+            Wait(self.marionette).until(lambda _: self._popup_state == "open")
+        elif trigger == "api":
+            self._ensure_popup_state("open")
+        else:
+            raise ValueError("Unknown opening method: \"{}\"".format(trigger))
 
     def is_open(self):
         return self._popup_state == "open"
diff --git a/tests/xpcshell/test_jsutils.js b/tests/xpcshell/test_jsutils.js
new file mode 100644
index 0000000..25169d9
--- /dev/null
+++ b/tests/xpcshell/test_jsutils.js
@@ -0,0 +1,20 @@
+Cu.import("chrome://rpcontinued/content/lib/utils/javascript.jsm");
+
+function run_test() {
+  "use strict";
+
+  test_arrayIncludes();
+}
+
+function test_arrayIncludes() {
+  "use strict";
+
+  let {arrayIncludes} = JSUtils;
+
+  strictEqual(true, arrayIncludes(["a", "b"], "a"));
+  strictEqual(true, arrayIncludes(["a", "b"], "b"));
+  strictEqual(false, arrayIncludes(["a", "b"], "c"));
+
+  strictEqual(true, arrayIncludes([0], 0));
+  strictEqual(false, arrayIncludes([0], "0"));
+}
diff --git a/tests/xpcshell/test_xulutils_keyboard_shortcuts.js b/tests/xpcshell/test_xulutils_keyboard_shortcuts.js
new file mode 100644
index 0000000..2c4763e
--- /dev/null
+++ b/tests/xpcshell/test_xulutils_keyboard_shortcuts.js
@@ -0,0 +1,66 @@
+Cu.import("chrome://rpcontinued/content/lib/utils/xul.jsm");
+
+function run_test() {
+  "use strict";
+
+  test_getKeyAttributesFromCombo();
+}
+
+function test_getKeyAttributesFromCombo() {
+  "use strict";
+
+  function testSuccess(combo, expectedModifiers, expectedKey) {
+    let rv = XULUtils.keyboardShortcuts.getKeyAttributesFromCombo(combo);
+    strictEqual(true, rv.success);
+    strictEqual(expectedModifiers, rv.modifiers);
+    strictEqual(expectedKey, rv.key);
+  }
+
+  function testFailure(combo) {
+    let rv = XULUtils.keyboardShortcuts.getKeyAttributesFromCombo(combo);
+    strictEqual(false, rv.success);
+    strictEqual("string", typeof rv.errorMessage);
+  }
+
+  testSuccess("shift a", "shift", "a");
+  testSuccess("alt b", "alt", "b");
+  testSuccess("meta c", "meta", "c");
+  testSuccess("control d", "control", "d");
+  testSuccess("accel e", "accel", "e");
+
+  // multiple modifiers
+  testSuccess("alt shift f", "alt shift", "f");
+  testSuccess("control alt g", "control alt", "g");
+  testSuccess("control alt shift h", "control alt shift", "h");
+
+  // no modifier
+  testSuccess("r", "", "r");
+
+  // Disallow these modifiers. They are allowed for <key> but
+  // not allowed in the SDK Hotkeys API.
+  testFailure("os r");
+  testFailure("access r");
+  testFailure("any r");
+
+  // redundant modifier
+  testSuccess("alt alt r", "alt", "r");
+  testSuccess("alt shift alt r", "alt shift", "r");
+
+  // invalid modifiers
+  testFailure("ctrl r");
+  testFailure("foobar r");
+
+  // No key
+  testFailure("alt");
+  testFailure("control shift");
+  // invalid key
+  testFailure("alt _");
+  testFailure("alt 0");
+  testFailure("alt rr");
+
+  // empty string
+  testFailure("");
+  // invalid separator
+  testFailure("alt,x");
+  testFailure("alt-y");
+}
diff --git a/tests/xpcshell/xpcshell.ini b/tests/xpcshell/xpcshell.ini
index d5bcf70..ca598ab 100644
--- a/tests/xpcshell/xpcshell.ini
+++ b/tests/xpcshell/xpcshell.ini
@@ -3,9 +3,11 @@ head = head_loadrp.js head_utils.js
 tail =
 
 [test_fileutil.js]
+[test_jsutils.js]
 [test_map_of_sets.js]
 [test_oldrules.js]
 ;[test_policymanager.js]
 [test_policystorage.js]
 ;[test_subscription.js]
 [test_wrap_function.js]
+[test_xulutils_keyboard_shortcuts.js]

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-mozext/requestpolicy.git



More information about the Pkg-mozext-commits mailing list