toolbox-options.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. const Services = require("Services");
  7. const defer = require("devtools/shared/defer");
  8. const {Task} = require("devtools/shared/task");
  9. const {gDevTools} = require("devtools/client/framework/devtools");
  10. const {LocalizationHelper} = require("devtools/shared/l10n");
  11. const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
  12. exports.OptionsPanel = OptionsPanel;
  13. function GetPref(name) {
  14. let type = Services.prefs.getPrefType(name);
  15. switch (type) {
  16. case Services.prefs.PREF_STRING:
  17. return Services.prefs.getCharPref(name);
  18. case Services.prefs.PREF_INT:
  19. return Services.prefs.getIntPref(name);
  20. case Services.prefs.PREF_BOOL:
  21. return Services.prefs.getBoolPref(name);
  22. default:
  23. throw new Error("Unknown type");
  24. }
  25. }
  26. function SetPref(name, value) {
  27. let type = Services.prefs.getPrefType(name);
  28. switch (type) {
  29. case Services.prefs.PREF_STRING:
  30. return Services.prefs.setCharPref(name, value);
  31. case Services.prefs.PREF_INT:
  32. return Services.prefs.setIntPref(name, value);
  33. case Services.prefs.PREF_BOOL:
  34. return Services.prefs.setBoolPref(name, value);
  35. default:
  36. throw new Error("Unknown type");
  37. }
  38. }
  39. function InfallibleGetBoolPref(key) {
  40. try {
  41. return Services.prefs.getBoolPref(key);
  42. } catch (ex) {
  43. return true;
  44. }
  45. }
  46. /**
  47. * Represents the Options Panel in the Toolbox.
  48. */
  49. function OptionsPanel(iframeWindow, toolbox) {
  50. this.panelDoc = iframeWindow.document;
  51. this.panelWin = iframeWindow;
  52. this.toolbox = toolbox;
  53. this.isReady = false;
  54. this._prefChanged = this._prefChanged.bind(this);
  55. this._themeRegistered = this._themeRegistered.bind(this);
  56. this._themeUnregistered = this._themeUnregistered.bind(this);
  57. this._disableJSClicked = this._disableJSClicked.bind(this);
  58. this.disableJSNode = this.panelDoc.getElementById(
  59. "devtools-disable-javascript");
  60. this._addListeners();
  61. const EventEmitter = require("devtools/shared/event-emitter");
  62. EventEmitter.decorate(this);
  63. }
  64. OptionsPanel.prototype = {
  65. get target() {
  66. return this.toolbox.target;
  67. },
  68. open: Task.async(function* () {
  69. // For local debugging we need to make the target remote.
  70. if (!this.target.isRemote) {
  71. yield this.target.makeRemote();
  72. }
  73. this.setupToolsList();
  74. this.setupToolbarButtonsList();
  75. this.setupThemeList();
  76. yield this.populatePreferences();
  77. this.isReady = true;
  78. this.emit("ready");
  79. return this;
  80. }),
  81. _addListeners: function () {
  82. Services.prefs.addObserver("devtools.cache.disabled", this._prefChanged, false);
  83. Services.prefs.addObserver("devtools.theme", this._prefChanged, false);
  84. gDevTools.on("theme-registered", this._themeRegistered);
  85. gDevTools.on("theme-unregistered", this._themeUnregistered);
  86. },
  87. _removeListeners: function () {
  88. Services.prefs.removeObserver("devtools.cache.disabled", this._prefChanged);
  89. Services.prefs.removeObserver("devtools.theme", this._prefChanged);
  90. gDevTools.off("theme-registered", this._themeRegistered);
  91. gDevTools.off("theme-unregistered", this._themeUnregistered);
  92. },
  93. _prefChanged: function (subject, topic, prefName) {
  94. if (prefName === "devtools.cache.disabled") {
  95. let cacheDisabled = data.newValue;
  96. let cbx = this.panelDoc.getElementById("devtools-disable-cache");
  97. cbx.checked = cacheDisabled;
  98. } else if (prefName === "devtools.theme") {
  99. this.updateCurrentTheme();
  100. }
  101. },
  102. _themeRegistered: function (event, themeId) {
  103. this.setupThemeList();
  104. },
  105. _themeUnregistered: function (event, theme) {
  106. let themeBox = this.panelDoc.getElementById("devtools-theme-box");
  107. let themeInput = themeBox.querySelector(`[value=${theme.id}]`);
  108. if (themeInput) {
  109. themeInput.parentNode.remove();
  110. }
  111. },
  112. setupToolbarButtonsList: function () {
  113. let enabledToolbarButtonsBox = this.panelDoc.getElementById(
  114. "enabled-toolbox-buttons-box");
  115. let toggleableButtons = this.toolbox.toolboxButtons;
  116. let setToolboxButtonsVisibility =
  117. this.toolbox.setToolboxButtonsVisibility.bind(this.toolbox);
  118. let onCheckboxClick = (checkbox) => {
  119. let toolDefinition = toggleableButtons.filter(
  120. toggleableButton => toggleableButton.id === checkbox.id)[0];
  121. Services.prefs.setBoolPref(
  122. toolDefinition.visibilityswitch, checkbox.checked);
  123. setToolboxButtonsVisibility();
  124. };
  125. let createCommandCheckbox = tool => {
  126. let checkboxLabel = this.panelDoc.createElement("label");
  127. let checkboxSpanLabel = this.panelDoc.createElement("span");
  128. checkboxSpanLabel.textContent = tool.label;
  129. let checkboxInput = this.panelDoc.createElement("input");
  130. checkboxInput.setAttribute("type", "checkbox");
  131. checkboxInput.setAttribute("id", tool.id);
  132. if (InfallibleGetBoolPref(tool.visibilityswitch)) {
  133. checkboxInput.setAttribute("checked", true);
  134. }
  135. checkboxInput.addEventListener("change",
  136. onCheckboxClick.bind(this, checkboxInput));
  137. checkboxLabel.appendChild(checkboxInput);
  138. checkboxLabel.appendChild(checkboxSpanLabel);
  139. return checkboxLabel;
  140. };
  141. for (let tool of toggleableButtons) {
  142. if (!tool.isTargetSupported(this.toolbox.target)) {
  143. continue;
  144. }
  145. enabledToolbarButtonsBox.appendChild(createCommandCheckbox(tool));
  146. }
  147. },
  148. setupToolsList: function () {
  149. let defaultToolsBox = this.panelDoc.getElementById("default-tools-box");
  150. let additionalToolsBox = this.panelDoc.getElementById(
  151. "additional-tools-box");
  152. let toolsNotSupportedLabel = this.panelDoc.getElementById(
  153. "tools-not-supported-label");
  154. let atleastOneToolNotSupported = false;
  155. let onCheckboxClick = function (id) {
  156. let toolDefinition = gDevTools._tools.get(id);
  157. // Set the kill switch pref boolean to true
  158. Services.prefs.setBoolPref(toolDefinition.visibilityswitch, this.checked);
  159. if (this.checked) {
  160. gDevTools.emit("tool-registered", id);
  161. } else {
  162. gDevTools.emit("tool-unregistered", toolDefinition);
  163. }
  164. };
  165. let createToolCheckbox = tool => {
  166. let checkboxLabel = this.panelDoc.createElement("label");
  167. let checkboxInput = this.panelDoc.createElement("input");
  168. checkboxInput.setAttribute("type", "checkbox");
  169. checkboxInput.setAttribute("id", tool.id);
  170. checkboxInput.setAttribute("title", tool.tooltip || "");
  171. let checkboxSpanLabel = this.panelDoc.createElement("span");
  172. if (tool.isTargetSupported(this.target)) {
  173. checkboxSpanLabel.textContent = tool.label;
  174. } else {
  175. atleastOneToolNotSupported = true;
  176. checkboxSpanLabel.textContent =
  177. L10N.getFormatStr("options.toolNotSupportedMarker", tool.label);
  178. checkboxInput.setAttribute("data-unsupported", "true");
  179. checkboxInput.setAttribute("disabled", "true");
  180. }
  181. if (InfallibleGetBoolPref(tool.visibilityswitch)) {
  182. checkboxInput.setAttribute("checked", "true");
  183. }
  184. checkboxInput.addEventListener("change",
  185. onCheckboxClick.bind(checkboxInput, tool.id));
  186. checkboxLabel.appendChild(checkboxInput);
  187. checkboxLabel.appendChild(checkboxSpanLabel);
  188. return checkboxLabel;
  189. };
  190. // Populating the default tools lists
  191. let toggleableTools = gDevTools.getDefaultTools().filter(tool => {
  192. return tool.visibilityswitch && !tool.hiddenInOptions;
  193. });
  194. for (let tool of toggleableTools) {
  195. defaultToolsBox.appendChild(createToolCheckbox(tool));
  196. }
  197. // Populating the additional tools list that came from add-ons.
  198. let atleastOneAddon = false;
  199. for (let tool of gDevTools.getAdditionalTools()) {
  200. atleastOneAddon = true;
  201. additionalToolsBox.appendChild(createToolCheckbox(tool));
  202. }
  203. if (!atleastOneAddon) {
  204. additionalToolsBox.style.display = "none";
  205. }
  206. if (!atleastOneToolNotSupported) {
  207. toolsNotSupportedLabel.style.display = "none";
  208. }
  209. this.panelWin.focus();
  210. },
  211. setupThemeList: function () {
  212. let themeBox = this.panelDoc.getElementById("devtools-theme-box");
  213. let themeLabels = themeBox.querySelectorAll("label");
  214. for (let label of themeLabels) {
  215. label.remove();
  216. }
  217. let createThemeOption = theme => {
  218. let inputLabel = this.panelDoc.createElement("label");
  219. let inputRadio = this.panelDoc.createElement("input");
  220. inputRadio.setAttribute("type", "radio");
  221. inputRadio.setAttribute("value", theme.id);
  222. inputRadio.setAttribute("name", "devtools-theme-item");
  223. inputRadio.addEventListener("change", function (e) {
  224. setPrefAndEmit(themeBox.getAttribute("data-pref"),
  225. e.target.value);
  226. });
  227. let inputSpanLabel = this.panelDoc.createElement("span");
  228. inputSpanLabel.textContent = theme.label;
  229. inputLabel.appendChild(inputRadio);
  230. inputLabel.appendChild(inputSpanLabel);
  231. return inputLabel;
  232. };
  233. // Populating the default theme list
  234. let themes = gDevTools.getThemeDefinitionArray();
  235. for (let theme of themes) {
  236. themeBox.appendChild(createThemeOption(theme));
  237. }
  238. this.updateCurrentTheme();
  239. },
  240. populatePreferences: function () {
  241. let prefCheckboxes = this.panelDoc.querySelectorAll(
  242. "input[type=checkbox][data-pref]");
  243. for (let prefCheckbox of prefCheckboxes) {
  244. if (GetPref(prefCheckbox.getAttribute("data-pref"))) {
  245. prefCheckbox.setAttribute("checked", true);
  246. }
  247. prefCheckbox.addEventListener("change", function (e) {
  248. let checkbox = e.target;
  249. setPrefAndEmit(checkbox.getAttribute("data-pref"), checkbox.checked);
  250. });
  251. }
  252. // Themes radio inputs are handled in setupThemeList
  253. let prefRadiogroups = this.panelDoc.querySelectorAll(
  254. ".radiogroup[data-pref]:not(#devtools-theme-box)");
  255. for (let radioGroup of prefRadiogroups) {
  256. let selectedValue = GetPref(radioGroup.getAttribute("data-pref"));
  257. for (let radioInput of radioGroup.querySelectorAll("input[type=radio]")) {
  258. if (radioInput.getAttribute("value") == selectedValue) {
  259. radioInput.setAttribute("checked", true);
  260. }
  261. radioInput.addEventListener("change", function (e) {
  262. setPrefAndEmit(radioGroup.getAttribute("data-pref"),
  263. e.target.value);
  264. });
  265. }
  266. }
  267. let prefSelects = this.panelDoc.querySelectorAll("select[data-pref]");
  268. for (let prefSelect of prefSelects) {
  269. let pref = GetPref(prefSelect.getAttribute("data-pref"));
  270. let options = [...prefSelect.options];
  271. options.some(function (option) {
  272. let value = option.value;
  273. // non strict check to allow int values.
  274. if (value == pref) {
  275. prefSelect.selectedIndex = options.indexOf(option);
  276. return true;
  277. }
  278. });
  279. prefSelect.addEventListener("change", function (e) {
  280. let select = e.target;
  281. setPrefAndEmit(select.getAttribute("data-pref"),
  282. select.options[select.selectedIndex].value);
  283. });
  284. }
  285. if (this.target.activeTab) {
  286. return this.target.client.attachTab(this.target.activeTab._actor)
  287. .then(([response, client]) => {
  288. this._origJavascriptEnabled = !response.javascriptEnabled;
  289. this.disableJSNode.checked = this._origJavascriptEnabled;
  290. this.disableJSNode.addEventListener("click",
  291. this._disableJSClicked, false);
  292. });
  293. }
  294. this.disableJSNode.hidden = true;
  295. },
  296. updateCurrentTheme: function () {
  297. let currentTheme = GetPref("devtools.theme");
  298. let themeBox = this.panelDoc.getElementById("devtools-theme-box");
  299. let themeRadioInput = themeBox.querySelector(`[value=${currentTheme}]`);
  300. if (themeRadioInput) {
  301. themeRadioInput.checked = true;
  302. } else {
  303. // If the current theme does not exist anymore, switch to light theme
  304. let lightThemeInputRadio = themeBox.querySelector("[value=light]");
  305. lightThemeInputRadio.checked = true;
  306. }
  307. },
  308. /**
  309. * Disables JavaScript for the currently loaded tab. We force a page refresh
  310. * here because setting docShell.allowJavascript to true fails to block JS
  311. * execution from event listeners added using addEventListener(), AJAX calls
  312. * and timers. The page refresh prevents these things from being added in the
  313. * first place.
  314. *
  315. * @param {Event} event
  316. * The event sent by checking / unchecking the disable JS checkbox.
  317. */
  318. _disableJSClicked: function (event) {
  319. let checked = event.target.checked;
  320. let options = {
  321. "javascriptEnabled": !checked
  322. };
  323. this.target.activeTab.reconfigure(options);
  324. },
  325. destroy: function () {
  326. if (this.destroyPromise) {
  327. return this.destroyPromise;
  328. }
  329. let deferred = defer();
  330. this.destroyPromise = deferred.promise;
  331. this._removeListeners();
  332. if (this.target.activeTab) {
  333. this.disableJSNode.removeEventListener("click", this._disableJSClicked);
  334. // FF41+ automatically cleans up state in actor on disconnect
  335. if (!this.target.activeTab.traits.noTabReconfigureOnClose) {
  336. let options = {
  337. "javascriptEnabled": this._origJavascriptEnabled,
  338. "performReload": false
  339. };
  340. this.target.activeTab.reconfigure(options, deferred.resolve);
  341. } else {
  342. deferred.resolve();
  343. }
  344. } else {
  345. deferred.resolve();
  346. }
  347. this.panelWin = this.panelDoc = this.disableJSNode = this.toolbox = null;
  348. return this.destroyPromise;
  349. }
  350. };
  351. /* Set a pref and emit the pref-changed event if needed. */
  352. function setPrefAndEmit(prefName, newValue) {
  353. let data = {
  354. pref: prefName,
  355. newValue: newValue
  356. };
  357. data.oldValue = GetPref(data.pref);
  358. SetPref(data.pref, data.newValue);
  359. if (data.newValue != data.oldValue) {
  360. gDevTools.emit("pref-changed", data);
  361. }
  362. }