From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- devtools/client/framework/toolbox-options.js | 613 +++++++++++++++++++++++++++ 1 file changed, 613 insertions(+) create mode 100644 devtools/client/framework/toolbox-options.js (limited to 'devtools/client/framework/toolbox-options.js') diff --git a/devtools/client/framework/toolbox-options.js b/devtools/client/framework/toolbox-options.js new file mode 100644 index 0000000000..57fa1202b7 --- /dev/null +++ b/devtools/client/framework/toolbox-options.js @@ -0,0 +1,613 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + gDevTools, +} = require("resource://devtools/client/framework/devtools.js"); + +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +loader.lazyRequireGetter( + this, + "openDocLink", + "resource://devtools/client/shared/link.js", + true +); + +exports.OptionsPanel = OptionsPanel; + +function GetPref(name) { + const type = Services.prefs.getPrefType(name); + switch (type) { + case Services.prefs.PREF_STRING: + return Services.prefs.getCharPref(name); + case Services.prefs.PREF_INT: + return Services.prefs.getIntPref(name); + case Services.prefs.PREF_BOOL: + return Services.prefs.getBoolPref(name); + default: + throw new Error("Unknown type"); + } +} + +function SetPref(name, value) { + const type = Services.prefs.getPrefType(name); + switch (type) { + case Services.prefs.PREF_STRING: + return Services.prefs.setCharPref(name, value); + case Services.prefs.PREF_INT: + return Services.prefs.setIntPref(name, value); + case Services.prefs.PREF_BOOL: + return Services.prefs.setBoolPref(name, value); + default: + throw new Error("Unknown type"); + } +} + +function InfallibleGetBoolPref(key) { + try { + return Services.prefs.getBoolPref(key); + } catch (ex) { + return true; + } +} + +/** + * Represents the Options Panel in the Toolbox. + */ +function OptionsPanel(iframeWindow, toolbox, commands) { + this.panelDoc = iframeWindow.document; + this.panelWin = iframeWindow; + + this.toolbox = toolbox; + this.commands = commands; + this.telemetry = toolbox.telemetry; + + this.setupToolsList = this.setupToolsList.bind(this); + this._prefChanged = this._prefChanged.bind(this); + this._themeRegistered = this._themeRegistered.bind(this); + this._themeUnregistered = this._themeUnregistered.bind(this); + this._disableJSClicked = this._disableJSClicked.bind(this); + + this.disableJSNode = this.panelDoc.getElementById( + "devtools-disable-javascript" + ); + + this._addListeners(); + + const EventEmitter = require("resource://devtools/shared/event-emitter.js"); + EventEmitter.decorate(this); +} + +OptionsPanel.prototype = { + get target() { + return this.toolbox.target; + }, + + async open() { + this.setupToolsList(); + this.setupToolbarButtonsList(); + this.setupThemeList(); + this.setupAdditionalOptions(); + await this.populatePreferences(); + return this; + }, + + _addListeners() { + Services.prefs.addObserver("devtools.cache.disabled", this._prefChanged); + Services.prefs.addObserver("devtools.theme", this._prefChanged); + Services.prefs.addObserver( + "devtools.source-map.client-service.enabled", + this._prefChanged + ); + gDevTools.on("theme-registered", this._themeRegistered); + gDevTools.on("theme-unregistered", this._themeUnregistered); + + // Refresh the tools list when a new tool or webextension has been + // registered to the toolbox. + this.toolbox.on("tool-registered", this.setupToolsList); + this.toolbox.on("webextension-registered", this.setupToolsList); + // Refresh the tools list when a new tool or webextension has been + // unregistered from the toolbox. + this.toolbox.on("tool-unregistered", this.setupToolsList); + this.toolbox.on("webextension-unregistered", this.setupToolsList); + }, + + _removeListeners() { + Services.prefs.removeObserver("devtools.cache.disabled", this._prefChanged); + Services.prefs.removeObserver("devtools.theme", this._prefChanged); + Services.prefs.removeObserver( + "devtools.source-map.client-service.enabled", + this._prefChanged + ); + + this.toolbox.off("tool-registered", this.setupToolsList); + this.toolbox.off("tool-unregistered", this.setupToolsList); + this.toolbox.off("webextension-registered", this.setupToolsList); + this.toolbox.off("webextension-unregistered", this.setupToolsList); + + gDevTools.off("theme-registered", this._themeRegistered); + gDevTools.off("theme-unregistered", this._themeUnregistered); + }, + + _prefChanged(subject, topic, prefName) { + if (prefName === "devtools.cache.disabled") { + const cacheDisabled = GetPref(prefName); + const cbx = this.panelDoc.getElementById("devtools-disable-cache"); + cbx.checked = cacheDisabled; + } else if (prefName === "devtools.theme") { + this.updateCurrentTheme(); + } else if (prefName === "devtools.source-map.client-service.enabled") { + this.updateSourceMapPref(); + } + }, + + _themeRegistered(themeId) { + this.setupThemeList(); + }, + + _themeUnregistered(theme) { + const themeBox = this.panelDoc.getElementById("devtools-theme-box"); + const themeInput = themeBox.querySelector(`[value=${theme.id}]`); + + if (themeInput) { + themeInput.parentNode.remove(); + } + }, + + async setupToolbarButtonsList() { + // Ensure the toolbox is open, and the buttons are all set up. + await this.toolbox.isOpen; + + const enabledToolbarButtonsBox = this.panelDoc.getElementById( + "enabled-toolbox-buttons-box" + ); + + const toolbarButtons = this.toolbox.toolbarButtons; + + if (!toolbarButtons) { + console.warn("The command buttons weren't initiated yet."); + return; + } + + const onCheckboxClick = checkbox => { + const commandButton = toolbarButtons.filter( + toggleableButton => toggleableButton.id === checkbox.id + )[0]; + + Services.prefs.setBoolPref( + commandButton.visibilityswitch, + checkbox.checked + ); + this.toolbox.updateToolboxButtonsVisibility(); + }; + + const createCommandCheckbox = button => { + const checkboxLabel = this.panelDoc.createElement("label"); + const checkboxSpanLabel = this.panelDoc.createElement("span"); + checkboxSpanLabel.textContent = button.description; + const checkboxInput = this.panelDoc.createElement("input"); + checkboxInput.setAttribute("type", "checkbox"); + checkboxInput.setAttribute("id", button.id); + + if (Services.prefs.getBoolPref(button.visibilityswitch, true)) { + checkboxInput.setAttribute("checked", true); + } + checkboxInput.addEventListener( + "change", + onCheckboxClick.bind(this, checkboxInput) + ); + + checkboxLabel.appendChild(checkboxInput); + checkboxLabel.appendChild(checkboxSpanLabel); + + return checkboxLabel; + }; + + for (const button of toolbarButtons) { + if (!button.isToolSupported(this.toolbox)) { + continue; + } + + enabledToolbarButtonsBox.appendChild(createCommandCheckbox(button)); + } + }, + + setupToolsList() { + const defaultToolsBox = this.panelDoc.getElementById("default-tools-box"); + const additionalToolsBox = this.panelDoc.getElementById( + "additional-tools-box" + ); + const toolsNotSupportedLabel = this.panelDoc.getElementById( + "tools-not-supported-label" + ); + let atleastOneToolNotSupported = false; + + // Signal tool registering/unregistering globally (for the tools registered + // globally) and per toolbox (for the tools registered to a single toolbox). + // This event handler expect this to be binded to the related checkbox element. + const onCheckboxClick = function (telemetry, tool) { + // Set the kill switch pref boolean to true + Services.prefs.setBoolPref(tool.visibilityswitch, this.checked); + + if (!tool.isWebExtension) { + gDevTools.emit( + this.checked ? "tool-registered" : "tool-unregistered", + tool.id + ); + // Record which tools were registered and unregistered. + telemetry.keyedScalarSet( + "devtools.tool.registered", + tool.id, + this.checked + ); + } + }; + + const createToolCheckbox = tool => { + const checkboxLabel = this.panelDoc.createElement("label"); + const checkboxInput = this.panelDoc.createElement("input"); + checkboxInput.setAttribute("type", "checkbox"); + checkboxInput.setAttribute("id", tool.id); + checkboxInput.setAttribute("title", tool.tooltip || ""); + + const checkboxSpanLabel = this.panelDoc.createElement("span"); + if (tool.isToolSupported(this.toolbox)) { + checkboxSpanLabel.textContent = tool.label; + } else { + atleastOneToolNotSupported = true; + checkboxSpanLabel.textContent = L10N.getFormatStr( + "options.toolNotSupportedMarker", + tool.label + ); + checkboxInput.setAttribute("data-unsupported", "true"); + checkboxInput.setAttribute("disabled", "true"); + } + + if (InfallibleGetBoolPref(tool.visibilityswitch)) { + checkboxInput.setAttribute("checked", "true"); + } + + checkboxInput.addEventListener( + "change", + onCheckboxClick.bind(checkboxInput, this.telemetry, tool) + ); + + checkboxLabel.appendChild(checkboxInput); + checkboxLabel.appendChild(checkboxSpanLabel); + + // We shouldn't have deprecated tools anymore, but we might have one in the future, + // when migrating the storage inspector to the application panel (Bug 1681059). + // Let's keep this code for now so we keep the l10n property around and avoid + // unnecessary translation work if we need it again in the future. + if (tool.deprecated) { + const deprecationURL = this.panelDoc.createElement("a"); + deprecationURL.title = deprecationURL.href = tool.deprecationURL; + deprecationURL.textContent = L10N.getStr("options.deprecationNotice"); + // Cannot use a real link when we are in the Browser Toolbox. + deprecationURL.addEventListener("click", e => { + e.preventDefault(); + openDocLink(tool.deprecationURL, { relatedToCurrent: true }); + }); + + const checkboxSpanDeprecated = this.panelDoc.createElement("span"); + checkboxSpanDeprecated.className = "deprecation-notice"; + checkboxLabel.appendChild(checkboxSpanDeprecated); + checkboxSpanDeprecated.appendChild(deprecationURL); + } + + return checkboxLabel; + }; + + // Clean up any existent default tools content. + for (const label of defaultToolsBox.querySelectorAll("label")) { + label.remove(); + } + + // Populating the default tools lists + const toggleableTools = gDevTools.getDefaultTools().filter(tool => { + return tool.visibilityswitch && !tool.hiddenInOptions; + }); + + const fragment = this.panelDoc.createDocumentFragment(); + for (const tool of toggleableTools) { + fragment.appendChild(createToolCheckbox(tool)); + } + + const toolsNotSupportedLabelNode = this.panelDoc.getElementById( + "tools-not-supported-label" + ); + defaultToolsBox.insertBefore(fragment, toolsNotSupportedLabelNode); + + // Clean up any existent additional tools content. + for (const label of additionalToolsBox.querySelectorAll("label")) { + label.remove(); + } + + // Populating the additional tools list. + let atleastOneAddon = false; + for (const tool of gDevTools.getAdditionalTools()) { + atleastOneAddon = true; + additionalToolsBox.appendChild(createToolCheckbox(tool)); + } + + // Populating the additional tools that came from the installed WebExtension add-ons. + for (const { uuid, name, pref } of this.toolbox.listWebExtensions()) { + atleastOneAddon = true; + + additionalToolsBox.appendChild( + createToolCheckbox({ + isWebExtension: true, + + // Use the preference as the unified webextensions tool id. + id: `webext-${uuid}`, + tooltip: name, + label: name, + // Disable the devtools extension using the given pref name: + // the toolbox options for the WebExtensions are not related to a single + // tool (e.g. a devtools panel created from the extension devtools_page) + // but to the entire devtools part of a webextension which is enabled + // by the Addon Manager (but it may be disabled by its related + // devtools about:config preference), and so the following + visibilityswitch: pref, + + // Only local tabs are currently supported as targets. + isToolSupported: toolbox => + toolbox.commands.descriptorFront.isLocalTab, + }) + ); + } + + if (!atleastOneAddon) { + additionalToolsBox.style.display = "none"; + } else { + additionalToolsBox.style.display = ""; + } + + if (!atleastOneToolNotSupported) { + toolsNotSupportedLabel.style.display = "none"; + } else { + toolsNotSupportedLabel.style.display = ""; + } + + this.panelWin.focus(); + }, + + setupThemeList() { + const themeBox = this.panelDoc.getElementById("devtools-theme-box"); + const themeLabels = themeBox.querySelectorAll("label"); + for (const label of themeLabels) { + label.remove(); + } + + const createThemeOption = theme => { + const inputLabel = this.panelDoc.createElement("label"); + const inputRadio = this.panelDoc.createElement("input"); + inputRadio.setAttribute("type", "radio"); + inputRadio.setAttribute("value", theme.id); + inputRadio.setAttribute("name", "devtools-theme-item"); + inputRadio.addEventListener("change", function (e) { + SetPref(themeBox.getAttribute("data-pref"), e.target.value); + }); + + const inputSpanLabel = this.panelDoc.createElement("span"); + inputSpanLabel.textContent = theme.label; + inputLabel.appendChild(inputRadio); + inputLabel.appendChild(inputSpanLabel); + + return inputLabel; + }; + + // Populating the default theme list + themeBox.appendChild( + createThemeOption({ + id: "auto", + label: L10N.getStr("options.autoTheme.label"), + }) + ); + + const themes = gDevTools.getThemeDefinitionArray(); + for (const theme of themes) { + themeBox.appendChild(createThemeOption(theme)); + } + + this.updateCurrentTheme(); + }, + + /** + * Add extra checkbox options bound to a boolean preference. + */ + setupAdditionalOptions() { + const prefDefinitions = [ + { + pref: "devtools.custom-formatters.enabled", + l10nLabelId: "options-enable-custom-formatters-label", + l10nTooltipId: "options-enable-custom-formatters-tooltip", + id: "devtools-custom-formatters", + parentId: "context-options", + }, + ]; + + const createPreferenceOption = ({ + pref, + label, + l10nLabelId, + l10nTooltipId, + id, + onChange, + }) => { + const inputLabel = this.panelDoc.createElement("label"); + if (l10nTooltipId) { + this.panelDoc.l10n.setAttributes(inputLabel, l10nTooltipId); + } + const checkbox = this.panelDoc.createElement("input"); + checkbox.setAttribute("type", "checkbox"); + if (GetPref(pref)) { + checkbox.setAttribute("checked", "checked"); + } + checkbox.setAttribute("id", id); + checkbox.addEventListener("change", e => { + SetPref(pref, e.target.checked); + if (onChange) { + onChange(e.target.checked); + } + }); + + const inputSpanLabel = this.panelDoc.createElement("span"); + if (l10nLabelId) { + this.panelDoc.l10n.setAttributes(inputSpanLabel, l10nLabelId); + } else if (label) { + inputSpanLabel.textContent = label; + } + inputLabel.appendChild(checkbox); + inputLabel.appendChild(inputSpanLabel); + + return inputLabel; + }; + + for (const prefDefinition of prefDefinitions) { + const parent = this.panelDoc.getElementById(prefDefinition.parentId); + // We want to insert the new definition after the last existing + // definition, but before any other element. + // For example in the "Advanced Settings" column there's indeed a + // text at the end, and we want that it stays at the end. + // The reference element can be `null` if there's no label or if there's + // no element after the last label. But that's OK and it will do what we + // want. + const referenceElement = parent.querySelector("label:last-of-type + *"); + parent.insertBefore( + createPreferenceOption(prefDefinition), + referenceElement + ); + } + }, + + async populatePreferences() { + const prefCheckboxes = this.panelDoc.querySelectorAll( + "input[type=checkbox][data-pref]" + ); + for (const prefCheckbox of prefCheckboxes) { + if (GetPref(prefCheckbox.getAttribute("data-pref"))) { + prefCheckbox.setAttribute("checked", true); + } + prefCheckbox.addEventListener("change", function (e) { + const checkbox = e.target; + SetPref(checkbox.getAttribute("data-pref"), checkbox.checked); + }); + } + // Themes radio inputs are handled in setupThemeList + const prefRadiogroups = this.panelDoc.querySelectorAll( + ".radiogroup[data-pref]:not(#devtools-theme-box)" + ); + for (const radioGroup of prefRadiogroups) { + const selectedValue = GetPref(radioGroup.getAttribute("data-pref")); + + for (const radioInput of radioGroup.querySelectorAll( + "input[type=radio]" + )) { + if (radioInput.getAttribute("value") == selectedValue) { + radioInput.setAttribute("checked", true); + } + + radioInput.addEventListener("change", function (e) { + SetPref(radioGroup.getAttribute("data-pref"), e.target.value); + }); + } + } + const prefSelects = this.panelDoc.querySelectorAll("select[data-pref]"); + for (const prefSelect of prefSelects) { + const pref = GetPref(prefSelect.getAttribute("data-pref")); + const options = [...prefSelect.options]; + options.some(function (option) { + const value = option.value; + // non strict check to allow int values. + if (value == pref) { + prefSelect.selectedIndex = options.indexOf(option); + return true; + } + return false; + }); + + prefSelect.addEventListener("change", function (e) { + const select = e.target; + SetPref( + select.getAttribute("data-pref"), + select.options[select.selectedIndex].value + ); + }); + } + + if (this.commands.descriptorFront.isTabDescriptor) { + const isJavascriptEnabled = + await this.commands.targetConfigurationCommand.isJavascriptEnabled(); + this.disableJSNode.checked = !isJavascriptEnabled; + this.disableJSNode.addEventListener("click", this._disableJSClicked); + } else { + // Hide the checkbox and label + this.disableJSNode.parentNode.style.display = "none"; + + const triggersPageRefreshLabel = this.panelDoc.getElementById( + "triggers-page-refresh-label" + ); + triggersPageRefreshLabel.style.display = "none"; + } + }, + + updateCurrentTheme() { + const currentTheme = GetPref("devtools.theme"); + const themeBox = this.panelDoc.getElementById("devtools-theme-box"); + const themeRadioInput = themeBox.querySelector(`[value=${currentTheme}]`); + + if (themeRadioInput) { + themeRadioInput.checked = true; + } else { + // If the current theme does not exist anymore, switch to auto theme + const autoThemeInputRadio = themeBox.querySelector("[value=auto]"); + autoThemeInputRadio.checked = true; + } + }, + + updateSourceMapPref() { + const prefName = "devtools.source-map.client-service.enabled"; + const enabled = GetPref(prefName); + const box = this.panelDoc.querySelector(`[data-pref="${prefName}"]`); + box.checked = enabled; + }, + + /** + * Disables JavaScript for the currently loaded tab. We force a page refresh + * here because setting browsingContext.allowJavascript to true fails to block + * JS execution from event listeners added using addEventListener(), AJAX + * calls and timers. The page refresh prevents these things from being added + * in the first place. + * + * @param {Event} event + * The event sent by checking / unchecking the disable JS checkbox. + */ + _disableJSClicked(event) { + const checked = event.target.checked; + + this.commands.targetConfigurationCommand.updateConfiguration({ + javascriptEnabled: !checked, + }); + }, + + destroy() { + if (this.destroyed) { + return; + } + this.destroyed = true; + + this._removeListeners(); + + this.disableJSNode.removeEventListener("click", this._disableJSClicked); + + this.panelWin = this.panelDoc = this.disableJSNode = this.toolbox = null; + }, +}; -- cgit v1.2.3