diff --git a/browser/components/customizableui/content/panelUI.inc.xhtml b/browser/components/customizableui/content/panelUI.inc.xhtml
index 3ce9282f287a..1a1e40af0e37 100644
--- a/browser/components/customizableui/content/panelUI.inc.xhtml
+++ b/browser/components/customizableui/content/panelUI.inc.xhtml
@@ -644,7 +644,87 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/themes/shared/customizableui/panelUI.inc.css b/browser/themes/shared/customizableui/panelUI.inc.css
index d5da7a2d92ee..c591b837cabd 100644
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -1831,7 +1831,7 @@ toolbarpaletteitem[place="menu-panel"] > .subviewbutton-nav::after {
height: calc(100% - 20px);
margin: 10px;
transition: opacity .25s;
- opacity: 1;
+ opacity: 1;
background-image: url(chrome://browser/skin/controlcenter/hero-message-background.svg);
background-repeat: no-repeat;
background-position: top right;
@@ -1963,3 +1963,282 @@ panelview[mainview] #PanelUI-whatsNew-content {
width: 100%;
text-align: end;
}
+
+#PanelUI-profiler {
+ /* On Linux, the popup was too wide for the content due to this rule. This is marked
+ as !important to get over the specificity of the CustomizeableUI base rules. */
+ min-width: 0 !important;
+}
+
+#PanelUI-profiler description {
+ max-width: 290px;
+}
+
+/* Make the width nicely sized for when the panel is in the overflow menu area. */
+#widget-overflow #PanelUI-profiler description {
+ max-width: 347px;
+}
+
+#widget-overflow #PanelUI-profiler-header-bar {
+ display: none;
+}
+
+#PanelUI-profiler-header[isinfocollapsed="false"] {
+ background: radial-gradient(circle farthest-side at top right, #03b1f4, #0556cd);
+ color: #fff;
+}
+
+#PanelUI-profiler-header-bar {
+ border-bottom: 1px solid #0003;
+}
+
+#PanelUI-profiler-header-bar label {
+ margin-inline-start: 32px;
+ padding: 12px;
+ font-weight: bold;
+ text-align: center;
+}
+
+#PanelUI-profiler-header[isinfocollapsed="true"] #PanelUI-profiler-info {
+ pointer-events: none;
+}
+
+#PanelUI-profiler-header[isinfocollapsed="true"] #PanelUI-profiler-header-bar {
+ border-bottom: 1px solid var(--panel-separator-color);
+}
+
+#PanelUI-profiler-header[isinfocollapsed="false"] #PanelUI-profiler-info-button {
+ background-color: #0003;
+}
+
+#PanelUI-profiler-header[isinfocollapsed="false"] #PanelUI-profiler-info-button:hover {
+ background-color: #0005;
+}
+
+#PanelUI-profiler-info {
+ height: 180px;
+ background: top 10px right no-repeat
+ url("chrome://devtools/skin/images/performance-new-popup-backdrop.png");
+ opacity: 1;
+}
+
+/* The header has an attribute "animationready" that is either set to "true" or to
+ "false". When the DOM is being initialized it is set to "false" so that animatios
+ do not run. */
+#PanelUI-profiler-header[animationready="true"] #PanelUI-profiler-info {
+ transition: margin-block-end 250ms, opacity 250ms;
+}
+
+#PanelUI-profiler-header[isinfocollapsed="true"] #PanelUI-profiler-info {
+ opacity: 0;
+}
+
+/* Hide the unused box that gets added here. */
+#PanelUI-profiler-learn-more > hbox {
+ display: none;
+}
+
+#PanelUI-profiler-content {
+ position: relative;
+}
+
+.PanelUI-profiler-toolbarbutton-container {
+ -moz-box-pack: center;
+ margin-inline-end: 4px;
+}
+
+#PanelUI-profiler-info label {
+ font-size: 16px;
+ font-weight: bold;
+ margin-block: 6px;
+ margin-inline: 15px;
+}
+
+#PanelUI-profiler-info description {
+ margin-block: 4px;
+ margin-inline: 15px;
+}
+
+#PanelUI-profiler-info button {
+ -moz-appearance: none;
+ padding: 2px;
+ margin: 4px 13px 13px;
+ color: #fff;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+#PanelUI-profiler-info button:active {
+ color: #fffc;
+}
+
+#PanelUI-profiler-info button:focus {
+ box-shadow: 0 0 0 2px #fff, 0 0 0 6px rgba(255, 255, 255, 0.3);
+ border-radius: 2px;
+ text-decoration: none;
+}
+
+#PanelUI-profiler-presets {
+ -moz-appearance: none;
+ width: 100%;
+ min-height: 24px;
+ background-color: #EDEDF0;
+ color: inherit !important;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ font-weight: 400;
+ padding-inline: 4px;
+ text-decoration: none;
+ font-size: 12px;
+ margin-inline: 15px;
+ margin-block: 8px;
+}
+
+/* This is dark-mode specific styling. */
+:root[lwt-popup-brighttext] #PanelUI-profiler-presets {
+ background-color: rgb(47, 47, 51);
+ color: #fff;
+ border: 1px solid #666168;
+}
+
+#PanelUI-profiler-presets:hover:not([disabled="true"]) {
+ border-color: #DDDDE0;
+}
+
+#PanelUI-profiler-presets:focus {
+ box-shadow: 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
+
+#PanelUI-profiler-presets menupopup {
+ -moz-appearance: none;
+ border: 1px solid #B7B7B7;
+ border-radius: 2px;
+ background-color: #FFF;
+ padding-block: 4px;
+}
+
+#PanelUI-profiler-presets menuitem {
+ -moz-appearance: none;
+ font-size: 1em;
+ color: #000;
+ padding-block: 0.2em;
+}
+
+#PanelUI-profiler-presets menuitem[_moz-menuactive] {
+ color: #fff;
+ background-color: #0060DF;
+}
+
+/* The dropmarker is generated by the menulist element */
+#PanelUI-profiler-presets dropmarker {
+ -moz-appearance: none;
+ display: block;
+ margin-block-start: 5px;
+ -moz-context-properties: fill;
+ list-style-image: url("chrome://global/skin/icons/arrow-dropdown-12.svg");
+ fill: currentColor;
+}
+
+#PanelUI-profiler-content-custom-button {
+ -moz-appearance: none;
+ margin: 0;
+ color: #0046A4;
+ font-size: 12px;
+ cursor: pointer;
+}
+
+:root[lwt-popup-brighttext] #PanelUI-profiler-content-custom-button {
+ color: #76B1FF;
+}
+
+#PanelUI-profiler-content-custom-button:hover {
+ text-decoration: underline;
+}
+
+#PanelUI-profiler-content-custom-button:active,
+#PanelUI-profiler-content-custom-button:focus {
+ text-decoration: underline;
+}
+
+#PanelUI-profiler-content-custom {
+ margin-inline: 15px;
+}
+
+.PanelUI-profiler-content-label {
+ font-weight: bold;
+ font-size: 13px;
+ margin-inline: 15px;
+ margin-block-start: 10px;
+}
+
+#PanelUI-profiler-content-description {
+ margin-inline: 15px;
+ margin-block: 4px;
+ font-size: 12px;
+ opacity: 0.75;
+}
+
+.PanelUI-profiler-button {
+ -moz-appearance: none;
+ background-color: #e0e0e1;
+ border-radius: 2px;
+ color: #000;
+ padding: 8px;
+ margin-block: 0;
+ margin-inline: 5px;
+ font-size: 13px;
+}
+
+.PanelUI-profiler-button:hover {
+ background-color: #c9c9ca;
+}
+
+.PanelUI-profiler-button:active {
+ background-color: #b1b1b1;
+}
+
+.PanelUI-profiler-button:focus {
+ box-shadow:
+ 0 0 0 1px #0a84ff inset,
+ 0 0 0 1px #0a84ff,
+ 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
+
+.PanelUI-profiler-button-primary {
+ background-color: #0060DF;
+ /* The !important designation here is to overcome the specificity of the button.css
+ styles that are being applied on linux for hovers. */
+ color: #fff !important;
+}
+
+.PanelUI-profiler-button-primary:hover {
+ background-color: #003eaa;
+}
+
+.PanelUI-profiler-button-primary:active {
+ background-color: #002275;
+}
+
+.PanelUI-profiler-shortcut {
+ padding-block-start: 5px;
+ opacity: 0.5;
+ text-align: center;
+}
+
+.PanelUI-profiler-buttons {
+ margin-inline: 10px;
+ margin-block: 7px 12px;
+}
+
+.PanelUI-profiler-info-icon {
+ margin-inline-end: 25px;
+ margin-block-start: 20px;
+ width: 50px;
+ list-style-image: url(chrome://devtools/skin/images/tool-profiler.svg);
+ -moz-context-properties: fill;
+ fill: #fff;
+}
+
+#PanelUI-profiler-presets[disabled="true"] .menulist-label-box {
+ opacity: 0.5;
+}
diff --git a/devtools/client/jar.mn b/devtools/client/jar.mn
index 5f7b45865783..cba4bedaede8 100644
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -98,6 +98,7 @@ devtools.jar:
skin/images/performance-details-waterfall.svg (themes/images/performance-details-waterfall.svg)
skin/images/performance-details-call-tree.svg (themes/images/performance-details-call-tree.svg)
skin/images/performance-details-flamegraph.svg (themes/images/performance-details-flamegraph.svg)
+ skin/images/performance-new-popup-backdrop.png (themes/images/performance-new-popup-backdrop.png)
skin/breadcrumbs.css (themes/breadcrumbs.css)
skin/chart.css (themes/chart.css)
skin/widgets.css (themes/widgets.css)
diff --git a/devtools/client/performance-new/@types/gecko.d.ts b/devtools/client/performance-new/@types/gecko.d.ts
index 5e6fc0500299..c4db449bfef6 100644
--- a/devtools/client/performance-new/@types/gecko.d.ts
+++ b/devtools/client/performance-new/@types/gecko.d.ts
@@ -156,8 +156,8 @@ declare namespace MockedExports {
// TS-TODO
const CustomizableUIJSM: any;
-
const CustomizableWidgetsJSM: any;
+ const PanelMultiViewJSM: any;
const Services: Services;
@@ -267,6 +267,10 @@ declare module "resource:///modules/CustomizableWidgets.jsm" {
export = MockedExports.CustomizableWidgetsJSM;
}
+declare module "resource:///modules/PanelMultiView.jsm" {
+ export = MockedExports.PanelMultiViewJSM;
+}
+
declare var ChromeUtils: MockedExports.ChromeUtils;
declare var Cu: MockedExports.ChromeUtils;
@@ -298,4 +302,31 @@ declare interface XULIframeElement extends XULElement {
src: string;
}
-declare interface ChromeWindow extends Window {}
+declare interface ChromeWindow extends Window {
+ openWebLinkIn: (
+ url: string,
+ where: "current" | "tab" | "tabshifted" | "window" | "save",
+ // TS-TODO
+ params?: unknown
+ ) => void;
+ openTrustedLinkIn: (
+ url: string,
+ where: "current" | "tab" | "tabshifted" | "window" | "save",
+ // TS-TODO
+ params?: unknown
+ ) => void;
+}
+
+declare interface MenuListElement extends XULElement {
+ value: string;
+ disabled: boolean;
+}
+
+declare interface XULCommandEvent extends Event {
+ target: XULElement
+}
+
+declare interface XULElementWithCommandHandler {
+ addEventListener: (type: "command", handler: (event: XULCommandEvent) => void, isCapture?: boolean) => void
+ removeEventListener: (type: "command", handler: (event: XULCommandEvent) => void, isCapture?: boolean) => void
+}
diff --git a/devtools/client/performance-new/@types/perf.d.ts b/devtools/client/performance-new/@types/perf.d.ts
index b433b24055b9..444ee9526095 100644
--- a/devtools/client/performance-new/@types/perf.d.ts
+++ b/devtools/client/performance-new/@types/perf.d.ts
@@ -368,6 +368,11 @@ export interface PerformancePref {
* and update it elsewhere.
*/
PopupEnabled: "devtools.performance.popup.enabled";
+ /**
+ * The profiler popup has some introductory text explaining what it is the first
+ * time that you open it. After that, it is not displayed by default.
+ */
+ PopupIntroDisplayed: "devtools.performance.popup.intro-displayed";
}
/**
diff --git a/devtools/client/performance-new/popup/menu-button.jsm.js b/devtools/client/performance-new/popup/menu-button.jsm.js
index 50ec2e33607b..9d73efaa01d1 100644
--- a/devtools/client/performance-new/popup/menu-button.jsm.js
+++ b/devtools/client/performance-new/popup/menu-button.jsm.js
@@ -6,7 +6,8 @@
/**
* @typedef {import("../@types/perf").PerformancePref} PerformancePref
- * */
+ */
+
/**
* This file controls the enabling and disabling of the menu button for the profiler.
* Care should be taken to keep it minimal as it can be run with browser initialization.
@@ -47,6 +48,13 @@ const lazyCustomizableWidgets = requireLazy(() =>
/** @type {import("resource:///modules/CustomizableWidgets.jsm")} */
(ChromeUtils.import("resource:///modules/CustomizableWidgets.jsm"))
);
+const lazyPopupPanel = requireLazy(() =>
+ /** @type {import("devtools/client/performance-new/popup/panel.jsm.js")} */
+ (ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/panel.jsm.js"
+ ))
+);
+
/** @type {PerformancePref["PopupEnabled"]} */
const BUTTON_ENABLED_PREF = "devtools.performance.popup.enabled";
const WIDGET_ID = "profiler-button";
@@ -132,15 +140,24 @@ function initialize() {
return;
}
- /** @typedef {() => void} Observer */
-
- /** @type {null | Observer} */
+ /** @type {null | (() => void)} */
let observer = null;
+ const viewId = "PanelUI-profiler";
+
+ /**
+ * This is mutable state that will be shared between panel displays.
+ *
+ * @type {import("devtools/client/performance-new/popup/panel.jsm.js").State}
+ */
+ const panelState = {
+ cleanup: [],
+ isInfoCollapsed: true,
+ };
const item = {
id: WIDGET_ID,
type: "view",
- viewId: "PanelUI-profiler",
+ viewId,
tooltiptext: "profiler-button.tooltiptext",
onViewShowing:
@@ -153,85 +170,55 @@ function initialize() {
* }) => void}
*/
event => {
- const panelview = event.target;
- const document = panelview.ownerDocument;
- if (!document) {
- throw new Error(
- "Expected to find a document on the panelview element."
- );
+ try {
+ // The popup logic is stored in a separate script so it doesn't have
+ // to be parsed at browser startup, and will only be lazily loaded
+ // when the popup is viewed.
+ const {
+ selectElementsInPanelview,
+ createViewControllers,
+ addPopupEventHandlers,
+ initializePopup,
+ } = lazyPopupPanel();
+
+ const panelElements = selectElementsInPanelview(event.target);
+ const panelView = createViewControllers(panelState, panelElements);
+ addPopupEventHandlers(panelState, panelElements, panelView);
+ initializePopup(panelState, panelElements, panelView);
+ } catch (error) {
+ // Surface any errors better in the console.
+ console.error(error);
}
-
- // Create an iframe and append it to the panelview.
- const iframe = document.createXULElement("iframe");
- iframe.id = "PanelUI-profilerIframe";
- iframe.className = "PanelUI-developer-iframe";
- iframe.src =
- "chrome://devtools/content/performance-new/popup/popup.xhtml";
-
- panelview.appendChild(iframe);
- /** @type {any} - Cast to an any since we're assigning values to the window object. */
- const contentWindow = iframe.contentWindow;
-
- // Provide a mechanism for the iframe to close the popup.
- contentWindow.gClosePopup = () => {
- CustomizableUI.hidePanelForNode(iframe);
- };
-
- // Provide a mechanism for the iframe to resize the popup.
- /** @type {(height: number) => void} */
- contentWindow.gResizePopup = height => {
- iframe.style.height = `${Math.min(600, height)}px`;
- };
-
- // Tell the iframe whether we're in dark mode or not. This approach
- // unfortunately has two flaws. First, since this is a boolean setting,
- // we ignore any theme information and just flip on our preset dark mode
- // if the theme is dark enough. It might look out of place depending on
- // the system or Firefox theme in use. Second, we won't detect all dark
- // mode cases here. If the user is using the default Firefox theme, which
- // normally adjusts to system-themes, we'll only be able to detect dark
- // mode on Windows and Macintosh. In Linux, instead of having a global
- // dark mode setting, we use GTK-theme colors directly for theming, and
- // those won't be detected here. Similarly, if a Windows user is not using
- // the particular dark mode system setting, but instead using a darker
- // system theme, then that will also not be detected here.
- contentWindow.gIsDarkMode = document.documentElement.hasAttribute(
- "lwt-popup-brighttext"
- );
-
- // The popup has an annoying rendering "blip" when first rendering the react
- // components. This adds a blocker until the content is ready to show.
- event.detail.addBlocker(
- new Promise(resolve => {
- contentWindow.gReportReady = () => {
- // Delete the function gReportReady so we don't leave any dangling
- // references between windows.
- delete contentWindow.gReportReady;
- // Now resolve this promise to open the window.
- resolve();
- };
- })
- );
},
/**
* @type {(event: { target: ChromeHTMLElement | XULElement }) => void}
*/
onViewHiding(event) {
- const document = event.target.ownerDocument;
-
- // Create the iframe, and append it.
- const iframe = document.getElementById("PanelUI-profilerIframe");
- if (!iframe) {
- throw new Error("Unable to select the PanelUI-profilerIframe.");
+ // Clean-up the view. This removes all of the event listeners.
+ for (const fn of panelState.cleanup) {
+ fn();
}
-
- // Remove the iframe so it doesn't leak.
- iframe.remove();
+ panelState.cleanup = [];
},
/** @type {(document: HTMLDocument) => void} */
onBeforeCreated: document => {
+ /** @type {PerformancePref["PopupIntroDisplayed"]} */
+ const popupIntroDisplayedPref =
+ "devtools.performance.popup.intro-displayed";
+
+ // Determine the state of the popup's info being collapsed BEFORE the view
+ // is shown, and update the collapsed state. This way the transition animation
+ // isn't run.
+ panelState.isInfoCollapsed = Services.prefs.getBoolPref(
+ popupIntroDisplayedPref
+ );
+ if (!panelState.isInfoCollapsed) {
+ // We have displayed the intro, don't show it again by default.
+ Services.prefs.setBoolPref(popupIntroDisplayedPref, true);
+ }
+
setMenuItemChecked(document, true);
},
diff --git a/devtools/client/performance-new/popup/moz.build b/devtools/client/performance-new/popup/moz.build
index 65e2737a32f3..57fd35f56c2a 100644
--- a/devtools/client/performance-new/popup/moz.build
+++ b/devtools/client/performance-new/popup/moz.build
@@ -6,6 +6,7 @@
DevToolsModules(
'background.jsm.js',
'menu-button.jsm.js',
+ 'panel.jsm.js',
)
with Files('**'):
diff --git a/devtools/client/performance-new/popup/panel.jsm.js b/devtools/client/performance-new/popup/panel.jsm.js
new file mode 100644
index 000000000000..1875fb2f14fb
--- /dev/null
+++ b/devtools/client/performance-new/popup/panel.jsm.js
@@ -0,0 +1,342 @@
+/* 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/. */
+// @ts-check
+"use strict";
+
+/**
+ * This file controls the logic of the profiler popup view.
+ */
+
+/**
+ * @typedef {ReturnType} Elements
+ * @typedef {ReturnType} ViewController
+ */
+
+/**
+ * @typedef {Object} State - The mutable state of the popup.
+ * @property {Array<() => void>} cleanup - Functions to cleanup once the view is hidden.
+ * @property {boolean} isInfoCollapsed
+ */
+
+/**
+ * TS-TODO
+ *
+ * This function replaces lazyRequireGetter, and TypeScript can understand it. It's
+ * currently duplicated until we have consensus that TypeScript is a good idea.
+ *
+ * @template T
+ * @type {(callback: () => T) => () => T}
+ */
+function requireLazy(callback) {
+ /** @type {T | undefined} */
+ let cache;
+ return () => {
+ if (cache === undefined) {
+ cache = callback();
+ }
+ return cache;
+ };
+}
+
+// Provide an exports object for the JSM to be properly read by TypeScript.
+/** @type {any} */ (this).module = {};
+
+const lazyServices = requireLazy(() =>
+ /** @type {import("resource://gre/modules/Services.jsm")} */
+ (ChromeUtils.import("resource://gre/modules/Services.jsm"))
+);
+const lazyPanelMultiView = requireLazy(() =>
+ /** @type {import("resource:///modules/PanelMultiView.jsm")} */
+ (ChromeUtils.import("resource:///modules/PanelMultiView.jsm"))
+);
+const lazyBackground = requireLazy(() =>
+ /** @type {import("resource://devtools/client/performance-new/popup/background.jsm.js")} */
+ (ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/background.jsm.js"
+ ))
+);
+
+/**
+ * This function collects all of the selection of the elements inside of the panel.
+ *
+ * @param {XULElement} panelview
+ */
+function selectElementsInPanelview(panelview) {
+ const document = panelview.ownerDocument;
+ /**
+ * Get an element or throw an error if it's not found. This is more friendly
+ * for TypeScript.
+ *
+ * @param {string} id
+ * @return {HTMLElement}
+ */
+ function getElementById(id) {
+ /** @type {HTMLElement | null} */
+ const element = document.getElementById(id);
+ if (!element) {
+ throw new Error(`Could not find the element from the ID "${id}"`);
+ }
+ return element;
+ }
+
+ return {
+ document,
+ panelview,
+ window: /** @type {ChromeWindow} */ (document.defaultView),
+ inactive: getElementById("PanelUI-profiler-inactive"),
+ active: getElementById("PanelUI-profiler-active"),
+ presetDescription: getElementById("PanelUI-profiler-content-description"),
+ presetCustom: getElementById("PanelUI-profiler-content-custom"),
+ presetsCustomButton: getElementById(
+ "PanelUI-profiler-content-custom-button"
+ ),
+ presetsMenuList: /** @type {MenuListElement} */ (getElementById(
+ "PanelUI-profiler-presets"
+ )),
+ header: getElementById("PanelUI-profiler-header"),
+ info: getElementById("PanelUI-profiler-info"),
+ menupopup: getElementById("PanelUI-profiler-presets-menupopup"),
+ infoButton: getElementById("PanelUI-profiler-info-button"),
+ learnMore: getElementById("PanelUI-profiler-learn-more"),
+ startRecording: getElementById("PanelUI-profiler-startRecording"),
+ stopAndDiscard: getElementById("PanelUI-profiler-stopAndDiscard"),
+ stopAndCapture: getElementById("PanelUI-profiler-stopAndCapture"),
+ };
+}
+
+/**
+ * This function returns an interface that can be used to control the view of the
+ * panel based on the current mutable State.
+ *
+ * @param {State} state
+ * @param {Elements} elements
+ */
+function createViewControllers(state, elements) {
+ return {
+ updateInfoCollapse() {
+ const { header, info } = elements;
+ header.setAttribute(
+ "isinfocollapsed",
+ state.isInfoCollapsed ? "true" : "false"
+ );
+
+ if (state.isInfoCollapsed) {
+ const { height } = info.getBoundingClientRect();
+ info.style.marginBlockEnd = `-${height}px`;
+ } else {
+ info.style.marginBlockEnd = "0";
+ }
+ },
+
+ updatePresets() {
+ const { presets, getRecordingPreferencesFromBrowser } = lazyBackground();
+ const { presetName } = getRecordingPreferencesFromBrowser();
+ const preset = presets[presetName];
+ if (preset) {
+ elements.presetDescription.style.display = "block";
+ elements.presetCustom.style.display = "none";
+ elements.presetDescription.textContent = preset.description;
+ elements.presetsMenuList.value = presetName;
+ } else {
+ elements.presetDescription.style.display = "none";
+ elements.presetCustom.style.display = "block";
+ }
+ const { PanelMultiView } = lazyPanelMultiView();
+ // Update the description height sizing.
+ PanelMultiView.forNode(elements.panelview).descriptionHeightWorkaround();
+ },
+
+ updateProfilerActive() {
+ const { Services } = lazyServices();
+ const isProfilerActive = Services.profiler.IsActive();
+ elements.inactive.setAttribute(
+ "hidden",
+ isProfilerActive ? "true" : "false"
+ );
+ elements.active.setAttribute(
+ "hidden",
+ isProfilerActive ? "false" : "true"
+ );
+ elements.presetsMenuList.disabled = isProfilerActive;
+ },
+
+ createPresetsList() {
+ // Check the DOM if the presets were built or not. We can't cache this value
+ // in the `State` object, as the `State` object will be removed if the
+ // button is removed from the toolbar, but the DOM changes will still persist.
+ if (elements.menupopup.getAttribute("presetsbuilt") === "true") {
+ // The presets were already built.
+ return;
+ }
+ const { Services } = lazyServices();
+ const { presets } = lazyBackground();
+ const currentPreset = Services.prefs.getCharPref(
+ "devtools.performance.recording.preset"
+ );
+
+ const menuitems = Object.entries(presets).map(([id, preset]) => {
+ const menuitem = elements.document.createXULElement("menuitem");
+ menuitem.setAttribute("label", preset.label);
+ menuitem.setAttribute("value", id);
+ if (id === currentPreset) {
+ elements.presetsMenuList.setAttribute("value", id);
+ }
+ return menuitem;
+ });
+
+ elements.menupopup.prepend(...menuitems);
+ elements.menupopup.setAttribute("presetsbuilt", "true");
+ },
+
+ hidePopup() {
+ const panel = elements.panelview.closest("panel");
+ if (!panel) {
+ throw new Error("Could not find the panel from the panelview.");
+ }
+ /** @type {any} */ (panel).hidePopup();
+ },
+ };
+}
+
+/**
+ * Perform all of the business logic to present the popup view once it is open.
+ *
+ * @param {State} state
+ * @param {Elements} elements
+ * @param {ViewController} view
+ */
+function initializePopup(state, elements, view) {
+ view.createPresetsList();
+
+ state.cleanup.push(() => {
+ // The UI should be collapsed by default for the next time the popup
+ // is open.
+ state.isInfoCollapsed = true;
+ view.updateInfoCollapse();
+ });
+
+ // Turn off all animations while initializing the popup.
+ elements.header.setAttribute("animationready", "false");
+
+ elements.window.requestAnimationFrame(() => {
+ // Allow the elements to layout once, the updateInfoCollapse implementation measures
+ // the size of the container. It needs to wait a second before the bounding box
+ // returns an actual size.
+ view.updateInfoCollapse();
+ view.updateProfilerActive();
+ view.updatePresets();
+
+ // XUL elements don't vertically size correctly, this is
+ // the workaround for it.
+ const { PanelMultiView } = lazyPanelMultiView();
+ PanelMultiView.forNode(elements.panelview).descriptionHeightWorkaround();
+
+ // Now wait for another rAF, and turn the animations back on.
+ elements.window.requestAnimationFrame(() => {
+ elements.header.setAttribute("animationready", "true");
+ });
+ });
+}
+
+/**
+ * This function is in charge of settings all of the events handlers for the view.
+ * The handlers must also add themselves to the `state.cleanup` for them to be
+ * properly cleaned up once the view is destroyed.
+ *
+ * @param {State} state
+ * @param {Elements} elements
+ * @param {ViewController} view
+ */
+function addPopupEventHandlers(state, elements, view) {
+ const {
+ changePreset,
+ startProfiler,
+ stopProfiler,
+ captureProfile,
+ } = lazyBackground();
+
+ /**
+ * Adds a handler that automatically is removed once the panel is hidden.
+ *
+ * @param {HTMLElement} element
+ * @param {string} type
+ * @param {(event: Event) => void} handler
+ */
+ function addHandler(element, type, handler) {
+ element.addEventListener(type, handler);
+ state.cleanup.push(() => {
+ element.removeEventListener(type, handler);
+ });
+ }
+
+ addHandler(elements.infoButton, "click", event => {
+ // Any button command event in the popup will cause it to close. Prevent this
+ // from happening on click.
+ event.preventDefault();
+
+ state.isInfoCollapsed = !state.isInfoCollapsed;
+ view.updateInfoCollapse();
+ });
+
+ addHandler(elements.startRecording, "click", () => {
+ startProfiler();
+ });
+
+ addHandler(elements.stopAndDiscard, "click", () => {
+ stopProfiler();
+ });
+
+ addHandler(elements.stopAndCapture, "click", () => {
+ captureProfile();
+ view.hidePopup();
+ });
+
+ addHandler(elements.learnMore, "click", () => {
+ elements.window.openWebLinkIn("https://profiler.firefox.com/docs/", "tab");
+ view.hidePopup();
+ });
+
+ addHandler(elements.presetsMenuList, "command", () => {
+ changePreset(elements.presetsMenuList.value);
+ view.updatePresets();
+ });
+
+ addHandler(elements.presetsMenuList, "popuphidden", event => {
+ // Changing a preset makes the popup autohide, this handler stops the
+ // propagation of that event, so that only the menulist's popup closes,
+ // and not the rest of the popup.
+ event.stopPropagation();
+ });
+
+ addHandler(elements.presetsMenuList, "click", event => {
+ // Clicking on a preset makes the popup autohide, this preventDefault stops
+ // the CustomizableUI from closing the popup.
+ event.preventDefault();
+ });
+
+ addHandler(elements.presetsCustomButton, "click", () => {
+ elements.window.openTrustedLinkIn("about:profiling", "tab");
+ view.hidePopup();
+ });
+
+ // Update the view when the profiler starts/stops.
+ const { Services } = lazyServices();
+ Services.obs.addObserver(view.updateProfilerActive, "profiler-started");
+ Services.obs.addObserver(view.updateProfilerActive, "profiler-stopped");
+ state.cleanup.push(() => {
+ Services.obs.removeObserver(view.updateProfilerActive, "profiler-started");
+ Services.obs.removeObserver(view.updateProfilerActive, "profiler-stopped");
+ });
+}
+
+module.exports = {
+ selectElementsInPanelview,
+ createViewControllers,
+ addPopupEventHandlers,
+ initializePopup,
+};
+
+// Object.keys() confuses the linting which expects a static array expression.
+// eslint-disable-next-line
+var EXPORTED_SYMBOLS = Object.keys(module.exports);
diff --git a/devtools/client/themes/images/performance-new-popup-backdrop.png b/devtools/client/themes/images/performance-new-popup-backdrop.png
new file mode 100644
index 000000000000..675b49a23426
Binary files /dev/null and b/devtools/client/themes/images/performance-new-popup-backdrop.png differ
diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js
index 09ea36efb3c4..636dcb173d48 100644
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -880,6 +880,8 @@ pref("devtools.performance.recording.threads", "[\"GeckoMain\",\"Compositor\",\"
// the host machine. This is used in order to look up symbol information from
// build artifacts of local builds.
pref("devtools.performance.recording.objdirs", "[]");
+// The popup will display some introductory text the first time it is displayed.
+pref("devtools.performance.popup.intro-displayed", false);
// view source
pref("view_source.editor.path", "");