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
|
const EventEmitter = require("devtools/toolkit/event-emitter");
const { Services } = require("resource://gre/modules/Services.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.$$ = document.querySelectorAll.bind(document);
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
// it's 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.emit(OPTIONS_SHOWN_EVENT);
},
/**
* Fired when the `menupopup` is closed, bound via XUL.
* Fires an event used in tests.
*/
_onPopupHidden: function () {
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 Services.prefs.getBoolPref(fullName);
},
/**
* Sets `prefName`'s `value`. Does not require the branch name.
*/
set: function (prefName, value) {
let fullName = this.branchName + prefName;
Services.prefs.setBoolPref(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);
}
};
|