diff options
Diffstat (limited to 'devtools/client/shared/options-view.js')
-rw-r--r-- | devtools/client/shared/options-view.js | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/devtools/client/shared/options-view.js b/devtools/client/shared/options-view.js new file mode 100644 index 000000000..bb583eaee --- /dev/null +++ b/devtools/client/shared/options-view.js @@ -0,0 +1,186 @@ +"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); + } +}; |