"use strict"; const EventEmitter = require("devtools/shared/event-emitter"); const Services = require("Services"); const { Preferences } = require("resource://gre/modules/Preferences.jsm"); const OPTIONS_SHOWN_EVENT = "options-shown"; const OPTIONS_HIDDEN_EVENT = "options-hidden"; const PREF_CHANGE_EVENT = "pref-changed"; /** * OptionsView constructor. Takes several options, all required: * - branchName: The name of the prefs branch, like "devtools.debugger." * - menupopup: The XUL `menupopup` item that contains the pref buttons. * * Fires an event, PREF_CHANGE_EVENT, with the preference name that changed as * the second argument. Fires events on opening/closing the XUL panel * (OPTIONS_SHOW_EVENT, OPTIONS_HIDDEN_EVENT) as the second argument in the * listener, used for tests mostly. */ const OptionsView = function (options = {}) { this.branchName = options.branchName; this.menupopup = options.menupopup; this.window = this.menupopup.ownerDocument.defaultView; let { document } = this.window; this.$ = document.querySelector.bind(document); this.$$ = (selector, parent = document) => parent.querySelectorAll(selector); // Get the corresponding button that opens the popup by looking // for an element with a `popup` attribute matching the menu's ID this.button = this.$(`[popup=${this.menupopup.getAttribute("id")}]`); this.prefObserver = new PrefObserver(this.branchName); EventEmitter.decorate(this); }; exports.OptionsView = OptionsView; OptionsView.prototype = { /** * Binds the events and observers for the OptionsView. */ initialize: function () { let { MutationObserver } = this.window; this._onPrefChange = this._onPrefChange.bind(this); this._onOptionChange = this._onOptionChange.bind(this); this._onPopupShown = this._onPopupShown.bind(this); this._onPopupHidden = this._onPopupHidden.bind(this); // We use a mutation observer instead of a click handler // because the click handler is fired before the XUL menuitem updates its // checked status, which cascades incorrectly with the Preference observer. this.mutationObserver = new MutationObserver(this._onOptionChange); let observerConfig = { attributes: true, attributeFilter: ["checked"]}; // Sets observers and default options for all options for (let $el of this.$$("menuitem", this.menupopup)) { let prefName = $el.getAttribute("data-pref"); if (this.prefObserver.get(prefName)) { $el.setAttribute("checked", "true"); } else { $el.removeAttribute("checked"); } this.mutationObserver.observe($el, observerConfig); } // Listen to any preference change in the specified branch this.prefObserver.register(); this.prefObserver.on(PREF_CHANGE_EVENT, this._onPrefChange); // Bind to menupopup's open and close event this.menupopup.addEventListener("popupshown", this._onPopupShown); this.menupopup.addEventListener("popuphidden", this._onPopupHidden); }, /** * Removes event handlers for all of the option buttons and * preference observer. */ destroy: function () { this.mutationObserver.disconnect(); this.prefObserver.off(PREF_CHANGE_EVENT, this._onPrefChange); this.menupopup.removeEventListener("popupshown", this._onPopupShown); this.menupopup.removeEventListener("popuphidden", this._onPopupHidden); }, /** * Returns the value for the specified `prefName` */ getPref: function (prefName) { return this.prefObserver.get(prefName); }, /** * Called when a preference is changed (either via clicking an option * button or by changing it in about:config). Updates the checked status * of the corresponding button. */ _onPrefChange: function (_, prefName) { let $el = this.$(`menuitem[data-pref="${prefName}"]`, this.menupopup); let value = this.prefObserver.get(prefName); // If options panel does not contain a menuitem for the // pref, emit an event and do nothing. if (!$el) { this.emit(PREF_CHANGE_EVENT, prefName); return; } if (value) { $el.setAttribute("checked", value); } else { $el.removeAttribute("checked"); } this.emit(PREF_CHANGE_EVENT, prefName); }, /** * Mutation handler for handling a change on an options button. * Sets the preference accordingly. */ _onOptionChange: function (mutations) { let { target } = mutations[0]; let prefName = target.getAttribute("data-pref"); let value = target.getAttribute("checked") === "true"; this.prefObserver.set(prefName, value); }, /** * Fired when the `menupopup` is opened, bound via XUL. * Fires an event used in tests. */ _onPopupShown: function () { this.button.setAttribute("open", true); this.emit(OPTIONS_SHOWN_EVENT); }, /** * Fired when the `menupopup` is closed, bound via XUL. * Fires an event used in tests. */ _onPopupHidden: function () { this.button.removeAttribute("open"); this.emit(OPTIONS_HIDDEN_EVENT); } }; /** * Constructor for PrefObserver. Small helper for observing changes * on a preference branch. Takes a `branchName`, like "devtools.debugger." * * Fires an event of PREF_CHANGE_EVENT with the preference name that changed * as the second argument in the listener. */ const PrefObserver = function (branchName) { this.branchName = branchName; this.branch = Services.prefs.getBranch(branchName); EventEmitter.decorate(this); }; PrefObserver.prototype = { /** * Returns `prefName`'s value. Does not require the branch name. */ get: function (prefName) { let fullName = this.branchName + prefName; return Preferences.get(fullName); }, /** * Sets `prefName`'s `value`. Does not require the branch name. */ set: function (prefName, value) { let fullName = this.branchName + prefName; Preferences.set(fullName, value); }, register: function () { this.branch.addObserver("", this, false); }, unregister: function () { this.branch.removeObserver("", this); }, observe: function (subject, topic, prefName) { this.emit(PREF_CHANGE_EVENT, prefName); } };