diff options
Diffstat (limited to 'devtools/client/framework/toolbox-options.js')
-rw-r--r-- | devtools/client/framework/toolbox-options.js | 640 |
1 files changed, 640 insertions, 0 deletions
diff --git a/devtools/client/framework/toolbox-options.js b/devtools/client/framework/toolbox-options.js new file mode 100644 index 0000000000..73b4f7e4bd --- /dev/null +++ b/devtools/client/framework/toolbox-options.js @@ -0,0 +1,640 @@ +/* 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 Services = require("Services"); +const { gDevTools } = require("devtools/client/framework/devtools"); + +const { LocalizationHelper } = require("devtools/shared/l10n"); +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +loader.lazyRequireGetter( + this, + "AppConstants", + "resource://gre/modules/AppConstants.jsm", + true +); +loader.lazyRequireGetter( + this, + "openDocLink", + "devtools/client/shared/link", + 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) { + this.panelDoc = iframeWindow.document; + this.panelWin = iframeWindow; + + this.toolbox = toolbox; + this.telemetry = toolbox.telemetry; + this.isReady = false; + + 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("devtools/shared/event-emitter"); + EventEmitter.decorate(this); +} + +OptionsPanel.prototype = { + get target() { + return this.toolbox.target; + }, + + async open() { + this.setupToolsList(); + this.setupToolbarButtonsList(); + this.setupThemeList(); + this.setupAdditionalOptions(); + await this.populatePreferences(); + this.isReady = true; + this.emit("ready"); + return this; + }, + + _addListeners: function() { + 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: function() { + 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: function(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: function(themeId) { + this.setupThemeList(); + }, + + _themeUnregistered: function(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.isTargetSupported(this.toolbox.target)) { + continue; + } + + enabledToolbarButtonsBox.appendChild(createCommandCheckbox(button)); + } + }, + + setupToolsList: function() { + 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.isTargetSupported(this.target)) { + 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. + isTargetSupported: target => target.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: function() { + 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 + 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: function() { + const prefDefinitions = []; + + const isNightly = AppConstants.NIGHTLY_BUILD; + if (isNightly) { + // Labels are hardcoded in english because this checkbox is Nightly only. + prefDefinitions.push({ + pref: "devtools.performance.new-panel-enabled", + label: "Enable new performance recorder (then re-open DevTools)", + id: "devtools-new-performance", + parentId: "context-options", + }); + } + + if (this.target.isParentProcess) { + // The Multiprocess Browser Toolbox is only displayed in the settings + // panel for the Browser Toolbox, or when debugging the main process in + // remote debugging. + prefDefinitions.push({ + pref: "devtools.browsertoolbox.fission", + label: L10N.getStr("options.enableMultiProcessToolbox"), + id: "devtools-browsertoolbox-fission", + parentId: "context-options", + // createPreferenceOption already updates the value of the preference + // for the current profile when the checkbox changes. Here we need a + // custom behavior for the Browser Toolbox, so we pass an additional + // onChange callback. + onChange: async checked => { + if (!this.toolbox.isBrowserToolbox()) { + // If we are debugging a parent process, but the toolbox is not a + // Browser Toolbox, it means we are remote debugging another + // browser. In this case, the value of devtools.browsertoolbox.fission + // should not be updated in the target browser. + return; + } + + // When setting this preference from the BrowserToolbox, we need to + // update the preference on the debugged Firefox profile as well. + // The devtools.browsertoolbox.fission preference is copied from the + // regular Firefox Profile to the Browser Toolbox profile. + // If the preference is not updated on the regular Firefox profile, the + // new value will be lost on the next Browser Toolbox restart. + const { mainRoot } = this.target.client; + const preferenceFront = await mainRoot.getFront("preference"); + preferenceFront.setBoolPref( + "devtools.browsertoolbox.fission", + checked + ); + }, + }); + } + + const createPreferenceOption = ({ pref, label, id, onChange }) => { + const inputLabel = this.panelDoc.createElement("label"); + 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"); + 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 <span> + // 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.target.chrome) { + this.disableJSNode.checked = !this.target.configureOptions + .javascriptEnabled; + 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: function() { + 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 light theme + const lightThemeInputRadio = themeBox.querySelector("[value=light]"); + lightThemeInputRadio.checked = true; + } + }, + + updateSourceMapPref: function() { + 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 docShell.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: function(event) { + const checked = event.target.checked; + + const options = { + javascriptEnabled: !checked, + }; + + this.target.reconfigure({ options }); + }, + + destroy: function() { + if (this.destroyed) { + return; + } + this.destroyed = true; + + this._removeListeners(); + + this.disableJSNode.removeEventListener("click", this._disableJSClicked); + + this.panelWin = this.panelDoc = this.disableJSNode = this.toolbox = null; + }, +}; |