From ac46df8daea09899ce30dc8fd70986e258c746bf Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 9 Feb 2018 06:46:43 -0500 Subject: Move Add-on SDK source to toolkit/jetpack --- toolkit/jetpack/sdk/preferences/native-options.js | 193 ++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 toolkit/jetpack/sdk/preferences/native-options.js (limited to 'toolkit/jetpack/sdk/preferences/native-options.js') 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,'; + +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; -- cgit v1.2.3