diff options
Diffstat (limited to 'toolkit/jetpack/sdk/preferences')
-rw-r--r-- | toolkit/jetpack/sdk/preferences/event-target.js | 61 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/preferences/native-options.js | 193 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/preferences/service.js | 137 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/preferences/utils.js | 42 |
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; |