summaryrefslogtreecommitdiffstats
path: root/devtools/startup
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/startup
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--devtools/startup/AboutDebuggingRegistration.jsm42
-rw-r--r--devtools/startup/AboutDevToolsToolboxRegistration.jsm43
-rw-r--r--devtools/startup/DevToolsShim.jsm332
-rw-r--r--devtools/startup/DevToolsStartup.jsm1339
-rw-r--r--devtools/startup/aboutdevtools/AboutDevToolsRegistration.jsm40
-rw-r--r--devtools/startup/aboutdevtools/aboutdevtools.css189
-rw-r--r--devtools/startup/aboutdevtools/aboutdevtools.js253
-rw-r--r--devtools/startup/aboutdevtools/aboutdevtools.xhtml108
-rw-r--r--devtools/startup/aboutdevtools/components.conf14
-rw-r--r--devtools/startup/aboutdevtools/images/dev-edition-logo.svg251
-rw-r--r--devtools/startup/aboutdevtools/images/external-link.svg7
-rw-r--r--devtools/startup/aboutdevtools/images/feature-console.svg9
-rw-r--r--devtools/startup/aboutdevtools/images/feature-debugger.svg9
-rw-r--r--devtools/startup/aboutdevtools/images/feature-inspector.svg9
-rw-r--r--devtools/startup/aboutdevtools/images/feature-memory.svg9
-rw-r--r--devtools/startup/aboutdevtools/images/feature-network.svg10
-rw-r--r--devtools/startup/aboutdevtools/images/feature-performance.svg14
-rw-r--r--devtools/startup/aboutdevtools/images/feature-responsive.svg14
-rw-r--r--devtools/startup/aboutdevtools/images/feature-storage.svg9
-rw-r--r--devtools/startup/aboutdevtools/images/feature-visualediting.svg9
-rw-r--r--devtools/startup/aboutdevtools/images/otter.svg29
-rw-r--r--devtools/startup/aboutdevtools/moz.build15
-rw-r--r--devtools/startup/aboutdevtools/subscribe.css94
-rw-r--r--devtools/startup/aboutdevtools/subscribe.js158
-rw-r--r--devtools/startup/aboutdevtools/test/.eslintrc.js6
-rw-r--r--devtools/startup/aboutdevtools/test/browser.ini11
-rw-r--r--devtools/startup/aboutdevtools/test/browser_aboutdevtools_closes_page.js25
-rw-r--r--devtools/startup/aboutdevtools/test/browser_aboutdevtools_enables_devtools.js39
-rw-r--r--devtools/startup/aboutdevtools/test/browser_aboutdevtools_focus_owner_tab.js91
-rw-r--r--devtools/startup/aboutdevtools/test/browser_aboutdevtools_reuse_existing.js46
-rw-r--r--devtools/startup/aboutdevtools/test/head.js113
-rw-r--r--devtools/startup/components.conf30
-rw-r--r--devtools/startup/enableDevToolsPopup.inc.xhtml14
-rw-r--r--devtools/startup/jar.mn27
-rw-r--r--devtools/startup/locales/en-US/aboutDevTools.ftl57
-rw-r--r--devtools/startup/locales/en-US/key-shortcuts.properties67
-rw-r--r--devtools/startup/locales/en-US/startup.properties8
-rw-r--r--devtools/startup/locales/jar.mn12
-rw-r--r--devtools/startup/locales/moz.build7
-rw-r--r--devtools/startup/moz.build29
-rw-r--r--devtools/startup/tests/browser/.eslintrc.js6
-rw-r--r--devtools/startup/tests/browser/browser.ini5
-rw-r--r--devtools/startup/tests/browser/browser_shim_disable_devtools.js133
-rw-r--r--devtools/startup/tests/xpcshell/.eslintrc.js5
-rw-r--r--devtools/startup/tests/xpcshell/test_devtools_shim.js214
-rw-r--r--devtools/startup/tests/xpcshell/xpcshell.ini6
46 files changed, 3957 insertions, 0 deletions
diff --git a/devtools/startup/AboutDebuggingRegistration.jsm b/devtools/startup/AboutDebuggingRegistration.jsm
new file mode 100644
index 0000000000..4228c265df
--- /dev/null
+++ b/devtools/startup/AboutDebuggingRegistration.jsm
@@ -0,0 +1,42 @@
+/* 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";
+
+// Register the about:debugging URL, that allows to debug tabs, extensions, workers on
+// the current instance of Firefox or on a remote Firefox.
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { nsIAboutModule } = Ci;
+
+function AboutDebugging() {}
+
+AboutDebugging.prototype = {
+ classDescription: "about:debugging",
+ classID: Components.ID("1060afaf-dc9e-43da-8646-23a2faf48493"),
+ contractID: "@mozilla.org/network/protocol/about;1?what=debugging",
+
+ QueryInterface: ChromeUtils.generateQI([nsIAboutModule]),
+
+ newChannel: function(_, loadInfo) {
+ const chan = Services.io.newChannelFromURIWithLoadInfo(
+ Services.io.newURI("chrome://devtools/content/aboutdebugging/index.html"),
+ loadInfo
+ );
+ chan.owner = Services.scriptSecurityManager.getSystemPrincipal();
+ return chan;
+ },
+
+ getURIFlags: function(uri) {
+ return nsIAboutModule.ALLOW_SCRIPT;
+ },
+
+ getChromeURI: function(_uri) {
+ return Services.io.newURI(
+ "chrome://devtools/content/aboutdebugging/index.html"
+ );
+ },
+};
+
+var EXPORTED_SYMBOLS = ["AboutDebugging"];
diff --git a/devtools/startup/AboutDevToolsToolboxRegistration.jsm b/devtools/startup/AboutDevToolsToolboxRegistration.jsm
new file mode 100644
index 0000000000..b68de6d000
--- /dev/null
+++ b/devtools/startup/AboutDevToolsToolboxRegistration.jsm
@@ -0,0 +1,43 @@
+/* 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";
+
+// Register about:devtools-toolbox which allows to open a devtools toolbox
+// in a Firefox tab or a custom html iframe in browser.html
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { nsIAboutModule } = Ci;
+
+function AboutDevtoolsToolbox() {}
+
+AboutDevtoolsToolbox.prototype = {
+ uri: Services.io.newURI("chrome://devtools/content/framework/toolbox.xhtml"),
+ classDescription: "about:devtools-toolbox",
+ classID: Components.ID("11342911-3135-45a8-8d71-737a2b0ad469"),
+ contractID: "@mozilla.org/network/protocol/about;1?what=devtools-toolbox",
+
+ QueryInterface: ChromeUtils.generateQI([nsIAboutModule]),
+
+ newChannel: function(uri, loadInfo) {
+ const chan = Services.io.newChannelFromURIWithLoadInfo(this.uri, loadInfo);
+ chan.owner = Services.scriptSecurityManager.getSystemPrincipal();
+ return chan;
+ },
+
+ getURIFlags: function(uri) {
+ return (
+ nsIAboutModule.ALLOW_SCRIPT |
+ nsIAboutModule.ENABLE_INDEXED_DB |
+ nsIAboutModule.HIDE_FROM_ABOUTABOUT
+ );
+ },
+
+ getChromeURI: function(_uri) {
+ return this.uri;
+ },
+};
+
+var EXPORTED_SYMBOLS = ["AboutDevtoolsToolbox"];
diff --git a/devtools/startup/DevToolsShim.jsm b/devtools/startup/DevToolsShim.jsm
new file mode 100644
index 0000000000..4d14002b3e
--- /dev/null
+++ b/devtools/startup/DevToolsShim.jsm
@@ -0,0 +1,332 @@
+/* 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 } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyGetter(this, "DevtoolsStartup", () => {
+ return Cc["@mozilla.org/devtools/startup-clh;1"].getService(
+ Ci.nsICommandLineHandler
+ ).wrappedJSObject;
+});
+
+// We don't want to spend time initializing the full loader here so we create
+// our own lazy require.
+XPCOMUtils.defineLazyGetter(this, "Telemetry", function() {
+ const { require } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+ );
+ // eslint-disable-next-line no-shadow
+ const Telemetry = require("devtools/client/shared/telemetry");
+
+ return Telemetry;
+});
+
+const DEVTOOLS_ENABLED_PREF = "devtools.enabled";
+const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
+
+const EXPORTED_SYMBOLS = ["DevToolsShim"];
+
+function removeItem(array, callback) {
+ const index = array.findIndex(callback);
+ if (index >= 0) {
+ array.splice(index, 1);
+ }
+}
+
+/**
+ * DevToolsShim is a singleton that provides a set of helpers to interact with DevTools,
+ * that work whether Devtools are enabled or not.
+ *
+ * It can be used to start listening to devtools events before DevTools are ready. As soon
+ * as DevTools are enabled, the DevToolsShim will forward all the requests received until
+ * then to the real DevTools instance.
+ */
+const DevToolsShim = {
+ _gDevTools: null,
+ listeners: [],
+
+ get telemetry() {
+ if (!this._telemetry) {
+ this._telemetry = new Telemetry();
+ this._telemetry.setEventRecordingEnabled(true);
+ }
+ return this._telemetry;
+ },
+
+ /**
+ * Returns true if DevTools are enabled for the current profile. If devtools are not
+ * enabled, initializing DevTools will open the onboarding page. Some entry points
+ * should no-op in this case.
+ */
+ isEnabled: function() {
+ const enabled = Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF);
+ return enabled && !this.isDisabledByPolicy();
+ },
+
+ /**
+ * Returns true if the devtools are completely disabled and can not be enabled. All
+ * entry points should return without throwing, initDevTools should never be called.
+ */
+ isDisabledByPolicy: function() {
+ return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
+ },
+
+ /**
+ * Check if DevTools have already been initialized.
+ *
+ * @return {Boolean} true if DevTools are initialized.
+ */
+ isInitialized: function() {
+ return !!this._gDevTools;
+ },
+
+ /**
+ * Returns the array of the existing toolboxes. This method is part of the compatibility
+ * layer for webextensions.
+ *
+ * @return {Array<Toolbox>}
+ * An array of toolboxes.
+ */
+ getToolboxes: function() {
+ if (this.isInitialized()) {
+ return this._gDevTools.getToolboxes();
+ }
+
+ return [];
+ },
+
+ /**
+ * Register an instance of gDevTools. Should be called by DevTools during startup.
+ *
+ * @param {DevTools} a devtools instance (from client/framework/devtools)
+ */
+ register: function(gDevTools) {
+ this._gDevTools = gDevTools;
+ this._onDevToolsRegistered();
+ this._gDevTools.emit("devtools-registered");
+ },
+
+ /**
+ * Unregister the current instance of gDevTools. Should be called by DevTools during
+ * shutdown.
+ */
+ unregister: function() {
+ if (this.isInitialized()) {
+ this._gDevTools.emit("devtools-unregistered");
+ this._gDevTools = null;
+ }
+ },
+
+ /**
+ * The following methods can be called before DevTools are initialized:
+ * - on
+ * - off
+ *
+ * If DevTools are not initialized when calling the method, DevToolsShim will call the
+ * appropriate method as soon as a gDevTools instance is registered.
+ */
+
+ /**
+ * This method is used by browser/components/extensions/ext-devtools.js for the events:
+ * - toolbox-created
+ * - toolbox-destroyed
+ */
+ on: function(event, listener) {
+ if (this.isInitialized()) {
+ this._gDevTools.on(event, listener);
+ } else {
+ this.listeners.push([event, listener]);
+ }
+ },
+
+ /**
+ * This method is currently only used by devtools code, but is kept here for consistency
+ * with on().
+ */
+ off: function(event, listener) {
+ if (this.isInitialized()) {
+ this._gDevTools.off(event, listener);
+ } else {
+ removeItem(this.listeners, ([e, l]) => e === event && l === listener);
+ }
+ },
+
+ /**
+ * Called from SessionStore.jsm in mozilla-central when saving the current state.
+ *
+ * @param {Object} state
+ * A SessionStore state object that gets modified by reference
+ */
+ saveDevToolsSession: function(state) {
+ if (!this.isInitialized()) {
+ return;
+ }
+
+ this._gDevTools.saveDevToolsSession(state);
+ },
+
+ /**
+ * Called from SessionStore.jsm in mozilla-central when restoring a previous session.
+ * Will always be called, even if the session does not contain DevTools related items.
+ */
+ restoreDevToolsSession: function(session) {
+ if (!this.isEnabled()) {
+ return;
+ }
+
+ const { browserConsole, browserToolbox } = session;
+ const hasDevToolsData = browserConsole || browserToolbox;
+ if (!hasDevToolsData) {
+ // Do not initialize DevTools unless there is DevTools specific data in the session.
+ return;
+ }
+
+ this.initDevTools("SessionRestore");
+ this._gDevTools.restoreDevToolsSession(session);
+ },
+
+ /**
+ * Called from nsContextMenu.js in mozilla-central when using the Inspect Accessibility
+ * context menu item.
+ *
+ * @param {XULTab} tab
+ * The browser tab on which inspect accessibility was used.
+ * @param {ElementIdentifier} domReference
+ * Identifier generated by ContentDOMReference. It is a unique pair of
+ * BrowsingContext ID and a numeric ID.
+ * @return {Promise} a promise that resolves when the accessible node is selected in the
+ * accessibility inspector or that resolves immediately if DevTools are not
+ * enabled.
+ */
+ inspectA11Y: function(tab, domReference) {
+ if (!this.isEnabled()) {
+ if (!this.isDisabledByPolicy()) {
+ DevtoolsStartup.openInstallPage("ContextMenu");
+ }
+ return Promise.resolve();
+ }
+
+ // Record the timing at which this event started in order to compute later in
+ // gDevTools.showToolbox, the complete time it takes to open the toolbox.
+ // i.e. especially take `DevtoolsStartup.initDevTools` into account.
+ const startTime = Cu.now();
+
+ this.initDevTools("ContextMenu");
+
+ return this._gDevTools.inspectA11Y(tab, domReference, startTime);
+ },
+
+ /**
+ * Called from nsContextMenu.js in mozilla-central when using the Inspect Element
+ * context menu item.
+ *
+ * @param {XULTab} tab
+ * The browser tab on which inspect node was used.
+ * @param {ElementIdentifier} domReference
+ * Identifier generated by ContentDOMReference. It is a unique pair of
+ * BrowsingContext ID and a numeric ID.
+ * @return {Promise} a promise that resolves when the node is selected in the inspector
+ * markup view or that resolves immediately if DevTools are not enabled.
+ */
+ inspectNode: function(tab, domReference) {
+ if (!this.isEnabled()) {
+ if (!this.isDisabledByPolicy()) {
+ DevtoolsStartup.openInstallPage("ContextMenu");
+ }
+ return Promise.resolve();
+ }
+
+ // Record the timing at which this event started in order to compute later in
+ // gDevTools.showToolbox, the complete time it takes to open the toolbox.
+ // i.e. especially take `DevtoolsStartup.initDevTools` into account.
+ const startTime = Cu.now();
+
+ this.initDevTools("ContextMenu");
+
+ return this._gDevTools.inspectNode(tab, domReference, startTime);
+ },
+
+ _onDevToolsRegistered: function() {
+ // Register all pending event listeners on the real gDevTools object.
+ for (const [event, listener] of this.listeners) {
+ this._gDevTools.on(event, listener);
+ }
+
+ this.listeners = [];
+ },
+
+ /**
+ * Initialize DevTools via DevToolsStartup if needed. This method throws if DevTools are
+ * not enabled.. If the entry point is supposed to trigger the onboarding, call it
+ * explicitly via DevtoolsStartup.openInstallPage().
+ *
+ * @param {String} reason
+ * optional, if provided should be a valid entry point for DEVTOOLS_ENTRY_POINT
+ * in toolkit/components/telemetry/Histograms.json
+ */
+ initDevTools: function(reason) {
+ if (!this.isEnabled()) {
+ throw new Error("DevTools are not enabled and can not be initialized.");
+ }
+
+ if (reason) {
+ const window = Services.wm.getMostRecentWindow("navigator:browser");
+
+ this.telemetry.addEventProperty(
+ window,
+ "open",
+ "tools",
+ null,
+ "shortcut",
+ ""
+ );
+ this.telemetry.addEventProperty(
+ window,
+ "open",
+ "tools",
+ null,
+ "entrypoint",
+ reason
+ );
+ }
+
+ if (!this.isInitialized()) {
+ DevtoolsStartup.initDevTools(reason);
+ }
+ },
+};
+
+/**
+ * Compatibility layer for webextensions.
+ *
+ * Those methods are called only after a DevTools webextension was loaded in DevTools,
+ * therefore DevTools should always be available when they are called.
+ */
+const webExtensionsMethods = [
+ "createDescriptorForTab",
+ "createWebExtensionInspectedWindowFront",
+ "getTargetForTab",
+ "getTheme",
+ "openBrowserConsole",
+];
+
+for (const method of webExtensionsMethods) {
+ DevToolsShim[method] = function() {
+ if (!this.isEnabled()) {
+ throw new Error(
+ "Could not call a DevToolsShim webextension method ('" +
+ method +
+ "'): DevTools are not initialized."
+ );
+ }
+
+ this.initDevTools();
+ return this._gDevTools[method].apply(this._gDevTools, arguments);
+ };
+}
diff --git a/devtools/startup/DevToolsStartup.jsm b/devtools/startup/DevToolsStartup.jsm
new file mode 100644
index 0000000000..2c0525c041
--- /dev/null
+++ b/devtools/startup/DevToolsStartup.jsm
@@ -0,0 +1,1339 @@
+/* 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/. */
+
+/**
+ * This XPCOM component is loaded very early.
+ * Be careful to lazy load dependencies as much as possible.
+ *
+ * It manages all the possible entry points for DevTools:
+ * - Handles command line arguments like -jsconsole,
+ * - Register all key shortcuts,
+ * - Listen for "Web Developer" system menu opening, under "Tools",
+ * - Inject the wrench icon in toolbar customization, which is used
+ * by the "Web Developer" list displayed in the hamburger menu,
+ * - Register the JSON Viewer protocol handler.
+ * - Inject the profiler recording button in toolbar customization.
+ *
+ * Only once any of these entry point is fired, this module ensures starting
+ * core modules like 'devtools-browser.js' that hooks the browser windows
+ * and ensure setting up tools.
+ **/
+
+"use strict";
+
+const kDebuggerPrefs = [
+ "devtools.debugger.remote-enabled",
+ "devtools.chrome.enabled",
+];
+
+const DEVTOOLS_ENABLED_PREF = "devtools.enabled";
+const DEVTOOLS_F12_DISABLED_PREF = "devtools.experiment.f12.shortcut_disabled";
+
+const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "Services",
+ "resource://gre/modules/Services.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "AppConstants",
+ "resource://gre/modules/AppConstants.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "CustomizableUI",
+ "resource:///modules/CustomizableUI.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "CustomizableWidgets",
+ "resource:///modules/CustomizableWidgets.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "ProfilerMenuButton",
+ "resource://devtools/client/performance-new/popup/menu-button.jsm.js"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "WebChannel",
+ "resource://gre/modules/WebChannel.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PanelMultiView",
+ "resource:///modules/PanelMultiView.jsm"
+);
+
+// We don't want to spend time initializing the full loader here so we create
+// our own lazy require.
+XPCOMUtils.defineLazyGetter(this, "Telemetry", function() {
+ const { require } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+ );
+ // eslint-disable-next-line no-shadow
+ const Telemetry = require("devtools/client/shared/telemetry");
+
+ return Telemetry;
+});
+
+XPCOMUtils.defineLazyGetter(this, "StartupBundle", function() {
+ const url = "chrome://devtools-startup/locale/startup.properties";
+ return Services.strings.createBundle(url);
+});
+
+XPCOMUtils.defineLazyGetter(this, "KeyShortcutsBundle", function() {
+ const url = "chrome://devtools-startup/locale/key-shortcuts.properties";
+ return Services.strings.createBundle(url);
+});
+
+/**
+ * Safely retrieve a localized DevTools key shortcut from KeyShortcutsBundle.
+ * If the shortcut is not available, this will return null. Consumer code
+ * should rely on this to skip unavailable shortcuts.
+ *
+ * Note that all shortcuts should always be available, but there is a notable
+ * exception, which is why we have to do this. When a localization change is
+ * uplifted to beta, language packs will not be updated immediately when the
+ * updated beta is available.
+ *
+ * This means that language pack users might get a new Beta version but will not
+ * have a language pack with the new strings yet.
+ */
+function getLocalizedKeyShortcut(id) {
+ try {
+ return KeyShortcutsBundle.GetStringFromName(id);
+ } catch (e) {
+ console.error("Failed to retrieve DevTools localized shortcut for id", id);
+ return null;
+ }
+}
+
+XPCOMUtils.defineLazyGetter(this, "KeyShortcuts", function() {
+ const isMac = AppConstants.platform == "macosx";
+
+ // Common modifier shared by most key shortcuts
+ const modifiers = isMac ? "accel,alt" : "accel,shift";
+
+ // List of all key shortcuts triggering installation UI
+ // `id` should match tool's id from client/definitions.js
+ const shortcuts = [
+ // The following keys are also registered in /client/menus.js
+ // And should be synced.
+
+ // Both are toggling the toolbox on the last selected panel
+ // or the default one.
+ {
+ id: "toggleToolbox",
+ shortcut: getLocalizedKeyShortcut("toggleToolbox.commandkey"),
+ modifiers,
+ },
+ // All locales are using F12
+ {
+ id: "toggleToolboxF12",
+ shortcut: getLocalizedKeyShortcut("toggleToolboxF12.commandkey"),
+ modifiers: "", // F12 is the only one without modifiers
+ },
+ // Open the Browser Toolbox
+ {
+ id: "browserToolbox",
+ shortcut: getLocalizedKeyShortcut("browserToolbox.commandkey"),
+ modifiers: "accel,alt,shift",
+ },
+ // Open the Browser Console
+ {
+ id: "browserConsole",
+ shortcut: getLocalizedKeyShortcut("browserConsole.commandkey"),
+ modifiers: "accel,shift",
+ },
+ // Toggle the Responsive Design Mode
+ {
+ id: "responsiveDesignMode",
+ shortcut: getLocalizedKeyShortcut("responsiveDesignMode.commandkey"),
+ modifiers,
+ },
+ // The following keys are also registered in /client/definitions.js
+ // and should be synced.
+
+ // Key for opening the Inspector
+ {
+ toolId: "inspector",
+ shortcut: getLocalizedKeyShortcut("inspector.commandkey"),
+ modifiers,
+ },
+ // Key for opening the Web Console
+ {
+ toolId: "webconsole",
+ shortcut: getLocalizedKeyShortcut("webconsole.commandkey"),
+ modifiers,
+ },
+ // Key for opening the Debugger
+ {
+ toolId: "jsdebugger",
+ shortcut: getLocalizedKeyShortcut("jsdebugger.commandkey2"),
+ modifiers,
+ },
+ // Key for opening the Network Monitor
+ {
+ toolId: "netmonitor",
+ shortcut: getLocalizedKeyShortcut("netmonitor.commandkey"),
+ modifiers,
+ },
+ // Key for opening the Style Editor
+ {
+ toolId: "styleeditor",
+ shortcut: getLocalizedKeyShortcut("styleeditor.commandkey"),
+ modifiers: "shift",
+ },
+ // Key for opening the Performance Panel
+ {
+ toolId: "performance",
+ shortcut: getLocalizedKeyShortcut("performance.commandkey"),
+ modifiers: "shift",
+ },
+ // Key for opening the Storage Panel
+ {
+ toolId: "storage",
+ shortcut: getLocalizedKeyShortcut("storage.commandkey"),
+ modifiers: "shift",
+ },
+ // Key for opening the DOM Panel
+ {
+ toolId: "dom",
+ shortcut: getLocalizedKeyShortcut("dom.commandkey"),
+ modifiers,
+ },
+ // Key for opening the Accessibility Panel
+ {
+ toolId: "accessibility",
+ shortcut: getLocalizedKeyShortcut("accessibilityF12.commandkey"),
+ modifiers: "shift",
+ },
+ ];
+
+ if (isMac) {
+ // Add the extra key command for macOS, so you can open the inspector with cmd+shift+C
+ // like on Chrome DevTools.
+ shortcuts.push({
+ id: "inspectorMac",
+ toolId: "inspector",
+ shortcut: getLocalizedKeyShortcut("inspector.commandkey"),
+ modifiers: "accel,shift",
+ });
+ }
+
+ if (ProfilerMenuButton.isInNavbar()) {
+ shortcuts.push(...getProfilerKeyShortcuts());
+ }
+
+ return shortcuts;
+});
+
+function getProfilerKeyShortcuts() {
+ return [
+ // Start/stop the profiler
+ {
+ id: "profilerStartStop",
+ shortcut: getLocalizedKeyShortcut("profilerStartStop.commandkey"),
+ modifiers: "control,shift",
+ },
+ // Capture a profile
+ {
+ id: "profilerCapture",
+ shortcut: getLocalizedKeyShortcut("profilerCapture.commandkey"),
+ modifiers: "control,shift",
+ },
+ ];
+}
+
+/**
+ * Validate the URL that will be used for the WebChannel for the profiler.
+ *
+ * @param {string} targetUrl
+ * @returns {string}
+ */
+function validateProfilerWebChannelUrl(targetUrl) {
+ const frontEndUrl = "https://profiler.firefox.com";
+
+ if (targetUrl !== frontEndUrl) {
+ // The user can specify either localhost or deploy previews as well as
+ // the official frontend URL for testing.
+ if (
+ // Allow a test URL.
+ targetUrl === "http://example.com" ||
+ // Allows the following:
+ // "http://localhost:4242"
+ // "http://localhost:4242/"
+ // "http://localhost:3"
+ // "http://localhost:334798455"
+ /^http:\/\/localhost:\d+\/?$/.test(targetUrl) ||
+ // Allows the following:
+ // "https://deploy-preview-1234--perf-html.netlify.com"
+ // "https://deploy-preview-1234--perf-html.netlify.com/"
+ // "https://deploy-preview-1234567--perf-html.netlify.app"
+ // "https://main--perf-html.netlify.app"
+ /^https:\/\/(?:deploy-preview-\d+|main)--perf-html\.netlify\.(?:com|app)\/?$/.test(
+ targetUrl
+ )
+ ) {
+ // This URL is one of the allowed ones to be used for configuration.
+ return targetUrl;
+ }
+
+ console.error(
+ `The preference "devtools.performance.recording.ui-base-url" was set to a ` +
+ "URL that is not allowed. No WebChannel messages will be sent between the " +
+ `browser and that URL. Falling back to ${frontEndUrl}. Only localhost ` +
+ "and deploy previews URLs are allowed.",
+ targetUrl
+ );
+ }
+
+ return frontEndUrl;
+}
+
+XPCOMUtils.defineLazyGetter(this, "ProfilerPopupBackground", function() {
+ return ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/background.jsm.js"
+ );
+});
+
+function DevToolsStartup() {
+ this.onEnabledPrefChanged = this.onEnabledPrefChanged.bind(this);
+ this.onWindowReady = this.onWindowReady.bind(this);
+ this.toggleProfilerKeyShortcuts = this.toggleProfilerKeyShortcuts.bind(this);
+}
+
+DevToolsStartup.prototype = {
+ /**
+ * Boolean flag to check if DevTools have been already initialized or not.
+ * By initialized, we mean that its main modules are loaded.
+ */
+ initialized: false,
+
+ /**
+ * Boolean flag to check if the devtools initialization was already sent to telemetry.
+ * We only want to record one devtools entry point per Firefox run, but we are not
+ * interested in all the entry points.
+ */
+ recorded: false,
+
+ get telemetry() {
+ if (!this._telemetry) {
+ this._telemetry = new Telemetry();
+ this._telemetry.setEventRecordingEnabled(true);
+ }
+ return this._telemetry;
+ },
+
+ /**
+ * Flag that indicates if the developer toggle was already added to customizableUI.
+ */
+ developerToggleCreated: false,
+
+ /**
+ * Flag that indicates if the profiler recording popup was already added to
+ * customizableUI.
+ */
+ profilerRecordingButtonCreated: false,
+
+ isDisabledByPolicy: function() {
+ return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
+ },
+
+ handle: function(cmdLine) {
+ const flags = this.readCommandLineFlags(cmdLine);
+
+ // handle() can be called after browser startup (e.g. opening links from other apps).
+ const isInitialLaunch =
+ cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH;
+ if (isInitialLaunch) {
+ // Enable devtools for all users on startup (onboarding experiment from Bug 1408969
+ // is over).
+ Services.prefs.setBoolPref(DEVTOOLS_ENABLED_PREF, true);
+
+ // The F12 shortcut might be disabled to avoid accidental usage.
+ // Users who are already considered as devtools users should not be
+ // impacted.
+ if (this.isDevToolsUser()) {
+ Services.prefs.setBoolPref(DEVTOOLS_F12_DISABLED_PREF, false);
+ }
+
+ // Store devtoolsFlag to check it later in onWindowReady.
+ this.devtoolsFlag = flags.devtools;
+
+ /* eslint-disable mozilla/balanced-observers */
+ // We are not expecting to remove those listeners until Firefox closes.
+
+ // Only top level Firefox Windows fire a browser-delayed-startup-finished event
+ Services.obs.addObserver(
+ this.onWindowReady,
+ "browser-delayed-startup-finished"
+ );
+
+ // Update menu items when devtools.enabled changes.
+ Services.prefs.addObserver(
+ DEVTOOLS_ENABLED_PREF,
+ this.onEnabledPrefChanged
+ );
+ /* eslint-enable mozilla/balanced-observers */
+
+ if (!this.isDisabledByPolicy()) {
+ if (AppConstants.MOZ_DEV_EDITION) {
+ // On DevEdition, the developer toggle is displayed by default in the navbar
+ // area and should be created before the first paint.
+ this.hookDeveloperToggle();
+ }
+
+ this.hookProfilerRecordingButton();
+ }
+ }
+
+ if (flags.console) {
+ this.commandLine = true;
+ this.handleConsoleFlag(cmdLine);
+ }
+ if (flags.debugger) {
+ this.commandLine = true;
+ const binaryPath =
+ typeof flags.debugger == "string" ? flags.debugger : null;
+ this.handleDebuggerFlag(cmdLine, binaryPath);
+ }
+
+ if (flags.devToolsServer) {
+ this.handleDevToolsServerFlag(cmdLine, flags.devToolsServer);
+ }
+ },
+
+ readCommandLineFlags(cmdLine) {
+ // All command line flags are disabled if DevTools are disabled by policy.
+ if (this.isDisabledByPolicy()) {
+ return {
+ console: false,
+ debugger: false,
+ devtools: false,
+ devToolsServer: false,
+ };
+ }
+
+ const console = cmdLine.handleFlag("jsconsole", false);
+ const devtools = cmdLine.handleFlag("devtools", false);
+
+ let devToolsServer;
+ try {
+ devToolsServer = cmdLine.handleFlagWithParam(
+ "start-debugger-server",
+ false
+ );
+ } catch (e) {
+ // We get an error if the option is given but not followed by a value.
+ // By catching and trying again, the value is effectively optional.
+ devToolsServer = cmdLine.handleFlag("start-debugger-server", false);
+ }
+
+ let debuggerFlag;
+ try {
+ debuggerFlag = cmdLine.handleFlagWithParam("jsdebugger", false);
+ } catch (e) {
+ // We get an error if the option is given but not followed by a value.
+ // By catching and trying again, the value is effectively optional.
+ debuggerFlag = cmdLine.handleFlag("jsdebugger", false);
+ }
+
+ return { console, debugger: debuggerFlag, devtools, devToolsServer };
+ },
+
+ /**
+ * Called when receiving the "browser-delayed-startup-finished" event for a new
+ * top-level window.
+ */
+ onWindowReady(window) {
+ if (this.isDisabledByPolicy()) {
+ this.removeDevToolsMenus(window);
+ return;
+ }
+
+ this.hookWindow(window);
+
+ // This listener is called for all Firefox windows, but we want to execute some code
+ // only once.
+ if (!this._firstWindowReadyReceived) {
+ this.onFirstWindowReady(window);
+ this._firstWindowReadyReceived = true;
+ }
+
+ JsonView.initialize();
+ },
+
+ removeDevToolsMenus(window) {
+ // This will hide the "Tools > Web Developer" menu.
+ window.document
+ .getElementById("webDeveloperMenu")
+ .setAttribute("hidden", "true");
+ // This will hide the "Web Developer" item in the hamburger menu.
+ PanelMultiView.getViewNode(
+ window.document,
+ "appMenu-developer-button"
+ ).setAttribute("hidden", "true");
+ },
+
+ onFirstWindowReady(window) {
+ if (this.devtoolsFlag) {
+ this.handleDevToolsFlag(window);
+
+ // In the case of the --jsconsole and --jsdebugger command line parameters
+ // there was no browser window when they were processed so we act on the
+ // this.commandline flag instead.
+ if (this.commandLine) {
+ this.sendEntryPointTelemetry("CommandLine");
+ }
+ }
+ },
+
+ /**
+ * Register listeners to all possible entry points for Developer Tools.
+ * But instead of implementing the actual actions, defer to DevTools codebase.
+ * In most cases, it only needs to call this.initDevTools which handles the rest.
+ * We do that to prevent loading any DevTools module until the user intent to use them.
+ */
+ hookWindow(window) {
+ // Key Shortcuts need to be added on all the created windows.
+ this.hookKeyShortcuts(window);
+
+ // In some situations (e.g. starting Firefox with --jsconsole) DevTools will be
+ // initialized before the first browser-delayed-startup-finished event is received.
+ // We use a dedicated flag because we still need to hook the developer toggle.
+ this.hookDeveloperToggle();
+ this.hookProfilerRecordingButton();
+
+ // The developer menu hook only needs to be added if devtools have not been
+ // initialized yet.
+ if (!this.initialized) {
+ this.hookWebDeveloperMenu(window);
+ }
+
+ this.createDevToolsEnableMenuItem(window);
+ this.updateDevToolsMenuItems(window);
+ },
+
+ /**
+ * Dynamically register a wrench icon in the customization menu.
+ * You can use this button by right clicking on Firefox toolbar
+ * and dragging it from the customization panel to the toolbar.
+ * (i.e. this isn't displayed by default to users!)
+ *
+ * _But_, the "Web Developer" entry in the hamburger menu (the menu with
+ * 3 horizontal lines), is using this "developer-button" view to populate
+ * its menu. So we have to register this button for the menu to work.
+ *
+ * Also, this menu duplicates its own entries from the "Web Developer"
+ * menu in the system menu, under "Tools" main menu item. The system
+ * menu is being hooked by "hookWebDeveloperMenu" which ends up calling
+ * devtools/client/framework/browser-menus to create the items for real,
+ * initDevTools, from onViewShowing is also calling browser-menu.
+ */
+ hookDeveloperToggle() {
+ if (this.developerToggleCreated) {
+ return;
+ }
+
+ const id = "developer-button";
+ const widget = CustomizableUI.getWidget(id);
+ if (widget && widget.provider == CustomizableUI.PROVIDER_API) {
+ return;
+ }
+ const item = {
+ id: id,
+ type: "view",
+ viewId: "PanelUI-developer",
+ shortcutId: "key_toggleToolbox",
+ tooltiptext: "developer-button.tooltiptext2",
+ onViewShowing: event => {
+ if (Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF)) {
+ // If DevTools are enabled, initialize DevTools to create all menuitems in the
+ // system menu before trying to copy them.
+ this.initDevTools("HamburgerMenu");
+ }
+
+ // Populate the subview with whatever menuitems are in the developer
+ // menu. We skip menu elements, because the menu panel has no way
+ // of dealing with those right now.
+ const doc = event.target.ownerDocument;
+
+ const menu = doc.getElementById("menuWebDeveloperPopup");
+
+ const itemsToDisplay = [...menu.children];
+ // Hardcode the addition of the "work offline" menuitem at the bottom:
+ itemsToDisplay.push({
+ localName: "menuseparator",
+ getAttribute: () => {},
+ });
+ itemsToDisplay.push(doc.getElementById("goOfflineMenuitem"));
+
+ const developerItems = PanelMultiView.getViewNode(
+ doc,
+ "PanelUI-developerItems"
+ );
+ CustomizableUI.clearSubview(developerItems);
+ CustomizableUI.fillSubviewFromMenuItems(itemsToDisplay, developerItems);
+ },
+ onInit(anchor) {
+ // Since onBeforeCreated already bails out when initialized, we can call
+ // it right away.
+ this.onBeforeCreated(anchor.ownerDocument);
+ },
+ onBeforeCreated: doc => {
+ // The developer toggle needs the "key_toggleToolbox" <key> element.
+ // In DEV EDITION, the toggle is added before 1st paint and hookKeyShortcuts() is
+ // not called yet when CustomizableUI creates the widget.
+ this.hookKeyShortcuts(doc.defaultView);
+
+ if (PanelMultiView.getViewNode(doc, "PanelUI-developerItems")) {
+ return;
+ }
+ const view = doc.createXULElement("panelview");
+ view.id = "PanelUI-developerItems";
+ const panel = doc.createXULElement("vbox");
+ panel.setAttribute("class", "panel-subview-body");
+ view.appendChild(panel);
+ doc.getElementById("PanelUI-multiView").appendChild(view);
+ },
+ };
+ CustomizableUI.createWidget(item);
+ CustomizableWidgets.push(item);
+
+ this.developerToggleCreated = true;
+ },
+
+ /**
+ * Register the profiler recording button. This button will be available
+ * in the customization palette for the Firefox toolbar. In addition, it can be
+ * enabled from profiler.firefox.com.
+ */
+ hookProfilerRecordingButton() {
+ if (this.profilerRecordingButtonCreated) {
+ return;
+ }
+ const featureFlagPref = "devtools.performance.popup.feature-flag";
+ const isPopupFeatureFlagEnabled = Services.prefs.getBoolPref(
+ featureFlagPref
+ );
+ this.profilerRecordingButtonCreated = true;
+
+ // Listen for messages from the front-end. This needs to happen even if the
+ // button isn't enabled yet. This will allow the front-end to turn on the
+ // popup for our users, regardless of if the feature is enabled by default.
+ this.initializeProfilerWebChannel();
+
+ if (isPopupFeatureFlagEnabled) {
+ // Initialize the CustomizableUI widget.
+ ProfilerMenuButton.initialize(this.toggleProfilerKeyShortcuts);
+ } else {
+ // The feature flag is not enabled, but watch for it to be enabled. If it is,
+ // initialize everything.
+ const enable = () => {
+ ProfilerMenuButton.initialize(this.toggleProfilerKeyShortcuts);
+ Services.prefs.removeObserver(featureFlagPref, enable);
+ };
+ Services.prefs.addObserver(featureFlagPref, enable);
+ }
+ },
+
+ /**
+ * Initialize the WebChannel for profiler.firefox.com. This function happens at
+ * startup, so care should be taken to minimize its performance impact. The WebChannel
+ * is a mechanism that is used to communicate between the browser, and front-end code.
+ */
+ initializeProfilerWebChannel() {
+ let channel;
+
+ // Register a channel for the URL in preferences. Also update the WebChannel if
+ // the URL changes.
+ const urlPref = "devtools.performance.recording.ui-base-url";
+
+ // This method is only run once per Firefox instance, so it should not be
+ // strictly necessary to remove observers here.
+ // eslint-disable-next-line mozilla/balanced-observers
+ Services.prefs.addObserver(urlPref, registerWebChannel);
+
+ registerWebChannel();
+
+ function registerWebChannel() {
+ if (channel) {
+ channel.stopListening();
+ }
+
+ const urlForWebChannel = Services.io.newURI(
+ validateProfilerWebChannelUrl(Services.prefs.getStringPref(urlPref))
+ );
+
+ channel = new WebChannel("profiler.firefox.com", urlForWebChannel);
+
+ channel.listen((id, message, target) => {
+ // Defer loading the ProfilerPopupBackground script until it's absolutely needed,
+ // as this code path gets loaded at startup.
+ ProfilerPopupBackground.handleWebChannelMessage(
+ channel,
+ id,
+ message,
+ target
+ );
+ });
+ }
+ },
+
+ /*
+ * We listen to the "Web Developer" system menu, which is under "Tools" main item.
+ * This menu item is hardcoded empty in Firefox UI. We listen for its opening to
+ * populate it lazily. Loading main DevTools module is going to populate it.
+ */
+ hookWebDeveloperMenu(window) {
+ const menu = window.document.getElementById("webDeveloperMenu");
+ const onPopupShowing = () => {
+ if (!Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF)) {
+ return;
+ }
+ menu.removeEventListener("popupshowing", onPopupShowing);
+ this.initDevTools("SystemMenu");
+ };
+ menu.addEventListener("popupshowing", onPopupShowing);
+ },
+
+ /**
+ * Create a new menu item to enable DevTools and insert it DevTools's submenu in the
+ * System Menu.
+ */
+ createDevToolsEnableMenuItem(window) {
+ const { document } = window;
+
+ // Create the menu item.
+ const item = document.createXULElement("menuitem");
+ item.id = "enableDeveloperTools";
+ item.setAttribute(
+ "label",
+ StartupBundle.GetStringFromName("enableDevTools.label")
+ );
+ item.setAttribute(
+ "accesskey",
+ StartupBundle.GetStringFromName("enableDevTools.accesskey")
+ );
+
+ // The menu item should open the install page for DevTools.
+ item.addEventListener("command", () => {
+ this.openInstallPage("SystemMenu");
+ });
+
+ // Insert the menu item in the DevTools submenu.
+ const systemMenuItem = document.getElementById("menuWebDeveloperPopup");
+ systemMenuItem.appendChild(item);
+ },
+
+ /**
+ * Update the visibility the menu item to enable DevTools.
+ */
+ updateDevToolsMenuItems(window) {
+ const item = window.document.getElementById("enableDeveloperTools");
+ item.hidden = Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF);
+ },
+
+ /**
+ * Loop on all windows and update the hidden attribute of the "enable DevTools" menu
+ * item.
+ */
+ onEnabledPrefChanged() {
+ for (const window of Services.wm.getEnumerator("navigator:browser")) {
+ if (window.gBrowserInit && window.gBrowserInit.delayedStartupFinished) {
+ this.updateDevToolsMenuItems(window);
+ }
+ }
+ },
+
+ /**
+ * Check if the user is a DevTools user by looking at our selfxss pref.
+ * This preference is incremented everytime the console is used (up to 5).
+ *
+ * @return {Boolean} true if the user can be considered as a devtools user.
+ */
+ isDevToolsUser() {
+ const selfXssCount = Services.prefs.getIntPref("devtools.selfxss.count", 0);
+ return selfXssCount > 0;
+ },
+
+ hookKeyShortcuts(window) {
+ const doc = window.document;
+
+ // hookKeyShortcuts can be called both from hookWindow and from the developer toggle
+ // onBeforeCreated. Make sure shortcuts are only added once per window.
+ if (doc.getElementById("devtoolsKeyset")) {
+ return;
+ }
+
+ const keyset = doc.createXULElement("keyset");
+ keyset.setAttribute("id", "devtoolsKeyset");
+
+ this.attachKeys(doc, KeyShortcuts, keyset);
+
+ // Appending a <key> element is not always enough. The <keyset> needs
+ // to be detached and reattached to make sure the <key> is taken into
+ // account (see bug 832984).
+ const mainKeyset = doc.getElementById("mainKeyset");
+ mainKeyset.parentNode.insertBefore(keyset, mainKeyset);
+ },
+
+ /**
+ * This method attaches on the key elements to the devtools keyset.
+ */
+ attachKeys(doc, keyShortcuts, keyset = doc.getElementById("devtoolsKeyset")) {
+ const window = doc.defaultView;
+ for (const key of keyShortcuts) {
+ if (!key.shortcut) {
+ // Shortcuts might be missing when a user relies on a language packs
+ // which is missing a recently uplifted shortcut. Language packs are
+ // typically updated a few days after a code uplift.
+ continue;
+ }
+ const xulKey = this.createKey(doc, key, () => this.onKey(window, key));
+ keyset.appendChild(xulKey);
+ }
+ },
+
+ /**
+ * This method removes keys from the devtools keyset.
+ */
+ removeKeys(doc, keyShortcuts) {
+ for (const key of keyShortcuts) {
+ const keyElement = doc.getElementById(this.getKeyElementId(key));
+ if (keyElement) {
+ keyElement.remove();
+ }
+ }
+ },
+
+ /**
+ * We only want to have the keyboard shortcuts active when the menu button is on.
+ * This function either adds or removes the elements.
+ * @param {boolean} isEnabled
+ */
+ toggleProfilerKeyShortcuts(isEnabled) {
+ const profilerKeyShortcuts = getProfilerKeyShortcuts();
+ for (const { document } of Services.wm.getEnumerator(null)) {
+ const devtoolsKeyset = document.getElementById("devtoolsKeyset");
+ const mainKeyset = document.getElementById("mainKeyset");
+
+ if (!devtoolsKeyset || !mainKeyset) {
+ // There may not be devtools keyset on this window.
+ continue;
+ }
+
+ const areProfilerKeysPresent = !!document.getElementById(
+ "key_profilerStartStop"
+ );
+ if (isEnabled === areProfilerKeysPresent) {
+ // Don't double add or double remove the shortcuts.
+ continue;
+ }
+ if (isEnabled) {
+ this.attachKeys(document, profilerKeyShortcuts);
+ } else {
+ this.removeKeys(document, profilerKeyShortcuts);
+ }
+ // Appending a <key> element is not always enough. The <keyset> needs
+ // to be detached and reattached to make sure the <key> is taken into
+ // account (see bug 832984).
+ mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
+ }
+ },
+
+ async onKey(window, key) {
+ try {
+ // The profiler doesn't care if DevTools is loaded, so provide a quick check
+ // first to bail out of checking if DevTools is available.
+ switch (key.id) {
+ case "profilerStartStop": {
+ ProfilerPopupBackground.toggleProfiler("aboutprofiling");
+ return;
+ }
+ case "profilerCapture": {
+ ProfilerPopupBackground.captureProfile("aboutprofiling");
+ return;
+ }
+ }
+ if (!Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF)) {
+ const id = key.toolId || key.id;
+ this.openInstallPage("KeyShortcut", id);
+ } else {
+ // Record the timing at which this event started in order to compute later in
+ // gDevTools.showToolbox, the complete time it takes to open the toolbox.
+ // i.e. especially take `initDevTools` into account.
+ const startTime = Cu.now();
+ const require = this.initDevTools("KeyShortcut", key);
+ const {
+ gDevToolsBrowser,
+ } = require("devtools/client/framework/devtools-browser");
+ await gDevToolsBrowser.onKeyShortcut(window, key, startTime);
+ }
+ } catch (e) {
+ console.error(`Exception while trigerring key ${key}: ${e}\n${e.stack}`);
+ }
+ },
+
+ getKeyElementId({ id, toolId }) {
+ return "key_" + (id || toolId);
+ },
+
+ // Create a <xul:key> DOM Element
+ createKey(doc, key, oncommand) {
+ const { shortcut, modifiers: mod } = key;
+ const k = doc.createXULElement("key");
+ k.id = this.getKeyElementId(key);
+
+ if (shortcut.startsWith("VK_")) {
+ k.setAttribute("keycode", shortcut);
+ if (shortcut.match(/^VK_\d$/)) {
+ // Add the event keydown attribute to ensure that shortcuts work for combinations
+ // such as ctrl shift 1.
+ k.setAttribute("event", "keydown");
+ }
+ } else {
+ k.setAttribute("key", shortcut);
+ }
+
+ if (mod) {
+ k.setAttribute("modifiers", mod);
+ }
+
+ // Bug 371900: command event is fired only if "oncommand" attribute is set.
+ k.setAttribute("oncommand", ";");
+ k.addEventListener("command", oncommand);
+
+ return k;
+ },
+
+ initDevTools: function(reason, key = "") {
+ // If an entry point is fired and tools are not enabled open the installation page
+ if (!Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF)) {
+ this.openInstallPage(reason);
+ return null;
+ }
+
+ // In the case of the --jsconsole and --jsdebugger command line parameters
+ // there is no browser window yet so we don't send any telemetry yet.
+ if (reason !== "CommandLine") {
+ this.sendEntryPointTelemetry(reason, key);
+ }
+
+ this.initialized = true;
+ const { require } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+ );
+ // Ensure loading main devtools module that hooks up into browser UI
+ // and initialize all devtools machinery.
+ require("devtools/client/framework/devtools-browser");
+ return require;
+ },
+
+ /**
+ * Open about:devtools to start the onboarding flow.
+ *
+ * @param {String} reason
+ * One of "KeyShortcut", "SystemMenu", "HamburgerMenu", "ContextMenu",
+ * "CommandLine".
+ * @param {String} keyId
+ * Optional. If the onboarding flow was triggered by a keyboard shortcut, pass
+ * the shortcut key id (or toolId) to about:devtools.
+ */
+ openInstallPage: function(reason, keyId) {
+ // If DevTools are completely disabled, bail out here as this might be called directly
+ // from other files.
+ if (this.isDisabledByPolicy()) {
+ return;
+ }
+
+ const { gBrowser } = Services.wm.getMostRecentWindow("navigator:browser");
+
+ // Focus about:devtools tab if there is already one opened in the current window.
+ for (const tab of gBrowser.tabs) {
+ const browser = tab.linkedBrowser;
+ // browser.documentURI might be undefined if the browser tab is still loading.
+ const location = browser.documentURI ? browser.documentURI.spec : "";
+ if (
+ location.startsWith("about:devtools") &&
+ !location.startsWith("about:devtools-toolbox")
+ ) {
+ // Focus the existing about:devtools tab and bail out.
+ gBrowser.selectedTab = tab;
+ return;
+ }
+ }
+
+ let url = "about:devtools";
+
+ const params = [];
+ if (reason) {
+ params.push("reason=" + encodeURIComponent(reason));
+ }
+
+ const selectedBrowser = gBrowser.selectedBrowser;
+ if (selectedBrowser) {
+ params.push("tabid=" + selectedBrowser.outerWindowID);
+ }
+
+ if (keyId) {
+ params.push("keyid=" + keyId);
+ }
+
+ if (params.length > 0) {
+ url += "?" + params.join("&");
+ }
+
+ // Set relatedToCurrent: true to open the tab next to the current one.
+ gBrowser.selectedTab = gBrowser.addTrustedTab(url, {
+ relatedToCurrent: true,
+ });
+ },
+
+ handleConsoleFlag: function(cmdLine) {
+ const window = Services.wm.getMostRecentWindow("devtools:webconsole");
+ if (!window) {
+ const require = this.initDevTools("CommandLine");
+ const {
+ BrowserConsoleManager,
+ } = require("devtools/client/webconsole/browser-console-manager");
+ BrowserConsoleManager.toggleBrowserConsole().catch(console.error);
+ } else {
+ // the Browser Console was already open
+ window.focus();
+ }
+
+ if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
+ cmdLine.preventDefault = true;
+ }
+ },
+
+ // Open the toolbox on the selected tab once the browser starts up.
+ handleDevToolsFlag: async function(window) {
+ const require = this.initDevTools("CommandLine");
+ const { gDevTools } = require("devtools/client/framework/devtools");
+ const { TargetFactory } = require("devtools/client/framework/target");
+ const target = await TargetFactory.forTab(window.gBrowser.selectedTab);
+ gDevTools.showToolbox(target);
+ },
+
+ _isRemoteDebuggingEnabled() {
+ let remoteDebuggingEnabled = false;
+ try {
+ remoteDebuggingEnabled = kDebuggerPrefs.every(pref => {
+ return Services.prefs.getBoolPref(pref);
+ });
+ } catch (ex) {
+ console.error(ex);
+ return false;
+ }
+ if (!remoteDebuggingEnabled) {
+ const errorMsg =
+ "Could not run chrome debugger! You need the following " +
+ "prefs to be set to true: " +
+ kDebuggerPrefs.join(", ");
+ console.error(new Error(errorMsg));
+ // Dump as well, as we're doing this from a commandline, make sure people
+ // don't miss it:
+ dump(errorMsg + "\n");
+ }
+ return remoteDebuggingEnabled;
+ },
+
+ handleDebuggerFlag: function(cmdLine, binaryPath) {
+ if (!this._isRemoteDebuggingEnabled()) {
+ return;
+ }
+
+ let devtoolsThreadResumed = false;
+ const pauseOnStartup = cmdLine.handleFlag("wait-for-jsdebugger", false);
+ if (pauseOnStartup) {
+ const observe = function(subject, topic, data) {
+ devtoolsThreadResumed = true;
+ Services.obs.removeObserver(observe, "devtools-thread-ready");
+ };
+ Services.obs.addObserver(observe, "devtools-thread-ready");
+ }
+
+ const { BrowserToolboxLauncher } = ChromeUtils.import(
+ "resource://devtools/client/framework/browser-toolbox/Launcher.jsm"
+ );
+ BrowserToolboxLauncher.init(null, null, null, binaryPath);
+
+ if (pauseOnStartup) {
+ // Spin the event loop until the debugger connects.
+ const tm = Cc["@mozilla.org/thread-manager;1"].getService();
+ tm.spinEventLoopUntil(() => {
+ return devtoolsThreadResumed;
+ });
+ }
+
+ if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
+ cmdLine.preventDefault = true;
+ }
+ },
+
+ /**
+ * Handle the --start-debugger-server command line flag. The options are:
+ * --start-debugger-server
+ * The portOrPath parameter is boolean true in this case. Reads and uses the defaults
+ * from devtools.debugger.remote-port and devtools.debugger.remote-websocket prefs.
+ * The default values of these prefs are port 6000, WebSocket disabled.
+ *
+ * --start-debugger-server 6789
+ * Start the non-WebSocket server on port 6789.
+ *
+ * --start-debugger-server /path/to/filename
+ * Start the server on a Unix domain socket.
+ *
+ * --start-debugger-server ws:6789
+ * Start the WebSocket server on port 6789.
+ *
+ * --start-debugger-server ws:
+ * Start the WebSocket server on the default port (taken from d.d.remote-port)
+ */
+ handleDevToolsServerFlag: function(cmdLine, portOrPath) {
+ if (!this._isRemoteDebuggingEnabled()) {
+ return;
+ }
+
+ let webSocket = false;
+ const defaultPort = Services.prefs.getIntPref(
+ "devtools.debugger.remote-port"
+ );
+ if (portOrPath === true) {
+ // Default to pref values if no values given on command line
+ webSocket = Services.prefs.getBoolPref(
+ "devtools.debugger.remote-websocket"
+ );
+ portOrPath = defaultPort;
+ } else if (portOrPath.startsWith("ws:")) {
+ webSocket = true;
+ const port = portOrPath.slice(3);
+ portOrPath = Number(port) ? port : defaultPort;
+ }
+
+ const { DevToolsLoader } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+ );
+
+ try {
+ // Create a separate loader instance, so that we can be sure to receive
+ // a separate instance of the DebuggingServer from the rest of the
+ // devtools. This allows us to safely use the tools against even the
+ // actors and DebuggingServer itself, especially since we can mark
+ // serverLoader as invisible to the debugger (unlike the usual loader
+ // settings).
+ const serverLoader = new DevToolsLoader({
+ invisibleToDebugger: true,
+ });
+ const { DevToolsServer: devToolsServer } = serverLoader.require(
+ "devtools/server/devtools-server"
+ );
+ const { SocketListener } = serverLoader.require(
+ "devtools/shared/security/socket"
+ );
+ devToolsServer.init();
+ devToolsServer.registerAllActors();
+ devToolsServer.allowChromeProcess = true;
+ const socketOptions = { portOrPath, webSocket };
+
+ const listener = new SocketListener(devToolsServer, socketOptions);
+ listener.open();
+ dump("Started devtools server on " + portOrPath + "\n");
+ } catch (e) {
+ dump("Unable to start devtools server on " + portOrPath + ": " + e);
+ }
+
+ if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
+ cmdLine.preventDefault = true;
+ }
+ },
+
+ /**
+ * Send entry point telemetry explaining how the devtools were launched. This
+ * functionality also lives inside `devtools/client/framework/browser-menus.js`
+ * because this codepath is only used the first time a toolbox is opened for a
+ * tab.
+ *
+ * @param {String} reason
+ * One of "KeyShortcut", "SystemMenu", "HamburgerMenu", "ContextMenu",
+ * "CommandLine".
+ * @param {String} key
+ * The key used by a key shortcut.
+ */
+ sendEntryPointTelemetry(reason, key = "") {
+ if (!reason) {
+ return;
+ }
+
+ let keys = "";
+
+ if (reason === "KeyShortcut") {
+ let { modifiers, shortcut } = key;
+
+ modifiers = modifiers.replace(",", "+");
+
+ if (shortcut.startsWith("VK_")) {
+ shortcut = shortcut.substr(3);
+ }
+
+ keys = `${modifiers}+${shortcut}`;
+ }
+
+ const window = Services.wm.getMostRecentWindow("navigator:browser");
+
+ this.telemetry.addEventProperty(
+ window,
+ "open",
+ "tools",
+ null,
+ "shortcut",
+ keys
+ );
+ this.telemetry.addEventProperty(
+ window,
+ "open",
+ "tools",
+ null,
+ "entrypoint",
+ reason
+ );
+
+ if (this.recorded) {
+ return;
+ }
+
+ // Only save the first call for each firefox run as next call
+ // won't necessarely start the tool. For example key shortcuts may
+ // only change the currently selected tool.
+ try {
+ this.telemetry.getHistogramById("DEVTOOLS_ENTRY_POINT").add(reason);
+ } catch (e) {
+ dump("DevTools telemetry entry point failed: " + e + "\n");
+ }
+ this.recorded = true;
+ },
+
+ // Used by tests and the toolbox to register the same key shortcuts in toolboxes loaded
+ // in a window window.
+ get KeyShortcuts() {
+ return KeyShortcuts;
+ },
+ get wrappedJSObject() {
+ return this;
+ },
+
+ /* eslint-disable max-len */
+ helpInfo:
+ " --jsconsole Open the Browser Console.\n" +
+ " --jsdebugger [<path>] Open the Browser Toolbox. Defaults to the local build\n" +
+ " but can be overridden by a firefox path.\n" +
+ " --wait-for-jsdebugger Spin event loop until JS debugger connects.\n" +
+ " Enables debugging (some) application startup code paths.\n" +
+ " Only has an effect when `--jsdebugger` is also supplied.\n" +
+ " --devtools Open DevTools on initial load.\n" +
+ " --start-debugger-server [ws:][ <port> | <path> ] Start the devtools server on\n" +
+ " a TCP port or Unix domain socket path. Defaults to TCP port\n" +
+ " 6000. Use WebSocket protocol if ws: prefix is specified.\n",
+ /* eslint-disable max-len */
+
+ classID: Components.ID("{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}"),
+ QueryInterface: ChromeUtils.generateQI(["nsICommandLineHandler"]),
+};
+
+/**
+ * Singleton object that represents the JSON View in-content tool.
+ * It has the same lifetime as the browser.
+ */
+const JsonView = {
+ initialized: false,
+
+ initialize: function() {
+ // Prevent loading the frame script multiple times if we call this more than once.
+ if (this.initialized) {
+ return;
+ }
+ this.initialized = true;
+
+ // Register for messages coming from the child process.
+ // This is never removed as there is no particular need to unregister
+ // it during shutdown.
+ Services.mm.addMessageListener("devtools:jsonview:save", this.onSave);
+ },
+
+ // Message handlers for events from child processes
+
+ /**
+ * Save JSON to a file needs to be implemented here
+ * in the parent process.
+ */
+ onSave: function(message) {
+ const browser = message.target;
+ const chrome = browser.ownerGlobal;
+ if (message.data === null) {
+ // Save original contents
+ chrome.saveBrowser(browser);
+ } else {
+ if (
+ !message.data.startsWith("blob:null") ||
+ !browser.contentPrincipal.isNullPrincipal
+ ) {
+ Cu.reportError("Got invalid request to save JSON data");
+ return;
+ }
+ // The following code emulates saveBrowser, but:
+ // - Uses the given blob URL containing the custom contents to save.
+ // - Obtains the file name from the URL of the document, not the blob.
+ // - avoids passing the document and explicitly passes system principal.
+ // We have a blob created by a null principal to save, and the null
+ // principal is from the child. Null principals don't survive crossing
+ // over IPC, so there's no other principal that'll work.
+ const persistable = browser.frameLoader;
+ persistable.startPersistence(null, {
+ onDocumentReady(doc) {
+ const uri = chrome.makeURI(doc.documentURI, doc.characterSet);
+ const filename = chrome.getDefaultFileName(undefined, uri, doc, null);
+ chrome.internalSave(
+ message.data,
+ null,
+ filename,
+ null,
+ doc.contentType,
+ false /* bypass cache */,
+ null /* filepicker title key */,
+ null /* file chosen */,
+ null /* referrer */,
+ doc.cookieJarSettings,
+ null /* initiating document */,
+ false /* don't skip prompt for a location */,
+ null /* cache key */,
+ PrivateBrowsingUtils.isBrowserPrivate(
+ browser
+ ) /* private browsing ? */,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ },
+ onError(status) {
+ throw new Error("JSON Viewer's onSave failed in startPersistence");
+ },
+ });
+ }
+ },
+};
+
+var EXPORTED_SYMBOLS = ["DevToolsStartup", "validateProfilerWebChannelUrl"];
diff --git a/devtools/startup/aboutdevtools/AboutDevToolsRegistration.jsm b/devtools/startup/aboutdevtools/AboutDevToolsRegistration.jsm
new file mode 100644
index 0000000000..f3ae2ea4a9
--- /dev/null
+++ b/devtools/startup/aboutdevtools/AboutDevToolsRegistration.jsm
@@ -0,0 +1,40 @@
+/* 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";
+
+// Register the about:devtools URL, that is opened whenever a user attempts to open
+// DevTools for the first time.
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { nsIAboutModule } = Ci;
+
+function AboutDevtools() {}
+
+AboutDevtools.prototype = {
+ uri: Services.io.newURI(
+ "chrome://devtools-startup/content/aboutdevtools/aboutdevtools.xhtml"
+ ),
+ classDescription: "about:devtools",
+ classID: Components.ID("3a16d383-92bd-4c24-ac10-0e2bd66883ab"),
+ contractID: "@mozilla.org/network/protocol/about;1?what=devtools",
+
+ QueryInterface: ChromeUtils.generateQI([nsIAboutModule]),
+
+ newChannel: function(uri, loadInfo) {
+ const chan = Services.io.newChannelFromURIWithLoadInfo(this.uri, loadInfo);
+ chan.owner = Services.scriptSecurityManager.getSystemPrincipal();
+ return chan;
+ },
+
+ getURIFlags: function(uri) {
+ return nsIAboutModule.ALLOW_SCRIPT;
+ },
+
+ getChromeURI: function(_uri) {
+ return this.uri;
+ },
+};
+
+var EXPORTED_SYMBOLS = ["AboutDevtools"];
diff --git a/devtools/startup/aboutdevtools/aboutdevtools.css b/devtools/startup/aboutdevtools/aboutdevtools.css
new file mode 100644
index 0000000000..e054b7566f
--- /dev/null
+++ b/devtools/startup/aboutdevtools/aboutdevtools.css
@@ -0,0 +1,189 @@
+/* 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/. */
+
+:root {
+ --white: #ffffff;
+
+ /* Shared variables */
+ --line-height: 1.5em;
+}
+
+html, body {
+ min-width: 600px;
+}
+
+body {
+ margin-top: 17vh;
+}
+
+p {
+ line-height: var(--line-height);
+}
+
+.box {
+ max-width: 850px;
+ display: flex;
+ flex-shrink: 0;
+ padding: 0 0 50px 0;
+ /* Compensate for the optional scrollbar. */
+ margin-right: calc(100% - 100vw);
+}
+
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.left-pane {
+ width: 300px;
+ height: 300px;
+ margin-inline-end: 20px;
+ background-image: url(images/otter.svg);
+ background-size: 100%;
+ background-position: 50%;
+ background-repeat: no-repeat;
+ flex-shrink: 0;
+}
+
+.features {
+ max-width: 980px;
+ border-top: 1px solid var(--in-content-border-color);
+}
+
+.features-list {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ grid-gap: 40px 20px;
+ margin: 60px 20px;
+ padding: 0;
+}
+
+.feature-icon {
+ width: 60%;
+ max-width: 180px;
+ margin-bottom: 10px;
+}
+
+.feature {
+ list-style: none;
+ text-align: center;
+ margin: 10px 0;
+}
+
+.feature-name {
+ display: block;
+ margin: 10px 0;
+ font-size: 28px;
+ font-weight: 300;
+}
+
+.feature-name span {
+ display: block;
+}
+
+.feature-desc {
+ margin: 1em 20px;
+}
+
+.feature-link {
+ display: block;
+ color: inherit;
+}
+
+.feature-more-link {
+ display: block;
+ margin-top: 10px;
+}
+
+.external::after {
+ content: "";
+
+ display: inline-block;
+ height: 16px;
+ width: 16px;
+
+ margin: -.3rem .15rem 0 0.25rem;
+ vertical-align: middle;
+
+ background-image: url(images/external-link.svg);
+ background-repeat: no-repeat;
+ background-size: 16px 16px;
+
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+.title {
+ font-weight: 300;
+ font-size: 32px;
+ margin-top: 16px;
+ line-height: 44px;
+}
+
+.buttons-container {
+ display: flex;
+ margin-top: 5px;
+}
+
+.buttons-container button:not(:last-child) {
+ margin-right: 10px;
+}
+
+button {
+ margin: 20px 0 0 0;
+ padding: 10px 20px;
+
+ font-size: 15px;
+ font-weight: 400;
+ line-height: 21px;
+ cursor: pointer;
+}
+
+/* Remove light gray outline when clicking on the button */
+button::-moz-focus-inner {
+ border: 0;
+}
+
+[hidden="true"] {
+ display: none;
+}
+
+footer {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ min-height: 300px;
+ flex-grow: 1;
+ padding-bottom: 15px;
+ color: var(--white);
+ background: linear-gradient(0, var(--blue-60), var(--blue-80));
+}
+
+.dev-edition-logo {
+ flex-shrink: 0;
+ width: 165px;
+ margin: 20px 50px 0 0;
+}
+
+.footer-message {
+ max-width: 460px;
+}
+
+.footer-message-title {
+ color: var(--white);
+}
+
+.footer-link {
+ display: block;
+ margin-top: 10px;
+}
+
+.footer-link,
+.footer-link:hover,
+.footer-link:visited,
+.footer-link:hover:active {
+ color: var(--white);
+}
diff --git a/devtools/startup/aboutdevtools/aboutdevtools.js b/devtools/startup/aboutdevtools/aboutdevtools.js
new file mode 100644
index 0000000000..9eac396675
--- /dev/null
+++ b/devtools/startup/aboutdevtools/aboutdevtools.js
@@ -0,0 +1,253 @@
+/* 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 } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const DEVTOOLS_ENABLED_PREF = "devtools.enabled";
+
+const MESSAGES = {
+ AboutDebugging: "about-debugging-message",
+ ContextMenu: "inspect-element-message",
+ HamburgerMenu: "menu-message",
+ KeyShortcut: "key-shortcut-message",
+ SystemMenu: "menu-message",
+};
+
+// Google analytics parameters that should be added to all outgoing links.
+const GA_PARAMETERS = [
+ ["utm_source", "devtools"],
+ ["utm_medium", "onboarding"],
+];
+
+const KEY_SHORTCUTS_STRINGS =
+ "chrome://devtools-startup/locale/key-shortcuts.properties";
+const keyShortcutsBundle = Services.strings.createBundle(KEY_SHORTCUTS_STRINGS);
+
+// URL constructor doesn't support about: scheme,
+// we have to use http in order to have working searchParams.
+const url = new URL(window.location.href.replace("about:", "http://"));
+const reason = url.searchParams.get("reason");
+const tabid = parseInt(url.searchParams.get("tabid"), 10);
+
+function getToolboxShortcut() {
+ const modifier = Services.appinfo.OS == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+";
+ return (
+ modifier + keyShortcutsBundle.GetStringFromName("toggleToolbox.commandkey")
+ );
+}
+
+function onInstallButtonClick() {
+ Services.prefs.setBoolPref("devtools.enabled", true);
+}
+
+function onCloseButtonClick() {
+ window.close();
+}
+
+function updatePage() {
+ const installPage = document.getElementById("install-page");
+ const welcomePage = document.getElementById("welcome-page");
+ const isEnabled = Services.prefs.getBoolPref("devtools.enabled");
+ if (isEnabled) {
+ installPage.setAttribute("hidden", "true");
+ welcomePage.removeAttribute("hidden");
+ } else {
+ welcomePage.setAttribute("hidden", "true");
+ installPage.removeAttribute("hidden");
+ }
+}
+
+/**
+ * Array of descriptors for features displayed on about:devtools.
+ * Each feature should contain:
+ * - icon: the name of the image to use
+ * - title: the key of the localized title (from aboutDevTools.ftl)
+ * - desc: the key of the localized description (from aboutDevTools.ftl)
+ * - link: the MDN documentation link
+ */
+const features = [
+ {
+ icon:
+ "chrome://devtools-startup/content/aboutdevtools/images/feature-inspector.svg",
+ title: "features-inspector-title",
+ desc: "features-inspector-desc",
+ link: "https://developer.mozilla.org/docs/Tools/Page_Inspector",
+ },
+ {
+ icon:
+ "chrome://devtools-startup/content/aboutdevtools/images/feature-console.svg",
+ title: "features-console-title",
+ desc: "features-console-desc",
+ link: "https://developer.mozilla.org/docs/Tools/Web_Console",
+ },
+ {
+ icon:
+ "chrome://devtools-startup/content/aboutdevtools/images/feature-debugger.svg",
+ title: "features-debugger-title",
+ desc: "features-debugger-desc",
+ link: "https://developer.mozilla.org/docs/Tools/Debugger",
+ },
+ {
+ icon:
+ "chrome://devtools-startup/content/aboutdevtools/images/feature-network.svg",
+ title: "features-network-title",
+ desc: "features-network-desc",
+ link: "https://developer.mozilla.org/docs/Tools/Network_Monitor",
+ },
+ {
+ icon:
+ "chrome://devtools-startup/content/aboutdevtools/images/feature-storage.svg",
+ title: "features-storage-title",
+ desc: "features-storage-desc",
+ link: "https://developer.mozilla.org/docs/Tools/Storage_Inspector",
+ },
+ {
+ icon:
+ "chrome://devtools-startup/content/aboutdevtools/images/feature-responsive.svg",
+ title: "features-responsive-title",
+ desc: "features-responsive-desc",
+ link: "https://developer.mozilla.org/docs/Tools/Responsive_Design_Mode",
+ },
+ {
+ icon:
+ "chrome://devtools-startup/content/aboutdevtools/images/feature-visualediting.svg",
+ title: "features-visual-editing-title",
+ desc: "features-visual-editing-desc",
+ link: "https://developer.mozilla.org/docs/Tools/Style_Editor",
+ },
+ {
+ icon:
+ "chrome://devtools-startup/content/aboutdevtools/images/feature-performance.svg",
+ title: "features-performance-title",
+ desc: "features-performance-desc",
+ link: "https://developer.mozilla.org/docs/Tools/Performance",
+ },
+ {
+ icon:
+ "chrome://devtools-startup/content/aboutdevtools/images/feature-memory.svg",
+ title: "features-memory-title",
+ desc: "features-memory-desc",
+ link: "https://developer.mozilla.org/docs/Tools/Memory",
+ },
+];
+
+/**
+ * Helper to create a DOM element to represent a DevTools feature.
+ */
+function createFeatureEl(feature) {
+ const li = document.createElement("li");
+ li.classList.add("feature");
+
+ const { icon, link, title, desc } = feature;
+ // eslint-disable-next-line no-unsanitized/property
+ li.innerHTML = `
+ <h3 class="feature-name">
+ <a class="feature-link" href="${link}" target="_blank">
+ <img class="feature-icon" src="${icon}" alt="" />
+ <span data-l10n-id="${title}"></span>
+ </a>
+ </h3>
+ <p class="feature-desc" data-l10n-id="${desc}">
+ <a class="feature-more-link external" href="${link}"
+ aria-hidden="true" tabindex="-1"
+ target="_blank" data-l10n-name="learn-more"></a>
+ </p>`;
+
+ return li;
+}
+
+window.addEventListener(
+ "load",
+ function() {
+ const inspectorShortcut = getToolboxShortcut();
+ const welcomeMessage = document.getElementById("welcome-message");
+
+ // Set the welcome message content with the correct keyboard shortcut for the current
+ // platform.
+ document.l10n.setAttributes(welcomeMessage, "welcome-message", {
+ shortcut: inspectorShortcut,
+ });
+
+ // Set the appropriate title message.
+ if (reason == "ContextMenu") {
+ document.getElementById("inspect-title").removeAttribute("hidden");
+ } else {
+ document.getElementById("common-title").removeAttribute("hidden");
+ }
+
+ // Display the message specific to the reason
+ const id = MESSAGES[reason];
+ if (id) {
+ const message = document.getElementById(id);
+ message.removeAttribute("hidden");
+ }
+
+ // Attach event listeners
+ document
+ .getElementById("install")
+ .addEventListener("click", onInstallButtonClick);
+ document
+ .getElementById("close")
+ .addEventListener("click", onCloseButtonClick);
+ Services.prefs.addObserver(DEVTOOLS_ENABLED_PREF, updatePage);
+
+ const featuresContainer = document.querySelector(".features-list");
+ for (const feature of features) {
+ featuresContainer.appendChild(createFeatureEl(feature));
+ }
+
+ // Add Google Analytics parameters to all the external links.
+ const externalLinks = document.querySelectorAll("a[href*='mozilla.org']");
+ for (const link of externalLinks) {
+ const linkUrl = new URL(link.getAttribute("href"));
+ GA_PARAMETERS.forEach(([key, value]) =>
+ linkUrl.searchParams.set(key, value)
+ );
+ link.setAttribute("href", linkUrl.href);
+ }
+
+ // Update the current page based on the current value of DEVTOOLS_ENABLED_PREF.
+ updatePage();
+ },
+ { once: true }
+);
+
+window.addEventListener(
+ "beforeunload",
+ function() {
+ // Focus the tab that triggered the DevTools onboarding.
+ if (document.visibilityState != "visible") {
+ // Only try to focus the correct tab if the current tab is the about:devtools page.
+ return;
+ }
+
+ // Retrieve the original tab if it is still available.
+ const browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ const { gBrowser } = browserWindow;
+ const originalBrowser = gBrowser.getBrowserForOuterWindowID(tabid);
+ const originalTab = gBrowser.getTabForBrowser(originalBrowser);
+
+ if (originalTab) {
+ // If the original tab was found, select it.
+ gBrowser.selectedTab = originalTab;
+ }
+ },
+ { once: true }
+);
+
+window.addEventListener(
+ "unload",
+ function() {
+ document
+ .getElementById("install")
+ .removeEventListener("click", onInstallButtonClick);
+ document
+ .getElementById("close")
+ .removeEventListener("click", onCloseButtonClick);
+ Services.prefs.removeObserver(DEVTOOLS_ENABLED_PREF, updatePage);
+ },
+ { once: true }
+);
diff --git a/devtools/startup/aboutdevtools/aboutdevtools.xhtml b/devtools/startup/aboutdevtools/aboutdevtools.xhtml
new file mode 100644
index 0000000000..5b0709e9a5
--- /dev/null
+++ b/devtools/startup/aboutdevtools/aboutdevtools.xhtml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE html [
+<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;">
+<head>
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" />
+ <title data-l10n-id="head-title"></title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>a
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools-startup/content/aboutdevtools/aboutdevtools.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools-startup/content/aboutdevtools/subscribe.css" type="text/css"/>
+ <script src="chrome://devtools-startup/content/aboutdevtools/aboutdevtools.js"></script>
+ <script src="chrome://devtools-startup/content/aboutdevtools/subscribe.js"></script>
+ <link rel="localization" href="devtools/startup/aboutDevTools.ftl" />
+</head>
+<body>
+ <div id="install-page" class="wrapper" hidden="true">
+ <div class="box">
+ <div class="left-pane" />
+ <div class="right-pane">
+ <h1 class="title" id="common-title" hidden="true" data-l10n-id="enable-title"></h1>
+ <h1 class="title" id="inspect-title" hidden="true" data-l10n-id="enable-inspect-element-title"></h1>
+
+ <!-- Include all the possible message, hidden by default
+ as we can't lazily load localized strings from dtd -->
+ <p id="about-debugging-message" hidden="true" data-l10n-id="enable-about-debugging-message"></p>
+ <p id="menu-message" hidden="true" data-l10n-id="enable-menu-message"></p>
+ <p id="key-shortcut-message" hidden="true" data-l10n-id="enable-key-shortcut-message"></p>
+ <p id="inspect-element-message" hidden="true" data-l10n-id="enable-inspect-element-message"></p>
+
+ <p data-l10n-id="enable-common-message"></p>
+ <a class="external installpage-link" href="https://developer.mozilla.org/docs/Tools" target="_blank" data-l10n-id="enable-learn-more-link"></a>
+ <div class="buttons-container">
+ <button class="primary" id="install" data-l10n-id="enable-enable-button"></button>
+ <button id="close" data-l10n-id="enable-close-button"></button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- This page, hidden by default is displayed once the add-on is installed -->
+ <div id="welcome-page" class="wrapper" hidden="true">
+ <div class="box">
+ <div class="left-pane" />
+ <div class="right-pane">
+ <h1 class="title" data-l10n-id="welcome-title"></h1>
+ <!-- The welcome message is dynamically updated with a keyboard shortcut at
+ runtime and added in aboutdevtools.js -->
+ <p id="welcome-message"></p>
+
+ <!-- Form dedicated to the newsletter subscription -->
+ <div class="newsletter">
+ <h2 class="newsletter-title" data-l10n-id="newsletter-title"></h2>
+ <p data-l10n-id="newsletter-message"></p>
+
+ <form id="newsletter-form" name="newsletter-form" action="https://www.mozilla.org/en-US/newsletter/" method="post">
+ <!-- "H" stands for the HTML format (->fmt). Alternative is T for text. -->
+ <input type="hidden" id="fmt" name="fmt" value="H" />
+ <!-- "app-dev" is the id of the Mozilla Developper newsletter -->
+ <input type="hidden" id="newsletters" name="newsletters" value="app-dev" />
+ <div id="newsletter-errors"></div>
+ <section id="newsletter-email" class="newsletter-form-section">
+ <input type="email" id="email" name="email" required="true" data-l10n-id="newsletter-email-placeholder" data-l10n-attrs="placeholder" />
+ </section>
+
+ <section id="newsletter-privacy" class="newsletter-form-section">
+ <input type="checkbox" id="privacy" name="privacy" required="true" />
+ <label for="privacy" data-l10n-id="newsletter-privacy-label">
+ <a class="external" href="https://www.mozilla.org/privacy/" data-l10n-name="privacy-policy"></a>
+ </label>
+ </section>
+ <button type="submit" id="newsletter-submit" class="primary" data-l10n-id="newsletter-subscribe-button"></button>
+ </form>
+ <div id="newsletter-thanks">
+ <h2 class="newsletter-title" data-l10n-id="newsletter-thanks-title"></h2>
+ <p data-l10n-id="newsletter-thanks-message"></p>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="features">
+ <ul class="features-list">
+ </ul>
+ </div>
+
+ <footer>
+ <img class="dev-edition-logo"
+ src="chrome://devtools-startup/content/aboutdevtools/images/dev-edition-logo.svg"
+ alt="" />
+ <div class="footer-message">
+ <h1 class="footer-message-title" data-l10n-id="footer-title"></h1>
+ <p data-l10n-id="footer-message"></p>
+ <a class="external footer-link"
+ href="https://www.mozilla.org/firefox/developer/"
+ target="_blank" data-l10n-id="footer-learn-more-link"></a>
+ </div>
+ </footer>
+ </div>
+
+</body>
+</html>
diff --git a/devtools/startup/aboutdevtools/components.conf b/devtools/startup/aboutdevtools/components.conf
new file mode 100644
index 0000000000..e9ea6846f8
--- /dev/null
+++ b/devtools/startup/aboutdevtools/components.conf
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{3a16d383-92bd-4c24-ac10-0e2bd66883ab}',
+ 'contract_ids': ['@mozilla.org/network/protocol/about;1?what=devtools'],
+ 'jsm': 'resource:///modules/AboutDevToolsRegistration.jsm',
+ 'constructor': 'AboutDevtools',
+ },
+]
diff --git a/devtools/startup/aboutdevtools/images/dev-edition-logo.svg b/devtools/startup/aboutdevtools/images/dev-edition-logo.svg
new file mode 100644
index 0000000000..65d1c49759
--- /dev/null
+++ b/devtools/startup/aboutdevtools/images/dev-edition-logo.svg
@@ -0,0 +1,251 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+ <defs>
+ <linearGradient x1="42%" y1="-10%" x2="61%" y2="114%" id="f">
+ <stop stop-color="#AAF2FF" offset="0%"/>
+ <stop stop-color="#0DF" offset="29%"/>
+ <stop stop-color="#0090ED" offset="61%"/>
+ <stop stop-color="#0250BB" offset="89%"/>
+ </linearGradient>
+ <linearGradient x1="38%" y1="0%" x2="63%" y2="124%" id="g">
+ <stop stop-color="#AAF2FF" offset="0%"/>
+ <stop stop-color="#0DF" offset="29%"/>
+ <stop stop-color="#0090ED" offset="74%"/>
+ <stop stop-color="#0250BB" offset="100%"/>
+ </linearGradient>
+ <linearGradient x1="86%" y1="15%" x2="20%" y2="84%" id="k">
+ <stop stop-color="#80EBFF" stop-opacity=".5" offset="24%"/>
+ <stop stop-color="#0DF" stop-opacity="0" offset="70%"/>
+ </linearGradient>
+ <linearGradient x1="61%" y1="-10%" x2="20%" y2="149%" id="l">
+ <stop stop-color="#BFF3FF" stop-opacity=".9" offset="0%"/>
+ <stop stop-color="#80EBFF" stop-opacity=".5" offset="100%"/>
+ </linearGradient>
+ <linearGradient x1="51%" y1="-2%" x2="48%" y2="139%" id="m">
+ <stop stop-color="#BFF3FF" offset="0%"/>
+ <stop stop-color="#0DF" stop-opacity="0" offset="100%"/>
+ </linearGradient>
+ <linearGradient x1="52%" y1="-4%" x2="47%" y2="143%" id="n">
+ <stop stop-color="#BFF3FF" offset="0%"/>
+ <stop stop-color="#AAF2FF" stop-opacity=".5" offset="100%"/>
+ </linearGradient>
+ <linearGradient x1="53%" y1="-2%" x2="47%" y2="142%" id="o">
+ <stop stop-color="#BFF3FF" offset="0%"/>
+ <stop stop-color="#0DF" stop-opacity="0" offset="100%"/>
+ </linearGradient>
+ <linearGradient x1="49%" y1="-2%" x2="57%" y2="123%" id="p">
+ <stop stop-color="#BFF3FF" stop-opacity=".8" offset="0%"/>
+ <stop stop-color="#80EBFF" stop-opacity=".2" offset="100%"/>
+ </linearGradient>
+ <linearGradient x1="117%" y1="-23%" x2="24%" y2="128%" id="q">
+ <stop stop-color="#BFF3FF" stop-opacity=".8" offset="0%"/>
+ <stop stop-color="#80EBFF" stop-opacity=".2" offset="100%"/>
+ </linearGradient>
+ <linearGradient x1="52%" y1="-1%" x2="47%" y2="137%" id="r">
+ <stop stop-color="#0DF" stop-opacity=".4" offset="0%"/>
+ <stop stop-color="#0DF" stop-opacity="0" offset="100%"/>
+ </linearGradient>
+ <linearGradient x1="50%" y1="-12%" x2="50%" y2="99%" id="s">
+ <stop stop-color="#BFF3FF" offset="0%"/>
+ <stop stop-color="#0DF" stop-opacity="0" offset="100%"/>
+ </linearGradient>
+ <linearGradient x1="53%" y1="-3%" x2="47%" y2="142%" id="t">
+ <stop stop-color="#BFF3FF" stop-opacity=".8" offset="0%"/>
+ <stop stop-color="#0DF" stop-opacity="0" offset="100%"/>
+ </linearGradient>
+ <radialGradient cx="55%" cy="25%" r="60%" id="a">
+ <stop stop-color="#0DF" stop-opacity=".4" offset="10%"/>
+ <stop stop-color="#7542E5" stop-opacity=".25" offset="90%"/>
+ </radialGradient>
+ <radialGradient cx="49.24%" cy="52.7%" fx="49%" fy="52.7%" r="51%" id="b">
+ <stop stop-color="#7542E5" stop-opacity="0" offset="91%"/>
+ <stop stop-color="#0DF" stop-opacity=".4" offset="100%"/>
+ </radialGradient>
+ <radialGradient cx="86%" cy="-13%" fx="86%" fy="-13%" r="128%" gradientTransform="matrix(.81263 0 0 1 .16 0)" id="c">
+ <stop stop-color="#80EBFF" offset="0%"/>
+ <stop stop-color="#0DF" offset="26%"/>
+ <stop stop-color="#0090ED" offset="53%"/>
+ <stop stop-color="#0060DF" offset="86%"/>
+ </radialGradient>
+ <radialGradient cx="47.73%" cy="40%" fx="48%" fy="40%" r="128%" gradientTransform="matrix(.81263 0 0 1 .09 0)" id="d">
+ <stop stop-color="#321C64" stop-opacity=".8" offset="30%"/>
+ <stop stop-color="#212F83" stop-opacity=".5" offset="37%"/>
+ <stop stop-color="#0A47AC" stop-opacity=".14" offset="48%"/>
+ <stop stop-color="#0250BB" stop-opacity="0" offset="53%"/>
+ </radialGradient>
+ <radialGradient cx="90%" cy="-50%" r="210%" gradientTransform="matrix(.83857 0 0 1 .13 0)" id="e">
+ <stop stop-color="#80EBFF" offset="0%"/>
+ <stop stop-color="#00B3F4" offset="47%"/>
+ <stop stop-color="#0060DF" offset="84%"/>
+ <stop stop-color="#592ACB" offset="100%"/>
+ </radialGradient>
+ <radialGradient cx="208%" cy="-104%" fx="208%" fy="-104%" r="300%" gradientTransform="matrix(1 0 0 .99746 0 0)" id="h">
+ <stop stop-color="#0DF" offset="0%"/>
+ <stop stop-color="#0090ED" offset="82%"/>
+ </radialGradient>
+ <radialGradient cx="155%" cy="-45%" fx="155%" fy="-45%" r="313%" gradientTransform="matrix(.8937 0 0 1 .17 0)" id="i">
+ <stop stop-color="#80EBFF" offset="29%"/>
+ <stop stop-color="#00B3F4" offset="100%"/>
+ </radialGradient>
+ <radialGradient cx="15%" cy="-43%" fx="15%" fy="-43%" r="230%" gradientTransform="matrix(.9814 0 0 1 0 0)" id="j">
+ <stop stop-color="#AAF2FF" offset="18%"/>
+ <stop stop-color="#0DF" offset="43%"/>
+ <stop stop-color="#0060DF" offset="69%"/>
+ </radialGradient>
+ </defs>
+ <path fill="#09204d" d="M146.62 289.1l15.14 41.98 22.76 30.7 53.41 10.18 25.56 2.18 43.62-10.9 18.8-18.02-179.29-56.12z"/>
+ <path fill="#09204d" d="M372.69 227.18l-5.35-17.75-15.3-28.85-17.49-18.44-18.57-13.26-16.12-6.33-19.35-6.88-31.85-2.03-20.09 3.63-16.39 5.04-29.08 17.02-18.18 20.27-16.76 27.23-6.6 23.16-1.22 35.15 6.28 23.96 13.87 27.28 9.77 13.18 13.52 12.4 16.89 11.42 20.82 9.05 34.85 4.1 18.2.5 26.92-7.59 24.07-13.2 15.49-13.22 12-14.32L370 285.46l3.75-22.73-1.06-35.55z"/>
+ <path fill="#0e56d7" d="M249.24 149.62l-14.27 14.15 23.28 9.43 17.3-21.46-26.31-2.12z"/>
+ <path fill="#1053d1" d="M258.25 173.2l45.05-1.36-27.74-20.1-17.32 21.46z"/>
+ <path fill="#0b4fbf" d="M299.95 151.58l-24.4.15 27.75 20.1 25.65-1.67-29-18.58z"/>
+ <path fill="#0b48b0" d="M328.95 170.16l-14.58-20.11 20.18 12.1-5.6 8.01z"/>
+ <path fill="#0c45a7" d="M348.26 182.47l-19.3-12.31 5.6-8.02 13.7 20.34z"/>
+ <path fill="#0f46af" d="M328.95 170.16l-3.23 13.78 17.66 7.06-14.43-20.84z"/>
+ <path fill="#0c47ad" d="M328.95 170.16l19.32 12.31-4.88 8.53-14.44-20.84z"/>
+ <path fill="#0e4fc3" d="M303.3 171.84l25.65-1.68-3.23 13.78-22.42-12.1z"/>
+ <path fill="#174dca" d="M303.3 171.84l-20.27 18.34 26.09 12.78-5.82-31.12z"/>
+ <path fill="#1650d4" d="M258.25 173.2l45.05-1.36-20.27 18.34-24.78-16.98z"/>
+ <path fill="#1246b2" d="M325.72 183.94L338 207.73l5.4-16.73-17.67-7.06z"/>
+ <path fill="#133ea1" d="M337.99 207.73l5.4-16.73 12.85 30.57-18.25-13.84z"/>
+ <path fill="#103d98" d="M343.38 191l19.6 14.34-6.74 16.23L343.38 191z"/>
+ <path fill="#0b3785" d="M348.26 182.47l3.79-1.9 10.92 24.77-14.7-22.86z"/>
+ <path fill="#0b3685" d="M362.97 205.34l4.37 4.1-15.3-28.86 10.93 24.76z"/>
+ <path d="M362.97 205.34l-14.7-22.86-4.88 8.52zm-28.42-43.2l13.71 20.34 3.79-1.9z" fill="#0c3b91"/>
+ <path fill="#0b3279" d="M371.7 241.04l-4.36-31.6 5.35 17.74-1.06 13.86h.07z"/>
+ <path fill="#0f388d" d="M362.97 205.34l8.73 35.7-4.36-31.6-4.37-4.1z"/>
+ <path fill="#0d3481" d="M356.24 221.57l15.46 19.47-8.73-35.7-6.73 16.23z"/>
+ <path fill="#0b2e71" d="M371.63 241.04l1.06-13.86 1.06 35.55-2.05-21.69h-.07z"/>
+ <path fill="#0e2f77" d="M369.11 269.14l4.64-6.4-2.05-21.7-2.59 28.1z"/>
+ <path fill="#0e2969" d="M370 285.46l-3.7 1.8 7.45-24.53-3.75 22.73z"/>
+ <path fill="#0c2a69" d="M366.3 287.25l7.45-24.52-4.64 6.4-2.82 18.12z"/>
+ <path fill="#123180" d="M369.11 269.14l2.6-28.1-7.95 9.37 5.35 18.73z"/>
+ <path fill="#112b70" d="M366.3 287.25l2.81-18.11-15.32 25.75 12.5-7.64z"/>
+ <path fill="#12378c" d="M356.24 221.57l15.46 19.47-7.94 9.37-7.52-28.84z"/>
+ <path fill="#122e7b" d="M369.11 269.14l-5.35-18.73-8.08 23.07 13.43-4.34z"/>
+ <path fill="#11296c" d="M353.02 318.7l13.27-31.45-12.41 22.92-.85 8.53z"/>
+ <path fill="#142a72" d="M353.88 310.17l12.41-22.92-12.5 7.64.09 15.28z"/>
+ <path d="M366.3 287.25l-13.28 31.45L370 285.46zm-25.28 45.77l12-14.32.86-8.52zm-15.49 13.21l15.49-13.2-16.37 7.52z" fill="#112768"/>
+ <path fill="#1c226a" d="M235.15 355.44l-15.65 3.82.48-10.92 15.17 7.1z"/>
+ <path fill="#182b7a" d="M148.16 206.82l4.32-1.6L141.56 230l6.6-23.17z"/>
+ <path fill="#1f308d" d="M141.57 229.99l8.26-1.54 2.64-23.22-10.9 24.76z"/>
+ <path fill="#1e2b7f" d="M142.06 259.73l-.5-29.74 8.27-1.54-7.77 31.28z"/>
+ <path d="M325.72 183.94l-16.6 19.02 28.87 4.77zm-144.9-19.51l21.2-14.5-18.92 9.4z" fill="#1346b5"/>
+ <path fill="#1244ac" d="M183.1 159.33l29.08-17.02-10.17 7.62-18.91 9.4"/>
+ <path fill="#0e4ab6" d="M202.01 149.93l26.56-12.66-16.39 5.04-10.17 7.62z"/>
+ <path d="M303.3 171.84l5.82 31.12 16.6-19.02zm-85.11-23.39l24.74-9.89-14.36-1.29z" fill="#0e4bba"/>
+ <path fill="#0a4ebc" d="M248.66 133.65l-20.09 3.62 14.36 1.29 5.73-4.92z"/>
+ <path fill="#0c54cc" d="M267.65 138.31l-24.72.25 5.73-4.92"/>
+ <path d="M314.37 150.05l-14.42 1.53 29 18.58zm-46.72-11.74l32.2 4.24-19.34-6.88z" fill="#0b4bb4"/>
+ <path fill="#0c3889" d="M314.37 150.05l-14.51-7.5 16.12 6.33-1.61 1.17z"/>
+ <path d="M146.62 289.1l9.52 7.83-14.08-37.2zm131.4 73.98l8.37-19.7-30.5 8.2zm8.37-19.7l25.5 7.08 1.54-19.72z" fill="#202575"/>
+ <path fill="#1b2773" d="M324.65 340.55l-11.22-9.81 27.11-18.02-15.89 27.83z"/>
+ <path fill="#15276d" d="M340.54 312.72l-15.89 27.83 16.37-7.53 12.86-22.85-13.34 2.55z"/>
+ <path d="M315.98 148.88l-1.61 1.17 20.18 12.1zm-16.03 2.7l14.42-1.53-14.51-7.5z" fill="#0c409d"/>
+ <path d="M248.66 133.65l19 4.66 12.85-2.64zm51.2 8.9l-32.2-4.24 32.3 13.27z" fill="#0b4cb7"/>
+ <path fill="#0b55ce" d="M267.65 138.31l7.9 13.42 24.4-.15-32.3-13.27z"/>
+ <path fill="#0a52c5" d="M267.65 138.31l7.9 13.42-26.31-2.11 18.41-11.3z"/>
+ <path fill="#0951c4" d="M242.93 138.56l6.31 11.06 18.41-11.3-24.72.24z"/>
+ <path fill="#0b50c4" d="M218.19 148.45l24.74-9.89 6.31 11.06-31.05-1.17z"/>
+ <path d="M202.01 149.93l16.18-1.48 10.38-11.18zm16.18-1.48l16.78 15.32 14.27-14.16z" fill="#0f4dbf"/>
+ <path fill="#1546b6" d="M218.19 148.45l-27.33 25.63 11.15-24.15 16.18-1.48z"/>
+ <path fill="#1549bd" d="M190.86 174.07l17.44 2.9 9.89-28.52-27.33 25.63z"/>
+ <path fill="#144bc3" d="M208.3 176.97l9.89-28.52 16.79 15.32-26.68 13.2z"/>
+ <path fill="#173794" d="M164.92 179.6l1.06 10.31 14.84-25.48-15.9 15.17z"/>
+ <path fill="#192f84" d="M152.47 205.23l12.45-25.62 1.06 10.3-13.5 15.32z"/>
+ <path fill="#1a389b" d="M165.98 189.91l14.84-25.48 10.04 9.64-24.88 15.84z"/>
+ <path fill="#1d3089" d="M165.09 212l.9-22.09-13.52 15.32L165.1 212z"/>
+ <path fill="#1e389f" d="M165.09 212l.9-22.09 17.52 14.23-18.42 7.87z"/>
+ <path fill="#1c40af" d="M183.51 204.14l7.35-30.06-24.88 15.83 17.53 14.23z"/>
+ <path fill="#1e41b7" d="M183.51 204.14l7.35-30.06 17.44 2.9-24.79 27.16z"/>
+ <path fill="#192c7d" d="M149.83 228.45l2.64-23.22L165.1 212l-15.26 16.44z"/>
+ <path d="M278.02 363.08l23.44-3.64 10.42-8.99zm-137.68-97.94l1.72-5.41-.5-29.74zm9.49-36.69l-7.77 31.28 14.08 37.2-6.87-43.1z" fill="#162467"/>
+ <path fill="#1a236a" d="M156.14 296.93l11.62 1.63-8.14-18.99-3.48 17.36z"/>
+ <path d="M324.65 340.55l-12.77 9.9-10.42 9 24.07-13.22zm-68.31 25.97l18.2.5 26.92-7.58-23.44 3.64zm-89.36-48l12.2 2.35-11.42-22.3z" fill="#132668"/>
+ <path d="M219.98 348.34l-20.67-15.9-8.59 8.25 28.78 18.57zm-73.36-59.24l13.87 27.28-4.35-19.45zm-6.28-23.96l6.28 23.96-4.56-29.37zm115.54 86.44l-35.9-3.23 15.17 7.09-15.65 3.82-18.83-5.88 20.82 9.05 34.84 4.1 21.68-3.45zm-88.12-53.02l-11.62-1.63 10.84 21.59zm-18.49-44.73l6.87 43.1 3.48-17.36zm29.92 67.03l-12.21-2.34-6.5-2.14 9.78 13.18 13.52 12.4 16.89 11.42-9.95-12.69z" fill="#1b2268"/>
+ <path fill="#212a81" d="M149.83 228.45l16.39 25.36-1.13-41.8-15.26 16.44z"/>
+ <path fill="#21267a" d="M149.27 253.83l10.35 25.74 6.6-25.77-16.95.03z"/>
+ <path fill="#262781" d="M159.62 279.57l6.6-25.77 8.19 20.37-14.79 5.4z"/>
+ <path fill="#272379" d="M167.76 298.56l31.42 19.47-24.77-43.86-6.65 24.4z"/>
+ <path d="M160.49 316.38l6.49 2.14-10.84-21.59zm69.4 11.2l-9.91 20.76 35.9 3.23zm-62.13-29.02l11.43 22.3 11.53 19.83 8.6-8.26-.14-14.4z" fill="#1e226d"/>
+ <path d="M313.43 330.74l-1.55 19.71 12.77-9.9zm-54.94-2.83l-2.6 23.66 30.5-8.2-1.1-26.6z" fill="#222475"/>
+ <path d="M149.27 253.83l16.94-.03-16.38-25.35zm136.03 62.95l1.09 26.6 27.04-12.64z" fill="#23277d"/>
+ <path fill="#1e2776" d="M285.3 316.78l17.4-15.76 10.73 29.72-28.13-13.96z"/>
+ <path fill="#222b86" d="M313.43 330.74l13.15-39.85-23.88 10.13 10.73 29.72z"/>
+ <path fill="#192974" d="M340.54 312.72l-27.11 18.02 13.16-39.84 13.95 21.82z"/>
+ <path fill="#172a75" d="M353.79 294.9l-27.2-4 13.95 21.82 13.34-2.55-.1-15.28z"/>
+ <path fill="#172d7d" d="M355.68 273.48l-29.1 17.41 27.2 4 15.33-25.75-13.43 4.34z"/>
+ <path d="M152.47 205.23l12.45-25.62-16.76 27.22zm192.36 31.71l10.85 36.54 8.08-23.07z" fill="#18338a"/>
+ <path fill="#153791" d="M363.76 250.41l-7.52-28.84-11.41 15.37 18.93 13.47z"/>
+ <path fill="#1b318b" d="M326.58 290.9l29.1-17.42-28.2-12.55-.9 29.96z"/>
+ <path fill="#242d8d" d="M326.58 290.9l-26.36-26.65 2.48 36.77 23.88-10.12z"/>
+ <path fill="#2a2b8f" d="M285.3 316.78l-20.24-17.93 37.64 2.17-17.4 15.76z"/>
+ <path d="M167.76 298.56l6.65-24.4-14.79 5.41zm97.3.3l-6.57 29.05 26.81-11.13z" fill="#27267f"/>
+ <path fill="#2d2381" d="M223.15 304.8l-19.49-9.9-4.48 23.13 30.72 9.55 28.6.33-35.35-23.11z"/>
+ <path fill="#2f2485" d="M258.5 327.9l6.56-29.05-41.91 5.95 35.34 23.1z"/>
+ <path fill="#233196" d="M166.22 253.8l8.4-26.83-9.53-14.96 1.13 41.8z"/>
+ <path fill="#293cb7" d="M183.51 204.14l36.9-10.75-19.49 39.8-17.4-29.05z"/>
+ <path fill="#25339c" d="M326.58 290.9l-26.36-26.65 27.26-3.32-.9 29.96z"/>
+ <path fill="#19348f" d="M355.68 273.48l-10.85-36.54-17.35 23.99 28.2 12.55z"/>
+ <path fill="#2a309a" d="M302.7 301.02l-30.15-29.16 27.67-7.6 2.48 36.76z"/>
+ <path fill="#302b96" d="M265.06 298.85l37.64 2.17-30.15-29.16-7.5 27z"/>
+ <path fill="#3630a7" d="M265.06 298.85l-20.63-16.79 28.12-10.2-7.5 27z"/>
+ <path d="M180.82 164.43l2.28-5.1-18.18 20.27zm157.17 43.3l6.84 29.21 11.4-15.37z" fill="#173da2"/>
+ <path fill="#2a36a8" d="M183.51 204.14l17.41 29.06-26.3-6.23 8.9-22.83z"/>
+ <path fill="#312e9f" d="M174.62 226.97l26.3 6.23-17.78 28-8.52-34.23z"/>
+ <path fill="#184dcd" d="M220.4 193.39l14.58-29.62-26.68 13.2 12.1 16.42z"/>
+ <path fill="#272986" d="M166.22 253.8l16.92 7.4-8.73 12.97-8.2-20.36z"/>
+ <path fill="#2c2584" d="M183.14 261.2l-8.73 12.97 24.77 43.86 4.48-23.14-20.52-33.68z"/>
+ <path fill="#32268b" d="M203.66 294.9l2.75-40.76-23.27 7.07 20.52 33.68z"/>
+ <path fill="#372999" d="M203.66 294.9l40.77-12.84-38.02-27.92-2.75 40.75z"/>
+ <path fill="#392da5" d="M244.43 282.06l-3.97-31.5-34.05 3.58 38.02 27.92z"/>
+ <path fill="#3338bc" d="M240.46 250.56l28.76-10.76 3.33 32.06-32.09-21.3z"/>
+ <path fill="#362896" d="M223.15 304.8l41.9-5.95-20.62-16.79-21.28 22.74z"/>
+ <path fill="#35268f" d="M223.15 304.8l-19.49-9.9 40.77-12.84-21.28 22.74z"/>
+ <path fill="#2648d3" d="M220.4 193.39l24.98 2.64-10.4-32.26-14.57 29.62z"/>
+ <path fill="#1057dc" d="M258.25 173.2l-23.27-9.43 10.4 32.26 12.87-22.83z"/>
+ <path fill="#1653da" d="M258.25 173.2l24.78 16.98-37.65 5.85 12.87-22.83z"/>
+ <path fill="#2b46d5" d="M245.38 196.03l36.93 19.58-13.1 24.2-23.83-43.78z"/>
+ <path fill="#244ad7" d="M282.31 215.61l.72-25.43-37.65 5.85 36.93 19.58z"/>
+ <path fill="#2547cf" d="M282.31 215.61l.72-25.43 26.09 12.78-26.81 12.65z"/>
+ <path fill="#333cc5" d="M245.38 196.03l23.84 43.77-41.81-20.94 17.97-22.83z"/>
+ <path fill="#3833b6" d="M240.46 250.56l28.76-10.76-41.81-20.94 13.05 31.7z"/>
+ <path fill="#2a41c6" d="M269.22 239.8l36.87-6.73-23.78-17.46-13.1 24.2z"/>
+ <path fill="#2045c1" d="M309.12 202.96l-3.03 30.1-23.78-17.45 26.8-12.65z"/>
+ <path fill="#1b43b7" d="M309.12 202.96l17.4 23.63-20.43 6.48 3.03-30.11z"/>
+ <path fill="#1742af" d="M337.99 207.73l-28.87-4.77 17.4 23.64L338 207.73z"/>
+ <path d="M190.86 174.07l11.15-24.14-21.2 14.5zM338 207.73l-11.47 18.87 18.3 10.35z" fill="#1740a9"/>
+ <path fill="#1b3ca4" d="M327.48 260.93l17.35-24-18.3-10.34.95 34.34z"/>
+ <path fill="#1f3dad" d="M327.48 260.93l-.96-34.34-20.43 6.48 21.4 27.86z"/>
+ <path d="M183.51 204.14l36.9-10.75-12.11-16.41zm85.7 35.66l31 24.45 5.88-31.18z" fill="#2543c3"/>
+ <path fill="#2e37b2" d="M272.55 271.86l27.67-7.6-31-24.46 3.33 32.06z"/>
+ <path fill="#2d40c7" d="M220.4 193.39l7 25.47 17.98-22.83-24.97-2.64z"/>
+ <path fill="#3537b8" d="M220.4 193.39l-19.48 39.8 26.49-14.33-7-25.47z"/>
+ <path fill="#3931b0" d="M227.4 218.86l-21 35.28 34.06-3.58-13.05-31.7z"/>
+ <path d="M240.46 250.56l3.97 31.5 28.12-10.2zm-34.05 3.58l21-35.28-26.49 14.34z" fill="#3735b8"/>
+ <path fill="#342892" d="M183.14 261.2l17.78-28 5.49 20.94-23.27 7.07z"/>
+ <path d="M200.67 353.38l18.83 5.88-28.78-18.57zm-1.36-20.94l20.67 15.9 9.92-20.76-30.72-9.55zm56.57 19.13l2.61-23.66-28.6-.33zm30.5-8.2l-8.36 19.7 33.86-12.62z" fill="#232372"/>
+ <path fill="#2a2f96" d="M166.22 253.8l8.4-26.83 8.52 34.24-16.92-7.4z"/>
+ <path d="M174.62 226.97l8.9-22.83-18.43 7.87zm131.47 6.1l-5.87 31.18 27.26-3.32z" fill="#2636a5"/>
+ <path d="M374.27 262.76v-.05l-1.06-35.55v-.04-.03l-.02-.05v-.01l-5.34-17.74v-.01l-.01-.03-.02-.04v-.01l-15.3-28.86v-.01l-.03-.03-.01-.03-.01-.01-.02-.02-.01-.02-17.5-18.44h-.01l-.01-.02-.04-.03-.02-.02-18.57-13.25-.02-.01a.36.36 0 00-.04-.02.3.3 0 00-.04-.02l-16.12-6.33h-.03l-19.34-6.88h-.05l-.04-.01h-.05l-31.86-2.03h-.05a.46.46 0 00-.05 0h-.02l-20.1 3.63h-.07l-16.37 5.04-.04.01-.04.02-.02.01-29.09 17.02-.03.02-.02.01-.02.02-.02.01-.02.03-.02.01-18.17 20.28-.02.02-.01.02-.02.02v.01l-.01.01-16.78 27.2v.02l-.02.02-.01.04-.02.03v.02l-6.6 23.17v.07a.26.26 0 000 .04v.01l-1.23 35.15v.08a.3.3 0 000 .05v.03l6.28 23.95v.01l.01.02a.52.52 0 00.02.05l.01.02 13.86 27.28.01.01a.5.5 0 00.03.05l9.78 13.18v.01l.04.03a.25.25 0 00.02.03l13.52 12.4.03.02.03.02 16.89 11.43.05.03h.02l20.83 9.06.02.01a.35.35 0 00.05.01l.02.01h.06l34.84 4.1h.05l18.2.5h.01a.57.57 0 00.1 0l.03-.01h.01l26.93-7.59h.04l.02-.02h.02l.03-.02 24.06-13.2h.01l.01-.01a.35.35 0 00.04-.03l.03-.02 15.49-13.21.01-.02.02-.02.02-.02h.01l12-14.32h.01a.53.53 0 00.04-.07l.02-.02 17.01-33.23.01-.02.02-.04v-.02a.3.3 0 000-.05l.02-.02 3.74-22.73v-.04zm-190.16 78.78l-3.5-3.21 7.88 6.17zm51.1 13.35l-12.3-5.76 29.13 2.62zm4.5-104.78l-32.32 3.4 19.93-33.48zm-11.28-30.15l39.48 19.77-27.16 10.16zM240 251.13l3.76 29.79-35.95-26.4zm3.29 30.74l-39.05 12.29 2.63-39.03zm-2.17-30.24l30.27 20.1-26.52 9.62zm.49-.94l27.15-10.16 3.15 30.28zm41.72-34.98l25.17-11.88-2.84 28.27zm21.47 17.06l-34.6 6.32 12.29-22.69zm-35.58 5.93l-22.54-41.38 34.92 18.51zm-1.27-.12l-39.73-19.9 17.08-21.7zM206.6 252.8l-5.06-19.33 24.44-13.23zm-3.35 40.41l-19.31-31.69 21.89-6.65zm39.43-10.04l-19.64 20.99-18-9.14zm29.1-10.46l-7.02 25.24-19.27-15.7zm-1.9-31.72l29.2 23.04L273 271.2zm.58-.87l34.98-6.4-5.58 29.59zm36.24-7.79l2.8-27.95 16.16 21.95zm19.32-5.02l.89 32.02-19.94-25.98zm-43.16-12.53l.67-23.76 24.37 11.94zm-1.05-.02l-34.79-18.44 35.47-5.51zm-54.17 2.96L221.12 194l23.26 2.46zm-.85.88l-24.67 13.36 18.15-37.08zm-21.02 35.2l-21.43 6.5 16.38-25.79zm-3.88 39.2l-26.76-18.96 7.98-11.85zm2.16 2.68l17.98 9.14-22.12 12.22zm40.43-12.9l19.31 15.73-39.24 5.56zm28.34-9.92l28.5 27.56-35.58-2.05zm.76-.73l26.16-7.2 2.35 34.76zm32.81-37.8l20.11 26.2-25.63 3.12zm20.68-6.81l16.99 9.6-16.1 22.25zm.18-1.11l10.52-17.3 6.27 26.79zm-.77-.75l-16.17-21.97 26.83 4.43zm-42.52-35.6l19-17.2 5.45 29.17zm-37.59 5.3l12.05-21.39 23.22 15.9zm-25.16-2.4l13.66-27.78 9.76 30.25zm-20.34 39.15l-16.56-27.64 35.1-10.23zm-17.52 27.79l-8-32.2 24.73 5.85zm-8.82 13.16l-7.35-18.25 15.17 6.63zm28.55 22.07l-4.15 21.4-22.9-40.57zm19.75 10.45l6.28 21.22-28.6-8.9zm41.54-6.09l-6.22 27.57-33.54-21.93zm37.05 1.99l-16.12 14.6-18.76-16.6zm-.58-35.84l24.83 25.08-22.5 9.54zm.5-.99l25.6-3.12-.85 28.13zm43.29-26.53l10.22 34.42-26.56-11.82zm-5.8-29.08l16.68 12.64-10.43 14.04zm-28.7-6.44l15.48-17.72 11.43 22.17zm-.7-.8l-5.41-28.97 20.87 11.27zm-26.45-12.28l-23.11-15.83 42.02-1.28zm-37.47 5.21l-9.69-30.04 21.67 8.78zm-25.2-2.34l-11.22-15.21 24.72-12.24zm-.78.72l-34.37 10.02 23.1-25.32zm-19.7 39.3l-24.5-5.8 8.28-21.27zm-17.47 27.9l-15.5-6.79 7.7-24.6zm-8.64 13.56l-13.34 4.88 5.94-23.25zm23.99 42.64l-29.35-18.18 6.22-22.78zm29.88 10.91l-27.76 4.4-.12-13.07zm-3.53-21.4l32.64 21.35-26.4-.3zm41.3-6.18l18.96 16.78-25.09 10.42zm37.1 2.1l10.02 27.75-26.26-13.03zm.9-.63l22.36-9.47-12.32 37.3zm24.62-39.58l26.56 11.82-27.4 16.4zm17.78-23.47l17.37 12.36-7.42 21.15zm-.2-1.43L356 222.76l6.88 26.38zm-6.96-29.29l4.84-15.02L355 219.96zm-.73-1.17l-11.04-21.42 15.9 6.36zm-32.71-34.12l23.12-1.51-2.91 12.42zm-45.79.4l16.28-20.19 26.1 18.9zm-.96-.48l-8.38-21.96 24.5 1.98zm-1.09.1l-21.41-8.68 13.13-13.03zm-48.1 3.67l9.2-26.56 15.63 14.26zm-24.7 26.33l6.74-27.6 16 2.67zm-10 23.55l-8.65-13.57 16.7-7.13zm-.47 1.23l-7.4 23.6-.98-36.75zm-8.51 27.29l-6.03 23.55-9.46-23.53zm-15.73-1.03l.5-23.13 14.94 23.1zm23.83 21.7l-6 21.96-7.31-17.09zm24.03 42.71l-18.19 2.58-10.4-20.29zm.99.92l.12 12.89-18-10.35zm30.33 9.62l-9.2 19.28-19.19-14.77zm1.08.2l24.32 22.46-33.6-3.03zm1.19-.33l26.65.3-2.43 22.05zm53.54-10.57l1.02 24.92-26.12-14.5zm1.06.1l26.35 13.07-25.34 11.85zm40.87-25.54l13.09 20.46-25.42 16.89zm.92-.53l25.19 3.7-12.26 16.52zm.46-1l26.95-16.12-1.75 19.83zm35.6-38.43l4.75 16.65-11.94 3.85zm-6.34-28.3l13.65 17.18-7.01 8.27zm-12.77-31.3l17.73 12.97-6.1 14.68zm-18.27-8.93l2.86-12.18 12.77 18.42zm-22.6-12.34l-3.07-18.61 26.66 17.07zm-1.17-.62l-25.4-18.41 22.35-.14zm-51.68-21.44l16.59-10.18 7.11 12.1zm-15.93 13.83l-15.35-14.02 28.41 1.07zm-27.01 13.34L192 173.73l25-23.43zM183.2 203.2l-16.32-13.25 23.16-14.74zm-.7.8l-16.85 7.19.82-20.21zm-16.86 47.93l-15.15-23.43 14.1-15.2zm-6.56 27.69l-2.88 14.33-5.67-35.6zm.73 1.7l7.1 16.58-10.14-1.42zm18.42 38.82l-10.7-2.06.68-17.5zm20.21 12.4l-7.6 7.3-10.21-17.55zm.91.58l18.26 14.05-25.85-6.76zm59.58-4.38L285 343.21l-28.5 7.66zm53.9 2.85l-1.43 18.18-23.5-6.53zm26.15-17.21l-14.47 25.34-10.22-8.93zm14.29-17.92l.08 13.27-11.58 2.21zm2.9-22.6l11.77-3.8-13.43 22.57zm8.17-23.33l6.68-7.89-2.18 23.64zm-7.5-29.06l6-14.45 7.77 31.8zm-12.77-30.63l4.22-7.36L361 203.24zm-.73-.84l-12.47-18 16.69 10.64zm-41.85-38.06l12.63-1.35 12.77 17.61zm-25.63-.74l-6.96-11.82 28.43 11.69zm-26.43-2.33l-5.6-9.8 21.93-.22zm-1.12.18l-27.63-1.04 22.02-8.8zm-55.84 22.79l9.9-21.43 14.34-1.3zm-1.78 1.33l-9.04-8.68 19.09-13.06zm-.67.81l-22.5 14.32 13.42-23.04zm-25.43 37.16l-11.28-6.07 12.09-13.7zm-.35 1l-13.7 14.77 2.37-20.86zm-15.49 41.43l-5.8 4.74 6.25-25.14zm.15 1.24l5.91 37.1-12.12-32.02zm18.31 44.2l-.68 17.44-9.47-18.87zm11.64 22.32l9.62 16.53-19.8-18.5zm40.59 27.4l-.42 9.58-25.25-16.29zm1.03.41l13.1 6.13-13.51 3.3zm34.9 3.05l.4 13.44-19.04-9.97zm30.1-8.03l-7.72 18.18-20.43-10.62zm1.21-.16l23.5 6.52-31.2 11.63zm27.18-12.19l9.96 8.7-11.32 8.79zm27.01-18.63l11.49-2.19-25.15 26.14zm13.52-5.07l-.08-12.94 10.58-6.46zm.93-14.78l12.85-21.6-2.36 15.19zm16.38-46.63l1.5 15.87-3.4 4.7zm-7.8-39.77l2.94 2.75 2.94 21.27zm-14.88-24.26l2.76-1.38 7.99 18.1zm-19.32-12.67l4.85-6.94 11.87 17.6zm-.75-.76l-12.65-17.43 17.5 10.48zM300.47 151l-.08-7.58 12.18 6.3zm-1.05-.2l-27.84-11.44 27.76 3.65zm-55.06-12.78l4.43-3.8 14.7 3.61zm-24.2 9.08l8.62-9.29 11.91 1.08zm-2.22.85l-13.05 1.19 21.42-10.21zm-35.88 15l1.44-3.24 11.97-5.95zm-15.72 25.3l-.87-8.44 13.03-12.44zm-.9 1.49l-10.8 12.24 9.95-20.48zM149.34 228l-6.9 1.28 9.11-20.68zm-.22 1.1l-6.6 26.58-.42-25.27zm5.83 66.16l-7.84-6.45-3.76-24.18zm10.99 22.35l-5-1.65-3.36-15zm21.4 20.63l-16.63-9.05-2.67-8.97zm3.03 2.85l7.2 9.18-22.01-17.24zm2.7 1.73l22.9 14.78-14.99-4.69zM235.08 356l17.8 9.31-30.94-6.1zm21.36-3.54l19.92 10.36-19.52 3.1zm52.88-.48l-8.09 6.98-18.2 2.82zm14.95-10.46l.67 4.35-10.45 3.24zm27.12-28l-10.75 19.1-13.68 6.3zm3-3.18l7.2-13.3-7.7 18.24zm15.22-40.99l2.72-3.76-4.37 14.37zm2.62-28.34v-.03l-.04-.28.27-3.56.28 9.22zm-.73-5.24l-2.72-19.7 3.37 11.18zm-8.09-30.73l-4.96-11.25 6.95 13.11zm-14.97-23.23l-9.91-14.7 12.65 13.33zm-33.12-31.79l.67-.48 7.62 5.44zm-1-.57l-5.15-2.67 5.72 2.25zm-43.5-11.24l9.65-1.99 14.54 5.17zm-3.16-.42l-13.15-3.24 22.05 1.4zm-24.9.24l-10.27-.93 14.36-2.59zm-36.2 9.16l5.86-4.4 9.46-2.9zm-4.83 2.3l-5.09 2.53 7.83-4.58zm-21.35 14.64l-6.04 5.77 6.9-7.7zm-28.29 40.69l-2.68 1 10.43-16.94zm-3.5 2.41l2.9-1.07-7.32 16.64zm-7.21 43.17l.15 9.26-.53 1.68zm.5 11.63l2.64 17.02-3.64-13.89zm13.78 35.2l3.4 15.2-10.83-21.32zm10.89 21.71l2.35 7.92-7-9.45zm52.6 40.77l.99 1.57-10.3-4.47zm2.65 2.24l-1.21-1.93 22.4 4.41zm54.85 1.9l-2.35 2.66-12.3-.34zm1.63-.27l14.65-2.27-16.83 4.74zm33.86-12.65l8.59-2.66-15.15 8.32zm13.76-5.7l-.67-4.36 12.55-5.77zm27.2-32.6l-.59 5.86-8.26 9.85zm13.6-24.98l2.12-1.02-9.7 19zm2.83-2.53l-2.38 1.15 4.78-15.75z" fill="url(#a)"/>
+ <path d="M374.27 262.76v-.05l-1.06-35.55v-.04-.03l-.02-.05v-.01l-5.34-17.74v-.01l-.01-.03-.02-.04v-.01l-15.3-28.86v-.01l-.03-.03-.01-.03-.01-.01-.02-.02-.01-.02-17.5-18.44h-.01l-.01-.02-.04-.03-.02-.02-18.57-13.25-.02-.01a.36.36 0 00-.04-.02.3.3 0 00-.04-.02l-16.12-6.33h-.03l-19.34-6.88h-.05l-.04-.01h-.05l-31.86-2.03h-.05a.46.46 0 00-.05 0h-.02l-20.1 3.63h-.07l-16.37 5.04-.04.01-.04.02-.02.01-29.09 17.02-.03.02-.02.01-.02.02-.02.01-.02.03-.02.01-18.17 20.28-.02.02-.01.02-.02.02v.01l-.01.01-16.78 27.2v.02l-.02.02-.01.04-.02.03v.02l-6.6 23.17v.07a.26.26 0 000 .04v.01l-1.23 35.15v.08a.3.3 0 000 .05v.03l6.28 23.95v.01l.01.02a.52.52 0 00.02.05l.01.02 13.86 27.28.01.01a.5.5 0 00.03.05l9.78 13.18v.01l.04.03a.25.25 0 00.02.03l13.52 12.4.03.02.03.02 16.89 11.43.05.03h.02l20.83 9.06.02.01a.35.35 0 00.05.01l.02.01h.06l34.84 4.1h.05l18.2.5h.01a.57.57 0 00.1 0l.03-.01h.01l26.93-7.59h.04l.02-.02h.02l.03-.02 24.06-13.2h.01l.01-.01a.35.35 0 00.04-.03l.03-.02 15.49-13.21.01-.02.02-.02.02-.02h.01l12-14.32h.01a.53.53 0 00.04-.07l.02-.02 17.01-33.23.01-.02.02-.04v-.02a.3.3 0 000-.05l.02-.02 3.74-22.73v-.04zm-190.16 78.78l-3.5-3.21 7.88 6.17zm51.1 13.35l-12.3-5.76 29.13 2.62zm4.5-104.78l-32.32 3.4 19.93-33.48zm-11.28-30.15l39.48 19.77-27.16 10.16zM240 251.13l3.76 29.79-35.95-26.4zm3.29 30.74l-39.05 12.29 2.63-39.03zm-2.17-30.24l30.27 20.1-26.52 9.62zm.49-.94l27.15-10.16 3.15 30.28zm41.72-34.98l25.17-11.88-2.84 28.27zm21.47 17.06l-34.6 6.32 12.29-22.69zm-35.58 5.93l-22.54-41.38 34.92 18.51zm-1.27-.12l-39.73-19.9 17.08-21.7zM206.6 252.8l-5.06-19.33 24.44-13.23zm-3.35 40.41l-19.31-31.69 21.89-6.65zm39.43-10.04l-19.64 20.99-18-9.14zm29.1-10.46l-7.02 25.24-19.27-15.7zm-1.9-31.72l29.2 23.04L273 271.2zm.58-.87l34.98-6.4-5.58 29.59zm36.24-7.79l2.8-27.95 16.16 21.95zm19.32-5.02l.89 32.02-19.94-25.98zm-43.16-12.53l.67-23.76 24.37 11.94zm-1.05-.02l-34.79-18.44 35.47-5.51zm-54.17 2.96L221.12 194l23.26 2.46zm-.85.88l-24.67 13.36 18.15-37.08zm-21.02 35.2l-21.43 6.5 16.38-25.79zm-3.88 39.2l-26.76-18.96 7.98-11.85zm2.16 2.68l17.98 9.14-22.12 12.22zm40.43-12.9l19.31 15.73-39.24 5.56zm28.34-9.92l28.5 27.56-35.58-2.05zm.76-.73l26.16-7.2 2.35 34.76zm32.81-37.8l20.11 26.2-25.63 3.12zm20.68-6.81l16.99 9.6-16.1 22.25zm.18-1.11l10.52-17.3 6.27 26.79zm-.77-.75l-16.17-21.97 26.83 4.43zm-42.52-35.6l19-17.2 5.45 29.17zm-37.59 5.3l12.05-21.39 23.22 15.9zm-25.16-2.4l13.66-27.78 9.76 30.25zm-20.34 39.15l-16.56-27.64 35.1-10.23zm-17.52 27.79l-8-32.2 24.73 5.85zm-8.82 13.16l-7.35-18.25 15.17 6.63zm28.55 22.07l-4.15 21.4-22.9-40.57zm19.75 10.45l6.28 21.22-28.6-8.9zm41.54-6.09l-6.22 27.57-33.54-21.93zm37.05 1.99l-16.12 14.6-18.76-16.6zm-.58-35.84l24.83 25.08-22.5 9.54zm.5-.99l25.6-3.12-.85 28.13zm43.29-26.53l10.22 34.42-26.56-11.82zm-5.8-29.08l16.68 12.64-10.43 14.04zm-28.7-6.44l15.48-17.72 11.43 22.17zm-.7-.8l-5.41-28.97 20.87 11.27zm-26.45-12.28l-23.11-15.83 42.02-1.28zm-37.47 5.21l-9.69-30.04 21.67 8.78zm-25.2-2.34l-11.22-15.21 24.72-12.24zm-.78.72l-34.37 10.02 23.1-25.32zm-19.7 39.3l-24.5-5.8 8.28-21.27zm-17.47 27.9l-15.5-6.79 7.7-24.6zm-8.64 13.56l-13.34 4.88 5.94-23.25zm23.99 42.64l-29.35-18.18 6.22-22.78zm29.88 10.91l-27.76 4.4-.12-13.07zm-3.53-21.4l32.64 21.35-26.4-.3zm41.3-6.18l18.96 16.78-25.09 10.42zm37.1 2.1l10.02 27.75-26.26-13.03zm.9-.63l22.36-9.47-12.32 37.3zm24.62-39.58l26.56 11.82-27.4 16.4zm17.78-23.47l17.37 12.36-7.42 21.15zm-.2-1.43L356 222.76l6.88 26.38zm-6.96-29.29l4.84-15.02L355 219.96zm-.73-1.17l-11.04-21.42 15.9 6.36zm-32.71-34.12l23.12-1.51-2.91 12.42zm-45.79.4l16.28-20.19 26.1 18.9zm-.96-.48l-8.38-21.96 24.5 1.98zm-1.09.1l-21.41-8.68 13.13-13.03zm-48.1 3.67l9.2-26.56 15.63 14.26zm-24.7 26.33l6.74-27.6 16 2.67zm-10 23.55l-8.65-13.57 16.7-7.13zm-.47 1.23l-7.4 23.6-.98-36.75zm-8.51 27.29l-6.03 23.55-9.46-23.53zm-15.73-1.03l.5-23.13 14.94 23.1zm23.83 21.7l-6 21.96-7.31-17.09zm24.03 42.71l-18.19 2.58-10.4-20.29zm.99.92l.12 12.89-18-10.35zm30.33 9.62l-9.2 19.28-19.19-14.77zm1.08.2l24.32 22.46-33.6-3.03zm1.19-.33l26.65.3-2.43 22.05zm53.54-10.57l1.02 24.92-26.12-14.5zm1.06.1l26.35 13.07-25.34 11.85zm40.87-25.54l13.09 20.46-25.42 16.89zm.92-.53l25.19 3.7-12.26 16.52zm.46-1l26.95-16.12-1.75 19.83zm35.6-38.43l4.75 16.65-11.94 3.85zm-6.34-28.3l13.65 17.18-7.01 8.27zm-12.77-31.3l17.73 12.97-6.1 14.68zm-18.27-8.93l2.86-12.18 12.77 18.42zm-22.6-12.34l-3.07-18.61 26.66 17.07zm-1.17-.62l-25.4-18.41 22.35-.14zm-51.68-21.44l16.59-10.18 7.11 12.1zm-15.93 13.83l-15.35-14.02 28.41 1.07zm-27.01 13.34L192 173.73l25-23.43zM183.2 203.2l-16.32-13.25 23.16-14.74zm-.7.8l-16.85 7.19.82-20.21zm-16.86 47.93l-15.15-23.43 14.1-15.2zm-6.56 27.69l-2.88 14.33-5.67-35.6zm.73 1.7l7.1 16.58-10.14-1.42zm18.42 38.82l-10.7-2.06.68-17.5zm20.21 12.4l-7.6 7.3-10.21-17.55zm.91.58l18.26 14.05-25.85-6.76zm59.58-4.38L285 343.21l-28.5 7.66zm53.9 2.85l-1.43 18.18-23.5-6.53zm26.15-17.21l-14.47 25.34-10.22-8.93zm14.29-17.92l.08 13.27-11.58 2.21zm2.9-22.6l11.77-3.8-13.43 22.57zm8.17-23.33l6.68-7.89-2.18 23.64zm-7.5-29.06l6-14.45 7.77 31.8zm-12.77-30.63l4.22-7.36L361 203.24zm-.73-.84l-12.47-18 16.69 10.64zm-41.85-38.06l12.63-1.35 12.77 17.61zm-25.63-.74l-6.96-11.82 28.43 11.69zm-26.43-2.33l-5.6-9.8 21.93-.22zm-1.12.18l-27.63-1.04 22.02-8.8zm-55.84 22.79l9.9-21.43 14.34-1.3zm-1.78 1.33l-9.04-8.68 19.09-13.06zm-.67.81l-22.5 14.32 13.42-23.04zm-25.43 37.16l-11.28-6.07 12.09-13.7zm-.35 1l-13.7 14.77 2.37-20.86zm-15.49 41.43l-5.8 4.74 6.25-25.14zm.15 1.24l5.91 37.1-12.12-32.02zm18.31 44.2l-.68 17.44-9.47-18.87zm11.64 22.32l9.62 16.53-19.8-18.5zm40.59 27.4l-.42 9.58-25.25-16.29zm1.03.41l13.1 6.13-13.51 3.3zm34.9 3.05l.4 13.44-19.04-9.97zm30.1-8.03l-7.72 18.18-20.43-10.62zm1.21-.16l23.5 6.52-31.2 11.63zm27.18-12.19l9.96 8.7-11.32 8.79zm27.01-18.63l11.49-2.19-25.15 26.14zm13.52-5.07l-.08-12.94 10.58-6.46zm.93-14.78l12.85-21.6-2.36 15.19zm16.38-46.63l1.5 15.87-3.4 4.7zm-7.8-39.77l2.94 2.75 2.94 21.27zm-14.88-24.26l2.76-1.38 7.99 18.1zm-19.32-12.67l4.85-6.94 11.87 17.6zm-.75-.76l-12.65-17.43 17.5 10.48zM300.47 151l-.08-7.58 12.18 6.3zm-1.05-.2l-27.84-11.44 27.76 3.65zm-55.06-12.78l4.43-3.8 14.7 3.61zm-24.2 9.08l8.62-9.29 11.91 1.08zm-2.22.85l-13.05 1.19 21.42-10.21zm-35.88 15l1.44-3.24 11.97-5.95zm-15.72 25.3l-.87-8.44 13.03-12.44zm-.9 1.49l-10.8 12.24 9.95-20.48zM149.34 228l-6.9 1.28 9.11-20.68zm-.22 1.1l-6.6 26.58-.42-25.27zm5.83 66.16l-7.84-6.45-3.76-24.18zm10.99 22.35l-5-1.65-3.36-15zm21.4 20.63l-16.63-9.05-2.67-8.97zm3.03 2.85l7.2 9.18-22.01-17.24zm2.7 1.73l22.9 14.78-14.99-4.69zM235.08 356l17.8 9.31-30.94-6.1zm21.36-3.54l19.92 10.36-19.52 3.1zm52.88-.48l-8.09 6.98-18.2 2.82zm14.95-10.46l.67 4.35-10.45 3.24zm27.12-28l-10.75 19.1-13.68 6.3zm3-3.18l7.2-13.3-7.7 18.24zm15.22-40.99l2.72-3.76-4.37 14.37zm2.62-28.34v-.03l-.04-.28.27-3.56.28 9.22zm-.73-5.24l-2.72-19.7 3.37 11.18zm-8.09-30.73l-4.96-11.25 6.95 13.11zm-14.97-23.23l-9.91-14.7 12.65 13.33zm-33.12-31.79l.67-.48 7.62 5.44zm-1-.57l-5.15-2.67 5.72 2.25zm-43.5-11.24l9.65-1.99 14.54 5.17zm-3.16-.42l-13.15-3.24 22.05 1.4zm-24.9.24l-10.27-.93 14.36-2.59zm-36.2 9.16l5.86-4.4 9.46-2.9zm-4.83 2.3l-5.09 2.53 7.83-4.58zm-21.35 14.64l-6.04 5.77 6.9-7.7zm-28.29 40.69l-2.68 1 10.43-16.94zm-3.5 2.41l2.9-1.07-7.32 16.64zm-7.21 43.17l.15 9.26-.53 1.68zm.5 11.63l2.64 17.02-3.64-13.89zm13.78 35.2l3.4 15.2-10.83-21.32zm10.89 21.71l2.35 7.92-7-9.45zm52.6 40.77l.99 1.57-10.3-4.47zm2.65 2.24l-1.21-1.93 22.4 4.41zm54.85 1.9l-2.35 2.66-12.3-.34zm1.63-.27l14.65-2.27-16.83 4.74zm33.86-12.65l8.59-2.66-15.15 8.32zm13.76-5.7l-.67-4.36 12.55-5.77zm27.2-32.6l-.59 5.86-8.26 9.85zm13.6-24.98l2.12-1.02-9.7 19zm2.83-2.53l-2.38 1.15 4.78-15.75z" fill="url(#b)"/>
+ <path d="M478.98 166.36c-10.46-25.12-31.64-52.25-48.27-60.82 13.54 26.5 21.37 53.1 24.37 72.93l.03.23.04.26c22.7 61.47 10.34 123.97-7.49 162.17-27.57 59.1-94.32 119.67-198.8 116.71-112.9-3.19-212.34-86.85-230.9-196.43-3.39-17.28 0-26.05 1.7-40.08-2.08 10.82-2.87 13.94-3.9 33.16 0 .4-.03.81-.03 1.22C15.73 388.42 123.45 496 256.33 496A240.56 240.56 0 00493.5 296.37c.4-3.07.73-6.16 1.1-9.27 4.78-41.2-.54-84.52-15.61-120.74z" fill="url(#c)"/>
+ <path d="M478.98 166.36c-10.46-25.12-31.64-52.25-48.27-60.82 13.54 26.5 21.37 53.1 24.37 72.93l.03.23.04.26c22.7 61.47 10.34 123.97-7.49 162.17-27.57 59.1-94.32 119.67-198.8 116.71-112.9-3.19-212.34-86.85-230.9-196.43-3.39-17.28 0-26.05 1.7-40.08-2.08 10.82-2.87 13.94-3.9 33.16 0 .4-.03.81-.03 1.22C15.73 388.42 123.45 496 256.33 496A240.56 240.56 0 00493.5 296.37c.4-3.07.73-6.16 1.1-9.27 4.78-41.2-.54-84.52-15.61-120.74z" fill="url(#d)"/>
+ <path d="M17.96 261.41c18.56 109.58 118 193.24 230.9 196.43 104.48 2.96 171.23-57.61 198.8-116.7 17.83-38.2 30.2-100.7 7.5-162.18l-.02-.03-.03-.23a.98.98 0 00-.03-.23l.04.4c8.54 55.66-19.81 109.58-64.12 146.04l-.14.32c-86.34 70.22-168.97 42.36-185.7 30.96a137.2 137.2 0 01-3.5-1.74c-50.34-24.03-71.13-69.84-66.67-109.12-42.51 0-57-35.81-57-35.81s38.16-27.18 88.46-3.54c46.58 21.9 90.33 3.54 90.33 3.54-.09-1.95-41.96-18.59-58.29-34.66-8.72-8.58-12.86-12.72-16.53-15.82a71.5 71.5 0 00-6.23-4.7 281.91 281.91 0 00-4.99-3.3c-17.55-11.4-52.45-10.77-53.6-10.74h-.11c-9.54-12.06-8.87-51.87-8.32-60.18-.12-.52-7.12 3.63-8.03 4.25a175.24 175.24 0 00-23.52 20.13 210.38 210.38 0 00-22.47 26.91l-.01.04v-.04a202.7 202.7 0 00-32.28 72.8c-.11.53-8.66 37.85-4.44 57.2z" fill="url(#e)"/>
+ <path d="M341.21 166.6a130.73 130.73 0 0122.34 29.1c1.32 1 2.56 1.99 3.61 2.96 54.55 50.2 25.97 121.2 23.84 126.25 44.3-36.46 72.66-90.38 64.12-146.04-27.2-67.73-73.34-95.04-111.01-154.5-1.9-3.01-3.81-6.03-5.67-9.2a73.15 73.15 0 01-2.65-4.98A43.74 43.74 0 01332.2.7a.63.63 0 00-.55-.65.82.82 0 00-.45 0l-.12.07-.17.1c-9.31 4.43-64.49 91.69 10.3 166.38z" fill="url(#f)"/>
+ <path d="M248.66 133.65l31.85 2.02 19.35 6.88 16.12 6.33 18.57 13.26 17.5 18.44 6.07 11.47c.82.5 2.5 1.58 3.93 2.55.53.37 1 .74 1.5 1.1a130.73 130.73 0 00-22.33-29.1C266.42 91.9 321.6 4.63 330.9.2l.1-.14c-60.44 35.36-80.95 100.76-82.83 133.49v.18z" fill="url(#g)"/>
+ <path d="M170.74 151.04a243.33 243.33 0 014.99 3.3 111.2 111.2 0 01-.68-58.73c-24.71 11.24-43.94 29.01-57.91 44.7 1.15-.03 36.05-.66 53.6 10.73z" fill="url(#h)"/>
+ <path d="M148.32 277.45s11.11-41.34 79.53-41.34c7.4 0 28.54-20.61 28.93-26.59 0 0-43.75 18.35-90.33-3.54-50.3-23.64-88.45 3.54-88.45 3.54s14.5 35.81 57 35.81c-4.45 39.28 16.34 85.09 66.68 109.12 1.13.54 2.19 1.12 3.34 1.64-29.4-15.17-53.66-43.83-56.7-78.64z" fill="url(#i)"/>
+ <path d="M367.16 198.66a47.53 47.53 0 00-3.61-2.95c-.49-.37-.97-.74-1.5-1.1-12.85-9.02-35.85-17.92-58.01-14.07 86.54 43.21 63.3 192.02-56.62 186.4a106.97 106.97 0 01-31.3-6.03 133 133 0 01-7.07-2.89 90.84 90.84 0 01-4.05-1.93l.16.1c16.73 11.4 99.35 39.25 185.7-30.97l.13-.31c2.14-5.05 30.72-76.05-23.83-126.25z" fill="url(#j)"/>
+ <path d="M478.96 166.35c-10.46-25.12-31.64-52.25-48.27-60.82 13.54 26.5 21.37 53.1 24.37 72.93l.04.4c-27.2-67.73-73.34-95.04-111.01-154.5-1.9-3.01-3.81-6.03-5.67-9.2a73.11 73.11 0 01-2.65-4.98 43.76 43.76 0 01-3.59-9.5.63.63 0 00-.54-.64.82.82 0 00-.46 0l-.12.07-.17.1.1-.14c-60.44 35.36-80.94 100.95-82.82 133.67l.49-.08 31.85 2.02 17.93 6.38 17.55 6.83 18.57 13.26 17.47 18.43s6.01 11.29 6.1 11.48c-13.95-8.3-34.21-14.98-54.1-11.52 86.54 43.21 63.3 192.02-56.61 186.4a106.94 106.94 0 01-31.32-6.04 131.8 131.8 0 01-7.07-2.88 91.96 91.96 0 01-4.05-1.94l.16.1a137.2 137.2 0 01-3.5-1.74c1.13.54 2.18 1.12 3.34 1.64-29.39-15.17-53.65-43.83-56.68-78.64 0 0 11.1-41.34 79.53-41.34 7.4 0 28.54-20.61 28.93-26.59-.09-1.95-41.97-18.59-58.29-34.65-8.72-8.59-12.87-12.73-16.53-15.83a71.5 71.5 0 00-6.24-4.7 111.2 111.2 0 01-.67-58.73c-24.72 11.24-43.94 29-57.92 44.7h-.1c-9.55-12.07-8.87-51.87-8.33-60.18-.11-.52-7.12 3.63-8.03 4.25a175.3 175.3 0 00-23.52 20.12 210.46 210.46 0 00-22.46 26.91.26.26 0 01-.02.04.27.27 0 00.02-.04 202.72 202.72 0 00-32.29 72.8l-.32 1.59a397.49 397.49 0 00-2.77 15.15c-.03.18.02-.18 0 0a279.13 279.13 0 00-3.55 33.53l-.03 1.23c0 132.7 107.72 240.28 240.6 240.28a240.57 240.57 0 00237.16-199.62c.4-3.07.73-6.16 1.1-9.27 4.78-41.2-.54-84.52-15.61-120.74zm-23.87 12.34l.04.26v-.03l-.04-.23z" fill="url(#k)"/>
+ <path d="M256.78 209.52s-1.02.43-2.9 1.07c-4.7 7.95-21.4 23.1-27.8 23.08-68.42-.3-79.7 40.58-79.7 40.58a87.34 87.34 0 0013.4 39.27 88.03 88.03 0 01-11.46-36.07s11.11-41.34 79.53-41.34c7.4 0 28.54-20.6 28.93-26.59z" fill="url(#l)"/>
+ <path d="M455.15 178.96v-.03l-.04-.23a2.12 2.12 0 00-.03-.23 5.8 5.8 0 00.04.4 239.04 239.04 0 00-5.17-11.9c-.6-1.31-1.27-2.54-1.9-3.82-1.2-2.47-2.4-4.94-3.65-7.3-.78-1.45-1.6-2.83-2.39-4.25-1.16-2.07-2.31-4.16-3.51-6.16-.88-1.47-1.79-2.87-2.68-4.3-1.17-1.86-2.33-3.72-3.53-5.53-.95-1.43-1.92-2.81-2.89-4.2-1.2-1.73-2.39-3.45-3.6-5.13-1-1.4-2.03-2.75-3.05-4.1a349.15 349.15 0 00-3.68-4.83q-1.58-2.03-3.2-4.03a486.63 486.63 0 00-3.75-4.62q-1.64-2-3.3-3.96l-1.8-2.16c.26.3.5.63.77.94-20.85-24.6-43.47-47.26-63.68-79.15-1.9-3-3.81-6.02-5.67-9.2a72.06 72.06 0 01-2.65-4.97 49.1 49.1 0 01-3.3-8.44l-.15-.5-.13-.57a.63.63 0 00-.55-.64.85.85 0 00-.45 0c-.04.01-.09.05-.12.07l-.18.09a7.4 7.4 0 00-1.46 1.07 17.27 17.27 0 00-.35.31 25.04 25.04 0 00-1.9 2.01l-.07.07.02-.01c-14.41 16.94-51.23 90.58 8.1 156.63-55.54-65.34-20.54-135.96-5.33-154.46a53.66 53.66 0 002.96 7.27 74.3 74.3 0 002.65 4.97c1.86 3.18 3.77 6.2 5.67 9.2 37.68 59.46 83.8 86.78 111.02 154.5a9.12 9.12 0 01-.05-.4 1.7 1.7 0 01.03.23l.04.24v.02c22.7 61.47 10.34 123.98-7.48 162.17a199.36 199.36 0 01-38.22 54.8 199.8 199.8 0 0041.13-57.7c17.82-38.2 30.18-100.7 7.48-162.17z" fill="url(#m)"/>
+ <path d="M330.9.2c.04-.04.1-.12.1-.14-54.44 31.85-76.47 88.08-81.68 122.84 5.98-32.12 26.57-81.17 72.7-112.14 3.8-6 7.15-9.73 8.89-10.56z" fill="url(#n)"/>
+ <path d="M390.86 325.23l.14-.32c2.13-5.05 30.7-76.05-23.84-126.25a47.53 47.53 0 00-3.61-2.96c-.49-.36-.97-.73-1.5-1.1-12.42-8.7-34.32-17.31-55.78-14.4h-.05q-1.1.15-2.18.34c85.56 42.72 63.8 188.67-52.6 186.53l.95.05c118 4.62 141.26-140.09 59.27-185.46 17.77-.17 34.95 6.32 45.57 13.16.54.35 1.03.7 1.53 1.04a47.2 47.2 0 013.73 2.81c56.47 48.03 30.7 120.1 28.76 125.23a72.28 72.28 0 01-.12.32" fill="url(#o)"/>
+ <path d="M167.09 208.38c39.38 17.06 75.93 6.22 87 2.14-10.2 3.52-47.89 14.14-87.64-4.54-45.22-21.25-80.62-1.44-87.32 2.78l-.32.2-.26.17-.2.13-.13.08-.09.06.12.26.14.31v.06l.21.45c.34.72.86 1.75 1.58 3 5.34 9.45 21.43 31.63 54.89 31.63-4.32 38.11 15.12 82.35 62.24 106.9-45-26-64-73-58.7-110.49-34.76-.92-50.31-21.76-54.75-30.59a95.75 95.75 0 0183.23-2.55z" fill="url(#p)"/>
+ <path d="M175.69 154.2l-1.18-.68 1.22.82-.04-.13zm-.25-.88a111.35 111.35 0 01-.39-57.71c-23.43 10.65-41.92 27.17-55.68 42.23 13.1-12.77 30.1-26.47 50.78-35.86-1.28 9.82-2.39 29.93 5.3 51.34z" fill="url(#q)"/>
+ <path d="M23.76 286.6l-.2-.69a220.74 220.74 0 01-5.6-24.5c-1.84-8.43-1.25-20.27 0-30.99-.93 5.57-1.51 11.23-2.2 24.07 0 .4-.03.81-.03 1.22C15.73 360.73 83.2 450 177.2 482.68 95.72 451.48 35.35 376.8 23.76 286.6z" fill="url(#r)"/>
+ <path d="M21.83 262.38c-4.21-19.35 4.33-56.67 4.45-57.2a202.7 202.7 0 0132.28-72.8v0a210.46 210.46 0 0122.47-26.9 175.23 175.23 0 0123.51-20.13 29.2 29.2 0 011.02-.63c-.08 18.28.95 49.12 9.42 59.23h.11c1.16-.06 36.18-1.6 54.1 9.33a273.28 273.28 0 015.09 3.16 71.85 71.85 0 016.38 4.53c3.76 3.01 8.03 7.04 17 15.4 13 12.08 41.36 24.11 53.6 29.84-11.92-6-40.07-18.84-52.77-31.34-8.72-8.58-12.86-12.72-16.53-15.83a71.5 71.5 0 00-6.23-4.7 281.91 281.91 0 00-4.99-3.3c-17.55-11.39-52.45-10.76-53.6-10.73h-.11c-8.72-11.03-8.9-45.22-8.46-57.36l.06-1.3.02-.43a119.8 119.8 0 01.02-.36l.04-.73c0-.03-.04-.05-.1-.04h-.07a1.02 1.02 0 00-.15.04l-.13.05a4.26 4.26 0 00-.39.15l-.23.11a16.6 16.6 0 00-.5.24l-.3.15-.24.13-.36.19-.2.1a95.1 95.1 0 00-5.36 3.13 175.24 175.24 0 00-23.52 20.12 210.38 210.38 0 00-22.47 26.91l-.01.04v-.04a202.7 202.7 0 00-32.28 72.8c-.11.52-8.66 37.84-4.44 57.2 15.8 93.25 90.17 167.72 181.56 189.81-89.6-23.23-162.11-96.9-177.69-188.84z" fill="url(#s)"/>
+ <path d="M478.98 166.36c-9.23-22.17-26.8-45.88-42.23-57.05l-.43-.3q-.71-.5-1.43-.98l-.54-.36q-.67-.43-1.34-.83l-.51-.31a42.52 42.52 0 00-1.79-.99c.65 1.27 1.28 2.55 1.9 3.82a394.89 394.89 0 011.15 2.37c11.67 24.42 18.56 48.45 21.32 66.75l.04.4c8.54 55.66-19.81 109.58-64.12 146.04l-.14.3c-55.08 44.8-108.64 49.69-144.55 44.4 35.78 6.68 92.23 3.86 150.37-43.42l.13-.31c44.31-36.46 72.66-90.39 64.13-146.04a12.55 12.55 0 01-.05-.4c-2.45-16.25-8.16-37.01-17.57-58.54a159 159 0 0126.32 37.48c17.04 34.76 24.73 77.04 22.23 117.82-.19 3.07-.34 6.13-.58 9.17-13.07 112.5-106.97 202.84-225.78 209.5a245.66 245.66 0 01-47.5-1.93 242.63 242.63 0 0038.32 3.05c119 0 217.82-86.29 237.16-199.63.4-3.07.73-6.16 1.1-9.27 4.78-41.2-.54-84.52-15.61-120.74zm-23.83 12.6v-.03l-.04-.23.04.26z" fill="url(#t)"/>
+</svg>
diff --git a/devtools/startup/aboutdevtools/images/external-link.svg b/devtools/startup/aboutdevtools/images/external-link.svg
new file mode 100644
index 0000000000..956c09ba63
--- /dev/null
+++ b/devtools/startup/aboutdevtools/images/external-link.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <path fill="context-fill #003eaa" d="M14.923 1.618A1 1 0 0 0 14 1H9a1 1 0 0 0 0 2h2.586L8.293 6.293a1 1 0 1 0 1.414 1.414L13 4.414V7a1 1 0 0 0 2 0V2a1 1 0 0 0-.077-.382z"/>
+ <path fill="context-fill #0060df" d="M14 10a1 1 0 0 0-1 1v2H3V3h2a1 1 0 0 0 0-2H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1z"/>
+</svg>
diff --git a/devtools/startup/aboutdevtools/images/feature-console.svg b/devtools/startup/aboutdevtools/images/feature-console.svg
new file mode 100644
index 0000000000..f24cad3606
--- /dev/null
+++ b/devtools/startup/aboutdevtools/images/feature-console.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg viewBox="0 0 220 120" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path fill-rule="evenodd" d="M159.832 106.154c6.121 0 11.427-4.63 11.427-10.749v-72a4.896 4.896 0 00-3.024-4.532 4.904 4.904 0 00-1.876-.373H54.399a4.899 4.899 0 00-4.9 4.903v.001l.001 72c0 6.12 5.306 10.75 11.427 10.75h98.905zM54.5 95.404c0 2.994 2.688 5.75 6.427 5.75h98.905c3.739 0 6.427-2.756 6.427-5.749V37.462H54.5v57.942z" fill="#0080FF"/>
+ <path fill-rule="evenodd" d="M61.117 28.88a3.19 3.19 0 01-3.187 3.191 3.19 3.19 0 010-6.381 3.19 3.19 0 013.187 3.19zm10.628 0a3.188 3.188 0 11-5.442-2.254 3.19 3.19 0 013.474-.693 3.188 3.188 0 011.968 2.947zm10.385 0a3.19 3.19 0 01-3.187 3.191 3.19 3.19 0 010-6.381 3.19 3.19 0 013.187 3.19" fill="#fff"/>
+ <path d="M77.575 88h12.586M65.863 74.33l6.123 6.127-6.123 6.13V74.33z" stroke="#00C7D8" stroke-width="3" stroke-linecap="round"/>
+ <path d="M156.5 47h-43a1.5 1.5 0 000 3h43a1.5 1.5 0 000-3zM102.5 47h-37a1.5 1.5 0 000 3h37a1.5 1.5 0 000-3zM144.5 59h-23a1.5 1.5 0 000 3h23a1.5 1.5 0 000-3zM112.5 59h-47a1.5 1.5 0 000 3h47a1.5 1.5 0 000-3z" fill="#1C2142"/>
+</svg>
diff --git a/devtools/startup/aboutdevtools/images/feature-debugger.svg b/devtools/startup/aboutdevtools/images/feature-debugger.svg
new file mode 100644
index 0000000000..d38a1aff6b
--- /dev/null
+++ b/devtools/startup/aboutdevtools/images/feature-debugger.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg viewBox="0 0 220 120" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M169.5 73.68h-119a2.5 2.5 0 000 5h119a2.5 2.5 0 100-5z" fill="#00C7D8"/>
+ <path d="M81.73 89.91H51.5a2.5 2.5 0 000 5h30.23a2.5 2.5 0 000-5z" fill="#0080FF"/>
+ <path d="M136.15 89.91H95.78a2.5 2.5 0 000 5h40.37a2.5 2.5 0 100-5z" fill="#1C2142"/>
+ <path d="M153.2 45.44v-3.97c0-3.04 2.45-5.5 5.47-5.5h10.62M141.37 55h7.78m7.17 0h7.78m-67.83 0h8.88m8.17 0h8.88M99.57 27h4.5c3.45 0 6.24 2.8 6.24 6.27v12.2M55.85 55h3.46m-7.22-11.82l2.7-4.15a6.8 6.8 0 019.43-2l11.1 7.3m-1.47-10.54l2.32 11.09-11.03 2.33m98.58-17.66l6.38 6.42-6.38 6.42V29.55zm-46.13 9.53l-7.29 7.32-7.29-7.32h14.58z" stroke="#0080FF" stroke-width="4" stroke-linecap="round"/>
+</svg>
diff --git a/devtools/startup/aboutdevtools/images/feature-inspector.svg b/devtools/startup/aboutdevtools/images/feature-inspector.svg
new file mode 100644
index 0000000000..953c498983
--- /dev/null
+++ b/devtools/startup/aboutdevtools/images/feature-inspector.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg viewBox="0 0 220 120" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M134.97 77.52h-64.4a8.92 8.92 0 01-8.87-8.96V44.42a8.92 8.92 0 018.88-8.96h64.4c4.9 0 8.87 4 8.87 8.96v24.14a8.91 8.91 0 01-8.88 8.96" fill="#0080FF"/>
+ <path d="M175.4 90.58H30.6M49.64 8.61v97.13V8.61zm105.54 0v97.13V8.61zm20.22 12.95H30.6h144.8z" stroke="#0080FF" stroke-width="4" stroke-linecap="round"/>
+ <path d="M122.97 113.11a15.27 15.27 0 01-15.25-15.32V58.96h66.7c8.42 0 15.25 6.86 15.25 15.32v23.51c0 8.46-6.83 15.32-15.25 15.32h-51.45z" fill="#1C2142"/>
+ <path d="M179.15 72.36h-12.7a1.47 1.47 0 100 2.94h12.7a1.47 1.47 0 100-2.94zM129.31 96.9h-12.7a1.47 1.47 0 100 2.94h12.7a1.47 1.47 0 100-2.94zM149.83 96.9h-12.7a1.47 1.47 0 100 2.94h12.7a1.47 1.47 0 100-2.94zM157.65 72.36h-41.04a1.47 1.47 0 100 2.94h41.04a1.47 1.47 0 100-2.94zM179.15 85.12h-62.54a1.47 1.47 0 100 2.94h62.54a1.47 1.47 0 100-2.94z" fill="#00C7D8"/>
+</svg>
diff --git a/devtools/startup/aboutdevtools/images/feature-memory.svg b/devtools/startup/aboutdevtools/images/feature-memory.svg
new file mode 100644
index 0000000000..f2bc1e4fbc
--- /dev/null
+++ b/devtools/startup/aboutdevtools/images/feature-memory.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg viewBox="0 0 220 120" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M152.49 65.74h2.5V64.2l-1.38-.7-1.12 2.24zm0-19.47l1.13 2.23 1.37-.7v-1.53h-2.5zM71 45.52h-2.5v1.54l1.37.7L71 45.51zm0 19.46l-1.13-2.23-1.37.7v1.53H71zm4.87 42.44h71.75v-5H75.87v5zm71.75 0c4.07 0 7.37-3.3 7.37-7.36h-5c0 1.3-1.06 2.36-2.37 2.36v5zm7.37-7.36V65.74h-5v34.32h5zM153.6 63.5c-3.3-1.67-5.17-4.07-5.17-7.5h-5c0 5.84 3.46 9.7 7.92 11.96l2.25-4.46zm-5.17-7.5c0-3.45 1.88-5.84 5.18-7.5l-2.26-4.47c-4.46 2.25-7.92 6.12-7.92 11.96h5zm6.55-9.74v-24.4h-5v24.4h5zm0-24.4c0-4.07-3.3-7.37-7.37-7.37v5c1.31 0 2.37 1.06 2.37 2.36h5zm-7.37-7.37H75.87v5h71.75v-5zm-71.75 0a7.36 7.36 0 00-7.37 7.36h5c0-1.3 1.06-2.36 2.37-2.36v-5zm-6 33.25c3.3 1.66 5.18 4.06 5.18 7.5h5c0-5.84-3.46-9.7-7.92-11.96l-2.26 4.46zm5.18 7.5c0 3.44-1.88 5.84-5.18 7.5l2.26 4.47c4.46-2.26 7.92-6.12 7.92-11.97h-5zm-6.55 9.73v35.52h5V64.98h-5zm0-43.12v23.66h5V21.86h-5zm0 78.64c0 1.63.49 3.41 1.82 4.8 1.35 1.42 3.28 2.12 5.55 2.12v-5c-1.23 0-1.73-.36-1.94-.57-.23-.24-.43-.67-.43-1.35h-5z" fill="#0080FF"/>
+ <path d="M118 85v5.22M129 85v5.22M107 85v5.22M96 85v5.22" stroke="#00C7D8" stroke-width="3" stroke-linecap="round"/>
+ <path d="M130.32 45h4.65m-4.65 8h4.65m-4.65 9h4.65M91 45h4.65M91 53h4.65M91 62h4.65M104 37.08v-4.63m8.5 4.63v-4.63m8.5 4.63v-4.63m-17 41.84v-4.63m8.5 4.63v-4.63m8.5 4.63v-4.63" stroke="#0022A9" stroke-width="4" stroke-linecap="round"/>
+ <path d="M125.9 37H99.76A3.76 3.76 0 0096 40.75v23.84a3.76 3.76 0 003.75 3.74h26.16a3.76 3.76 0 003.75-3.74V40.75a3.76 3.76 0 00-3.75-3.75z" stroke="#0022A9" stroke-width="4" stroke-linecap="round"/>
+</svg>
diff --git a/devtools/startup/aboutdevtools/images/feature-network.svg b/devtools/startup/aboutdevtools/images/feature-network.svg
new file mode 100644
index 0000000000..d37e7d5db7
--- /dev/null
+++ b/devtools/startup/aboutdevtools/images/feature-network.svg
@@ -0,0 +1,10 @@
+<!-- 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/. -->
+<svg viewBox="0 0 220 120" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M48.47 33.67H170.1M164 38.7V30m-36 8.7V30m-37 8.7V30m-34 8.7V30" stroke="#031537" stroke-width="5" stroke-linecap="round"/>
+ <path d="M102.57 55H51.5a2.5 2.5 0 000 5h51.07a2.5 2.5 0 100-5z" fill="#0080FF"/>
+ <path d="M163.58 87H135.5a2.5 2.5 0 100 5h28.08a2.5 2.5 0 100-5z" fill="#00C7D8"/>
+ <path d="M144.58 55H116.5a2.5 2.5 0 100 5h28.08a2.5 2.5 0 100-5z" fill="#0080FF"/>
+ <path fill-rule="evenodd" d="M89 73.5a2.5 2.5 0 012.51-2.5h82.78a2.5 2.5 0 110 5H91.51A2.5 2.5 0 0189 73.5z" fill="#1C2142"/>
+</svg>
diff --git a/devtools/startup/aboutdevtools/images/feature-performance.svg b/devtools/startup/aboutdevtools/images/feature-performance.svg
new file mode 100644
index 0000000000..520527c91a
--- /dev/null
+++ b/devtools/startup/aboutdevtools/images/feature-performance.svg
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+<svg viewBox="0 0 220 120" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M156.54 32.33v57.72c0 4.05-3.45 7.34-7.7 7.34H57.16c-4.26 0-7.7-3.29-7.7-7.34V32.33" stroke="#0080FF" stroke-width="5"/>
+ <path fill-rule="evenodd" d="M154.44 18.45H51.55a2.1 2.1 0 00-2.1 2.1v10.33h107.09V20.56a2.1 2.1 0 00-2.1-2.1z" fill="#0080FF"/>
+ <path d="M154.44 18.45H51.55a2.1 2.1 0 00-2.1 2.1v10.33h107.09V20.56a2.1 2.1 0 00-2.1-2.1z" stroke="#0080FF" stroke-width="5"/>
+ <path fill-rule="evenodd" d="M57.32 25.44a2.8 2.8 0 01-2.75 2.84 2.8 2.8 0 01-2.75-2.84 2.8 2.8 0 012.75-2.84 2.8 2.8 0 012.75 2.84zm9.17 0a2.8 2.8 0 01-2.75 2.84A2.8 2.8 0 0161 25.44a2.8 2.8 0 012.75-2.84 2.8 2.8 0 012.75 2.84zm8.96 0a2.8 2.8 0 01-2.75 2.84 2.8 2.8 0 01-2.75-2.84 2.8 2.8 0 012.75-2.84 2.8 2.8 0 012.75 2.84zM128.79 103.63h49.97a34.28 34.28 0 002.85-13.73A34.54 34.54 0 00147 55.4h-.17a34.54 34.54 0 00-34.6 34.49c0 4.88 1.02 9.52 2.85 13.73h13.7z" fill="#fff"/>
+ <path d="M128.79 103.63h49.97a34.28 34.28 0 002.85-13.73A34.54 34.54 0 00147 55.4h-.17a34.54 34.54 0 00-34.6 34.49c0 4.88 1.02 9.52 2.85 13.73h13.7v0z" stroke="#00C7D8" stroke-width="5" stroke-linecap="round"/>
+ <path d="M124.08 91.3h6.04m16.42-25.26v6.5-6.5zm-17.5 8.33l4.27 4.25-4.27-4.25z" stroke="#0080FF" stroke-width="3" stroke-linecap="round"/>
+ <path fill-rule="evenodd" d="M155.2 87.76c0 4.56-3.7 8.24-8.28 8.24a8.26 8.26 0 01-8.26-8.24 8.25 8.25 0 0116.54 0z" fill="#0022A9"/>
+ <path fill-rule="evenodd" d="M146.92 87.76l17.37-17.3z" fill="#005482"/>
+ <path d="M146.92 87.76l17.37-17.3" stroke="#137EFF" stroke-width="3" stroke-linecap="round"/>
+</svg>
diff --git a/devtools/startup/aboutdevtools/images/feature-responsive.svg b/devtools/startup/aboutdevtools/images/feature-responsive.svg
new file mode 100644
index 0000000000..e76870ea7b
--- /dev/null
+++ b/devtools/startup/aboutdevtools/images/feature-responsive.svg
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+<svg viewBox="0 0 220 120" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M119.5 52h-60a1.5 1.5 0 000 3h60a1.5 1.5 0 000-3zM114.5 65h-55a1.5 1.5 0 000 3h55a1.5 1.5 0 000-3zM97.5 40h-38a1.5 1.5 0 000 3h38a1.5 1.5 0 000-3zM97.5 77h-38a1.5 1.5 0 000 3h38a1.5 1.5 0 000-3z" fill="#00C7D8"/>
+ <path d="M156.53 28.7v59.15c0 4.16-3.55 7.52-7.92 7.52H54.38c-4.37 0-7.92-3.36-7.92-7.52V28.69" stroke="#0080FF" stroke-width="5"/>
+ <path fill-rule="evenodd" d="M154.37 14.46H48.63c-1.2 0-2.17.97-2.17 2.16V27.2h110.07V16.62c0-1.2-.97-2.16-2.16-2.16" fill="#0080FF"/>
+ <path d="M154.37 14.46H48.63c-1.2 0-2.17.97-2.17 2.16V27.2h110.07V16.62c0-1.2-.97-2.16-2.16-2.16v0z" stroke="#0080FF" stroke-width="5"/>
+ <path fill-rule="evenodd" d="M54.55 21.63c0 1.6-1.27 2.91-2.83 2.91a2.87 2.87 0 01-2.82-2.91c0-1.6 1.26-2.91 2.82-2.91 1.56 0 2.83 1.3 2.83 2.91zm9.43 0c0 1.6-1.27 2.91-2.83 2.91a2.87 2.87 0 01-2.83-2.91c0-1.6 1.27-2.91 2.83-2.91s2.83 1.3 2.83 2.91zm9.2 0c0 1.6-1.26 2.91-2.82 2.91a2.87 2.87 0 01-2.83-2.91c0-1.6 1.27-2.91 2.83-2.91s2.83 1.3 2.83 2.91zM131.85 25.85h51.02v82.53h-51.02V25.85z" fill="#fff"/>
+ <path d="M173.5 47.27h-32.2a1.5 1.5 0 000 3h32.2a1.5 1.5 0 100-3zM168.6 56.27h-27.3a1.5 1.5 0 000 3h27.3a1.5 1.5 0 100-3zM164.32 37.27H141.3a1.5 1.5 0 000 3h23.02a1.5 1.5 0 100-3zM160.32 66.27H141.3a1.5 1.5 0 000 3h19.02a1.5 1.5 0 100-3z" fill="#1C2142"/>
+ <path d="M174.15 106.93h-34.49a9.27 9.27 0 01-9.26-9.23V33.64a9.27 9.27 0 019.26-9.24h34.49c5.1 0 9.26 4.16 9.26 9.24V97.7a9.28 9.28 0 01-9.26 9.23v0z" stroke="#0E59E1" stroke-width="5" stroke-linecap="round"/>
+ <path d="M183.41 82h-53" stroke="#0E59E1" stroke-width="4" stroke-linecap="round"/>
+ <path fill-rule="evenodd" d="M156.4 97.68a3.94 3.94 0 110-7.88 3.94 3.94 0 010 7.88z" fill="#0080FF"/>
+</svg>
diff --git a/devtools/startup/aboutdevtools/images/feature-storage.svg b/devtools/startup/aboutdevtools/images/feature-storage.svg
new file mode 100644
index 0000000000..f816ee5ca4
--- /dev/null
+++ b/devtools/startup/aboutdevtools/images/feature-storage.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg viewBox="0 0 220 120" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M146.02 101.91h-69.6c-4.95 0-8.99-3.22-8.99-7.15V81.88c0-3.93 4.04-7.15 8.99-7.15h69.6c4.94 0 8.98 3.22 8.98 7.15v12.88c0 3.93-4.04 7.15-8.98 7.15h0z" stroke="#0022A9" stroke-width="5" stroke-linecap="round"/>
+ <path d="M146.02 74.73h-69.6c-4.95 0-8.99-3.21-8.99-7.15V54.7c0-3.93 4.04-7.15 8.99-7.15h69.6c4.94 0 8.98 3.22 8.98 7.15v12.88c0 3.94-4.04 7.15-8.98 7.15h0z" stroke="#137EFF" stroke-width="5" stroke-linecap="round"/>
+ <path d="M146.02 47.56h-69.6c-4.95 0-8.99-3.22-8.99-7.15V27.53c0-3.94 4.04-7.15 8.99-7.15h69.6c4.94 0 8.98 3.21 8.98 7.15V40.4c0 3.93-4.04 7.15-8.98 7.15h0z" stroke="#00C7D8" stroke-width="5" stroke-linecap="round"/>
+ <path fill-rule="evenodd" d="M81.8 37.63a3.8 3.8 0 11.01-7.62 3.8 3.8 0 010 7.62zm3.8 23.18a3.8 3.8 0 11-7.61.02 3.8 3.8 0 017.61-.02zm-3.8 30.82a3.8 3.8 0 11.01-7.62 3.8 3.8 0 010 7.62z" fill="#1C2142"/>
+</svg>
diff --git a/devtools/startup/aboutdevtools/images/feature-visualediting.svg b/devtools/startup/aboutdevtools/images/feature-visualediting.svg
new file mode 100644
index 0000000000..18c0834916
--- /dev/null
+++ b/devtools/startup/aboutdevtools/images/feature-visualediting.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg viewBox="0 0 220 120" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M74.9 77.01l76.85-33.52" stroke="#00C7D8" stroke-width="3" stroke-linecap="round"/>
+ <path d="M119.1 27h-15.8 15.8zm-17.28.38a7.38 7.38 0 10-14.76 0 7.38 7.38 0 0014.76 0h0zM108.83 90.88h15.8-15.8zm17.28 1.38a7.38 7.38 0 1114.75 0 7.38 7.38 0 01-14.75 0h0z" stroke="#0022A9" stroke-width="4" stroke-linecap="round"/>
+ <path d="M71 93.02c18.14 4.2 36.25-7.22 40.47-25.47l.9-3.94 2-8.6c4.2-18.25 17.2-30.19 35.33-26" stroke="#0080FF" stroke-width="5" stroke-linecap="round"/>
+ <path d="M71.16 99.3a6.15 6.15 0 100-12.3 6.15 6.15 0 000 12.3zM150.16 33.3a6.15 6.15 0 100-12.3 6.15 6.15 0 000 12.3z" fill="#0080FF"/>
+</svg>
diff --git a/devtools/startup/aboutdevtools/images/otter.svg b/devtools/startup/aboutdevtools/images/otter.svg
new file mode 100644
index 0000000000..25e251c83b
--- /dev/null
+++ b/devtools/startup/aboutdevtools/images/otter.svg
@@ -0,0 +1,29 @@
+<!-- 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/. -->
+<svg width="566" height="368" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M115.24 184.24a35.8 35.8 0 00-17.78 5.28 32.24 32.24 0 01-17.78 4.82 32.22 32.22 0 01-17.78-4.82 36 36 0 00-19.7-5.32 36 36 0 00-19.68 5.3 32.22 32.22 0 01-17.78 4.82 2 2 0 000 4 36 36 0 0019.68-5.3 32.22 32.22 0 0117.78-4.82 32.22 32.22 0 0117.8 4.82 36 36 0 0019.68 5.3 36 36 0 0019.68-5.3 32.22 32.22 0 0117.78-4.82h.76v-.76c-.96-1-1.84-2.08-2.66-3.2zm-22.5-3.56a1 1 0 00.4 0l1.84-.88A1.02 1.02 0 1094 178l-1.76.84a1.02 1.02 0 10.4 2l.1-.16zm-51.72-7.34a33.04 33.04 0 0118.26 4.94c1.26.68 2.54 1.38 3.9 2a1 1 0 00.42 0 1.02 1.02 0 10.42-2 56.9 56.9 0 01-3.8-2 35.16 35.16 0 00-19.2-5.18 1 1 0 100 2v.24zm38.1 8.14a1 1 0 100 2c2.05-.02 4.1-.2 6.12-.52a1 1 0 00.43-1.82 1.01 1.01 0 00-.75-.18c-1.92.32-3.86.5-5.8.52zm37.7 18.22a1 1 0 100 2c1.38 0 2.62 0 3.8.18a8 8 0 01-1.38-2c-.8-.14-1.56-.18-2.42-.18zm294.46 27.5a36 36 0 00-19.68 5.3 32.22 32.22 0 01-17.78 4.82 2 2 0 000 4 36 36 0 0019.68-5.3 32.22 32.22 0 0117.6-4.82c.28-1.36.58-2.7.92-4h-.74zm149.84 0a36 36 0 00-19.68 5.3 35.15 35.15 0 01-35.56 0 36 36 0 00-19.68-5.3 34.64 34.64 0 00-16.1 3.44 135 135 0 01.88 4 30.75 30.75 0 0115.22-3.48A32.22 32.22 0 01504 236a39.12 39.12 0 0039.36 0 32.22 32.22 0 0117.78-4.82 2 2 0 100-4l-.02.02zm-80.9-12.6a1 1 0 00-.62 1.74 1 1 0 00.72.26h.1c1.51-.16 3.04-.24 4.56-.24a1 1 0 00.7-1.7 1 1 0 00-.7-.3c-1.59 0-3.18.08-4.76.24zm-70.22-.26a1 1 0 100 2c1.85 0 3.7.12 5.54.36.28-.68.56-1.3.84-2-2.11-.28-4.25-.4-6.38-.36zm98.9 35.5a56.9 56.9 0 01-3.8-2 35.16 35.16 0 00-19.2-5.18 1 1 0 100 2 33.06 33.06 0 0118.26 4.94c1.26.68 2.54 1.38 3.9 2a1 1 0 00.42 0 1.02 1.02 0 10.42-2v.24zm51.88-7.14c-1.58 0-3.16.08-4.74.24a1 1 0 00-.62 1.74 1 1 0 00.72.26h.1a38.6 38.6 0 014.56-.22 1 1 0 00.7-1.7 1 1 0 00-.7-.3l-.02-.02zm-21.8 6.58l-1.76.84a1 1 0 00.08 1.8c.24.1.5.11.74.02l1.84-.88a1 1 0 10-.9-1.78zm-9.16 3.06c-1.93.3-3.87.47-5.82.48a1 1 0 100 2c2.05-.02 4.1-.2 6.12-.52a1.01 1.01 0 10-.32-2l.02.04zm-379.48 11.38A64.28 64.28 0 01138 221.54c-23.52 8-38.3 19.48-38.3 32.3 0 24 52 43.5 116 43.5 3.75 0 7.45-.07 11.12-.2-1.52-.88-3-1.78-4.4-2.7-30.86.88-57.04-10.18-72.08-30.72zm164.1 70.78a36 36 0 00-19.68 5.3 35.15 35.15 0 01-35.56 0 39.12 39.12 0 00-39.36 0 35.15 35.15 0 01-35.56 0 36 36 0 00-19.68-5.3 36 36 0 00-19.68 5.3 32.22 32.22 0 01-17.78 4.82 2 2 0 000 4 36 36 0 0019.68-5.3 35.14 35.14 0 0135.56 0 39.14 39.14 0 0039.36 0 35.16 35.16 0 0135.56 0 39.12 39.12 0 0039.36 0 32.22 32.22 0 0117.78-4.82 2 2 0 100-4zm-80.68-10.62c1.52-.16 3.04-.24 4.56-.24a1 1 0 00.7-1.7 1 1 0 00-.7-.3c-1.6 0-3.2.07-4.78.24a1 1 0 00-.62 1.74 1 1 0 00.72.26h.12zM215.12 331a1 1 0 00.4 0l1.84-.88a1 1 0 10-.9-1.78l-1.76.84a1.02 1.02 0 10.4 2l.02-.18z" fill="#EAEAEE"/>
+ <path d="M201.54 333.8c2.05-.02 4.1-.2 6.12-.52a1.03 1.03 0 00.66-.4 1.02 1.02 0 00.04-1.13 1.02 1.02 0 00-1.02-.47c-1.93.3-3.87.47-5.82.48a1 1 0 100 2l.02.04zm-19.88-5.2c1.26.68 2.54 1.38 3.9 2a1 1 0 00.42 0 1.02 1.02 0 10.42-2 56.9 56.9 0 01-3.8-2 35.16 35.16 0 00-19.2-5.18 1 1 0 100 2c6.46-.1 12.81 1.7 18.26 5.18zm101.48 31.06c-1.93.3-3.87.47-5.82.48a1 1 0 100 2c2.05-.02 4.1-.2 6.12-.52a1.03 1.03 0 00.66-.4 1.02 1.02 0 00.04-1.13 1 1 0 00-1.02-.47l.02.04zm-20.92-2.5a56.9 56.9 0 01-3.8-2 35.16 35.16 0 00-19.2-5.18 1 1 0 100 2 33.06 33.06 0 0118.26 4.94c1.26.68 2.54 1.38 3.9 2a1 1 0 00.42 0 1.02 1.02 0 10.42-2v.24zm30.08-.56l-1.76.84a1 1 0 00.08 1.8c.24.1.5.11.74.02l1.84-.88a.99.99 0 00.55-.58 1.01 1.01 0 00-.3-1.11.98.98 0 00-.76-.23 1 1 0 00-.39.14zM314 350c-1.58 0-3.17.08-4.74.24a1 1 0 00-.62 1.74 1 1 0 00.72.26h.1c1.51-.16 3.04-.24 4.56-.22a1 1 0 00.7-1.7 1 1 0 00-.7-.3L314 350z" fill="#EAEAEE"/>
+ <path d="M321.2 223.08c.24-.44.52-.86.78-1.28l-.66-.48 1.38-.6a20.29 20.29 0 014.58-4.58l-1.3-4.3c-4.94 4-13.38 6.24-19.56 7.38 4.61 2.24 9 4.92 13.1 8 .4-1.44.97-2.82 1.68-4.14zM338.04 215.34c9.45 0 17.12-9.35 17.12-20.88s-7.67-20.88-17.12-20.88c-9.46 0-17.12 9.35-17.12 20.88s7.66 20.88 17.12 20.88z" fill="#fff"/>
+ <path d="M335.84 233.84s-15.95-40.36-16.8-44.88l-1.76-5.84c-.37.15-.72.33-1.06.54-4.34 2.9-15.6 4.22-26.94 1.42a39.14 39.14 0 01-16.54-8.24 10.2 10.2 0 001.39-7.18 10.33 10.33 0 00-1.51-3.86c4.82-7.72 6.46-17.16 4.76-27.74a62 62 0 00-3.6-12.86 28.52 28.52 0 005-21.82 19.48 19.48 0 00-12.9-15c-10.74-3.64-19.76 1.68-25.5 7.7a75.22 75.22 0 00-34.82-8 25.34 25.34 0 00-18.96-7.86 8 8 0 00-7.58 5.78 41.85 41.85 0 00-17.32 1.74 8 8 0 00-5.42 6.26c-.25 1.6-.2 3.24.18 4.82a87.1 87.1 0 00-34.28 22c-8.56-2.54-20.18-2.82-27.76 6.52a19.48 19.48 0 00-2.8 19.58 28.44 28.44 0 0015.36 15.44 44 44 0 008.24 21.88 31.32 31.32 0 002.78 3.22v.76c-.41 3.55-.1 7.14.94 10.56.37 1.15.98 2.2 1.78 3.1a8 8 0 006.5 2.6c.38 0 .88 0 1.44-.2.7 2.53 1.83 4.93 3.34 7.08a8 8 0 005.56 3.4l1.18.12a82.59 82.59 0 00-.68 6.66 64.28 64.28 0 0012.36 42.18c15.06 20.54 41.24 31.62 72 30.72 1.4.9 2.88 1.8 4.4 2.7a175.42 175.42 0 0084.12 22.42c20.7 0 41.4-4.38 59.02-15.44a8 8 0 003.2-9.7c-11.34-28.88-24.96-48-37.32-60.58z" fill="#fff"/>
+ <path d="M287.86 190.92c-10.56-2.6-18.9-7.86-23.84-14.96a5.06 5.06 0 004.2-3.56c.3-1.28.16-3.6-3.38-6 6.1-7.04 8.42-16.42 6.66-27.3a55.73 55.73 0 00-4.64-14.8 22.6 22.6 0 006-19.62A13.44 13.44 0 00264 94.14c-11.02-3.74-19.16 5.62-22 9.56a71.74 71.74 0 00-37.4-9.52h-1.58c-4.42-4.86-8.82-7.84-16.38-7.84a2 2 0 00-2 2c-.02 1.85.1 3.7.36 5.54-8.72-4.46-20.82-.64-21.46-.44a2 2 0 00-1.36 1.56 7.2 7.2 0 001.46 5.34 8.54 8.54 0 002 1.84c-25.26 6.94-38 21.2-41.6 25.68-4.48-1.84-17.46-6-24.92 3.24a13.44 13.44 0 00-1.88 13.68A22.58 22.58 0 00112.7 158c.48 16.16 8.12 24.5 11.76 27.1 0 .36-.12.78-.2 1.22-.79 3.56-.67 7.27.34 10.78a2 2 0 002 1.42c2.57-.41 5.11-.97 7.62-1.66a13.2 13.2 0 002.64 11.14 2.01 2.01 0 001.4.86 12 12 0 008.28-2.18c-5.42 19.6-2.42 38.4 8.66 53.54 14.16 19.32 39.24 29.54 68.92 28.2 17.34 11.74 42.2 20.64 66.68 23.82 6.63.88 13.31 1.32 20 1.34 17.72 0 38-3.3 55.88-14.5a2 2 0 00.8-2.42c-9.52-24.34-20.6-41.42-31.02-53.36l-2-6.84a2.02 2.02 0 01.13-1.57 2 2 0 011.23-.99l8.94-2.72c.65.38 1.14 1 1.36 1.72l4.52 14.94a1.01 1.01 0 00.88.72c.13.01.27 0 .4-.04.12 0 .22-.16.36-.2a4.4 4.4 0 002.18-1.04.96.96 0 00.58-.3 21.37 21.37 0 00-14.3-35.58l-.32-1.04h.2a2 2 0 001-.26c9.08-5.18 10.36-17.1 5.84-24.98a12.17 12.17 0 00-6.46-5.72 11.18 11.18 0 00-9.38 1.24c-.1.07-.18.15-.26.24l-1.08-3.54a21.51 21.51 0 008.3-16.12 21.38 21.38 0 00-16.04-21.52.96.96 0 00-.44 0c-.66 0-.9.14-1.72 0-.44.12-.88.27-1.3.44a1.03 1.03 0 00-.72.88c-.01.14 0 .27.04.4l4.52 14.94c.22.73.15 1.52-.18 2.2l-9 2.72a1.99 1.99 0 01-2.2-.64 2 2 0 01-.36-.72l-4.42-14.56a.93.93 0 00-1-.7.95.95 0 00-.42.14.98.98 0 00-1.3 0 20.4 20.4 0 0012.44 36l.72 2.4 1.76 5.84c-5.62 3.38-17.92 5.14-31.1 1.88zm49.06 62a1 1 0 00.4 0 1.04 1.04 0 00.6-.5v-.14a.98.98 0 00.46-.16 180.84 180.84 0 0124.78 44.4c-42 25.08-106.46 9.44-137.38-11.82a2 2 0 00-1.14-.36h-.1c-28.58 1.52-52.7-8.16-66.16-26.52-11.58-15.8-13.66-36-5.86-56.76a2 2 0 00-2.36-2.64l-.66.16a2 2 0 00-1.16.82s-3.88 5.5-8.72 5.52c-3.76-6.14-.28-9.34.12-9.68a2 2 0 00-1.74-3.5c-2.88.78-7.42 2-10 2.48-.41-2.4-.35-4.86.18-7.24.23-1.03.34-2.08.32-3.14a2 2 0 00-1.14-1.66c-1.76-.82-10.68-8.6-10.68-25.86a2 2 0 00-1.68-2 18.79 18.79 0 01-14-11.02 9.55 9.55 0 011.26-9.76c6.28-7.86 18.9-2.66 21.16-1.64a44.42 44.42 0 00-6.7 15.18 1 1 0 00.78 1.18h.2a1 1 0 00.98-.8c1.38-5.82 4-11.27 7.66-16 .7-.84 1.45-1.63 2.26-2.36a.99.99 0 00.32-.78c5.38-6 19.44-18.84 44.5-23.86a2 2 0 00-.2-4A9.1 9.1 0 01166.9 98a3.55 3.55 0 01-.6-1.14c4.34-1.06 16.84-3.28 21.46 4.98a2.04 2.04 0 001.22.94 2.02 2.02 0 002.28-2.94 20.13 20.13 0 01-2.54-9.44c5.18.5 8.3 2.94 12 7.04a2 2 0 001.46.68h2.5c11-.3 21.9 2.12 31.72 7.08a65.3 65.3 0 0112.56 8.8 1 1 0 001.71-.71 1 1 0 00-.3-.71 47.06 47.06 0 00-6.37-4.88c.12-.1.23-.23.32-.36 0-.14 8-12.98 18.36-9.44a9.54 9.54 0 016.32 7.54 18.77 18.77 0 01-5.88 16.84 2 2 0 00-.58 2l.16.54c.04.14.1.27.16.4.14.24 13.1 24.84-2.64 40.12a2.01 2.01 0 00-.22 2.61 2 2 0 00.72.61c2.92 1.46 3.54 2.66 3.58 2.84.04.18-1.06.88-3.68.88a2 2 0 00-2 1.96c-.01.35.07.7.24 1 5.12 9.4 15.06 16.34 28 19.52 12 2.94 24.84 2.26 32.64-1.54l4.88 14.62c-5.22 5.44-20.32 8-26 8.44a2 2 0 00-1.64 1.2 65.03 65.03 0 00-17.18-3.22 1 1 0 100 2c6.04.31 12 1.49 17.7 3.48a2 2 0 00.8.46c.18 0 8.82 2.4 20.64 11.76a20.34 20.34 0 0018 20.9l.22.06zM326 211.84l1.3 4.3a20.31 20.31 0 00-4.58 4.58c-.26.34-.48.7-.72 1.08-.24.38-.54.84-.78 1.28a20.5 20.5 0 00-1.68 4.24 79.6 79.6 0 00-13.1-8c6.16-1.32 14.6-3.48 19.56-7.48zm31.74 15.22a19.22 19.22 0 01-2.98 17.12l-3.88-12.78a3.98 3.98 0 00-5.04-2.7l-10.62 3.3a4 4 0 00-2.7 5.04l3.88 12.78a19.39 19.39 0 01-4.74-33.96 1 1 0 00.38-1.1l-2.72-8.96c2.67 1.5 5.58 2.5 8.6 2.98l1.2 3.88a1 1 0 00.92.7 19.37 19.37 0 0117.68 13.7h.02zm-23.08-43.46c1.57-.8 3.4-.96 5.08-.44a8.26 8.26 0 014.3 4c2.62 4.6 3.08 12.82-2 17.68a2 2 0 00-1.08 0c-1.54.52-7.24-.5-11.58-3.62a7.85 7.85 0 01-3.8-5.82c.08-6.1 7.54-10.88 9.08-11.8zm-16.8-22.5l.6-.18-.6.18zm-15.38 3.74a19.26 19.26 0 013.04-17.2l3.88 12.78a4.02 4.02 0 005.04 2.7L325 160a4 4 0 002.7-5.04l-3.88-12.78a19.38 19.38 0 014.74 33.96 1 1 0 00-.38 1.1l1.52 4.92a25.02 25.02 0 00-5.74 6l-2.68-8.86a1 1 0 00-.92-.72 19.34 19.34 0 01-17.88-13.74z" fill="url(#paint0_linear)"/>
+ <path d="M319.68 238.6a20.34 20.34 0 01-.86-6.56c-11.82-9.36-20.46-11.72-20.64-11.76a2 2 0 01-.8-.46c-5.7-2-11.66-3.17-17.7-3.48a1 1 0 110-2c5.85.3 11.63 1.38 17.18 3.22a2 2 0 011.64-1.2c5.62-.52 20.7-3 26-8.44l-4.88-14.62c-7.8 3.8-20.7 4.48-32.64 1.54-12.92-3.18-22.86-10.12-28-19.52a2.01 2.01 0 01.77-2.7 2 2 0 01.99-.26c2.62 0 3.58-.7 3.68-.88.1-.18-.64-1.38-3.58-2.84a2 2 0 01-.5-3.22c15.74-15.26 2.76-39.86 2.64-40.12a2.04 2.04 0 01-.16-.4l-.16-.54a2 2 0 01.58-2 18.77 18.77 0 005.88-16.84 9.53 9.53 0 00-6.38-7.52c-10.44-3.54-18.28 9.3-18.36 9.44-.1.13-.2.25-.32.36a47.11 47.11 0 016.3 4.92.98.98 0 01.3.71 1 1 0 01-.63.93 1.02 1.02 0 01-1.09-.22 65.27 65.27 0 00-12.66-8.78 66.28 66.28 0 00-31.72-7.08h-2.5a2 2 0 01-1.46-.68c-3.62-4.12-6.74-6.56-12-7.04.06 3.3.93 6.55 2.54 9.44a2.01 2.01 0 11-3.5 2c-4.64-8.24-17.14-6-21.46-4.98.13.41.33.8.6 1.14a9.1 9.1 0 006.3 2.52 2 2 0 01.2 4c-25.04 5.04-39.12 17.88-44.5 23.86a.98.98 0 01-.32.78c-.8.73-1.56 1.52-2.26 2.36a42 42 0 00-7.66 16 1 1 0 01-.98.8h-.2a1 1 0 01-.78-1.18 44.43 44.43 0 016.8-15.3c-2.26-1.02-14.88-6.22-21.16 1.64a9.54 9.54 0 00-1.26 9.76 18.79 18.79 0 0014 11.02 2 2 0 011.68 2c0 17.24 8.92 25.04 10.68 25.86a2 2 0 011.14 1.66c.02 1.06-.09 2.11-.32 3.14-.5 2.35-.55 4.77-.14 7.14 2.56-.54 7.08-1.7 10-2.48a2 2 0 011.78 3.5c-.4.34-3.88 3.54-.12 9.68 4.84 0 8.68-5.48 8.72-5.52a2 2 0 011.16-.82l.66-.16a2 2 0 012.36 2.64c-7.8 20.8-5.72 40.96 5.86 56.76 13.46 18.36 37.58 28 66.16 26.52h.1a2 2 0 011.14.36c30.92 21.26 95.44 36.9 137.38 11.82a180.84 180.84 0 00-24.78-44.4c-.14.1-.3.15-.46.16v.14a1.01 1.01 0 01-.6.5 1 1 0 01-.4 0 20.38 20.38 0 01-17.28-14.32zm25 41.74c2.3 7.66-8 17.1-32 16-44-2-62.64-13.8-70-18a144.85 144.85 0 01-24-16.24c-.44 2.6-.44 5.25 0 7.84a2 2 0 01-1.56 2.36h-.4a2 2 0 01-2-1.6 29.83 29.83 0 017.28-24.56c3.64-3.72 10.26-7.74 20.98-5.78h.2a1 1 0 011.34.44 17.36 17.36 0 0012.32 9.22 4.3 4.3 0 004-1.36c5.42-7.22-1.6-14.4-2-14.7-9.66-8.16-28.2-13.16-51.12-2.54-12.66 5.88-21.18 14.64-21.18 21.84a1 1 0 01-1.7.7 1 1 0 01-.3-.7c0-5.34 3.84-11.2 10.36-16.38-9.62-13.14-17.3-24.76-14.36-30.62 0 0 15 3 37-6 11.02-4.5 17.28-13.54 20.68-19.7a86.97 86.97 0 01-33.58 14.28c-35 7.06-67.28-6.58-72-30.46-4.72-23.88 19.64-48.98 54.64-56 35-7.02 67.28 6.58 72 30.46 2.76 13.64-4 27.66-16.9 38.5 2.52 8.5 10.32 32.42 20.14 38.96 12 8 18 6 36 12s40.14 32.04 46.14 52.04h.02z" fill="url(#paint1_linear)"/>
+ <path d="M227.14 168.4a2 2 0 00-2.42 1.46 8.76 8.76 0 01-3.78 5.26 6.2 6.2 0 01-5.38-.58 1.99 1.99 0 00-2.74 1.04 6.45 6.45 0 01-3.72 4 10.6 10.6 0 01-7.44-1.08 2.01 2.01 0 00-2 3.48 16.4 16.4 0 007.28 2 9.7 9.7 0 008.68-5.1 9.1 9.1 0 007-.16c4.5-2 6-7.7 6-8a2 2 0 00-1.48-2.32zM164.28 159.36a3.04 3.04 0 003.92 1.8 3.05 3.05 0 001.8-3.92s-1.2-3.18-2.92-7.88a3 3 0 00-5.64 2c1.64 4.8 2.84 8 2.84 8zM230 141.48a3 3 0 004.8 1.4 3 3 0 00.96-3.2l-2.1-6.74a3 3 0 00-5.72 1.78l2.06 6.76zM175.82 158.7a2 2 0 00-2.28 1.66 5.8 5.8 0 01-2.58 4.54c-2.33.86-4.9.77-7.16-.24a2 2 0 10-1.58 3.68c1.9.77 3.94 1.18 6 1.2a9.73 9.73 0 004.74-1.12 9.6 9.6 0 004.62-7.42 2 2 0 00-1.76-2.3zM242.32 141.38a2 2 0 00-2.44 1.42 5.8 5.8 0 01-3.04 4.26c-2.4.62-4.95.28-7.1-.96a2.02 2.02 0 00-2 3.5 16 16 0 006.98 1.84 9.2 9.2 0 003.62-.7 9.62 9.62 0 005.36-6.92 2 2 0 00-1.38-2.44zM211.42 172.32c.23.03.45.03.68 0 1.45-.51 2.8-1.27 4-2.24a33.53 33.53 0 009.46-10.22 7.8 7.8 0 00.72-6 4 4 0 00-3.16-2.66c-5.12-1.2-20.44 2.48-27.12 10.54a4 4 0 00-.48 4.48c1.64 3.22 10.22 5.58 14.48 6 .47.06.94.1 1.42.1z" fill="#01C8D7"/>
+ <path d="M242.3 244.3a2 2 0 001.66-.48 18.95 18.95 0 0012.72 8.28 6.15 6.15 0 005.76-2.16c6.44-8.6-2-17.22-2.16-17.36-15.54-13.16-37.72-10.12-53.32-2.88a59.5 59.5 0 00-12 7.28c-6.52 5.18-10.36 11.04-10.36 16.38a1 1 0 001.7.7 1 1 0 00.3-.7c0-7.18 8.5-16 21.18-21.84 22.92-10.62 41.46-5.62 51.12 2.54.3.3 7.32 7.48 2 14.7a4.32 4.32 0 01-4 1.36 17.33 17.33 0 01-12.32-9.22 1 1 0 00-1.34-.44h-.2c-10.72-2-17.34 2-20.98 5.78a29.82 29.82 0 00-7.34 24.58 2 2 0 002 1.6h.4a1.99 1.99 0 001.56-1.58c.05-.26.05-.52 0-.78-.45-2.6-.45-5.25 0-7.84a24.4 24.4 0 016.2-13.14c4.34-4.52 10.2-6.1 17.42-4.78z" fill="#00C8D7"/>
+ <path d="M452 177.26a8 8 0 00-13.54-2 153.95 153.95 0 00-17 25.54 17.82 17.82 0 00-1.84 7.7 73.99 73.99 0 00-4.04 8.34A79.59 79.59 0 00412 227.2c-.34 1.3-.66 2.62-.92 4-.12.6-.24 1.2-.34 1.82a20.97 20.97 0 000 6.54 34.54 34.54 0 00-3.22 10c-.2.92-.44 2-.68 3.02-1.6 6.8 1.4 11.18 3.58 14.38a21.64 21.64 0 014.28 10.46c-.68 3.48-14.26 11.06-30.12 15.26a8 8 0 00-5.4 4.76c-2.12 5.3 0 10.98 5.22 14.48s14.2 5.32 24.12 5.32c14.34 0 30.56-4 39.74-13.18 16.84-16.84 24.34-34 24.34-55.66-.12-4.6-.68-9.17-1.68-13.66-.26-1.32-.56-2.66-.88-4-5.08-22.14-16.36-49.6-18.04-53.48zm-8 122.5c-8.16 8.16-22.66 11.36-35.24 11.36-9.04 0-17.1-1.64-21.08-4.28-3.98-2.64-3.46-6-2.96-7.24a2 2 0 011.34-1.2c9.42-2.48 36-11.8 34.52-21.78a27.66 27.66 0 00-5.28-13.12c-1.9-2.78-3.68-5.4-2.7-9.62.26-1.12.5-2.14.7-3.06 1.08-4.82 1.74-7.74 3.76-9.82a16.83 16.83 0 01-.48-6.98 72.14 72.14 0 018.26-22.7 2 2 0 01.78-.74c-.3-2.43.09-4.9 1.14-7.12a147.29 147.29 0 0116.26-24.38 2 2 0 013.38.5c.82 2 20.16 48.6 20.16 68.76 0 20.16-6.86 35.78-22.56 51.4v.02z" fill="#fff"/>
+ <path d="M444.96 178.36a2 2 0 00-1.84.7 147.35 147.35 0 00-16.26 24.38 12.87 12.87 0 00-1.14 7.12c-.32.17-.6.43-.78.74a72.11 72.11 0 00-8.24 22.7c-.33 2.34-.17 4.71.48 6.98-2 2-2.68 5-3.76 9.82-.22.94-.44 2-.7 3.06-1 4.22.8 6.84 2.7 9.62a27.66 27.66 0 015.28 13.12c1.42 10-25.12 19.3-34.52 21.78a2.01 2.01 0 00-1.34 1.2c-.5 1.26-1.22 4.48 2.96 7.24s12 4.28 21.08 4.28c12.58 0 27.08-3.18 35.24-11.36 15.62-15.62 22.58-31.48 22.58-51.42s-19.34-66.8-20.16-68.76a2 2 0 00-1.58-1.2z" fill="#00C8D7"/>
+ <path d="M430.46 205.22c-4.4 8.78 9.78 25.86 15.62 31.7a2 2 0 01-2.82 2.82c-.68-.68-12.28-12.4-16.26-23.8a68.4 68.4 0 00-6.36 18.72c-.3 3.4.4 6.8 2 9.82l13.58 18.68a2 2 0 01-3.04 2.58 80.6 80.6 0 01-14-19.22l-.44-.6c-.6 1.89-1.1 3.81-1.48 5.76-.22.94-.44 2-.7 3.1-.56 2.42.24 3.72 2.1 6.46a31.53 31.53 0 016 14.8c2 14.26-28.6 23.74-36.18 25.86.31.68.84 1.23 1.5 1.58 8.48 5.58 39.16 5.54 51.28-6.58 14.8-14.82 21.42-29.8 21.42-48.58 0-16.46-14.18-53.12-18.56-64a138.1 138.1 0 00-13.66 20.9z" fill="#CCFBFF"/>
+ <path d="M293.76 83.8H503.3a2.36 2.36 0 000-4.7h-48.68c-4.34-8-15.14-25.14-28.62-27.72-18-3.42-21.48 14.44-21.48 14.44s-12-30.96-42-26.86c-14.4 2-19.76 9.56-21.18 17.6a1.2 1.2 0 011.13 0c.34.18.58.55.61.94 0 .78.14 1.54.26 2.28a1.18 1.18 0 01-1 1.32c-.34 0-.66-.06-.93-.3-.3-.26-.39-.6-.39-.98-.2 3.75.14 7.52.98 11.18h1.72c-.16-.42-.34-.94-.54-1.52a1.18 1.18 0 012.22-.76c.62 1.82 1.12 2.94 1.12 2.96a1.19 1.19 0 01-.09 1.12 1.18 1.18 0 01-.99.54h-2.86a46.2 46.2 0 002 5.74h-50.82a2.36 2.36 0 000 4.7v.02z" fill="#fff"/>
+ <path d="M321.28 179.32l2.72 8.86c1.57-2.3 3.5-4.33 5.74-6l-1.52-4.92a1 1 0 01.38-1.1 19.46 19.46 0 008.03-14.53 19.39 19.39 0 00-12.77-19.43l3.88 12.78A4 4 0 01325 160l-10.62 3.22a3.99 3.99 0 01-5.04-2.7l-3.88-12.78c-.54.72-1.03 1.47-1.46 2.26a19.4 19.4 0 0016.42 28.52 1.01 1.01 0 01.86.8zM339.08 212.62l-1.2-3.88a25.8 25.8 0 01-8.6-2.98l2.72 8.96a1 1 0 01-.38 1.1 19.49 19.49 0 00-8.03 14.53 19.38 19.38 0 0012.77 19.43L332.48 237a4 4 0 012.7-5.04l10.62-3.22a3.99 3.99 0 015.04 2.7l3.88 12.78a19.38 19.38 0 00-14.72-30.9 1 1 0 01-.92-.7zM325.66 195.34a7.83 7.83 0 003.8 5.82c4.32 3.12 10 4.14 11.58 3.62a2 2 0 011.08 0c5-4.86 4.54-13.08 2-17.68a8.26 8.26 0 00-4.3-4 6.76 6.76 0 00-5.08.44c-1.62.98-9.08 5.76-9.08 11.8z" fill="#CCFBFF"/>
+ <path d="M70.86 36.18H187.8a2.36 2.36 0 000-4.72h-27.68c-2.4-4.42-8.44-14-16-15.5a9.24 9.24 0 00-5.45.2c-3.13 1.1-5.76 4.64-6.53 7.84 0 0-6.66-17.24-23.46-14.96-12.36 1.68-12.8 10.8-11.64 16.86h2.5a1.18 1.18 0 110 2.36h-2c.3 1.08.66 2.15 1.1 3.18H70.86a2.36 2.36 0 000 4.72v.02z" fill="#fff"/>
+ <defs>
+ <linearGradient id="paint0_linear" x1="57.78" y1="17.22" x2="465.72" y2="425.14" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#00C8D7"/>
+ <stop offset="1" stop-color="#0A84FF"/>
+ </linearGradient>
+ <linearGradient id="paint1_linear" x1="82.96" y1="50.04" x2="441.7" y2="408.76" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#CCFBFF"/>
+ <stop offset="1" stop-color="#C9E4FF"/>
+ </linearGradient>
+ </defs>
+</svg>
diff --git a/devtools/startup/aboutdevtools/moz.build b/devtools/startup/aboutdevtools/moz.build
new file mode 100644
index 0000000000..2b25782368
--- /dev/null
+++ b/devtools/startup/aboutdevtools/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES += [
+ "AboutDevToolsRegistration.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser.ini"]
diff --git a/devtools/startup/aboutdevtools/subscribe.css b/devtools/startup/aboutdevtools/subscribe.css
new file mode 100644
index 0000000000..0096bc1e8f
--- /dev/null
+++ b/devtools/startup/aboutdevtools/subscribe.css
@@ -0,0 +1,94 @@
+/* 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/. */
+
+/**
+ * This file contains the styles for the newsletter subscription form on about:devtools.
+ * It is largely inspired from https://mozilla.github.io/basket-example/
+ */
+
+.newsletter-title {
+ font-size: 17px;
+ font-weight: 500;
+ margin-top: 26px;
+ margin-bottom: -4px;
+}
+
+#newsletter-errors {
+ /* Hidden by default */
+ display: none;
+
+ margin-bottom: 20px;
+ padding: 10px;
+ border-radius: 2px;
+
+ background-color: var(--red-50);
+ color: var(--white);
+}
+
+#newsletter-errors.show {
+ display: block;
+}
+
+#newsletter-errors .error {
+ margin: 0;
+ margin-bottom: 10px;
+}
+
+#newsletter-errors .error:last-child {
+ margin-bottom: 0;
+}
+
+#newsletter-thanks {
+ /* Hidden by default */
+ display: none;
+}
+
+#newsletter-thanks.show {
+ display: block;
+}
+
+.newsletter-form-section {
+ display: block;
+ margin-bottom: 20px;
+ width: 320px;
+}
+
+#newsletter-privacy {
+ display: flex;
+
+ /* The privacy section is hidden by default and only displayed on focus */
+ height: 0;
+ overflow: hidden;
+
+ padding: 3px 0 0 3px;
+ margin: -3px 0 -20px -3px;
+}
+
+#newsletter-privacy.animate {
+ transition: all 0.25s cubic-bezier(.15,.75,.35,.9);
+}
+
+#newsletter-privacy label {
+ line-height: var(--line-height);
+}
+
+#privacy {
+ width: 20px;
+ height: 20px;
+ margin-top: 2px;
+ margin-inline-end: 10px;
+ flex-shrink: 0;
+}
+
+#email {
+ width: 100%;
+ box-sizing: border-box;
+ margin: 4px 0;
+ padding: 8px;
+}
+
+#newsletter-submit {
+ display: block;
+ padding: 8px 20px;
+}
diff --git a/devtools/startup/aboutdevtools/subscribe.js b/devtools/startup/aboutdevtools/subscribe.js
new file mode 100644
index 0000000000..7e18b5a578
--- /dev/null
+++ b/devtools/startup/aboutdevtools/subscribe.js
@@ -0,0 +1,158 @@
+/* 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";
+
+/**
+ * This file handles the newsletter subscription form on about:devtools.
+ * It is largely inspired from https://mozilla.github.io/basket-example/
+ */
+
+window.addEventListener(
+ "load",
+ function() {
+ // Timeout for the subscribe XHR.
+ const REQUEST_TIMEOUT = 5000;
+
+ const emailInput = document.getElementById("email");
+ const newsletterErrors = document.getElementById("newsletter-errors");
+ const newsletterForm = document.getElementById("newsletter-form");
+ const newsletterPrivacySection = document.getElementById(
+ "newsletter-privacy"
+ );
+ const newsletterThanks = document.getElementById("newsletter-thanks");
+
+ /**
+ * Update the error panel to display the provided errors. If the argument is null or
+ * empty, a default error message will be displayed.
+ *
+ * @param {Array} errors
+ * Array of strings, each item being an error message to display.
+ */
+ async function updateErrorPanel(errors) {
+ clearErrorPanel();
+
+ if (!errors || errors.length == 0) {
+ errors = [
+ await document.l10n.formatValues([
+ { id: "newsletter-error-unknown" },
+ ]),
+ ];
+ }
+
+ // Create errors markup.
+ const fragment = document.createDocumentFragment();
+ for (const error of errors) {
+ const item = document.createElement("p");
+ item.classList.add("error");
+ item.appendChild(document.createTextNode(error));
+ fragment.appendChild(item);
+ }
+
+ newsletterErrors.appendChild(fragment);
+ newsletterErrors.classList.add("show");
+ }
+
+ /**
+ * Hide the error panel and remove all errors.
+ */
+ function clearErrorPanel() {
+ newsletterErrors.classList.remove("show");
+ newsletterErrors.innerHTML = "";
+ }
+
+ // Show the additional form fields on focus of the email input.
+ function onEmailInputFocus() {
+ // Create a hidden measuring container, append it to the parent of the privacy section
+ const container = document.createElement("div");
+ container.style.cssText =
+ "visibility: hidden; overflow: hidden; position: absolute";
+ newsletterPrivacySection.parentNode.appendChild(container);
+
+ // Clone the privacy section, append the clone to the measuring container.
+ const clone = newsletterPrivacySection.cloneNode(true);
+ container.appendChild(clone);
+
+ // Measure the target height of the privacy section.
+ clone.style.height = "auto";
+ const height = clone.offsetHeight;
+
+ // Cleanup the measuring container.
+ container.remove();
+
+ // Set the animate class and set the height to the measured height.
+ newsletterPrivacySection.classList.add("animate");
+ newsletterPrivacySection.style.cssText = `height: ${height}px; margin-bottom: 0;`;
+ }
+
+ // XHR subscribe; handle errors; display thanks message on success.
+ function onFormSubmit(evt) {
+ evt.preventDefault();
+ evt.stopPropagation();
+
+ // New submission, clear old errors
+ clearErrorPanel();
+
+ const xhr = new XMLHttpRequest();
+
+ xhr.onload = async function(r) {
+ if (r.target.status >= 200 && r.target.status < 300) {
+ const { response } = r.target;
+
+ if (response.success === true) {
+ // Hide form and show success message.
+ newsletterForm.style.display = "none";
+ newsletterThanks.classList.add("show");
+ } else {
+ // We trust the error messages from the service to be meaningful for the user.
+ updateErrorPanel(response.errors);
+ }
+ } else {
+ const { status, statusText } = r.target;
+ const statusInfo = `${status} - ${statusText}`;
+ const error = await document.l10n.formatValues([
+ {
+ id: "newsletter-error-common",
+ args: { errorDescription: statusInfo },
+ },
+ ]);
+ updateErrorPanel([error]);
+ }
+ };
+
+ xhr.onerror = () => {
+ updateErrorPanel();
+ };
+
+ xhr.ontimeout = async () => {
+ const error = await document.l10n.formatValues([
+ { id: "newsletter-error-timeout" },
+ ]);
+ updateErrorPanel([error]);
+ };
+
+ const url = newsletterForm.getAttribute("action");
+
+ xhr.open("POST", url, true);
+ xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ xhr.timeout = REQUEST_TIMEOUT;
+ xhr.responseType = "json";
+
+ // Create form data.
+ const formData = new FormData(newsletterForm);
+ formData.append("source_url", document.location.href);
+
+ const params = new URLSearchParams(formData);
+
+ // Send the request.
+ xhr.send(params.toString());
+ }
+
+ // Attach event listeners.
+ newsletterForm.addEventListener("submit", onFormSubmit);
+ emailInput.addEventListener("focus", onEmailInputFocus);
+ },
+ { once: true }
+);
diff --git a/devtools/startup/aboutdevtools/test/.eslintrc.js b/devtools/startup/aboutdevtools/test/.eslintrc.js
new file mode 100644
index 0000000000..3d0bd99e1b
--- /dev/null
+++ b/devtools/startup/aboutdevtools/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/startup/aboutdevtools/test/browser.ini b/devtools/startup/aboutdevtools/test/browser.ini
new file mode 100644
index 0000000000..bcac4d3fbf
--- /dev/null
+++ b/devtools/startup/aboutdevtools/test/browser.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+
+[browser_aboutdevtools_closes_page.js]
+[browser_aboutdevtools_enables_devtools.js]
+[browser_aboutdevtools_focus_owner_tab.js]
+[browser_aboutdevtools_reuse_existing.js]
+skip-if = (verify && (os == 'mac' || os == 'linux'))
diff --git a/devtools/startup/aboutdevtools/test/browser_aboutdevtools_closes_page.js b/devtools/startup/aboutdevtools/test/browser_aboutdevtools_closes_page.js
new file mode 100644
index 0000000000..baa8f3f43e
--- /dev/null
+++ b/devtools/startup/aboutdevtools/test/browser_aboutdevtools_closes_page.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint-env browser */
+
+add_task(async function() {
+ pushPref("devtools.enabled", false);
+
+ const { doc, win } = await openAboutDevTools();
+
+ info("Check that the close button is available on the page");
+ const closeButton = doc.getElementById("close");
+ ok(closeButton, "close button is displayed");
+
+ const onWindowUnload = new Promise(r =>
+ win.addEventListener("unload", r, { once: true })
+ );
+ info("Click on the install button to enable DevTools.");
+ EventUtils.synthesizeMouseAtCenter(closeButton, {}, win);
+
+ info("Wait for the about:devtools tab to be closed");
+ await onWindowUnload;
+});
diff --git a/devtools/startup/aboutdevtools/test/browser_aboutdevtools_enables_devtools.js b/devtools/startup/aboutdevtools/test/browser_aboutdevtools_enables_devtools.js
new file mode 100644
index 0000000000..e0f7324ed8
--- /dev/null
+++ b/devtools/startup/aboutdevtools/test/browser_aboutdevtools_enables_devtools.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint-env browser */
+
+add_task(async function() {
+ pushPref("devtools.enabled", false);
+
+ const { tab, doc, win } = await openAboutDevTools();
+
+ const installPage = doc.getElementById("install-page");
+ const welcomePage = doc.getElementById("welcome-page");
+
+ info(
+ "Check that about:devtools is in the correct state with devtools.enabled=false"
+ );
+ ok(!installPage.hasAttribute("hidden"), "install screen is visible");
+ ok(welcomePage.hasAttribute("hidden"), "welcome screen is hidden");
+
+ info("Click on the install button to enable DevTools.");
+ const installButton = doc.getElementById("install");
+ EventUtils.synthesizeMouseAtCenter(installButton, {}, win);
+
+ info("Wait until the UI updates");
+ await waitUntil(() => installPage.hasAttribute("hidden") === true);
+ ok(!welcomePage.hasAttribute("hidden"), "welcome screen is visible");
+ ok(
+ Services.prefs.getBoolPref("devtools.enabled"),
+ "The preference devtools.enabled has been flipped to true."
+ );
+
+ // Flip the devtools.enabled preference back to false, otherwise the pushPref cleanup
+ // times out.
+ Services.prefs.setBoolPref("devtools.enabled", false);
+
+ await removeTab(tab);
+});
diff --git a/devtools/startup/aboutdevtools/test/browser_aboutdevtools_focus_owner_tab.js b/devtools/startup/aboutdevtools/test/browser_aboutdevtools_focus_owner_tab.js
new file mode 100644
index 0000000000..d0d9a96d79
--- /dev/null
+++ b/devtools/startup/aboutdevtools/test/browser_aboutdevtools_focus_owner_tab.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint-env browser */
+
+/**
+ * When closing about:devtools, test that the tab where the user triggered about:devtools
+ * is selected again.
+ */
+add_task(async function() {
+ await pushPref("devtools.enabled", false);
+
+ info("Add an about:blank tab");
+ const tab1 = await addTab("data:text/html;charset=utf-8,tab1");
+ const tab2 = await addTab("data:text/html;charset=utf-8,tab2");
+ ok(
+ tab1 === gBrowser.tabs[1],
+ "tab1 is the second tab in the current browser window"
+ );
+
+ info("Select the first tab");
+ gBrowser.selectedTab = tab1;
+
+ synthesizeToggleToolboxKey();
+
+ info("Wait for the about:devtools tab to be selected");
+ await waitUntil(() => isAboutDevtoolsTab(gBrowser.selectedTab));
+ info("about:devtools was opened as expected.");
+
+ const aboutDevtoolsTab = gBrowser.selectedTab;
+ ok(
+ aboutDevtoolsTab === gBrowser.tabs[2],
+ "about:devtools was opened next to its owner tab"
+ );
+
+ info("Move the owner tab to the end of the tabs array.");
+ gBrowser.moveTabTo(tab1, gBrowser.tabs.length - 1);
+ await removeTab(aboutDevtoolsTab);
+
+ await waitUntil(() => tab1 == gBrowser.selectedTab);
+ info("The correct tab was selected after closing about:devtools.");
+
+ await removeTab(tab1);
+ await removeTab(tab2);
+});
+
+/**
+ * When closing about:devtools, test that the current tab is not updated if
+ * about:devtools was not the selectedTab.
+ */
+add_task(async function() {
+ await pushPref("devtools.enabled", false);
+
+ info("Add an about:blank tab");
+ const tab1 = await addTab("data:text/html;charset=utf-8,tab1");
+ const tab2 = await addTab("data:text/html;charset=utf-8,tab2");
+ ok(
+ tab1 === gBrowser.tabs[1],
+ "tab1 is the second tab in the current browser window"
+ );
+
+ info("Select the first tab");
+ gBrowser.selectedTab = tab1;
+
+ synthesizeToggleToolboxKey();
+
+ info("Wait for the about:devtools tab to be selected");
+ await waitUntil(() => isAboutDevtoolsTab(gBrowser.selectedTab));
+ info("about:devtools was opened as expected.");
+
+ const aboutDevtoolsTab = gBrowser.selectedTab;
+ ok(
+ aboutDevtoolsTab === gBrowser.tabs[2],
+ "about:devtools was opened next to its owner tab"
+ );
+
+ info("Select the second tab");
+ gBrowser.selectedTab = tab2;
+
+ const aboutDevtoolsDocument = aboutDevtoolsTab.linkedBrowser.contentDocument;
+ await waitUntil(() => aboutDevtoolsDocument.visibilityState === "hidden");
+
+ await removeTab(aboutDevtoolsTab);
+
+ ok(tab2 == gBrowser.selectedTab, "Tab 2 should still be selected.");
+
+ await removeTab(tab1);
+ await removeTab(tab2);
+});
diff --git a/devtools/startup/aboutdevtools/test/browser_aboutdevtools_reuse_existing.js b/devtools/startup/aboutdevtools/test/browser_aboutdevtools_reuse_existing.js
new file mode 100644
index 0000000000..96c7ddc10c
--- /dev/null
+++ b/devtools/startup/aboutdevtools/test/browser_aboutdevtools_reuse_existing.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint-env browser */
+
+/**
+ * Test that only one tab of about:devtools is used for a given window.
+ */
+add_task(async function() {
+ await pushPref("devtools.enabled", false);
+
+ info("Add an about:blank tab");
+ const tab = await addTab("about:blank");
+
+ synthesizeToggleToolboxKey();
+
+ info("Wait for the about:devtools tab to be selected");
+ await waitUntil(() => isAboutDevtoolsTab(gBrowser.selectedTab));
+
+ // Keep a reference on this tab to assert it later on.
+ const aboutDevtoolsTab = gBrowser.selectedTab;
+
+ info("Select the about:blank tab again");
+ gBrowser.selectedTab = tab;
+
+ synthesizeToggleToolboxKey();
+
+ info("Wait for the about:devtools tab to be selected");
+ await waitUntil(() => isAboutDevtoolsTab(gBrowser.selectedTab));
+
+ // filter is not available on gBrowser.tabs.
+ const aboutDevtoolsTabs = [...gBrowser.tabs].filter(isAboutDevtoolsTab);
+ ok(
+ aboutDevtoolsTabs.length === 1,
+ "Only one tab of about:devtools was opened."
+ );
+ ok(
+ aboutDevtoolsTabs[0] === aboutDevtoolsTab,
+ "The existing about:devtools tab was reused."
+ );
+
+ await removeTab(aboutDevtoolsTab);
+ await removeTab(tab);
+});
diff --git a/devtools/startup/aboutdevtools/test/head.js b/devtools/startup/aboutdevtools/test/head.js
new file mode 100644
index 0000000000..d5f85d0aca
--- /dev/null
+++ b/devtools/startup/aboutdevtools/test/head.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+// All test are asynchronous
+waitForExplicitFinish();
+
+/**
+ * Waits until a predicate returns true.
+ *
+ * @param function predicate
+ * Invoked once in a while until it returns true.
+ * @param number interval [optional]
+ * How often the predicate is invoked, in milliseconds.
+ */
+const waitUntil = function(predicate, interval = 100) {
+ if (predicate()) {
+ return Promise.resolve(true);
+ }
+ return new Promise(resolve => {
+ setTimeout(function() {
+ waitUntil(predicate, interval).then(() => resolve(true));
+ }, interval);
+ });
+};
+
+/**
+ * Open the provided url in a new tab.
+ */
+const addTab = async function(url) {
+ info("Adding a new tab with URL: " + url);
+
+ const { gBrowser } = window;
+
+ const tab = BrowserTestUtils.addTab(gBrowser, url);
+ gBrowser.selectedTab = tab;
+
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("Tab added and finished loading");
+
+ return tab;
+};
+
+/**
+ * Remove the given tab.
+ * @param {Object} tab The tab to be removed.
+ * @return Promise<undefined> resolved when the tab is successfully removed.
+ */
+const removeTab = async function(tab) {
+ info("Removing tab.");
+
+ const { gBrowser } = tab.ownerGlobal;
+
+ await new Promise(resolve => {
+ gBrowser.tabContainer.addEventListener("TabClose", resolve, { once: true });
+ gBrowser.removeTab(tab);
+ });
+
+ info("Tab removed and finished closing");
+};
+
+/**
+ * Open a new tab on about:devtools
+ */
+const openAboutDevTools = async function() {
+ info("Open about:devtools programmatically in a new tab");
+ const tab = await addTab("about:devtools");
+
+ const browser = tab.linkedBrowser;
+ const doc = browser.contentDocument;
+ const win = browser.contentWindow;
+
+ return { tab, doc, win };
+};
+
+/**
+ * Copied from devtools shared-head.js.
+ * Set a temporary value for a preference, that will be cleaned up after the test.
+ */
+const pushPref = function(preferenceName, value) {
+ return new Promise(resolve => {
+ const options = { set: [[preferenceName, value]] };
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+};
+
+/**
+ * Helper to call the toggle devtools shortcut.
+ */
+function synthesizeToggleToolboxKey() {
+ info("Trigger the toogle toolbox shortcut");
+ if (Services.appinfo.OS == "Darwin") {
+ EventUtils.synthesizeKey("i", { accelKey: true, altKey: true });
+ } else {
+ EventUtils.synthesizeKey("i", { accelKey: true, shiftKey: true });
+ }
+}
+
+/**
+ * Helper to check if a given tab is about:devtools.
+ */
+function isAboutDevtoolsTab(tab) {
+ const browser = tab.linkedBrowser;
+ // browser.documentURI might be unavailable if the tab is loading.
+ if (browser && browser.documentURI) {
+ const location = browser.documentURI.spec;
+ return location.startsWith("about:devtools");
+ }
+ return false;
+}
diff --git a/devtools/startup/components.conf b/devtools/startup/components.conf
new file mode 100644
index 0000000000..f91c54c550
--- /dev/null
+++ b/devtools/startup/components.conf
@@ -0,0 +1,30 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = []
+
+if buildconfig.substs['MOZ_DEVTOOLS'] == 'all':
+ Classes += [
+ {
+ 'cid': '{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}',
+ 'contract_ids': ['@mozilla.org/devtools/startup-clh;1'],
+ 'jsm': 'resource:///modules/DevToolsStartup.jsm',
+ 'constructor': 'DevToolsStartup',
+ 'categories': {'command-line-handler': 'm-devtools'},
+ },
+ {
+ 'cid': '{1060afaf-dc9e-43da-8646-23a2faf48493}',
+ 'contract_ids': ['@mozilla.org/network/protocol/about;1?what=debugging'],
+ 'jsm': 'resource:///modules/AboutDebuggingRegistration.jsm',
+ 'constructor': 'AboutDebugging',
+ },
+ {
+ 'cid': '{11342911-3135-45a8-8d71-737a2b0ad469}',
+ 'contract_ids': ['@mozilla.org/network/protocol/about;1?what=devtools-toolbox'],
+ 'jsm': 'resource:///modules/AboutDevToolsToolboxRegistration.jsm',
+ 'constructor': 'AboutDevtoolsToolbox',
+ },
+ ]
diff --git a/devtools/startup/enableDevToolsPopup.inc.xhtml b/devtools/startup/enableDevToolsPopup.inc.xhtml
new file mode 100644
index 0000000000..ca0cfdd3ce
--- /dev/null
+++ b/devtools/startup/enableDevToolsPopup.inc.xhtml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+
+<!-- Wrap the arrow panel in a template to avoid loading images on startup -->
+<html:template id="wrapper-enable-devtools-popup">
+ <panel id="enable-devtools-popup"
+ type="arrow"
+ role="alert"
+ noautofocus="true"
+ orient="vertical">
+ <description data-l10n-id="enable-devtools-popup-description"></description>
+ </panel>
+</html:template>
diff --git a/devtools/startup/jar.mn b/devtools/startup/jar.mn
new file mode 100644
index 0000000000..1e14a43481
--- /dev/null
+++ b/devtools/startup/jar.mn
@@ -0,0 +1,27 @@
+# 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/.
+
+devtools-startup.jar:
+% content devtools-startup %content/
+ content/aboutdevtools/aboutdevtools.xhtml (aboutdevtools/aboutdevtools.xhtml)
+ content/aboutdevtools/aboutdevtools.css (aboutdevtools/aboutdevtools.css)
+ content/aboutdevtools/aboutdevtools.js (aboutdevtools/aboutdevtools.js)
+ content/aboutdevtools/subscribe.css (aboutdevtools/subscribe.css)
+ content/aboutdevtools/subscribe.js (aboutdevtools/subscribe.js)
+
+ content/aboutdevtools/images/otter.svg (aboutdevtools/images/otter.svg)
+
+ content/aboutdevtools/images/dev-edition-logo.svg (aboutdevtools/images/dev-edition-logo.svg)
+ content/aboutdevtools/images/external-link.svg (aboutdevtools/images/external-link.svg)
+ content/aboutdevtools/images/feature-inspector.svg (aboutdevtools/images/feature-inspector.svg)
+ content/aboutdevtools/images/feature-console.svg (aboutdevtools/images/feature-console.svg)
+ content/aboutdevtools/images/feature-debugger.svg (aboutdevtools/images/feature-debugger.svg)
+ content/aboutdevtools/images/feature-network.svg (aboutdevtools/images/feature-network.svg)
+ content/aboutdevtools/images/feature-memory.svg (aboutdevtools/images/feature-memory.svg)
+ content/aboutdevtools/images/feature-visualediting.svg (aboutdevtools/images/feature-visualediting.svg)
+ content/aboutdevtools/images/feature-responsive.svg (aboutdevtools/images/feature-responsive.svg)
+ content/aboutdevtools/images/feature-storage.svg (aboutdevtools/images/feature-storage.svg)
+ content/aboutdevtools/images/feature-performance.svg (aboutdevtools/images/feature-performance.svg)
+
+ content/DevToolsShim.jsm (DevToolsShim.jsm)
diff --git a/devtools/startup/locales/en-US/aboutDevTools.ftl b/devtools/startup/locales/en-US/aboutDevTools.ftl
new file mode 100644
index 0000000000..db2146d39c
--- /dev/null
+++ b/devtools/startup/locales/en-US/aboutDevTools.ftl
@@ -0,0 +1,57 @@
+# 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/.
+
+head-title = About Developer Tools
+enable-title = Enable Firefox Developer Tools
+enable-inspect-element-title = Enable Firefox Developer Tools to use Inspect Element
+enable-inspect-element-message = Examine and edit HTML and CSS with the Developer Tools’ Inspector.
+enable-about-debugging-message = Develop and debug WebExtensions, web workers, service workers and more with Firefox Developer Tools.
+enable-key-shortcut-message = You activated a Developer Tools shortcut. If that was a mistake, you can close this Tab.
+enable-menu-message = Perfect your website’s HTML, CSS, and JavaScript with tools like Inspector and Debugger.
+enable-common-message = Firefox Developer Tools are disabled by default to give you more control over your browser.
+enable-learn-more-link = Learn more about Developer Tools
+enable-enable-button = Enable Developer Tools
+enable-close-button = Close this Tab
+
+welcome-title = Welcome to Firefox Developer Tools!
+newsletter-title = Mozilla Developer Newsletter
+newsletter-message = Get developer news, tricks and resources sent straight to your inbox.
+newsletter-email-placeholder =
+ .placeholder = Email
+newsletter-privacy-label = I’m okay with Mozilla handling my info as explained in this <a data-l10n-name="privacy-policy">Privacy Policy</a>.
+newsletter-subscribe-button = Subscribe
+newsletter-thanks-title = Thanks!
+newsletter-thanks-message = If you haven’t previously confirmed a subscription to a Mozilla-related newsletter you may have to do so. Please check your inbox or your spam filter for an email from us.
+
+footer-title = Firefox Developer Edition
+footer-message = Looking for more than just Developer Tools? Check out the Firefox browser that is built specifically for developers and modern workflows.
+footer-learn-more-link = Learn more
+
+features-learn-more = Learn more
+features-inspector-title = Inspector
+features-inspector-desc = Inspect and refine code to build pixel-perfect layouts. <a data-l10n-name="learn-more">{ features-learn-more }</a>
+features-console-title = Console
+features-console-desc = Track CSS, JavaScript, security and network issues. <a data-l10n-name="learn-more">{ features-learn-more }</a>
+features-debugger-title = Debugger
+features-debugger-desc = Powerful JavaScript debugger with support for your framework. <a data-l10n-name="learn-more">{ features-learn-more }</a>
+features-network-title = Network
+features-network-desc = Monitor network requests that can slow or block your site. <a data-l10n-name="learn-more">{ features-learn-more }</a>
+features-storage-title = Storage
+features-storage-desc = Add, modify and remove cache, cookies, databases and session data. <a data-l10n-name="learn-more">{ features-learn-more }</a>
+features-responsive-title = Responsive Design Mode
+features-responsive-desc = Test sites on emulated devices in your browser. <a data-l10n-name="learn-more">{ features-learn-more }</a>
+features-visual-editing-title = Visual Editing
+features-visual-editing-desc = Fine-tune animations, alignment and padding. <a data-l10n-name="learn-more">{ features-learn-more }</a>
+features-performance-title = Performance
+features-performance-desc = Unblock bottlenecks, streamline processes, optimize assets. <a data-l10n-name="learn-more">{ features-learn-more }</a>
+features-memory-title = Memory
+features-memory-desc = Find memory leaks and make your application zippy. <a data-l10n-name="learn-more">{ features-learn-more }</a>
+# Variables:
+# $errorDescription (String) - The error that occurred e.g. 404 - Not Found
+newsletter-error-common = Subscription request failed ({ $errorDescription }).
+newsletter-error-unknown = An unexpected error occurred.
+newsletter-error-timeout = Subscription request timed out.
+# Variables:
+# $shortcut (String) - The keyboard shortcut used for the tool
+welcome-message = You’ve successfully enabled Developer Tools! To get started, explore the Web Developer menu or open the tools with { $shortcut }.
diff --git a/devtools/startup/locales/en-US/key-shortcuts.properties b/devtools/startup/locales/en-US/key-shortcuts.properties
new file mode 100644
index 0000000000..a5bdf43188
--- /dev/null
+++ b/devtools/startup/locales/en-US/key-shortcuts.properties
@@ -0,0 +1,67 @@
+# 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/.
+
+# LOCALIZATION NOTE (toggleToolbox.commandkey):
+# Key pressed to open a toolbox with the default panel selected
+toggleToolbox.commandkey=I
+
+# LOCALIZATION NOTE (toggleToolboxF12.commandkey):
+# Alternative key pressed to open a toolbox with the default panel selected
+toggleToolboxF12.commandkey=VK_F12
+
+# LOCALIZATION NOTE (browserToolbox.commandkey):
+# Key pressed to open the Browser Toolbox, used for debugging Firefox itself
+browserToolbox.commandkey=I
+
+# LOCALIZATION NOTE (browserConsole.commandkey):
+# Key pressed to open the Browser Console, used for debugging Firefox itself
+browserConsole.commandkey=J
+
+# LOCALIZATION NOTE (responsiveDesignMode.commandkey):
+# Key pressed to toggle on the Responsive Design Mode
+responsiveDesignMode.commandkey=M
+
+# LOCALIZATION NOTE (inspector.commandkey):
+# Key pressed to open a toolbox with the inspector panel selected
+inspector.commandkey=C
+
+# LOCALIZATION NOTE (webconsole.commandkey):
+# Key pressed to open a toolbox with the web console panel selected
+webconsole.commandkey=K
+
+# LOCALIZATION NOTE (jsdebugger.commandkey2):
+# Key pressed to open a toolbox with the debugger panel selected
+jsdebugger.commandkey2=Z
+
+# LOCALIZATION NOTE (netmonitor.commandkey):
+# Key pressed to open a toolbox with the network monitor panel selected
+netmonitor.commandkey=E
+
+# LOCALIZATION NOTE (styleeditor.commandkey):
+# Key pressed to open a toolbox with the style editor panel selected
+styleeditor.commandkey=VK_F7
+
+# LOCALIZATION NOTE (performance.commandkey):
+# Key pressed to open a toolbox with the performance panel selected
+performance.commandkey=VK_F5
+
+# LOCALIZATION NOTE (storage.commandkey):
+# Key pressed to open a toolbox with the storage panel selected
+storage.commandkey=VK_F9
+
+# LOCALIZATION NOTE (dom.commandkey):
+# Key pressed to open a toolbox with the DOM panel selected
+dom.commandkey=W
+
+# LOCALIZATION NOTE (accessibilityF12.commandkey):
+# Key pressed to open a toolbox with the accessibility panel selected
+accessibilityF12.commandkey=VK_F12
+
+# LOCALIZATION NOTE (profilerStartStop.commandkey):
+# Key pressed to start or stop the performance profiler
+profilerStartStop.commandkey=VK_1
+
+# LOCALIZATION NOTE (profilerCapture.commandkey):
+# Key pressed to capture a recorded performance profile
+profilerCapture.commandkey=VK_2
diff --git a/devtools/startup/locales/en-US/startup.properties b/devtools/startup/locales/en-US/startup.properties
new file mode 100644
index 0000000000..773dd92d79
--- /dev/null
+++ b/devtools/startup/locales/en-US/startup.properties
@@ -0,0 +1,8 @@
+# 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/.
+
+# LOCALIZATION NOTE (enableDevTools.label):
+# Label for the menu item displayed in Tools > Developer Tools when DevTools are disabled.
+enableDevTools.label=Enable Developer Tools…
+enableDevTools.accesskey=E
diff --git a/devtools/startup/locales/jar.mn b/devtools/startup/locales/jar.mn
new file mode 100644
index 0000000000..053610753e
--- /dev/null
+++ b/devtools/startup/locales/jar.mn
@@ -0,0 +1,12 @@
+#filter substitution
+# 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/.
+
+[localization] @AB_CD@.jar:
+ devtools/startup (%*.ftl)
+
+@AB_CD@.jar:
+% locale devtools-startup @AB_CD@ %locale/@AB_CD@/devtools/startup/
+ locale/@AB_CD@/devtools/startup/ (%*.dtd)
+ locale/@AB_CD@/devtools/startup/ (%*.properties)
diff --git a/devtools/startup/locales/moz.build b/devtools/startup/locales/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/devtools/startup/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/devtools/startup/moz.build b/devtools/startup/moz.build
new file mode 100644
index 0000000000..07341e57fb
--- /dev/null
+++ b/devtools/startup/moz.build
@@ -0,0 +1,29 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+# Register the startup components only for 'all' builds.
+if CONFIG["MOZ_DEVTOOLS"] == "all":
+ EXTRA_JS_MODULES += [
+ "AboutDebuggingRegistration.jsm",
+ "AboutDevToolsToolboxRegistration.jsm",
+ "DevToolsStartup.jsm",
+ ]
+
+ DIRS += [
+ "aboutdevtools",
+ "locales",
+ ]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"]
diff --git a/devtools/startup/tests/browser/.eslintrc.js b/devtools/startup/tests/browser/.eslintrc.js
new file mode 100644
index 0000000000..3d0bd99e1b
--- /dev/null
+++ b/devtools/startup/tests/browser/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/startup/tests/browser/browser.ini b/devtools/startup/tests/browser/browser.ini
new file mode 100644
index 0000000000..68b802d2aa
--- /dev/null
+++ b/devtools/startup/tests/browser/browser.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+
+[browser_shim_disable_devtools.js]
diff --git a/devtools/startup/tests/browser/browser_shim_disable_devtools.js b/devtools/startup/tests/browser/browser_shim_disable_devtools.js
new file mode 100644
index 0000000000..0a7c5871ba
--- /dev/null
+++ b/devtools/startup/tests/browser/browser_shim_disable_devtools.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint-env browser */
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { CustomizableUI } = ChromeUtils.import(
+ "resource:///modules/CustomizableUI.jsm"
+);
+const { AppConstants } = require("resource://gre/modules/AppConstants.jsm");
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+/**
+ * Test that the preference devtools.policy.disabled disables entry points for devtools.
+ */
+add_task(async function() {
+ info(
+ "Disable DevTools entry points (does not apply to the already created window"
+ );
+ await new Promise(resolve => {
+ const options = { set: [["devtools.policy.disabled", true]] };
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+
+ // In DEV_EDITION the browser starts with the developer-button in the toolbar. This
+ // applies to all new windows and forces creating keyboard shortcuts. The preference
+ // tested should not change without restart, but for the needs of the test, remove the
+ // developer-button from the UI before opening a new window.
+ if (AppConstants.MOZ_DEV_EDITION) {
+ CustomizableUI.removeWidgetFromArea("developer-button");
+ }
+
+ info(
+ "Open a new window, all window-specific hooks for DevTools will be disabled."
+ );
+ const win = OpenBrowserWindow({ private: false });
+ await waitForDelayedStartupFinished(win);
+
+ info(
+ "Open a new tab on the new window to ensure the focus is on the new window"
+ );
+ const tab = BrowserTestUtils.addTab(
+ win.gBrowser,
+ "data:text/html;charset=utf-8,<title>foo</title>"
+ );
+ await BrowserTestUtils.browserLoaded(win.gBrowser.getBrowserForTab(tab));
+
+ info(
+ "Synthesize a DevTools shortcut, the toolbox should not open on this new window."
+ );
+ synthesizeToggleToolboxKey(win);
+
+ // There is no event to wait for here as this shortcut should have no effect.
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(r => setTimeout(r, 1000));
+
+ is(gDevTools._toolboxes.size, 0, "No toolbox has been opened");
+
+ const browser = gBrowser.selectedTab.linkedBrowser;
+ const location = browser.documentURI.spec;
+ ok(
+ !location.startsWith("about:devtools"),
+ "The current tab is not about:devtools"
+ );
+
+ info("Open the context menu for the content page.");
+ const contextMenu = win.document.getElementById("contentAreaContextMenu");
+ const popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ win.document.documentElement,
+ { type: "contextmenu", button: 2 },
+ win
+ );
+ await popupShownPromise;
+
+ const inspectElementItem = contextMenu.querySelector(`#context-inspect`);
+ ok(
+ inspectElementItem.hidden,
+ "The inspect element item is hidden in the context menu"
+ );
+
+ info("Close the context menu");
+ const onContextMenuHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await onContextMenuHidden;
+
+ const toolsMenu = win.document.getElementById("webDeveloperMenu");
+ ok(toolsMenu.hidden, "The Web Developer item of the tools menu is hidden");
+ const hamburgerMenu = win.document.getElementById("appMenu-developer-button");
+ is(
+ hamburgerMenu,
+ null,
+ "The Web Developer item of the hamburger menu should not be available"
+ );
+
+ win.gBrowser.removeTab(tab);
+
+ info("Close the test window");
+ const winClosed = BrowserTestUtils.windowClosed(win);
+ win.BrowserTryToCloseWindow();
+ await winClosed;
+});
+
+function waitForDelayedStartupFinished(win) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subject, topic) {
+ if (win == subject) {
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }
+ }, "browser-delayed-startup-finished");
+ });
+}
+
+/**
+ * Helper to call the toggle devtools shortcut.
+ */
+function synthesizeToggleToolboxKey(win) {
+ info("Trigger the toogle toolbox shortcut");
+ if (Services.appinfo.OS == "Darwin") {
+ EventUtils.synthesizeKey("i", { accelKey: true, altKey: true }, win);
+ } else {
+ EventUtils.synthesizeKey("i", { accelKey: true, shiftKey: true }, win);
+ }
+}
diff --git a/devtools/startup/tests/xpcshell/.eslintrc.js b/devtools/startup/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..6581a0bf32
--- /dev/null
+++ b/devtools/startup/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: "../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/startup/tests/xpcshell/test_devtools_shim.js b/devtools/startup/tests/xpcshell/test_devtools_shim.js
new file mode 100644
index 0000000000..e728290263
--- /dev/null
+++ b/devtools/startup/tests/xpcshell/test_devtools_shim.js
@@ -0,0 +1,214 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { DevToolsShim } = ChromeUtils.import(
+ "chrome://devtools-startup/content/DevToolsShim.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// Test the DevToolsShim
+
+/**
+ * Create a mocked version of DevTools that records all calls made to methods expected
+ * to be called by DevToolsShim.
+ */
+function createMockDevTools() {
+ const methods = [
+ "on",
+ "off",
+ "emit",
+ "saveDevToolsSession",
+ "restoreDevToolsSession",
+ ];
+
+ const mock = {
+ callLog: {},
+ };
+
+ for (const method of methods) {
+ // Create a stub for method, that only pushes its arguments in the inner callLog
+ mock[method] = function(...args) {
+ mock.callLog[method].push(args);
+ };
+ mock.callLog[method] = [];
+ }
+
+ return mock;
+}
+
+/**
+ * Check if a given method was called an expected number of times, and finally check the
+ * arguments provided to the last call, if appropriate.
+ */
+function checkCalls(mock, method, length, lastArgs) {
+ ok(
+ mock.callLog[method].length === length,
+ "Devtools.on was called the expected number of times"
+ );
+
+ // If we don't want to check the last call or if the method was never called, bail out.
+ if (!lastArgs || length === 0) {
+ return;
+ }
+
+ for (let i = 0; i < lastArgs.length; i++) {
+ const expectedArg = lastArgs[i];
+ ok(
+ mock.callLog[method][length - 1][i] === expectedArg,
+ `Devtools.${method} was called with the expected argument (index ${i})`
+ );
+ }
+}
+
+function test_register_unregister() {
+ ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+
+ DevToolsShim.register(createMockDevTools());
+ ok(DevToolsShim.isInitialized(), "DevTools are initialized");
+
+ DevToolsShim.unregister();
+ ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+}
+
+function test_on_is_forwarded_to_devtools() {
+ ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+
+ function cb1() {}
+ function cb2() {}
+ const mock = createMockDevTools();
+
+ DevToolsShim.on("test_event", cb1);
+ DevToolsShim.register(mock);
+ checkCalls(mock, "on", 1, ["test_event", cb1]);
+
+ DevToolsShim.on("other_event", cb2);
+ checkCalls(mock, "on", 2, ["other_event", cb2]);
+}
+
+function test_off_called_before_registering_devtools() {
+ ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+
+ function cb1() {}
+ const mock = createMockDevTools();
+
+ DevToolsShim.on("test_event", cb1);
+ DevToolsShim.off("test_event", cb1);
+
+ DevToolsShim.register(mock);
+ checkCalls(mock, "on", 0);
+}
+
+function test_off_called_before_with_bad_callback() {
+ ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+
+ function cb1() {}
+ function cb2() {}
+ const mock = createMockDevTools();
+
+ DevToolsShim.on("test_event", cb1);
+ DevToolsShim.off("test_event", cb2);
+
+ DevToolsShim.register(mock);
+ // on should still be called
+ checkCalls(mock, "on", 1, ["test_event", cb1]);
+ // Calls to off should not be held and forwarded.
+ checkCalls(mock, "off", 0);
+}
+
+function test_events() {
+ ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+
+ const mock = createMockDevTools();
+ // Check emit was not called.
+ checkCalls(mock, "emit", 0);
+
+ // Check emit is called once with the devtools-registered event.
+ DevToolsShim.register(mock);
+ checkCalls(mock, "emit", 1, ["devtools-registered"]);
+
+ // Check emit is called once with the devtools-unregistered event.
+ DevToolsShim.unregister();
+ checkCalls(mock, "emit", 2, ["devtools-unregistered"]);
+}
+
+function test_restore_session_apis() {
+ // Backup method and preferences that will be updated for the test.
+ const initDevToolsBackup = DevToolsShim.initDevTools;
+ const devtoolsEnabledValue = Services.prefs.getBoolPref("devtools.enabled");
+
+ // Create fake session objects to restore.
+ const sessionWithoutDevTools = {};
+ const sessionWithDevTools = {
+ browserConsole: true,
+ };
+
+ function checkRestoreSessionNotApplied(policyDisabled, enabled) {
+ Services.prefs.setBoolPref("devtools.enabled", enabled);
+ Services.prefs.setBoolPref("devtools.policy.disabled", policyDisabled);
+ ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+ ok(!DevToolsShim.isEnabled(), "DevTools are not enabled");
+
+ // Check that save & restore DevToolsSession don't initialize the tools and don't
+ // crash.
+ DevToolsShim.saveDevToolsSession({});
+ DevToolsShim.restoreDevToolsSession(sessionWithDevTools);
+ ok(!DevToolsShim.isInitialized(), "DevTools are still not initialized");
+ }
+
+ // Tools are disabled by policy and not enabled
+ checkRestoreSessionNotApplied(true, false);
+ // Tools are not disabled by policy, but not enabled
+ checkRestoreSessionNotApplied(false, false);
+ // Tools are disabled by policy and "considered" as enabled (see Bug 1440675)
+ checkRestoreSessionNotApplied(true, true);
+
+ Services.prefs.setBoolPref("devtools.enabled", true);
+ Services.prefs.setBoolPref("devtools.policy.disabled", false);
+ ok(DevToolsShim.isEnabled(), "DevTools are enabled");
+ ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+
+ // Check that DevTools are not initialized when calling restoreDevToolsSession without
+ // DevTools related data.
+ DevToolsShim.restoreDevToolsSession(sessionWithoutDevTools);
+ ok(!DevToolsShim.isInitialized(), "DevTools are still not initialized");
+
+ const mock = createMockDevTools();
+ DevToolsShim.initDevTools = () => {
+ // Next call to restoreDevToolsSession is expected to initialize DevTools, which we
+ // simulate here by registering our mock.
+ DevToolsShim.register(mock);
+ };
+
+ DevToolsShim.restoreDevToolsSession(sessionWithDevTools);
+ checkCalls(mock, "restoreDevToolsSession", 1, [sessionWithDevTools]);
+
+ ok(DevToolsShim.isInitialized(), "DevTools are initialized");
+
+ DevToolsShim.saveDevToolsSession({});
+ checkCalls(mock, "saveDevToolsSession", 1, []);
+
+ // Restore initial backups.
+ DevToolsShim.initDevTools = initDevToolsBackup;
+ Services.prefs.setBoolPref("devtools.enabled", devtoolsEnabledValue);
+}
+
+function run_test() {
+ test_register_unregister();
+ DevToolsShim.unregister();
+
+ test_on_is_forwarded_to_devtools();
+ DevToolsShim.unregister();
+
+ test_off_called_before_registering_devtools();
+ DevToolsShim.unregister();
+
+ test_off_called_before_with_bad_callback();
+ DevToolsShim.unregister();
+
+ test_restore_session_apis();
+ DevToolsShim.unregister();
+
+ test_events();
+}
diff --git a/devtools/startup/tests/xpcshell/xpcshell.ini b/devtools/startup/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..c329a440d9
--- /dev/null
+++ b/devtools/startup/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+tags = devtools
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_devtools_shim.js]