options-view.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. "use strict";
  2. const EventEmitter = require("devtools/shared/event-emitter");
  3. const Services = require("Services");
  4. const { Preferences } = require("resource://gre/modules/Preferences.jsm");
  5. const OPTIONS_SHOWN_EVENT = "options-shown";
  6. const OPTIONS_HIDDEN_EVENT = "options-hidden";
  7. const PREF_CHANGE_EVENT = "pref-changed";
  8. /**
  9. * OptionsView constructor. Takes several options, all required:
  10. * - branchName: The name of the prefs branch, like "devtools.debugger."
  11. * - menupopup: The XUL `menupopup` item that contains the pref buttons.
  12. *
  13. * Fires an event, PREF_CHANGE_EVENT, with the preference name that changed as
  14. * the second argument. Fires events on opening/closing the XUL panel
  15. * (OPTIONS_SHOW_EVENT, OPTIONS_HIDDEN_EVENT) as the second argument in the
  16. * listener, used for tests mostly.
  17. */
  18. const OptionsView = function (options = {}) {
  19. this.branchName = options.branchName;
  20. this.menupopup = options.menupopup;
  21. this.window = this.menupopup.ownerDocument.defaultView;
  22. let { document } = this.window;
  23. this.$ = document.querySelector.bind(document);
  24. this.$$ = (selector, parent = document) => parent.querySelectorAll(selector);
  25. // Get the corresponding button that opens the popup by looking
  26. // for an element with a `popup` attribute matching the menu's ID
  27. this.button = this.$(`[popup=${this.menupopup.getAttribute("id")}]`);
  28. this.prefObserver = new PrefObserver(this.branchName);
  29. EventEmitter.decorate(this);
  30. };
  31. exports.OptionsView = OptionsView;
  32. OptionsView.prototype = {
  33. /**
  34. * Binds the events and observers for the OptionsView.
  35. */
  36. initialize: function () {
  37. let { MutationObserver } = this.window;
  38. this._onPrefChange = this._onPrefChange.bind(this);
  39. this._onOptionChange = this._onOptionChange.bind(this);
  40. this._onPopupShown = this._onPopupShown.bind(this);
  41. this._onPopupHidden = this._onPopupHidden.bind(this);
  42. // We use a mutation observer instead of a click handler
  43. // because the click handler is fired before the XUL menuitem updates its
  44. // checked status, which cascades incorrectly with the Preference observer.
  45. this.mutationObserver = new MutationObserver(this._onOptionChange);
  46. let observerConfig = { attributes: true, attributeFilter: ["checked"]};
  47. // Sets observers and default options for all options
  48. for (let $el of this.$$("menuitem", this.menupopup)) {
  49. let prefName = $el.getAttribute("data-pref");
  50. if (this.prefObserver.get(prefName)) {
  51. $el.setAttribute("checked", "true");
  52. } else {
  53. $el.removeAttribute("checked");
  54. }
  55. this.mutationObserver.observe($el, observerConfig);
  56. }
  57. // Listen to any preference change in the specified branch
  58. this.prefObserver.register();
  59. this.prefObserver.on(PREF_CHANGE_EVENT, this._onPrefChange);
  60. // Bind to menupopup's open and close event
  61. this.menupopup.addEventListener("popupshown", this._onPopupShown);
  62. this.menupopup.addEventListener("popuphidden", this._onPopupHidden);
  63. },
  64. /**
  65. * Removes event handlers for all of the option buttons and
  66. * preference observer.
  67. */
  68. destroy: function () {
  69. this.mutationObserver.disconnect();
  70. this.prefObserver.off(PREF_CHANGE_EVENT, this._onPrefChange);
  71. this.menupopup.removeEventListener("popupshown", this._onPopupShown);
  72. this.menupopup.removeEventListener("popuphidden", this._onPopupHidden);
  73. },
  74. /**
  75. * Returns the value for the specified `prefName`
  76. */
  77. getPref: function (prefName) {
  78. return this.prefObserver.get(prefName);
  79. },
  80. /**
  81. * Called when a preference is changed (either via clicking an option
  82. * button or by changing it in about:config). Updates the checked status
  83. * of the corresponding button.
  84. */
  85. _onPrefChange: function (_, prefName) {
  86. let $el = this.$(`menuitem[data-pref="${prefName}"]`, this.menupopup);
  87. let value = this.prefObserver.get(prefName);
  88. // If options panel does not contain a menuitem for the
  89. // pref, emit an event and do nothing.
  90. if (!$el) {
  91. this.emit(PREF_CHANGE_EVENT, prefName);
  92. return;
  93. }
  94. if (value) {
  95. $el.setAttribute("checked", value);
  96. } else {
  97. $el.removeAttribute("checked");
  98. }
  99. this.emit(PREF_CHANGE_EVENT, prefName);
  100. },
  101. /**
  102. * Mutation handler for handling a change on an options button.
  103. * Sets the preference accordingly.
  104. */
  105. _onOptionChange: function (mutations) {
  106. let { target } = mutations[0];
  107. let prefName = target.getAttribute("data-pref");
  108. let value = target.getAttribute("checked") === "true";
  109. this.prefObserver.set(prefName, value);
  110. },
  111. /**
  112. * Fired when the `menupopup` is opened, bound via XUL.
  113. * Fires an event used in tests.
  114. */
  115. _onPopupShown: function () {
  116. this.button.setAttribute("open", true);
  117. this.emit(OPTIONS_SHOWN_EVENT);
  118. },
  119. /**
  120. * Fired when the `menupopup` is closed, bound via XUL.
  121. * Fires an event used in tests.
  122. */
  123. _onPopupHidden: function () {
  124. this.button.removeAttribute("open");
  125. this.emit(OPTIONS_HIDDEN_EVENT);
  126. }
  127. };
  128. /**
  129. * Constructor for PrefObserver. Small helper for observing changes
  130. * on a preference branch. Takes a `branchName`, like "devtools.debugger."
  131. *
  132. * Fires an event of PREF_CHANGE_EVENT with the preference name that changed
  133. * as the second argument in the listener.
  134. */
  135. const PrefObserver = function (branchName) {
  136. this.branchName = branchName;
  137. this.branch = Services.prefs.getBranch(branchName);
  138. EventEmitter.decorate(this);
  139. };
  140. PrefObserver.prototype = {
  141. /**
  142. * Returns `prefName`'s value. Does not require the branch name.
  143. */
  144. get: function (prefName) {
  145. let fullName = this.branchName + prefName;
  146. return Preferences.get(fullName);
  147. },
  148. /**
  149. * Sets `prefName`'s `value`. Does not require the branch name.
  150. */
  151. set: function (prefName, value) {
  152. let fullName = this.branchName + prefName;
  153. Preferences.set(fullName, value);
  154. },
  155. register: function () {
  156. this.branch.addObserver("", this, false);
  157. },
  158. unregister: function () {
  159. this.branch.removeObserver("", this);
  160. },
  161. observe: function (subject, topic, prefName) {
  162. this.emit(PREF_CHANGE_EVENT, prefName);
  163. }
  164. };