tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

menu-button.sys.mjs (11504B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 // @ts-check
      5 
      6 /**
      7 * This file controls the enabling and disabling of the menu button for the profiler.
      8 * Care should be taken to keep it minimal as it can be run with browser initialization.
      9 */
     10 
     11 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     12 import { createLazyLoaders } from "resource://devtools/client/performance-new/shared/typescript-lazy-load.sys.mjs";
     13 
     14 const lazy = createLazyLoaders({
     15  CustomizableUI: () =>
     16    ChromeUtils.importESModule(
     17      "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs"
     18    ),
     19  CustomizableWidgets: () =>
     20    ChromeUtils.importESModule(
     21      "moz-src:///browser/components/customizableui/CustomizableWidgets.sys.mjs"
     22    ),
     23  PopupLogic: () =>
     24    ChromeUtils.importESModule(
     25      "resource://devtools/client/performance-new/popup/logic.sys.mjs"
     26    ),
     27  Background: () =>
     28    ChromeUtils.importESModule(
     29      "resource://devtools/client/performance-new/shared/background.sys.mjs"
     30    ),
     31 });
     32 
     33 const WIDGET_ID = "profiler-button";
     34 const DROPMARKER_ID = "profiler-button-dropmarker";
     35 
     36 /**
     37 * Add the profiler button to the navbar.
     38 *
     39 * @return {void}
     40 */
     41 function addToNavbar() {
     42  const { CustomizableUI } = lazy.CustomizableUI();
     43 
     44  CustomizableUI.addWidgetToArea(WIDGET_ID, CustomizableUI.AREA_NAVBAR);
     45 }
     46 
     47 /**
     48 * Remove the widget and place it in the customization palette. This will also
     49 * disable the shortcuts.
     50 *
     51 * @return {void}
     52 */
     53 function remove() {
     54  const { CustomizableUI } = lazy.CustomizableUI();
     55  CustomizableUI.removeWidgetFromArea(WIDGET_ID);
     56 }
     57 
     58 /**
     59 * See if the profiler menu button is in the navbar, or other active areas. The
     60 * placement is null when it's inactive in the customization palette.
     61 *
     62 * @return {boolean}
     63 */
     64 function isInNavbar() {
     65  if (AppConstants.MOZ_APP_NAME == "thunderbird") {
     66    return false;
     67  }
     68 
     69  const { CustomizableUI } = lazy.CustomizableUI();
     70  return Boolean(CustomizableUI.getPlacementOfWidget("profiler-button"));
     71 }
     72 
     73 function ensureButtonInNavbar() {
     74  // 1. Ensure the widget is enabled.
     75  const featureFlagPref = "devtools.performance.popup.feature-flag";
     76  const isPopupFeatureFlagEnabled = Services.prefs.getBoolPref(featureFlagPref);
     77  if (!isPopupFeatureFlagEnabled) {
     78    // Setting the pref will also run the menubutton initialization thanks to
     79    // the observer set in DevtoolsStartup.
     80    Services.prefs.setBoolPref("devtools.performance.popup.feature-flag", true);
     81  }
     82 
     83  // 2. Ensure it's added to the nav bar
     84  if (!isInNavbar()) {
     85    // Enable the profiler menu button.
     86    addToNavbar();
     87 
     88    // Dispatch the change event manually, so that the shortcuts will also be
     89    // added.
     90    const { CustomizableUI } = lazy.CustomizableUI();
     91    CustomizableUI.dispatchToolboxEvent("customizationchange");
     92  }
     93 }
     94 
     95 /**
     96 * Opens the popup for the profiler.
     97 *
     98 * @param {Document} document
     99 */
    100 function openPopup(document) {
    101  // First find the button.
    102  /** @type {HTMLButtonElement | null} */
    103  const button = document.querySelector("#profiler-button");
    104  if (!button) {
    105    throw new Error("Could not find the profiler button.");
    106  }
    107 
    108  // Sending a click event anywhere on the button could start the profiler
    109  // instead of opening the popup. Sending a command event on a view widget
    110  // will make CustomizableUI show the view.
    111  const cmdEvent = document.createEvent("xulcommandevent");
    112  // @ts-ignore - Bug 1674368
    113  cmdEvent.initCommandEvent("command", true, true, button.ownerGlobal);
    114  button.dispatchEvent(cmdEvent);
    115 }
    116 
    117 /**
    118 * This function creates the widget definition for the CustomizableUI. It should
    119 * only be run if the profiler button is enabled.
    120 *
    121 * @param {(isEnabled: boolean) => void} toggleProfilerKeyShortcuts
    122 * @return {void}
    123 */
    124 function initialize(toggleProfilerKeyShortcuts) {
    125  const { CustomizableUI } = lazy.CustomizableUI();
    126  const { CustomizableWidgets } = lazy.CustomizableWidgets();
    127 
    128  const widget = CustomizableUI.getWidget(WIDGET_ID);
    129  if (widget && widget.provider == CustomizableUI.PROVIDER_API) {
    130    // This widget has already been created.
    131    return;
    132  }
    133 
    134  const viewId = "PanelUI-profiler";
    135 
    136  /**
    137   * This is mutable state that will be shared between panel displays.
    138   *
    139   * @type {import("devtools/client/performance-new/popup/logic.sys.mjs").State}
    140   */
    141  const panelState = {
    142    cleanup: [],
    143    isInfoCollapsed: true,
    144  };
    145 
    146  /**
    147   * Handle when the customization changes for the button. This event is not
    148   * very specific, and fires for any CustomizableUI widget. This event is
    149   * pretty rare to fire, and only affects users of the profiler button,
    150   * so it shouldn't have much overhead even if it runs a lot.
    151   */
    152  function handleCustomizationChange() {
    153    const isEnabled = isInNavbar();
    154    toggleProfilerKeyShortcuts(isEnabled);
    155 
    156    if (!isEnabled) {
    157      // The profiler menu button is no longer in the navbar, make sure that the
    158      // "intro-displayed" preference is reset.
    159      /** @type {import("../@types/perf").PerformancePref["PopupIntroDisplayed"]} */
    160      const popupIntroDisplayedPref =
    161        "devtools.performance.popup.intro-displayed";
    162      Services.prefs.setBoolPref(popupIntroDisplayedPref, false);
    163 
    164      // We stop the profiler when the button is removed for normal users,
    165      // but we try to avoid interfering with profiling of automated tests.
    166      if (
    167        Services.profiler.IsActive() &&
    168        (!Cu.isInAutomation || !Services.env.exists("MOZ_PROFILER_STARTUP"))
    169      ) {
    170        Services.profiler.StopProfiler();
    171      }
    172    }
    173  }
    174 
    175  const item = {
    176    id: WIDGET_ID,
    177    type: "button-and-view",
    178    viewId,
    179    l10nId: "profiler-popup-button-idle",
    180 
    181    onViewShowing:
    182      /**
    183       * @type {(event: {
    184       *   target: ChromeHTMLElement | XULElement,
    185       *   detail: {
    186       *     addBlocker: (blocker: Promise<void>) => void
    187       *   }
    188       * }) => void}
    189       */
    190      event => {
    191        try {
    192          // The popup logic is stored in a separate script so it doesn't have
    193          // to be parsed at browser startup, and will only be lazily loaded
    194          // when the popup is viewed.
    195          const { initializePopup } = lazy.PopupLogic();
    196 
    197          initializePopup(panelState, event.target);
    198        } catch (error) {
    199          // Surface any errors better in the console.
    200          console.error(error);
    201        }
    202      },
    203 
    204    /**
    205     * @type {(event: { target: ChromeHTMLElement | XULElement }) => void}
    206     */
    207    onViewHiding() {
    208      // Clean-up the view. This removes all of the event listeners.
    209      for (const fn of panelState.cleanup) {
    210        fn();
    211      }
    212      panelState.cleanup = [];
    213    },
    214 
    215    /**
    216     * Perform any general initialization for this widget. This is called once per
    217     * browser window.
    218     *
    219     * @type {(document: HTMLDocument) => void}
    220     */
    221    onBeforeCreated: document => {
    222      /** @type {import("../@types/perf").PerformancePref["PopupIntroDisplayed"]} */
    223      const popupIntroDisplayedPref =
    224        "devtools.performance.popup.intro-displayed";
    225 
    226      // Determine the state of the popup's info being collapsed BEFORE the view
    227      // is shown, and update the collapsed state. This way the transition animation
    228      // isn't run.
    229      panelState.isInfoCollapsed = Services.prefs.getBoolPref(
    230        popupIntroDisplayedPref
    231      );
    232      if (!panelState.isInfoCollapsed) {
    233        // We have displayed the intro, don't show it again by default.
    234        Services.prefs.setBoolPref(popupIntroDisplayedPref, true);
    235      }
    236 
    237      // Handle customization event changes. If the profiler is no longer in the
    238      // navbar, then reset the popup intro preference.
    239      const window = document.defaultView;
    240      if (window) {
    241        /** @type {any} */ (window).gNavToolbox.addEventListener(
    242          "customizationchange",
    243          handleCustomizationChange
    244        );
    245      }
    246 
    247      toggleProfilerKeyShortcuts(isInNavbar());
    248    },
    249 
    250    /**
    251     * This method is used when we need to operate upon the button element itself.
    252     * This is called once per browser window.
    253     *
    254     * @type {(node: ChromeHTMLElement) => void}
    255     */
    256    onCreated: node => {
    257      const document = node.ownerDocument;
    258      const window = document?.defaultView;
    259      if (!document || !window) {
    260        console.error(
    261          "Unable to find the document or the window of the profiler toolbar item."
    262        );
    263        return;
    264      }
    265 
    266      const firstButton = node.firstElementChild;
    267      if (!firstButton) {
    268        console.error(
    269          "Unable to find the button element inside the profiler toolbar item."
    270        );
    271        return;
    272      }
    273 
    274      // Assign the null-checked button element to a new variable so that
    275      // TypeScript doesn't require additional null checks in the functions
    276      // below.
    277      const buttonElement = firstButton;
    278 
    279      // This class is needed to show the subview arrow when our button
    280      // is in the overflow menu.
    281      buttonElement.classList.add("subviewbutton-nav");
    282 
    283      // Add l10n attributes for the dropmarker.
    284      const dropmarker = node.querySelector("#" + DROPMARKER_ID);
    285      if (dropmarker) {
    286        document.l10n.setAttributes(dropmarker, DROPMARKER_ID);
    287      }
    288 
    289      function setButtonActive() {
    290        document.l10n.setAttributes(
    291          buttonElement,
    292          "profiler-popup-button-recording"
    293        );
    294        buttonElement.classList.toggle("profiler-active", true);
    295        buttonElement.classList.toggle("profiler-paused", false);
    296      }
    297      function setButtonPaused() {
    298        document.l10n.setAttributes(
    299          buttonElement,
    300          "profiler-popup-button-capturing"
    301        );
    302        buttonElement.classList.toggle("profiler-active", false);
    303        buttonElement.classList.toggle("profiler-paused", true);
    304      }
    305      function setButtonInactive() {
    306        document.l10n.setAttributes(
    307          buttonElement,
    308          "profiler-popup-button-idle"
    309        );
    310        buttonElement.classList.toggle("profiler-active", false);
    311        buttonElement.classList.toggle("profiler-paused", false);
    312      }
    313 
    314      if (Services.profiler.IsPaused()) {
    315        setButtonPaused();
    316      }
    317      if (Services.profiler.IsActive()) {
    318        setButtonActive();
    319      }
    320 
    321      Services.obs.addObserver(setButtonActive, "profiler-started");
    322      Services.obs.addObserver(setButtonInactive, "profiler-stopped");
    323      Services.obs.addObserver(setButtonPaused, "profiler-paused");
    324 
    325      window.addEventListener("unload", () => {
    326        Services.obs.removeObserver(setButtonActive, "profiler-started");
    327        Services.obs.removeObserver(setButtonInactive, "profiler-stopped");
    328        Services.obs.removeObserver(setButtonPaused, "profiler-paused");
    329      });
    330    },
    331 
    332    onCommand: () => {
    333      if (Services.profiler.IsPaused()) {
    334        // A profile is already being captured, ignore this event.
    335        return;
    336      }
    337      const { startProfiler, captureProfile } = lazy.Background();
    338      if (Services.profiler.IsActive()) {
    339        captureProfile("aboutprofiling");
    340      } else {
    341        startProfiler("aboutprofiling");
    342      }
    343    },
    344  };
    345 
    346  CustomizableUI.createWidget(item);
    347  CustomizableWidgets.push(item);
    348 }
    349 
    350 export const ProfilerMenuButton = {
    351  initialize,
    352  addToNavbar,
    353  ensureButtonInNavbar,
    354  isInNavbar,
    355  openPopup,
    356  remove,
    357 };