Bug 1861305 - Renew data collection for audio_process_per_codec_name r=padenot
[gecko.git] / devtools / client / framework / toolbox-options.js
blob57fa1202b77a2a4ad175c3bdb46c984042dd959b
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/. */
5 "use strict";
7 const {
8   gDevTools,
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(
17   this,
18   "openDocLink",
19   "resource://devtools/client/shared/link.js",
20   true
23 exports.OptionsPanel = OptionsPanel;
25 function GetPref(name) {
26   const type = Services.prefs.getPrefType(name);
27   switch (type) {
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);
34     default:
35       throw new Error("Unknown type");
36   }
39 function SetPref(name, value) {
40   const type = Services.prefs.getPrefType(name);
41   switch (type) {
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);
48     default:
49       throw new Error("Unknown type");
50   }
53 function InfallibleGetBoolPref(key) {
54   try {
55     return Services.prefs.getBoolPref(key);
56   } catch (ex) {
57     return true;
58   }
61 /**
62  * Represents the Options Panel in the Toolbox.
63  */
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"
80   );
82   this._addListeners();
84   const EventEmitter = require("resource://devtools/shared/event-emitter.js");
85   EventEmitter.decorate(this);
88 OptionsPanel.prototype = {
89   get target() {
90     return this.toolbox.target;
91   },
93   async open() {
94     this.setupToolsList();
95     this.setupToolbarButtonsList();
96     this.setupThemeList();
97     this.setupAdditionalOptions();
98     await this.populatePreferences();
99     return this;
100   },
102   _addListeners() {
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",
107       this._prefChanged
108     );
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);
120   },
122   _removeListeners() {
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",
127       this._prefChanged
128     );
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);
137   },
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();
148     }
149   },
151   _themeRegistered(themeId) {
152     this.setupThemeList();
153   },
155   _themeUnregistered(theme) {
156     const themeBox = this.panelDoc.getElementById("devtools-theme-box");
157     const themeInput = themeBox.querySelector(`[value=${theme.id}]`);
159     if (themeInput) {
160       themeInput.parentNode.remove();
161     }
162   },
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"
170     );
172     const toolbarButtons = this.toolbox.toolbarButtons;
174     if (!toolbarButtons) {
175       console.warn("The command buttons weren't initiated yet.");
176       return;
177     }
179     const onCheckboxClick = checkbox => {
180       const commandButton = toolbarButtons.filter(
181         toggleableButton => toggleableButton.id === checkbox.id
182       )[0];
184       Services.prefs.setBoolPref(
185         commandButton.visibilityswitch,
186         checkbox.checked
187       );
188       this.toolbox.updateToolboxButtonsVisibility();
189     };
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);
201       }
202       checkboxInput.addEventListener(
203         "change",
204         onCheckboxClick.bind(this, checkboxInput)
205       );
207       checkboxLabel.appendChild(checkboxInput);
208       checkboxLabel.appendChild(checkboxSpanLabel);
210       return checkboxLabel;
211     };
213     for (const button of toolbarButtons) {
214       if (!button.isToolSupported(this.toolbox)) {
215         continue;
216       }
218       enabledToolbarButtonsBox.appendChild(createCommandCheckbox(button));
219     }
220   },
222   setupToolsList() {
223     const defaultToolsBox = this.panelDoc.getElementById("default-tools-box");
224     const additionalToolsBox = this.panelDoc.getElementById(
225       "additional-tools-box"
226     );
227     const toolsNotSupportedLabel = this.panelDoc.getElementById(
228       "tools-not-supported-label"
229     );
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) {
240         gDevTools.emit(
241           this.checked ? "tool-registered" : "tool-unregistered",
242           tool.id
243         );
244         // Record which tools were registered and unregistered.
245         telemetry.keyedScalarSet(
246           "devtools.tool.registered",
247           tool.id,
248           this.checked
249         );
250       }
251     };
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;
263       } else {
264         atleastOneToolNotSupported = true;
265         checkboxSpanLabel.textContent = L10N.getFormatStr(
266           "options.toolNotSupportedMarker",
267           tool.label
268         );
269         checkboxInput.setAttribute("data-unsupported", "true");
270         checkboxInput.setAttribute("disabled", "true");
271       }
273       if (InfallibleGetBoolPref(tool.visibilityswitch)) {
274         checkboxInput.setAttribute("checked", "true");
275       }
277       checkboxInput.addEventListener(
278         "change",
279         onCheckboxClick.bind(checkboxInput, this.telemetry, tool)
280       );
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 => {
295           e.preventDefault();
296           openDocLink(tool.deprecationURL, { relatedToCurrent: true });
297         });
299         const checkboxSpanDeprecated = this.panelDoc.createElement("span");
300         checkboxSpanDeprecated.className = "deprecation-notice";
301         checkboxLabel.appendChild(checkboxSpanDeprecated);
302         checkboxSpanDeprecated.appendChild(deprecationURL);
303       }
305       return checkboxLabel;
306     };
308     // Clean up any existent default tools content.
309     for (const label of defaultToolsBox.querySelectorAll("label")) {
310       label.remove();
311     }
313     // Populating the default tools lists
314     const toggleableTools = gDevTools.getDefaultTools().filter(tool => {
315       return tool.visibilityswitch && !tool.hiddenInOptions;
316     });
318     const fragment = this.panelDoc.createDocumentFragment();
319     for (const tool of toggleableTools) {
320       fragment.appendChild(createToolCheckbox(tool));
321     }
323     const toolsNotSupportedLabelNode = this.panelDoc.getElementById(
324       "tools-not-supported-label"
325     );
326     defaultToolsBox.insertBefore(fragment, toolsNotSupportedLabelNode);
328     // Clean up any existent additional tools content.
329     for (const label of additionalToolsBox.querySelectorAll("label")) {
330       label.remove();
331     }
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));
338     }
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(
345         createToolCheckbox({
346           isWebExtension: true,
348           // Use the preference as the unified webextensions tool id.
349           id: `webext-${uuid}`,
350           tooltip: name,
351           label: name,
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,
363         })
364       );
365     }
367     if (!atleastOneAddon) {
368       additionalToolsBox.style.display = "none";
369     } else {
370       additionalToolsBox.style.display = "";
371     }
373     if (!atleastOneToolNotSupported) {
374       toolsNotSupportedLabel.style.display = "none";
375     } else {
376       toolsNotSupportedLabel.style.display = "";
377     }
379     this.panelWin.focus();
380   },
382   setupThemeList() {
383     const themeBox = this.panelDoc.getElementById("devtools-theme-box");
384     const themeLabels = themeBox.querySelectorAll("label");
385     for (const label of themeLabels) {
386       label.remove();
387     }
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);
397       });
399       const inputSpanLabel = this.panelDoc.createElement("span");
400       inputSpanLabel.textContent = theme.label;
401       inputLabel.appendChild(inputRadio);
402       inputLabel.appendChild(inputSpanLabel);
404       return inputLabel;
405     };
407     // Populating the default theme list
408     themeBox.appendChild(
409       createThemeOption({
410         id: "auto",
411         label: L10N.getStr("options.autoTheme.label"),
412       })
413     );
415     const themes = gDevTools.getThemeDefinitionArray();
416     for (const theme of themes) {
417       themeBox.appendChild(createThemeOption(theme));
418     }
420     this.updateCurrentTheme();
421   },
423   /**
424    * Add extra checkbox options bound to a boolean preference.
425    */
426   setupAdditionalOptions() {
427     const prefDefinitions = [
428       {
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",
434       },
435     ];
437     const createPreferenceOption = ({
438       pref,
439       label,
440       l10nLabelId,
441       l10nTooltipId,
442       id,
443       onChange,
444     }) => {
445       const inputLabel = this.panelDoc.createElement("label");
446       if (l10nTooltipId) {
447         this.panelDoc.l10n.setAttributes(inputLabel, l10nTooltipId);
448       }
449       const checkbox = this.panelDoc.createElement("input");
450       checkbox.setAttribute("type", "checkbox");
451       if (GetPref(pref)) {
452         checkbox.setAttribute("checked", "checked");
453       }
454       checkbox.setAttribute("id", id);
455       checkbox.addEventListener("change", e => {
456         SetPref(pref, e.target.checked);
457         if (onChange) {
458           onChange(e.target.checked);
459         }
460       });
462       const inputSpanLabel = this.panelDoc.createElement("span");
463       if (l10nLabelId) {
464         this.panelDoc.l10n.setAttributes(inputSpanLabel, l10nLabelId);
465       } else if (label) {
466         inputSpanLabel.textContent = label;
467       }
468       inputLabel.appendChild(checkbox);
469       inputLabel.appendChild(inputSpanLabel);
471       return inputLabel;
472     };
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
482       // want.
483       const referenceElement = parent.querySelector("label:last-of-type + *");
484       parent.insertBefore(
485         createPreferenceOption(prefDefinition),
486         referenceElement
487       );
488     }
489   },
491   async populatePreferences() {
492     const prefCheckboxes = this.panelDoc.querySelectorAll(
493       "input[type=checkbox][data-pref]"
494     );
495     for (const prefCheckbox of prefCheckboxes) {
496       if (GetPref(prefCheckbox.getAttribute("data-pref"))) {
497         prefCheckbox.setAttribute("checked", true);
498       }
499       prefCheckbox.addEventListener("change", function (e) {
500         const checkbox = e.target;
501         SetPref(checkbox.getAttribute("data-pref"), checkbox.checked);
502       });
503     }
504     // Themes radio inputs are handled in setupThemeList
505     const prefRadiogroups = this.panelDoc.querySelectorAll(
506       ".radiogroup[data-pref]:not(#devtools-theme-box)"
507     );
508     for (const radioGroup of prefRadiogroups) {
509       const selectedValue = GetPref(radioGroup.getAttribute("data-pref"));
511       for (const radioInput of radioGroup.querySelectorAll(
512         "input[type=radio]"
513       )) {
514         if (radioInput.getAttribute("value") == selectedValue) {
515           radioInput.setAttribute("checked", true);
516         }
518         radioInput.addEventListener("change", function (e) {
519           SetPref(radioGroup.getAttribute("data-pref"), e.target.value);
520         });
521       }
522     }
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.
530         if (value == pref) {
531           prefSelect.selectedIndex = options.indexOf(option);
532           return true;
533         }
534         return false;
535       });
537       prefSelect.addEventListener("change", function (e) {
538         const select = e.target;
539         SetPref(
540           select.getAttribute("data-pref"),
541           select.options[select.selectedIndex].value
542         );
543       });
544     }
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);
551     } else {
552       // Hide the checkbox and label
553       this.disableJSNode.parentNode.style.display = "none";
555       const triggersPageRefreshLabel = this.panelDoc.getElementById(
556         "triggers-page-refresh-label"
557       );
558       triggersPageRefreshLabel.style.display = "none";
559     }
560   },
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;
569     } else {
570       // If the current theme does not exist anymore, switch to auto theme
571       const autoThemeInputRadio = themeBox.querySelector("[value=auto]");
572       autoThemeInputRadio.checked = true;
573     }
574   },
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;
581   },
583   /**
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.
589    *
590    * @param {Event} event
591    *        The event sent by checking / unchecking the disable JS checkbox.
592    */
593   _disableJSClicked(event) {
594     const checked = event.target.checked;
596     this.commands.targetConfigurationCommand.updateConfiguration({
597       javascriptEnabled: !checked,
598     });
599   },
601   destroy() {
602     if (this.destroyed) {
603       return;
604     }
605     this.destroyed = true;
607     this._removeListeners();
609     this.disableJSNode.removeEventListener("click", this._disableJSClicked);
611     this.panelWin = this.panelDoc = this.disableJSNode = this.toolbox = null;
612   },