diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/performance-new/popup/panel.jsm.js | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/performance-new/popup/panel.jsm.js')
-rw-r--r-- | devtools/client/performance-new/popup/panel.jsm.js | 386 |
1 files changed, 386 insertions, 0 deletions
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 0000000000..33e2e6fb4f --- /dev/null +++ b/devtools/client/performance-new/popup/panel.jsm.js @@ -0,0 +1,386 @@ +/* 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<typeof selectElementsInPanelview>} Elements + * @typedef {ReturnType<typeof createViewControllers>} 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 + */ + +const { createLazyLoaders } = ChromeUtils.import( + "resource://devtools/client/performance-new/typescript-lazy-load.jsm.js" +); + +const lazy = createLazyLoaders({ + Services: () => ChromeUtils.import("resource://gre/modules/Services.jsm"), + PanelMultiView: () => + ChromeUtils.import("resource:///modules/PanelMultiView.jsm"), + Background: () => + 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} */ + // @ts-ignore - Bug 1674368 + const { PanelMultiView } = lazy.PanelMultiView(); + const element = PanelMultiView.getViewNode(document, 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"), + locked: getElementById("PanelUI-profiler-locked"), + 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"), + settingsSection: getElementById("PanelUI-profiler-content-settings"), + contentRecording: getElementById("PanelUI-profiler-content-recording"), + }; +} + +/** + * 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, infoButton, panelview } = elements; + header.setAttribute( + "isinfocollapsed", + state.isInfoCollapsed ? "true" : "false" + ); + // @ts-ignore - Bug 1674368 + panelview + .closest("panel") + .setAttribute( + "isinfoexpanded", + state.isInfoCollapsed ? "false" : "true" + ); + // @ts-ignore - Bug 1674368 + infoButton.checked = !state.isInfoCollapsed; + + if (state.isInfoCollapsed) { + const { height } = info.getBoundingClientRect(); + info.style.marginBlockEnd = `-${height}px`; + } else { + info.style.marginBlockEnd = "0"; + } + }, + + updatePresets() { + const { Services } = lazy.Services(); + const { presets, getRecordingPreferences } = lazy.Background(); + const { presetName } = getRecordingPreferences( + "aboutprofiling", + Services.profiler.GetFeatures() + ); + 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; + // This works around XULElement height issues. + const { height } = elements.presetDescription.getBoundingClientRect(); + elements.presetDescription.style.height = `${height}px`; + } else { + elements.presetDescription.style.display = "none"; + elements.presetCustom.style.display = "block"; + elements.presetsMenuList.value = "custom"; + } + const { PanelMultiView } = lazy.PanelMultiView(); + // Update the description height sizing. + PanelMultiView.forNode(elements.panelview).descriptionHeightWorkaround(); + }, + + updateProfilerState() { + const { Services } = lazy.Services(); + /** + * Convert two boolean values into a "profilerState" enum. + * + * @type {"active" | "inactive" | "locked"} + */ + let profilerState = Services.profiler.IsActive() ? "active" : "inactive"; + if (!Services.profiler.CanProfile()) { + // In private browsing mode, the profiler is locked. + profilerState = "locked"; + } + + switch (profilerState) { + case "active": + elements.inactive.setAttribute("hidden", "true"); + elements.active.setAttribute("hidden", "false"); + elements.settingsSection.setAttribute("hidden", "true"); + elements.contentRecording.setAttribute("hidden", "false"); + elements.locked.setAttribute("hidden", "true"); + break; + case "inactive": + elements.inactive.setAttribute("hidden", "false"); + elements.active.setAttribute("hidden", "true"); + elements.settingsSection.setAttribute("hidden", "false"); + elements.contentRecording.setAttribute("hidden", "true"); + elements.locked.setAttribute("hidden", "true"); + break; + case "locked": { + elements.inactive.setAttribute("hidden", "true"); + elements.active.setAttribute("hidden", "true"); + elements.settingsSection.setAttribute("hidden", "true"); + elements.contentRecording.setAttribute("hidden", "true"); + elements.locked.setAttribute("hidden", "false"); + // This works around XULElement height issues. + const { height } = elements.locked.getBoundingClientRect(); + elements.locked.style.height = `${height}px`; + break; + } + default: + throw new Error("Unhandled profiler state."); + } + }, + + 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 } = lazy.Services(); + const { presets } = lazy.Background(); + 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."); + } + panel.removeAttribute("isinfoexpanded"); + /** @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.updateProfilerState(); + view.updatePresets(); + + // XUL <description> elements don't vertically size correctly, this is + // the workaround for it. + const { PanelMultiView } = lazy.PanelMultiView(); + 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, + } = lazy.Background(); + + /** + * 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("aboutprofiling"); + }); + + addHandler(elements.stopAndDiscard, "click", () => { + stopProfiler(); + }); + + addHandler(elements.stopAndCapture, "click", () => { + captureProfile("aboutprofiling"); + view.hidePopup(); + }); + + addHandler(elements.learnMore, "click", () => { + elements.window.openWebLinkIn("https://profiler.firefox.com/docs/", "tab"); + view.hidePopup(); + }); + + addHandler(elements.presetsMenuList, "command", () => { + changePreset( + "aboutprofiling", + elements.presetsMenuList.value, + Services.profiler.GetFeatures() + ); + 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 } = lazy.Services(); + + // These are all events that can affect the current state of the profiler. + const events = [ + "profiler-started", + "profiler-stopped", + "chrome-document-global-created", // This is potentially a private browser. + "last-pb-context-exited", + ]; + for (const event of events) { + Services.obs.addObserver(view.updateProfilerState, event); + state.cleanup.push(() => { + Services.obs.removeObserver(view.updateProfilerState, event); + }); + } +} + +// Provide an exports object for the JSM to be properly read by TypeScript. +/** @type {any} */ (this).module = {}; + +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); |