1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
9 } = require("resource://devtools/client/framework/devtools.js");
11 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
12 const L10N = new LocalizationHelper(
13 "devtools/client/locales/toolbox.properties"
16 loader.lazyRequireGetter(
19 "resource://devtools/client/shared/link.js",
23 exports.OptionsPanel = OptionsPanel;
25 function GetPref(name) {
26 const type = Services.prefs.getPrefType(name);
28 case Services.prefs.PREF_STRING:
29 return Services.prefs.getCharPref(name);
30 case Services.prefs.PREF_INT:
31 return Services.prefs.getIntPref(name);
32 case Services.prefs.PREF_BOOL:
33 return Services.prefs.getBoolPref(name);
35 throw new Error("Unknown type");
39 function SetPref(name, value) {
40 const type = Services.prefs.getPrefType(name);
42 case Services.prefs.PREF_STRING:
43 return Services.prefs.setCharPref(name, value);
44 case Services.prefs.PREF_INT:
45 return Services.prefs.setIntPref(name, value);
46 case Services.prefs.PREF_BOOL:
47 return Services.prefs.setBoolPref(name, value);
49 throw new Error("Unknown type");
53 function InfallibleGetBoolPref(key) {
55 return Services.prefs.getBoolPref(key);
62 * Represents the Options Panel in the Toolbox.
64 function OptionsPanel(iframeWindow, toolbox, commands) {
65 this.panelDoc = iframeWindow.document;
66 this.panelWin = iframeWindow;
68 this.toolbox = toolbox;
69 this.commands = commands;
70 this.telemetry = toolbox.telemetry;
72 this.setupToolsList = this.setupToolsList.bind(this);
73 this._prefChanged = this._prefChanged.bind(this);
74 this._themeRegistered = this._themeRegistered.bind(this);
75 this._themeUnregistered = this._themeUnregistered.bind(this);
76 this._disableJSClicked = this._disableJSClicked.bind(this);
78 this.disableJSNode = this.panelDoc.getElementById(
79 "devtools-disable-javascript"
84 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
85 EventEmitter.decorate(this);
88 OptionsPanel.prototype = {
90 return this.toolbox.target;
94 this.setupToolsList();
95 this.setupToolbarButtonsList();
96 this.setupThemeList();
97 this.setupAdditionalOptions();
98 await this.populatePreferences();
103 Services.prefs.addObserver("devtools.cache.disabled", this._prefChanged);
104 Services.prefs.addObserver("devtools.theme", this._prefChanged);
105 Services.prefs.addObserver(
106 "devtools.source-map.client-service.enabled",
109 gDevTools.on("theme-registered", this._themeRegistered);
110 gDevTools.on("theme-unregistered", this._themeUnregistered);
112 // Refresh the tools list when a new tool or webextension has been
113 // registered to the toolbox.
114 this.toolbox.on("tool-registered", this.setupToolsList);
115 this.toolbox.on("webextension-registered", this.setupToolsList);
116 // Refresh the tools list when a new tool or webextension has been
117 // unregistered from the toolbox.
118 this.toolbox.on("tool-unregistered", this.setupToolsList);
119 this.toolbox.on("webextension-unregistered", this.setupToolsList);
123 Services.prefs.removeObserver("devtools.cache.disabled", this._prefChanged);
124 Services.prefs.removeObserver("devtools.theme", this._prefChanged);
125 Services.prefs.removeObserver(
126 "devtools.source-map.client-service.enabled",
130 this.toolbox.off("tool-registered", this.setupToolsList);
131 this.toolbox.off("tool-unregistered", this.setupToolsList);
132 this.toolbox.off("webextension-registered", this.setupToolsList);
133 this.toolbox.off("webextension-unregistered", this.setupToolsList);
135 gDevTools.off("theme-registered", this._themeRegistered);
136 gDevTools.off("theme-unregistered", this._themeUnregistered);
139 _prefChanged(subject, topic, prefName) {
140 if (prefName === "devtools.cache.disabled") {
141 const cacheDisabled = GetPref(prefName);
142 const cbx = this.panelDoc.getElementById("devtools-disable-cache");
143 cbx.checked = cacheDisabled;
144 } else if (prefName === "devtools.theme") {
145 this.updateCurrentTheme();
146 } else if (prefName === "devtools.source-map.client-service.enabled") {
147 this.updateSourceMapPref();
151 _themeRegistered(themeId) {
152 this.setupThemeList();
155 _themeUnregistered(theme) {
156 const themeBox = this.panelDoc.getElementById("devtools-theme-box");
157 const themeInput = themeBox.querySelector(`[value=${theme.id}]`);
160 themeInput.parentNode.remove();
164 async setupToolbarButtonsList() {
165 // Ensure the toolbox is open, and the buttons are all set up.
166 await this.toolbox.isOpen;
168 const enabledToolbarButtonsBox = this.panelDoc.getElementById(
169 "enabled-toolbox-buttons-box"
172 const toolbarButtons = this.toolbox.toolbarButtons;
174 if (!toolbarButtons) {
175 console.warn("The command buttons weren't initiated yet.");
179 const onCheckboxClick = checkbox => {
180 const commandButton = toolbarButtons.filter(
181 toggleableButton => toggleableButton.id === checkbox.id
184 Services.prefs.setBoolPref(
185 commandButton.visibilityswitch,
188 this.toolbox.updateToolboxButtonsVisibility();
191 const createCommandCheckbox = button => {
192 const checkboxLabel = this.panelDoc.createElement("label");
193 const checkboxSpanLabel = this.panelDoc.createElement("span");
194 checkboxSpanLabel.textContent = button.description;
195 const checkboxInput = this.panelDoc.createElement("input");
196 checkboxInput.setAttribute("type", "checkbox");
197 checkboxInput.setAttribute("id", button.id);
199 if (Services.prefs.getBoolPref(button.visibilityswitch, true)) {
200 checkboxInput.setAttribute("checked", true);
202 checkboxInput.addEventListener(
204 onCheckboxClick.bind(this, checkboxInput)
207 checkboxLabel.appendChild(checkboxInput);
208 checkboxLabel.appendChild(checkboxSpanLabel);
210 return checkboxLabel;
213 for (const button of toolbarButtons) {
214 if (!button.isToolSupported(this.toolbox)) {
218 enabledToolbarButtonsBox.appendChild(createCommandCheckbox(button));
223 const defaultToolsBox = this.panelDoc.getElementById("default-tools-box");
224 const additionalToolsBox = this.panelDoc.getElementById(
225 "additional-tools-box"
227 const toolsNotSupportedLabel = this.panelDoc.getElementById(
228 "tools-not-supported-label"
230 let atleastOneToolNotSupported = false;
232 // Signal tool registering/unregistering globally (for the tools registered
233 // globally) and per toolbox (for the tools registered to a single toolbox).
234 // This event handler expect this to be binded to the related checkbox element.
235 const onCheckboxClick = function (telemetry, tool) {
236 // Set the kill switch pref boolean to true
237 Services.prefs.setBoolPref(tool.visibilityswitch, this.checked);
239 if (!tool.isWebExtension) {
241 this.checked ? "tool-registered" : "tool-unregistered",
244 // Record which tools were registered and unregistered.
245 telemetry.keyedScalarSet(
246 "devtools.tool.registered",
253 const createToolCheckbox = tool => {
254 const checkboxLabel = this.panelDoc.createElement("label");
255 const checkboxInput = this.panelDoc.createElement("input");
256 checkboxInput.setAttribute("type", "checkbox");
257 checkboxInput.setAttribute("id", tool.id);
258 checkboxInput.setAttribute("title", tool.tooltip || "");
260 const checkboxSpanLabel = this.panelDoc.createElement("span");
261 if (tool.isToolSupported(this.toolbox)) {
262 checkboxSpanLabel.textContent = tool.label;
264 atleastOneToolNotSupported = true;
265 checkboxSpanLabel.textContent = L10N.getFormatStr(
266 "options.toolNotSupportedMarker",
269 checkboxInput.setAttribute("data-unsupported", "true");
270 checkboxInput.setAttribute("disabled", "true");
273 if (InfallibleGetBoolPref(tool.visibilityswitch)) {
274 checkboxInput.setAttribute("checked", "true");
277 checkboxInput.addEventListener(
279 onCheckboxClick.bind(checkboxInput, this.telemetry, tool)
282 checkboxLabel.appendChild(checkboxInput);
283 checkboxLabel.appendChild(checkboxSpanLabel);
285 // We shouldn't have deprecated tools anymore, but we might have one in the future,
286 // when migrating the storage inspector to the application panel (Bug 1681059).
287 // Let's keep this code for now so we keep the l10n property around and avoid
288 // unnecessary translation work if we need it again in the future.
289 if (tool.deprecated) {
290 const deprecationURL = this.panelDoc.createElement("a");
291 deprecationURL.title = deprecationURL.href = tool.deprecationURL;
292 deprecationURL.textContent = L10N.getStr("options.deprecationNotice");
293 // Cannot use a real link when we are in the Browser Toolbox.
294 deprecationURL.addEventListener("click", e => {
296 openDocLink(tool.deprecationURL, { relatedToCurrent: true });
299 const checkboxSpanDeprecated = this.panelDoc.createElement("span");
300 checkboxSpanDeprecated.className = "deprecation-notice";
301 checkboxLabel.appendChild(checkboxSpanDeprecated);
302 checkboxSpanDeprecated.appendChild(deprecationURL);
305 return checkboxLabel;
308 // Clean up any existent default tools content.
309 for (const label of defaultToolsBox.querySelectorAll("label")) {
313 // Populating the default tools lists
314 const toggleableTools = gDevTools.getDefaultTools().filter(tool => {
315 return tool.visibilityswitch && !tool.hiddenInOptions;
318 const fragment = this.panelDoc.createDocumentFragment();
319 for (const tool of toggleableTools) {
320 fragment.appendChild(createToolCheckbox(tool));
323 const toolsNotSupportedLabelNode = this.panelDoc.getElementById(
324 "tools-not-supported-label"
326 defaultToolsBox.insertBefore(fragment, toolsNotSupportedLabelNode);
328 // Clean up any existent additional tools content.
329 for (const label of additionalToolsBox.querySelectorAll("label")) {
333 // Populating the additional tools list.
334 let atleastOneAddon = false;
335 for (const tool of gDevTools.getAdditionalTools()) {
336 atleastOneAddon = true;
337 additionalToolsBox.appendChild(createToolCheckbox(tool));
340 // Populating the additional tools that came from the installed WebExtension add-ons.
341 for (const { uuid, name, pref } of this.toolbox.listWebExtensions()) {
342 atleastOneAddon = true;
344 additionalToolsBox.appendChild(
346 isWebExtension: true,
348 // Use the preference as the unified webextensions tool id.
349 id: `webext-${uuid}`,
352 // Disable the devtools extension using the given pref name:
353 // the toolbox options for the WebExtensions are not related to a single
354 // tool (e.g. a devtools panel created from the extension devtools_page)
355 // but to the entire devtools part of a webextension which is enabled
356 // by the Addon Manager (but it may be disabled by its related
357 // devtools about:config preference), and so the following
358 visibilityswitch: pref,
360 // Only local tabs are currently supported as targets.
361 isToolSupported: toolbox =>
362 toolbox.commands.descriptorFront.isLocalTab,
367 if (!atleastOneAddon) {
368 additionalToolsBox.style.display = "none";
370 additionalToolsBox.style.display = "";
373 if (!atleastOneToolNotSupported) {
374 toolsNotSupportedLabel.style.display = "none";
376 toolsNotSupportedLabel.style.display = "";
379 this.panelWin.focus();
383 const themeBox = this.panelDoc.getElementById("devtools-theme-box");
384 const themeLabels = themeBox.querySelectorAll("label");
385 for (const label of themeLabels) {
389 const createThemeOption = theme => {
390 const inputLabel = this.panelDoc.createElement("label");
391 const inputRadio = this.panelDoc.createElement("input");
392 inputRadio.setAttribute("type", "radio");
393 inputRadio.setAttribute("value", theme.id);
394 inputRadio.setAttribute("name", "devtools-theme-item");
395 inputRadio.addEventListener("change", function (e) {
396 SetPref(themeBox.getAttribute("data-pref"), e.target.value);
399 const inputSpanLabel = this.panelDoc.createElement("span");
400 inputSpanLabel.textContent = theme.label;
401 inputLabel.appendChild(inputRadio);
402 inputLabel.appendChild(inputSpanLabel);
407 // Populating the default theme list
408 themeBox.appendChild(
411 label: L10N.getStr("options.autoTheme.label"),
415 const themes = gDevTools.getThemeDefinitionArray();
416 for (const theme of themes) {
417 themeBox.appendChild(createThemeOption(theme));
420 this.updateCurrentTheme();
424 * Add extra checkbox options bound to a boolean preference.
426 setupAdditionalOptions() {
427 const prefDefinitions = [
429 pref: "devtools.custom-formatters.enabled",
430 l10nLabelId: "options-enable-custom-formatters-label",
431 l10nTooltipId: "options-enable-custom-formatters-tooltip",
432 id: "devtools-custom-formatters",
433 parentId: "context-options",
437 const createPreferenceOption = ({
445 const inputLabel = this.panelDoc.createElement("label");
447 this.panelDoc.l10n.setAttributes(inputLabel, l10nTooltipId);
449 const checkbox = this.panelDoc.createElement("input");
450 checkbox.setAttribute("type", "checkbox");
452 checkbox.setAttribute("checked", "checked");
454 checkbox.setAttribute("id", id);
455 checkbox.addEventListener("change", e => {
456 SetPref(pref, e.target.checked);
458 onChange(e.target.checked);
462 const inputSpanLabel = this.panelDoc.createElement("span");
464 this.panelDoc.l10n.setAttributes(inputSpanLabel, l10nLabelId);
466 inputSpanLabel.textContent = label;
468 inputLabel.appendChild(checkbox);
469 inputLabel.appendChild(inputSpanLabel);
474 for (const prefDefinition of prefDefinitions) {
475 const parent = this.panelDoc.getElementById(prefDefinition.parentId);
476 // We want to insert the new definition after the last existing
477 // definition, but before any other element.
478 // For example in the "Advanced Settings" column there's indeed a <span>
479 // text at the end, and we want that it stays at the end.
480 // The reference element can be `null` if there's no label or if there's
481 // no element after the last label. But that's OK and it will do what we
483 const referenceElement = parent.querySelector("label:last-of-type + *");
485 createPreferenceOption(prefDefinition),
491 async populatePreferences() {
492 const prefCheckboxes = this.panelDoc.querySelectorAll(
493 "input[type=checkbox][data-pref]"
495 for (const prefCheckbox of prefCheckboxes) {
496 if (GetPref(prefCheckbox.getAttribute("data-pref"))) {
497 prefCheckbox.setAttribute("checked", true);
499 prefCheckbox.addEventListener("change", function (e) {
500 const checkbox = e.target;
501 SetPref(checkbox.getAttribute("data-pref"), checkbox.checked);
504 // Themes radio inputs are handled in setupThemeList
505 const prefRadiogroups = this.panelDoc.querySelectorAll(
506 ".radiogroup[data-pref]:not(#devtools-theme-box)"
508 for (const radioGroup of prefRadiogroups) {
509 const selectedValue = GetPref(radioGroup.getAttribute("data-pref"));
511 for (const radioInput of radioGroup.querySelectorAll(
514 if (radioInput.getAttribute("value") == selectedValue) {
515 radioInput.setAttribute("checked", true);
518 radioInput.addEventListener("change", function (e) {
519 SetPref(radioGroup.getAttribute("data-pref"), e.target.value);
523 const prefSelects = this.panelDoc.querySelectorAll("select[data-pref]");
524 for (const prefSelect of prefSelects) {
525 const pref = GetPref(prefSelect.getAttribute("data-pref"));
526 const options = [...prefSelect.options];
527 options.some(function (option) {
528 const value = option.value;
529 // non strict check to allow int values.
531 prefSelect.selectedIndex = options.indexOf(option);
537 prefSelect.addEventListener("change", function (e) {
538 const select = e.target;
540 select.getAttribute("data-pref"),
541 select.options[select.selectedIndex].value
546 if (this.commands.descriptorFront.isTabDescriptor) {
547 const isJavascriptEnabled =
548 await this.commands.targetConfigurationCommand.isJavascriptEnabled();
549 this.disableJSNode.checked = !isJavascriptEnabled;
550 this.disableJSNode.addEventListener("click", this._disableJSClicked);
552 // Hide the checkbox and label
553 this.disableJSNode.parentNode.style.display = "none";
555 const triggersPageRefreshLabel = this.panelDoc.getElementById(
556 "triggers-page-refresh-label"
558 triggersPageRefreshLabel.style.display = "none";
562 updateCurrentTheme() {
563 const currentTheme = GetPref("devtools.theme");
564 const themeBox = this.panelDoc.getElementById("devtools-theme-box");
565 const themeRadioInput = themeBox.querySelector(`[value=${currentTheme}]`);
567 if (themeRadioInput) {
568 themeRadioInput.checked = true;
570 // If the current theme does not exist anymore, switch to auto theme
571 const autoThemeInputRadio = themeBox.querySelector("[value=auto]");
572 autoThemeInputRadio.checked = true;
576 updateSourceMapPref() {
577 const prefName = "devtools.source-map.client-service.enabled";
578 const enabled = GetPref(prefName);
579 const box = this.panelDoc.querySelector(`[data-pref="${prefName}"]`);
580 box.checked = enabled;
584 * Disables JavaScript for the currently loaded tab. We force a page refresh
585 * here because setting browsingContext.allowJavascript to true fails to block
586 * JS execution from event listeners added using addEventListener(), AJAX
587 * calls and timers. The page refresh prevents these things from being added
588 * in the first place.
590 * @param {Event} event
591 * The event sent by checking / unchecking the disable JS checkbox.
593 _disableJSClicked(event) {
594 const checked = event.target.checked;
596 this.commands.targetConfigurationCommand.updateConfiguration({
597 javascriptEnabled: !checked,
602 if (this.destroyed) {
605 this.destroyed = true;
607 this._removeListeners();
609 this.disableJSNode.removeEventListener("click", this._disableJSClicked);
611 this.panelWin = this.panelDoc = this.disableJSNode = this.toolbox = null;