/* 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"; 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; const { 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() { const { 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); const observerConfig = { attributes: true, attributeFilter: ["checked"] }; // Sets observers and default options for all options for (const $el of this.$$("menuitem", this.menupopup)) { const 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) { const $el = this.$(`menuitem[data-pref="${prefName}"]`, this.menupopup); const 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) { const { target } = mutations[0]; const prefName = target.getAttribute("data-pref"); const 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) { const fullName = this.branchName + prefName; return Preferences.get(fullName); }, /** * Sets `prefName`'s `value`. Does not require the branch name. */ set: function(prefName, value) { const fullName = this.branchName + prefName; Preferences.set(fullName, value); }, register: function() { this.branch.addObserver("", this); }, unregister: function() { this.branch.removeObserver("", this); }, observe: function(subject, topic, prefName) { this.emit(PREF_CHANGE_EVENT, prefName); }, };