tor-browser

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

logic.sys.mjs (10582B)


      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 logic of the profiler popup view.
      8 */
      9 
     10 /**
     11 * @typedef {ReturnType<typeof selectElementsInPanelview>} Elements
     12 * @typedef {ReturnType<typeof createViewControllers>} ViewController
     13 */
     14 
     15 /**
     16 * @typedef {object} State - The mutable state of the popup.
     17 * @property {Array<() => void>} cleanup - Functions to cleanup once the view is hidden.
     18 * @property {boolean} isInfoCollapsed
     19 */
     20 
     21 import { createLazyLoaders } from "resource://devtools/client/performance-new/shared/typescript-lazy-load.sys.mjs";
     22 
     23 const lazy = createLazyLoaders({
     24  PanelMultiView: () =>
     25    ChromeUtils.importESModule(
     26      "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs"
     27    ),
     28  Background: () =>
     29    ChromeUtils.importESModule(
     30      "resource://devtools/client/performance-new/shared/background.sys.mjs"
     31    ),
     32  PrefsPresets: () =>
     33    ChromeUtils.importESModule(
     34      "resource://devtools/shared/performance-new/prefs-presets.sys.mjs"
     35    ),
     36 });
     37 
     38 /**
     39 * This function collects all of the selection of the elements inside of the panel.
     40 *
     41 * @param {XULElement} panelview
     42 */
     43 function selectElementsInPanelview(panelview) {
     44  const document = panelview.ownerDocument;
     45 
     46  // Forcefully cast the window to the type Window
     47  /** @type {any} */
     48  const windowAny = document.defaultView;
     49  /** @type {Window} */
     50  const window = windowAny;
     51 
     52  /**
     53   * Get an element or throw an error if it's not found. This is more friendly
     54   * for TypeScript.
     55   *
     56   * @param {string} id
     57   * @return {HTMLElement}
     58   */
     59  function getElementById(id) {
     60    /** @type {HTMLElement | null} */
     61    // @ts-ignore - Bug 1674368
     62    const { PanelMultiView } = lazy.PanelMultiView();
     63    const element = PanelMultiView.getViewNode(document, id);
     64    if (!element) {
     65      throw new Error(`Could not find the element from the ID "${id}"`);
     66    }
     67    return element;
     68  }
     69 
     70  return {
     71    document,
     72    panelview,
     73    window,
     74    inactive: getElementById("PanelUI-profiler-inactive"),
     75    active: getElementById("PanelUI-profiler-active"),
     76    presetDescription: getElementById("PanelUI-profiler-content-description"),
     77    presetsEditSettings: getElementById(
     78      "PanelUI-profiler-content-edit-settings"
     79    ),
     80    presetsMenuList: /** @type {MenuListElement} */ (
     81      getElementById("PanelUI-profiler-presets")
     82    ),
     83    header: getElementById("PanelUI-profiler-header"),
     84    info: getElementById("PanelUI-profiler-info"),
     85    menupopup: getElementById("PanelUI-profiler-presets-menupopup"),
     86    infoButton: getElementById("PanelUI-profiler-info-button"),
     87    learnMore: getElementById("PanelUI-profiler-learn-more"),
     88    startRecording: getElementById("PanelUI-profiler-startRecording"),
     89    stopAndDiscard: getElementById("PanelUI-profiler-stopAndDiscard"),
     90    stopAndCapture: getElementById("PanelUI-profiler-stopAndCapture"),
     91    settingsSection: getElementById("PanelUI-profiler-content-settings"),
     92    contentRecording: getElementById("PanelUI-profiler-content-recording"),
     93  };
     94 }
     95 
     96 /**
     97 * This function returns an interface that can be used to control the view of the
     98 * panel based on the current mutable State.
     99 *
    100 * @param {State} state
    101 * @param {Elements} elements
    102 */
    103 function createViewControllers(state, elements) {
    104  return {
    105    updateInfoCollapse() {
    106      const { header, info, infoButton } = elements;
    107      header.setAttribute(
    108        "isinfocollapsed",
    109        state.isInfoCollapsed ? "true" : "false"
    110      );
    111      // @ts-ignore - Bug 1674368
    112      infoButton.checked = !state.isInfoCollapsed;
    113 
    114      if (state.isInfoCollapsed) {
    115        const { height } = info.getBoundingClientRect();
    116        info.style.marginBlockEnd = `-${height}px`;
    117      } else {
    118        info.style.marginBlockEnd = "0";
    119      }
    120    },
    121 
    122    updatePresets() {
    123      const { presets, getRecordingSettings } = lazy.PrefsPresets();
    124      const { presetName } = getRecordingSettings(
    125        "aboutprofiling",
    126        Services.profiler.GetFeatures()
    127      );
    128      const preset = presets[presetName];
    129      if (preset) {
    130        elements.presetDescription.style.display = "block";
    131        elements.document.l10n.setAttributes(
    132          elements.presetDescription,
    133          preset.l10nIds.popup.description
    134        );
    135        elements.presetsMenuList.value = presetName;
    136      } else {
    137        elements.presetDescription.style.display = "none";
    138        // We don't remove the l10n-id attribute as the element is hidden anyway.
    139        // It will be updated again when it's displayed next time.
    140        elements.presetsMenuList.value = "custom";
    141      }
    142    },
    143 
    144    updateProfilerState() {
    145      if (Services.profiler.IsActive()) {
    146        elements.inactive.hidden = true;
    147        elements.active.hidden = false;
    148        elements.settingsSection.hidden = true;
    149        elements.contentRecording.hidden = false;
    150      } else {
    151        elements.inactive.hidden = false;
    152        elements.active.hidden = true;
    153        elements.settingsSection.hidden = false;
    154        elements.contentRecording.hidden = true;
    155      }
    156    },
    157 
    158    createPresetsList() {
    159      // Check the DOM if the presets were built or not. We can't cache this value
    160      // in the `State` object, as the `State` object will be removed if the
    161      // button is removed from the toolbar, but the DOM changes will still persist.
    162      if (elements.menupopup.getAttribute("presetsbuilt") === "true") {
    163        // The presets were already built.
    164        return;
    165      }
    166 
    167      const { presets } = lazy.PrefsPresets();
    168      const currentPreset = Services.prefs.getCharPref(
    169        "devtools.performance.recording.preset"
    170      );
    171 
    172      const menuitems = Object.entries(presets).map(([id, preset]) => {
    173        const { document, presetsMenuList } = elements;
    174        const menuitem = document.createXULElement("menuitem");
    175        document.l10n.setAttributes(menuitem, preset.l10nIds.popup.label);
    176        menuitem.setAttribute("value", id);
    177        if (id === currentPreset) {
    178          presetsMenuList.setAttribute("value", id);
    179        }
    180        return menuitem;
    181      });
    182 
    183      elements.menupopup.prepend(...menuitems);
    184      elements.menupopup.setAttribute("presetsbuilt", "true");
    185    },
    186 
    187    hidePopup() {
    188      const panel = elements.panelview.closest("panel");
    189      if (!panel) {
    190        throw new Error("Could not find the panel from the panelview.");
    191      }
    192      /** @type {any} */ (panel).hidePopup();
    193    },
    194  };
    195 }
    196 
    197 /**
    198 * Perform all of the business logic to present the popup view once it is open.
    199 *
    200 * @param {State} state
    201 * @param {Elements} elements
    202 * @param {ViewController} view
    203 */
    204 function initializeView(state, elements, view) {
    205  view.createPresetsList();
    206 
    207  state.cleanup.push(() => {
    208    // The UI should be collapsed by default for the next time the popup
    209    // is open.
    210    state.isInfoCollapsed = true;
    211    view.updateInfoCollapse();
    212  });
    213 
    214  // Turn off all animations while initializing the popup.
    215  elements.header.setAttribute("animationready", "false");
    216 
    217  elements.window.requestAnimationFrame(() => {
    218    // Allow the elements to layout once, the updateInfoCollapse implementation measures
    219    // the size of the container. It needs to wait a second before the bounding box
    220    // returns an actual size.
    221    view.updateInfoCollapse();
    222    view.updateProfilerState();
    223    view.updatePresets();
    224 
    225    // Now wait for another rAF, and turn the animations back on.
    226    elements.window.requestAnimationFrame(() => {
    227      elements.header.setAttribute("animationready", "true");
    228    });
    229  });
    230 }
    231 
    232 /**
    233 * This function is in charge of settings all of the events handlers for the view.
    234 * The handlers must also add themselves to the `state.cleanup` for them to be
    235 * properly cleaned up once the view is destroyed.
    236 *
    237 * @param {State} state
    238 * @param {Elements} elements
    239 * @param {ViewController} view
    240 */
    241 function addPopupEventHandlers(state, elements, view) {
    242  const { startProfiler, stopProfiler, captureProfile } = lazy.Background();
    243 
    244  /**
    245   * Adds a handler that automatically is removed once the panel is hidden.
    246   *
    247   * @param {HTMLElement} element
    248   * @param {string} type
    249   * @param {(event: Event) => void} handler
    250   */
    251  function addHandler(element, type, handler) {
    252    element.addEventListener(type, handler);
    253    state.cleanup.push(() => {
    254      element.removeEventListener(type, handler);
    255    });
    256  }
    257 
    258  addHandler(elements.infoButton, "click", event => {
    259    // Any button command event in the popup will cause it to close. Prevent this
    260    // from happening on click.
    261    event.preventDefault();
    262 
    263    state.isInfoCollapsed = !state.isInfoCollapsed;
    264    view.updateInfoCollapse();
    265  });
    266 
    267  addHandler(elements.startRecording, "click", () => {
    268    startProfiler("aboutprofiling");
    269  });
    270 
    271  addHandler(elements.stopAndDiscard, "click", () => {
    272    stopProfiler();
    273  });
    274 
    275  addHandler(elements.stopAndCapture, "click", () => {
    276    captureProfile("aboutprofiling");
    277    view.hidePopup();
    278  });
    279 
    280  addHandler(elements.learnMore, "click", () => {
    281    elements.window.openWebLinkIn("https://profiler.firefox.com/docs/", "tab");
    282    view.hidePopup();
    283  });
    284 
    285  addHandler(elements.presetsMenuList, "command", () => {
    286    lazy
    287      .PrefsPresets()
    288      .changePreset(
    289        "aboutprofiling",
    290        elements.presetsMenuList.value,
    291        Services.profiler.GetFeatures()
    292      );
    293    view.updatePresets();
    294  });
    295 
    296  addHandler(elements.presetsEditSettings, "click", () => {
    297    elements.window.openTrustedLinkIn("about:profiling", "tab");
    298    view.hidePopup();
    299  });
    300 
    301  // Update the view when the profiler starts/stops.
    302  // These are all events that can affect the current state of the profiler.
    303  const events = ["profiler-started", "profiler-stopped"];
    304  for (const event of events) {
    305    Services.obs.addObserver(view.updateProfilerState, event);
    306    state.cleanup.push(() => {
    307      Services.obs.removeObserver(view.updateProfilerState, event);
    308    });
    309  }
    310 }
    311 
    312 /**
    313 * Initialize everything needed for the popup to work fine.
    314 *
    315 * @param {State} panelState
    316 * @param {XULElement} panelview
    317 */
    318 export function initializePopup(panelState, panelview) {
    319  const panelElements = selectElementsInPanelview(panelview);
    320  const panelviewControllers = createViewControllers(panelState, panelElements);
    321  addPopupEventHandlers(panelState, panelElements, panelviewControllers);
    322  initializeView(panelState, panelElements, panelviewControllers);
    323 }