summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/options-view.js
blob: bb583eaee964d9c18bef71e640186ae9a90a3def (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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);
  }
};