summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/preferences
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-10 02:51:36 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-10 02:51:36 -0500
commit37d5300335d81cecbecc99812747a657588c63eb (patch)
tree765efa3b6a56bb715d9813a8697473e120436278 /toolkit/jetpack/sdk/preferences
parentb2bdac20c02b12f2057b9ef70b0a946113a00e00 (diff)
parent4fb11cd5966461bccc3ed1599b808237be6b0de9 (diff)
downloadUXP-37d5300335d81cecbecc99812747a657588c63eb.tar
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.gz
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.lz
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.xz
UXP-37d5300335d81cecbecc99812747a657588c63eb.zip
Merge branch 'ext-work'
Diffstat (limited to 'toolkit/jetpack/sdk/preferences')
-rw-r--r--toolkit/jetpack/sdk/preferences/event-target.js61
-rw-r--r--toolkit/jetpack/sdk/preferences/native-options.js193
-rw-r--r--toolkit/jetpack/sdk/preferences/service.js137
-rw-r--r--toolkit/jetpack/sdk/preferences/utils.js42
4 files changed, 433 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/preferences/event-target.js b/toolkit/jetpack/sdk/preferences/event-target.js
new file mode 100644
index 000000000..b64ba303c
--- /dev/null
+++ b/toolkit/jetpack/sdk/preferences/event-target.js
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { Cc, Ci } = require('chrome');
+const { Class } = require('../core/heritage');
+const { EventTarget } = require('../event/target');
+const { Branch } = require('./service');
+const { emit, off } = require('../event/core');
+const { when: unload } = require('../system/unload');
+
+const prefTargetNS = require('../core/namespace').ns();
+
+const PrefsTarget = Class({
+ extends: EventTarget,
+ initialize: function(options) {
+ options = options || {};
+ EventTarget.prototype.initialize.call(this, options);
+
+ let branchName = options.branchName || '';
+ let branch = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService).
+ getBranch(branchName).
+ QueryInterface(Ci.nsIPrefBranch2);
+ prefTargetNS(this).branch = branch;
+
+ // provides easy access to preference values
+ this.prefs = Branch(branchName);
+
+ // start listening to preference changes
+ let observer = prefTargetNS(this).observer = onChange.bind(this);
+ branch.addObserver('', observer, false);
+
+ // Make sure to destroy this on unload
+ unload(destroy.bind(this));
+ }
+});
+exports.PrefsTarget = PrefsTarget;
+
+/* HELPERS */
+
+function onChange(subject, topic, name) {
+ if (topic === 'nsPref:changed') {
+ emit(this, name, name);
+ emit(this, '', name);
+ }
+}
+
+function destroy() {
+ off(this);
+
+ // stop listening to preference changes
+ let branch = prefTargetNS(this).branch;
+ branch.removeObserver('', prefTargetNS(this).observer, false);
+ prefTargetNS(this).observer = null;
+}
diff --git a/toolkit/jetpack/sdk/preferences/native-options.js b/toolkit/jetpack/sdk/preferences/native-options.js
new file mode 100644
index 000000000..840997df9
--- /dev/null
+++ b/toolkit/jetpack/sdk/preferences/native-options.js
@@ -0,0 +1,193 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { Cc, Ci, Cu } = require('chrome');
+const { on } = require('../system/events');
+const { id, preferencesBranch } = require('../self');
+const { localizeInlineOptions } = require('../l10n/prefs');
+const { Services } = require("resource://gre/modules/Services.jsm");
+const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm");
+const { defer } = require("sdk/core/promise");
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";;
+const DEFAULT_OPTIONS_URL = 'data:text/xml,<placeholder/>';
+
+const VALID_PREF_TYPES = ['bool', 'boolint', 'integer', 'string', 'color',
+ 'file', 'directory', 'control', 'menulist', 'radio'];
+
+const isFennec = require("sdk/system/xul-app").is("Fennec");
+
+function enable({ preferences, id }) {
+ let enabled = defer();
+
+ validate(preferences);
+
+ setDefaults(preferences, preferencesBranch);
+
+ // allow the use of custom options.xul
+ AddonManager.getAddonByID(id, (addon) => {
+ on('addon-options-displayed', onAddonOptionsDisplayed, true);
+ enabled.resolve({ id: id });
+ });
+
+ function onAddonOptionsDisplayed({ subject: doc, data }) {
+ if (data === id) {
+ let parent;
+
+ if (isFennec) {
+ parent = doc.querySelector('.options-box');
+
+ // NOTE: This disable the CSS rule that makes the options invisible
+ let item = doc.querySelector('#addons-details .addon-item');
+ item.removeAttribute("optionsURL");
+ } else {
+ parent = doc.getElementById('detail-downloads').parentNode;
+ }
+
+ if (parent) {
+ injectOptions({
+ preferences: preferences,
+ preferencesBranch: preferencesBranch,
+ document: doc,
+ parent: parent,
+ id: id
+ });
+ localizeInlineOptions(doc);
+ } else {
+ throw Error("Preferences parent node not found in Addon Details. The configured custom preferences will not be visible.");
+ }
+ }
+ }
+
+ return enabled.promise;
+}
+exports.enable = enable;
+
+// centralized sanity checks
+function validate(preferences) {
+ for (let { name, title, type, label, options } of preferences) {
+ // make sure the title is set and non-empty
+ if (!title)
+ throw Error("The '" + name + "' pref requires a title");
+
+ // make sure that pref type is a valid inline option type
+ if (!~VALID_PREF_TYPES.indexOf(type))
+ throw Error("The '" + name + "' pref must be of valid type");
+
+ // if it's a control, make sure it has a label
+ if (type === 'control' && !label)
+ throw Error("The '" + name + "' control requires a label");
+
+ // if it's a menulist or radio, make sure it has options
+ if (type === 'menulist' || type === 'radio') {
+ if (!options)
+ throw Error("The '" + name + "' pref requires options");
+
+ // make sure each option has a value and a label
+ for (let item of options) {
+ if (!('value' in item) || !('label' in item))
+ throw Error("Each option requires both a value and a label");
+ }
+ }
+
+ // TODO: check that pref type matches default value type
+ }
+}
+exports.validate = validate;
+
+// initializes default preferences, emulates defaults/prefs.js
+function setDefaults(preferences, preferencesBranch) {
+ const branch = Cc['@mozilla.org/preferences-service;1'].
+ getService(Ci.nsIPrefService).
+ getDefaultBranch('extensions.' + preferencesBranch + '.');
+ for (let { name, value } of preferences) {
+ switch (typeof value) {
+ case 'boolean':
+ branch.setBoolPref(name, value);
+ break;
+ case 'number':
+ // must be integer, ignore otherwise
+ if (value % 1 === 0) {
+ branch.setIntPref(name, value);
+ }
+ break;
+ case 'string':
+ let str = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ str.data = value;
+ branch.setComplexValue(name, Ci.nsISupportsString, str);
+ break;
+ }
+ }
+}
+exports.setDefaults = setDefaults;
+
+// dynamically injects inline options into about:addons page at runtime
+// NOTE: on Firefox Desktop the about:addons page is a xul page document,
+// on Firefox for Android the about:addons page is an xhtml page, to support both
+// the XUL xml namespace have to be enforced.
+function injectOptions({ preferences, preferencesBranch, document, parent, id }) {
+ preferences.forEach(({name, type, hidden, title, description, label, options, on, off}) => {
+ if (hidden) {
+ return;
+ }
+
+ let setting = document.createElementNS(XUL_NS, 'setting');
+ setting.setAttribute('pref-name', name);
+ setting.setAttribute('data-jetpack-id', id);
+ setting.setAttribute('pref', 'extensions.' + preferencesBranch + '.' + name);
+ setting.setAttribute('type', type);
+ setting.setAttribute('title', title);
+ if (description)
+ setting.setAttribute('desc', description);
+
+ if (type === 'file' || type === 'directory') {
+ setting.setAttribute('fullpath', 'true');
+ }
+ else if (type === 'control') {
+ let button = document.createElementNS(XUL_NS, 'button');
+ button.setAttribute('pref-name', name);
+ button.setAttribute('data-jetpack-id', id);
+ button.setAttribute('label', label);
+ button.addEventListener('command', function() {
+ Services.obs.notifyObservers(null, `${id}-cmdPressed`, name);
+ }, true);
+ setting.appendChild(button);
+ }
+ else if (type === 'boolint') {
+ setting.setAttribute('on', on);
+ setting.setAttribute('off', off);
+ }
+ else if (type === 'menulist') {
+ let menulist = document.createElementNS(XUL_NS, 'menulist');
+ let menupopup = document.createElementNS(XUL_NS, 'menupopup');
+ for (let { value, label } of options) {
+ let menuitem = document.createElementNS(XUL_NS, 'menuitem');
+ menuitem.setAttribute('value', value);
+ menuitem.setAttribute('label', label);
+ menupopup.appendChild(menuitem);
+ }
+ menulist.appendChild(menupopup);
+ setting.appendChild(menulist);
+ }
+ else if (type === 'radio') {
+ let radiogroup = document.createElementNS(XUL_NS, 'radiogroup');
+ for (let { value, label } of options) {
+ let radio = document.createElementNS(XUL_NS, 'radio');
+ radio.setAttribute('value', value);
+ radio.setAttribute('label', label);
+ radiogroup.appendChild(radio);
+ }
+ setting.appendChild(radiogroup);
+ }
+
+ parent.appendChild(setting);
+ });
+}
+exports.injectOptions = injectOptions;
diff --git a/toolkit/jetpack/sdk/preferences/service.js b/toolkit/jetpack/sdk/preferences/service.js
new file mode 100644
index 000000000..231cd8e14
--- /dev/null
+++ b/toolkit/jetpack/sdk/preferences/service.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+// The minimum and maximum integers that can be set as preferences.
+// The range of valid values is narrower than the range of valid JS values
+// because the native preferences code treats integers as NSPR PRInt32s,
+// which are 32-bit signed integers on all platforms.
+const MAX_INT = 0x7FFFFFFF;
+const MIN_INT = -0x80000000;
+
+const {Cc,Ci,Cr} = require("chrome");
+
+const prefService = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService);
+const prefSvc = prefService.getBranch(null);
+const defaultBranch = prefService.getDefaultBranch(null);
+
+const { Preferences } = require("resource://gre/modules/Preferences.jsm");
+const prefs = new Preferences({});
+
+const branchKeys = branchName =>
+ keys(branchName).map($ => $.replace(branchName, ""));
+
+const Branch = function(branchName) {
+ return new Proxy(Branch.prototype, {
+ getOwnPropertyDescriptor(target, name, receiver) {
+ return {
+ configurable: true,
+ enumerable: true,
+ writable: false,
+ value: this.get(target, name, receiver)
+ };
+ },
+ ownKeys(target) {
+ return branchKeys(branchName);
+ },
+ get(target, name, receiver) {
+ return get(`${branchName}${name}`);
+ },
+ set(target, name, value, receiver) {
+ set(`${branchName}${name}`, value);
+ return true;
+ },
+ has(target, name) {
+ return this.hasOwn(target, name);
+ },
+ hasOwn(target, name) {
+ return has(`${branchName}${name}`);
+ },
+ deleteProperty(target, name) {
+ reset(`${branchName}${name}`);
+ return true;
+ }
+ });
+}
+
+
+function get(name, defaultValue) {
+ return prefs.get(name, defaultValue);
+}
+exports.get = get;
+
+
+function set(name, value) {
+ var prefType;
+ if (typeof value != "undefined" && value != null)
+ prefType = value.constructor.name;
+
+ switch (prefType) {
+ case "Number":
+ if (value % 1 != 0)
+ throw new Error("cannot store non-integer number: " + value);
+ }
+
+ prefs.set(name, value);
+}
+exports.set = set;
+
+const has = prefs.has.bind(prefs)
+exports.has = has;
+
+function keys(root) {
+ return prefSvc.getChildList(root);
+}
+exports.keys = keys;
+
+const isSet = prefs.isSet.bind(prefs);
+exports.isSet = isSet;
+
+function reset(name) {
+ try {
+ prefSvc.clearUserPref(name);
+ }
+ catch (e) {
+ // The pref service throws NS_ERROR_UNEXPECTED when the caller tries
+ // to reset a pref that doesn't exist or is already set to its default
+ // value. This interface fails silently in those cases, so callers
+ // can unconditionally reset a pref without having to check if it needs
+ // resetting first or trap exceptions after the fact. It passes through
+ // other exceptions, however, so callers know about them, since we don't
+ // know what other exceptions might be thrown and what they might mean.
+ if (e.result != Cr.NS_ERROR_UNEXPECTED) {
+ throw e;
+ }
+ }
+}
+exports.reset = reset;
+
+function getLocalized(name, defaultValue) {
+ let value = null;
+ try {
+ value = prefSvc.getComplexValue(name, Ci.nsIPrefLocalizedString).data;
+ }
+ finally {
+ return value || defaultValue;
+ }
+}
+exports.getLocalized = getLocalized;
+
+function setLocalized(name, value) {
+ // We can't use `prefs.set` here as we have to use `getDefaultBranch`
+ // (instead of `getBranch`) in order to have `mIsDefault` set to true, here:
+ // http://mxr.mozilla.org/mozilla-central/source/modules/libpref/src/nsPrefBranch.cpp#233
+ // Otherwise, we do not enter into this expected condition:
+ // http://mxr.mozilla.org/mozilla-central/source/modules/libpref/src/nsPrefBranch.cpp#244
+ defaultBranch.setCharPref(name, value);
+}
+exports.setLocalized = setLocalized;
+
+exports.Branch = Branch;
+
diff --git a/toolkit/jetpack/sdk/preferences/utils.js b/toolkit/jetpack/sdk/preferences/utils.js
new file mode 100644
index 000000000..1d5769c37
--- /dev/null
+++ b/toolkit/jetpack/sdk/preferences/utils.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { openTab, getBrowserForTab, getTabId } = require("sdk/tabs/utils");
+const { on, off } = require("sdk/system/events");
+const { getMostRecentBrowserWindow } = require('../window/utils');
+
+// Opens about:addons in a new tab, then displays the inline
+// preferences of the provided add-on
+const open = ({ id }) => new Promise((resolve, reject) => {
+ // opening the about:addons page in a new tab
+ let tab = openTab(getMostRecentBrowserWindow(), "about:addons");
+ let browser = getBrowserForTab(tab);
+
+ // waiting for the about:addons page to load
+ browser.addEventListener("load", function onPageLoad() {
+ browser.removeEventListener("load", onPageLoad, true);
+ let window = browser.contentWindow;
+
+ // wait for the add-on's "addon-options-displayed"
+ on("addon-options-displayed", function onPrefDisplayed({ subject: doc, data }) {
+ if (data === id) {
+ off("addon-options-displayed", onPrefDisplayed);
+ resolve({
+ id: id,
+ tabId: getTabId(tab),
+ "document": doc
+ });
+ }
+ }, true);
+
+ // display the add-on inline preferences page
+ window.gViewController.commands.cmd_showItemDetails.doCommand({ id: id }, true);
+ }, true);
+});
+exports.open = open;