tor-browser

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

ScreenshotsUtils.sys.mjs (44081B)


      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 
      5 import { getFilename } from "chrome://browser/content/screenshots/fileHelpers.mjs";
      6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 
      8 const SCREENSHOTS_LAST_SCREENSHOT_METHOD_PREF =
      9  "screenshots.browser.component.last-screenshot-method";
     10 const SCREENSHOTS_LAST_SAVED_METHOD_PREF =
     11  "screenshots.browser.component.last-saved-method";
     12 const SCREENSHOTS_ENABLED_PREF = "screenshots.browser.component.enabled";
     13 
     14 const lazy = {};
     15 
     16 ChromeUtils.defineESModuleGetters(lazy, {
     17  CustomizableUI:
     18    "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
     19  Downloads: "resource://gre/modules/Downloads.sys.mjs",
     20  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
     21  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     22  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     23 });
     24 
     25 XPCOMUtils.defineLazyServiceGetters(lazy, {
     26  AlertsService: ["@mozilla.org/alerts-service;1", Ci.nsIAlertsService],
     27 });
     28 
     29 XPCOMUtils.defineLazyPreferenceGetter(
     30  lazy,
     31  "SCREENSHOTS_LAST_SAVED_METHOD",
     32  SCREENSHOTS_LAST_SAVED_METHOD_PREF,
     33  "download"
     34 );
     35 
     36 XPCOMUtils.defineLazyPreferenceGetter(
     37  lazy,
     38  "SCREENSHOTS_LAST_SCREENSHOT_METHOD",
     39  SCREENSHOTS_LAST_SCREENSHOT_METHOD_PREF,
     40  "visible"
     41 );
     42 
     43 XPCOMUtils.defineLazyPreferenceGetter(
     44  lazy,
     45  "SCREENSHOTS_ENABLED",
     46  SCREENSHOTS_ENABLED_PREF,
     47  true,
     48  () => ScreenshotsUtils.monitorScreenshotsPref()
     49 );
     50 
     51 ChromeUtils.defineLazyGetter(lazy, "screenshotsLocalization", () => {
     52  return new Localization(["browser/screenshots.ftl"], true);
     53 });
     54 
     55 const AlertNotification = Components.Constructor(
     56  "@mozilla.org/alert-notification;1",
     57  "nsIAlertNotification",
     58  "initWithObject"
     59 );
     60 
     61 // The max dimension for a canvas is 32,767 https://searchfox.org/mozilla-central/rev/f40d29a11f2eb4685256b59934e637012ea6fb78/gfx/cairo/cairo/src/cairo-image-surface.c#62.
     62 // The max number of pixels for a canvas is 472,907,776 pixels (i.e., 22,528 x 20,992) https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
     63 // We have to limit screenshots to these dimensions otherwise it will cause an error.
     64 export const MAX_CAPTURE_DIMENSION = 32766;
     65 export const MAX_CAPTURE_AREA = 472907776;
     66 export const MAX_SNAPSHOT_DIMENSION = 1024;
     67 
     68 export class ScreenshotsComponentParent extends JSWindowActorParent {
     69  async receiveMessage(message) {
     70    let region, title;
     71    let browser = message.target.browsingContext.topFrameElement;
     72    // ignore message from child actors with no associated browser element
     73    if (!browser) {
     74      return;
     75    }
     76    if (
     77      ScreenshotsUtils.getUIPhase(browser) == UIPhases.CLOSED &&
     78      !ScreenshotsUtils.browserToScreenshotsState.has(browser)
     79    ) {
     80      // We've already exited or never opened and there's no UI or state that could
     81      // handle this message. We additionally check for screenshot-state to ensure we
     82      // don't ignore an overlay message when there is no current selection - which
     83      // otherwise looks like the UIPhases.CLOSED state.
     84      return;
     85    }
     86 
     87    switch (message.name) {
     88      case "Screenshots:CancelScreenshot": {
     89        let { reason } = message.data;
     90        ScreenshotsUtils.cancel(browser, reason);
     91        break;
     92      }
     93      case "Screenshots:CopyScreenshot":
     94        ScreenshotsUtils.closePanel(browser);
     95        ({ region } = message.data);
     96        await ScreenshotsUtils.copyScreenshotFromRegion(region, browser);
     97        ScreenshotsUtils.exit(browser);
     98        break;
     99      case "Screenshots:DownloadScreenshot":
    100        ScreenshotsUtils.closePanel(browser);
    101        ({ title, region } = message.data);
    102        await ScreenshotsUtils.downloadScreenshotFromRegion(
    103          title,
    104          region,
    105          browser
    106        );
    107        ScreenshotsUtils.exit(browser);
    108        break;
    109      case "Screenshots:OverlaySelection":
    110        ScreenshotsUtils.setPerBrowserState(browser, {
    111          hasOverlaySelection: message.data.hasSelection,
    112          overlayState: message.data.overlayState,
    113        });
    114        break;
    115      case "Screenshots:ShowPanel":
    116        ScreenshotsUtils.openPanel(browser);
    117        break;
    118      case "Screenshots:HidePanel":
    119        ScreenshotsUtils.closePanel(browser);
    120        break;
    121      case "Screenshots:MoveFocusToParent":
    122        ScreenshotsUtils.focusPanel(browser, message.data);
    123        break;
    124    }
    125  }
    126 
    127  didDestroy() {
    128    // When restoring a crashed tab the browser is null
    129    let browser = this.browsingContext.topFrameElement;
    130    if (browser) {
    131      ScreenshotsUtils.exit(browser);
    132    }
    133  }
    134 }
    135 
    136 export class ScreenshotsHelperParent extends JSWindowActorParent {
    137  receiveMessage(message) {
    138    switch (message.name) {
    139      case "ScreenshotsHelper:GetElementRectFromPoint": {
    140        let cxt = BrowsingContext.get(message.data.bcId);
    141        return cxt.currentWindowGlobal
    142          .getActor("ScreenshotsHelper")
    143          .sendQuery("ScreenshotsHelper:GetElementRectFromPoint", message.data);
    144      }
    145    }
    146    return null;
    147  }
    148 }
    149 
    150 export const UIPhases = {
    151  CLOSED: 0, // nothing showing
    152  INITIAL: 1, // panel and overlay showing
    153  OVERLAYSELECTION: 2, // something selected in the overlay
    154  PREVIEW: 3, // preview dialog showing
    155 };
    156 
    157 export var ScreenshotsUtils = {
    158  browserToScreenshotsState: new WeakMap(),
    159  initialized: false,
    160  methodsUsed: {},
    161 
    162  /**
    163   * Figures out which of various states the screenshots UI is in, for the given browser.
    164   *
    165   * @param browser The selected browser
    166   * @returns One of the `UIPhases` constants
    167   */
    168  getUIPhase(browser) {
    169    let perBrowserState = this.browserToScreenshotsState.get(browser);
    170    if (perBrowserState?.previewDialog) {
    171      return UIPhases.PREVIEW;
    172    }
    173    const buttonsPanel = this.panelForBrowser(browser);
    174    if (buttonsPanel && !buttonsPanel.hidden) {
    175      return UIPhases.INITIAL;
    176    }
    177    if (perBrowserState?.hasOverlaySelection) {
    178      return UIPhases.OVERLAYSELECTION;
    179    }
    180    return UIPhases.CLOSED;
    181  },
    182 
    183  resetMethodsUsed() {
    184    this.methodsUsed = { fullpage: 0, visible: 0 };
    185  },
    186 
    187  monitorScreenshotsPref() {
    188    if (lazy.SCREENSHOTS_ENABLED) {
    189      this.initialize();
    190    } else {
    191      this.uninitialize();
    192    }
    193 
    194    this.screenshotsEnabled = lazy.SCREENSHOTS_ENABLED;
    195  },
    196 
    197  initialize() {
    198    if (!this.initialized) {
    199      if (!lazy.SCREENSHOTS_ENABLED) {
    200        return;
    201      }
    202      ScreenshotsCustomizableWidget.init();
    203      this.resetMethodsUsed();
    204      Services.obs.addObserver(this, "menuitem-screenshot");
    205      this.initialized = true;
    206      if (Cu.isInAutomation) {
    207        Services.obs.notifyObservers(null, "screenshots-component-initialized");
    208      }
    209    }
    210  },
    211 
    212  uninitialize() {
    213    if (this.initialized) {
    214      ScreenshotsCustomizableWidget.uninit();
    215      Services.obs.removeObserver(this, "menuitem-screenshot");
    216      for (let browser of ChromeUtils.nondeterministicGetWeakMapKeys(
    217        this.browserToScreenshotsState
    218      )) {
    219        this.exit(browser);
    220      }
    221      this.initialized = false;
    222      if (Cu.isInAutomation) {
    223        Services.obs.notifyObservers(
    224          null,
    225          "screenshots-component-uninitialized"
    226        );
    227      }
    228    }
    229  },
    230 
    231  handleEvent(event) {
    232    switch (event.type) {
    233      case "keydown":
    234        this.handleKeyDownEvent(event);
    235        break;
    236      case "TabSelect":
    237        this.handleTabSelect(event);
    238        break;
    239      case "SwapDocShells":
    240        this.handleDocShellSwapEvent(event);
    241        break;
    242      case "EndSwapDocShells":
    243        this.handleEndDocShellSwapEvent(event);
    244        break;
    245    }
    246  },
    247 
    248  handleKeyDownEvent(event) {
    249    let browser =
    250      event.view.browsingContext.topChromeWindow.gBrowser.selectedBrowser;
    251    if (!browser) {
    252      return;
    253    }
    254 
    255    switch (event.key) {
    256      case "Escape":
    257        // The chromeEventHandler in the child actor will handle events that
    258        // don't match this
    259        if (event.target.parentElement === this.panelForBrowser(browser)) {
    260          this.cancel(browser, "Escape");
    261        }
    262        break;
    263      case "ArrowLeft":
    264      case "ArrowUp":
    265      case "ArrowRight":
    266      case "ArrowDown":
    267        this.handleArrowKeyDown(event, browser);
    268        break;
    269      case "Tab":
    270        this.maybeLockFocus(event);
    271        break;
    272    }
    273  },
    274 
    275  /**
    276   * When we swap docshells for a given screenshots browser, we need to update
    277   * the browserToScreenshotsState WeakMap to the correct browser. If the old
    278   * browser is in a state other than OVERLAYSELECTION, we will close
    279   * screenshots.
    280   *
    281   * @param {Event} event The SwapDocShells event
    282   */
    283  handleDocShellSwapEvent(event) {
    284    let oldBrowser = event.target;
    285    let newBrowser = event.detail;
    286 
    287    const currentUIPhase = this.getUIPhase(oldBrowser);
    288    if (currentUIPhase === UIPhases.OVERLAYSELECTION) {
    289      newBrowser.addEventListener("SwapDocShells", this);
    290      newBrowser.addEventListener("EndSwapDocShells", this);
    291      oldBrowser.removeEventListener("SwapDocShells", this);
    292 
    293      let perBrowserState =
    294        this.browserToScreenshotsState.get(oldBrowser) || {};
    295      this.browserToScreenshotsState.set(newBrowser, perBrowserState);
    296      this.browserToScreenshotsState.delete(oldBrowser);
    297 
    298      this.getActor(oldBrowser).sendAsyncMessage(
    299        "Screenshots:RemoveEventListeners"
    300      );
    301    } else {
    302      this.cancel(oldBrowser, "Navigation");
    303    }
    304  },
    305 
    306  /**
    307   * When we swap docshells for a given screenshots browser, we need to add the
    308   * event listeners to the new browser because we removed event listeners in
    309   * handleDocShellSwapEvent.
    310   *
    311   * We attach the overlay event listeners to this.docShell.chromeEventHandler
    312   * in ScreenshotsComponentChild.sys.mjs which is the browser when the page is
    313   * loaded via the parent process (about:config, about:robots, etc) and when
    314   * this is the case, we lose the event listeners on the original browser.
    315   * To fix this, we remove the event listeners on the old browser and add the
    316   * event listeners to the new browser when a SwapDocShells occurs.
    317   *
    318   * @param {Event} event The EndSwapDocShells event
    319   */
    320  handleEndDocShellSwapEvent(event) {
    321    let browser = event.target;
    322    this.getActor(browser).sendAsyncMessage("Screenshots:AddEventListeners");
    323    browser.removeEventListener("EndSwapDocShells", this);
    324  },
    325 
    326  /**
    327   * When we receive a TabSelect event, we will close screenshots in the
    328   * previous tab if the previous tab was in the initial state.
    329   *
    330   * @param {Event} event The TabSelect event
    331   */
    332  handleTabSelect(event) {
    333    let previousTab = event.detail.previousTab;
    334    if (this.getUIPhase(previousTab.linkedBrowser) === UIPhases.INITIAL) {
    335      this.cancel(previousTab.linkedBrowser, "Navigation");
    336    }
    337  },
    338 
    339  /**
    340   * If the overlay state is crosshairs or dragging, move the native cursor
    341   * respective to the arrow key pressed.
    342   *
    343   * @param {Event} event A keydown event
    344   * @param {Browser} browser The selected browser
    345   * @returns
    346   */
    347  handleArrowKeyDown(event, browser) {
    348    // Wayland doesn't support `sendNativeMouseEvent` so just return
    349    if (Services.appinfo.isWayland) {
    350      return;
    351    }
    352 
    353    let { overlayState } = this.browserToScreenshotsState.get(browser);
    354 
    355    if (!["crosshairs", "dragging"].includes(overlayState)) {
    356      return;
    357    }
    358 
    359    let left = 0;
    360    let top = 0;
    361    let exponent = event.shiftKey ? 1 : 0;
    362    switch (event.key) {
    363      case "ArrowLeft":
    364        left -= 10 ** exponent;
    365        break;
    366      case "ArrowUp":
    367        top -= 10 ** exponent;
    368        break;
    369      case "ArrowRight":
    370        left += 10 ** exponent;
    371        break;
    372      case "ArrowDown":
    373        top += 10 ** exponent;
    374        break;
    375      default:
    376        return;
    377    }
    378 
    379    // Clear and move focus to browser so the child actor can capture events
    380    this.clearContentFocus(browser);
    381    Services.focus.clearFocus(browser.ownerGlobal);
    382    Services.focus.setFocus(browser, 0);
    383 
    384    let x = {};
    385    let y = {};
    386    let win = browser.ownerGlobal;
    387    win.windowUtils.getLastOverWindowPointerLocationInCSSPixels(x, y);
    388 
    389    this.moveCursor(
    390      {
    391        left: (x.value + left) * win.devicePixelRatio,
    392        top: (y.value + top) * win.devicePixelRatio,
    393      },
    394      browser
    395    );
    396  },
    397 
    398  /**
    399   * Move the native cursor to the given position. Clamp the position to the
    400   * window just in case.
    401   *
    402   * @param {object} position An object containing the left and top position
    403   * @param {Browser} browser The selected browser
    404   */
    405  moveCursor(position, browser) {
    406    let { left, top } = position;
    407    let win = browser.ownerGlobal;
    408 
    409    const windowLeft = win.mozInnerScreenX * win.devicePixelRatio;
    410    const windowTop = win.mozInnerScreenY * win.devicePixelRatio;
    411    const contentTop =
    412      (win.mozInnerScreenY + (win.innerHeight - browser.clientHeight)) *
    413      win.devicePixelRatio;
    414    const windowRight =
    415      (win.mozInnerScreenX + win.innerWidth) * win.devicePixelRatio;
    416    const windowBottom =
    417      (win.mozInnerScreenY + win.innerHeight) * win.devicePixelRatio;
    418 
    419    left += windowLeft;
    420    top += windowTop;
    421 
    422    // Clamp left and top to content dimensions
    423    let parsedLeft = Math.round(
    424      Math.min(Math.max(left, windowLeft), windowRight)
    425    );
    426    let parsedTop = Math.round(
    427      Math.min(Math.max(top, contentTop), windowBottom)
    428    );
    429 
    430    win.windowUtils.sendNativeMouseEvent(
    431      parsedLeft,
    432      parsedTop,
    433      win.windowUtils.NATIVE_MOUSE_MESSAGE_MOVE,
    434      0,
    435      0,
    436      win.document.documentElement
    437    );
    438  },
    439 
    440  observe(subj, topic, data) {
    441    let { gBrowser } = subj;
    442    let browser = gBrowser.selectedBrowser;
    443 
    444    switch (topic) {
    445      case "menuitem-screenshot": {
    446        const uiPhase = this.getUIPhase(browser);
    447        if (uiPhase !== UIPhases.CLOSED) {
    448          // toggle from already-open to closed
    449          this.cancel(browser, data);
    450          return;
    451        }
    452        this.start(browser, data);
    453        break;
    454      }
    455    }
    456  },
    457 
    458  /**
    459   * Notify screenshots when screenshot command is used.
    460   *
    461   * @param window The current window the screenshot command was used.
    462   * @param type The type of screenshot taken. Used for telemetry.
    463   */
    464  notify(window, type) {
    465    Services.obs.notifyObservers(
    466      window.event.currentTarget.ownerGlobal,
    467      "menuitem-screenshot",
    468      type
    469    );
    470  },
    471 
    472  /**
    473   * Creates/gets and returns a Screenshots actor.
    474   *
    475   * @param browser The current browser.
    476   * @returns JSWindowActor The screenshot actor.
    477   */
    478  getActor(browser) {
    479    let actor = browser.browsingContext.currentWindowGlobal.getActor(
    480      "ScreenshotsComponent"
    481    );
    482    return actor;
    483  },
    484 
    485  /**
    486   * Show the Screenshots UI and start the capture flow
    487   *
    488   * @param browser The current browser.
    489   * @param reason [string] Optional reason string passed along when recording telemetry events
    490   */
    491  start(browser, reason = "") {
    492    const uiPhase = this.getUIPhase(browser);
    493    switch (uiPhase) {
    494      case UIPhases.CLOSED: {
    495        this.captureFocusedElement(browser, "previousFocusRef");
    496        this.showPanelAndOverlay(browser, reason);
    497        browser.addEventListener("SwapDocShells", this);
    498        let gBrowser = browser.getTabBrowser();
    499        gBrowser.tabContainer.addEventListener("TabSelect", this);
    500        browser.ownerDocument.addEventListener("keydown", this);
    501        break;
    502      }
    503      case UIPhases.INITIAL:
    504        // nothing to do, panel & overlay are already open
    505        break;
    506      case UIPhases.PREVIEW: {
    507        this.closeDialogBox(browser);
    508        this.showPanelAndOverlay(browser, reason);
    509        break;
    510      }
    511    }
    512  },
    513 
    514  /**
    515   * Exit the Screenshots UI for the given browser
    516   * Closes any of open UI elements (preview dialog, panel, overlay) and cleans up internal state.
    517   *
    518   * @param browser The current browser.
    519   */
    520  exit(browser) {
    521    this.captureFocusedElement(browser, "currentFocusRef");
    522    this.closeDialogBox(browser);
    523    this.closePanel(browser);
    524    this.closeOverlay(browser);
    525    this.resetMethodsUsed();
    526    this.attemptToRestoreFocus(browser);
    527 
    528    this.revokeBlobURL(browser);
    529 
    530    browser.removeEventListener("SwapDocShells", this);
    531    const gBrowser = browser.getTabBrowser();
    532    gBrowser.tabContainer.removeEventListener("TabSelect", this);
    533    browser.ownerDocument.removeEventListener("keydown", this);
    534 
    535    this.browserToScreenshotsState.delete(browser);
    536    if (Cu.isInAutomation) {
    537      Services.obs.notifyObservers(null, "screenshots-exit");
    538    }
    539  },
    540 
    541  /**
    542   * Cancel/abort the screenshots operation for the given browser
    543   *
    544   * @param browser The current browser.
    545   */
    546  cancel(browser, reason) {
    547    this.recordTelemetryEvent("canceled" + reason);
    548    this.exit(browser);
    549  },
    550 
    551  /**
    552   * Update internal UI state associated with the given browser
    553   *
    554   * @param browser The current browser.
    555   * @param nameValues {object} An object with one or more named property values
    556   */
    557  setPerBrowserState(browser, nameValues = {}) {
    558    if (!this.browserToScreenshotsState.has(browser)) {
    559      // we should really have this state already, created when the preview dialog was opened
    560      this.browserToScreenshotsState.set(browser, {});
    561    }
    562    let perBrowserState = this.browserToScreenshotsState.get(browser);
    563    Object.assign(perBrowserState, nameValues);
    564  },
    565 
    566  maybeLockFocus(event) {
    567    let browser = event.view.gBrowser.selectedBrowser;
    568 
    569    if (!Services.focus.focusedElement) {
    570      event.preventDefault();
    571      this.focusPanel(browser);
    572      return;
    573    }
    574 
    575    let target = event.explicitOriginalTarget;
    576 
    577    if (!target.closest("moz-button-group")) {
    578      return;
    579    }
    580 
    581    let isElementFirst = !!target.nextElementSibling;
    582 
    583    if (isElementFirst && event.shiftKey) {
    584      event.preventDefault();
    585      this.moveFocusToContent(browser, "backward");
    586    } else if (!isElementFirst && !event.shiftKey) {
    587      event.preventDefault();
    588      this.moveFocusToContent(browser);
    589    }
    590  },
    591 
    592  focusPanel(browser, { direction } = {}) {
    593    let buttonsPanel = this.panelForBrowser(browser);
    594    if (direction) {
    595      buttonsPanel
    596        .querySelector("screenshots-buttons")
    597        .focusButton(direction === "forward" ? "first" : "last");
    598    } else {
    599      buttonsPanel
    600        .querySelector("screenshots-buttons")
    601        .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD);
    602    }
    603  },
    604 
    605  moveFocusToContent(browser, direction = "forward") {
    606    this.getActor(browser).sendAsyncMessage(
    607      "Screenshots:MoveFocusToContent",
    608      direction
    609    );
    610  },
    611 
    612  clearContentFocus(browser) {
    613    this.getActor(browser).sendAsyncMessage("Screenshots:ClearFocus");
    614  },
    615 
    616  /**
    617   * Attempt to place focus on the element that had focus before screenshots UI was shown
    618   *
    619   * @param browser The current browser.
    620   */
    621  attemptToRestoreFocus(browser) {
    622    const document = browser.ownerDocument;
    623    const window = browser.ownerGlobal;
    624 
    625    const doFocus = () => {
    626      // Move focus it back to where it was previously.
    627      prevFocus.setAttribute("refocused-by-panel", true);
    628      try {
    629        let fm = Services.focus;
    630        fm.setFocus(prevFocus, fm.FLAG_NOSCROLL);
    631      } catch (e) {
    632        prevFocus.focus();
    633      }
    634      prevFocus.removeAttribute("refocused-by-panel");
    635      let focusedElement;
    636      try {
    637        focusedElement = document.commandDispatcher.focusedElement;
    638        if (!focusedElement) {
    639          focusedElement = document.activeElement;
    640        }
    641      } catch (ex) {
    642        focusedElement = document.activeElement;
    643      }
    644    };
    645 
    646    let perBrowserState = this.browserToScreenshotsState.get(browser) || {};
    647    let prevFocus = perBrowserState.previousFocusRef?.get();
    648    let currentFocus = perBrowserState.currentFocusRef?.get();
    649    delete perBrowserState.currentFocusRef;
    650 
    651    // Avoid changing focus if focus changed during exit - perhaps exit was caused
    652    // by a user action which resulted in focus moving
    653    let nowFocus;
    654    try {
    655      nowFocus = document.commandDispatcher.focusedElement;
    656    } catch (e) {
    657      nowFocus = document.activeElement;
    658    }
    659    if (nowFocus && nowFocus != currentFocus) {
    660      return;
    661    }
    662 
    663    let dialog = this.getDialog(browser);
    664    let panel = this.panelForBrowser(browser);
    665 
    666    if (prevFocus) {
    667      // Try to restore focus
    668      try {
    669        if (document.commandDispatcher.focusedWindow != window) {
    670          // Focus has already been set to a different window
    671          return;
    672        }
    673      } catch (ex) {}
    674 
    675      if (!currentFocus) {
    676        doFocus();
    677        return;
    678      }
    679      while (currentFocus) {
    680        if (
    681          (dialog && currentFocus == dialog) ||
    682          (panel && currentFocus == panel) ||
    683          currentFocus == browser
    684        ) {
    685          doFocus();
    686          return;
    687        }
    688        currentFocus = currentFocus.parentNode;
    689        if (
    690          currentFocus &&
    691          currentFocus.nodeType == currentFocus.DOCUMENT_FRAGMENT_NODE &&
    692          currentFocus.host
    693        ) {
    694          // focus was in a shadowRoot, we'll try the host",
    695          currentFocus = currentFocus.host;
    696        }
    697      }
    698      doFocus();
    699    }
    700  },
    701 
    702  /**
    703   * Set a flag so we don't try to exit when preview dialog next closes.
    704   *
    705   * @param browser The current browser.
    706   * @param reason [string] Optional reason string passed along when recording telemetry events
    707   */
    708  scheduleRetry(browser, reason) {
    709    let perBrowserState = this.browserToScreenshotsState.get(browser);
    710    if (!perBrowserState?.closedPromise) {
    711      console.warn(
    712        "Expected perBrowserState with a closedPromise for the preview dialog"
    713      );
    714      return;
    715    }
    716    this.setPerBrowserState(browser, { exitOnPreviewClose: false });
    717    perBrowserState?.closedPromise.then(() => {
    718      this.start(browser, reason);
    719    });
    720  },
    721 
    722  /**
    723   * Open the tab dialog for preview
    724   *
    725   * @param browser The current browser
    726   */
    727  async openPreviewDialog(browser) {
    728    let dialogBox = browser.ownerGlobal.gBrowser.getTabDialogBox(browser);
    729    let { dialog, closedPromise } = await dialogBox.open(
    730      `chrome://browser/content/screenshots/screenshots-preview.html?browsingContextId=${browser.browsingContext.id}`,
    731      {
    732        features: "resizable=no",
    733        sizeTo: "available",
    734        allowDuplicateDialogs: false,
    735      },
    736      browser
    737    );
    738 
    739    this.setPerBrowserState(browser, {
    740      previewDialog: dialog,
    741      exitOnPreviewClose: true,
    742      closedPromise: closedPromise.finally(() => {
    743        this.onDialogClose(browser);
    744      }),
    745    });
    746    return dialog;
    747  },
    748 
    749  /**
    750   * Take a weak-reference to whatever element currently has focus and associate it with
    751   * the UI state for this browser.
    752   *
    753   * @param browser The current browser.
    754   * @param {string} stateRefName The property name for this element reference.
    755   */
    756  captureFocusedElement(browser, stateRefName) {
    757    let document = browser.ownerDocument;
    758    let focusedElement;
    759    try {
    760      focusedElement = document.commandDispatcher.focusedElement;
    761      if (!focusedElement) {
    762        focusedElement = document.activeElement;
    763      }
    764    } catch (ex) {
    765      focusedElement = document.activeElement;
    766    }
    767    this.setPerBrowserState(browser, {
    768      [stateRefName]: Cu.getWeakReference(focusedElement),
    769    });
    770  },
    771 
    772  /**
    773   * Returns the buttons panel for the given browser if the panel exists.
    774   * Otherwise creates the buttons panel and returns the buttons panel.
    775   *
    776   * @param browser The current browser
    777   * @returns The buttons panel
    778   */
    779  panelForBrowser(browser) {
    780    let buttonsPanel = browser.ownerDocument.getElementById(
    781      "screenshotsPagePanel"
    782    );
    783    if (!buttonsPanel) {
    784      let doc = browser.ownerDocument;
    785      let template = doc.getElementById("screenshotsPagePanelTemplate");
    786      let fragmentClone = template.content.cloneNode(true);
    787      buttonsPanel = fragmentClone.firstElementChild;
    788      template.replaceWith(buttonsPanel);
    789      browser.closest("#tabbrowser-tabbox").prepend(buttonsPanel);
    790    }
    791 
    792    return (
    793      buttonsPanel ??
    794      browser.ownerDocument.getElementById("screenshotsPagePanel")
    795    );
    796  },
    797 
    798  /**
    799   * Open the buttons panel.
    800   *
    801   * @param browser The current browser
    802   */
    803  openPanel(browser) {
    804    let buttonsPanel = this.panelForBrowser(browser);
    805    if (!buttonsPanel.hidden) {
    806      return null;
    807    }
    808    buttonsPanel.hidden = false;
    809 
    810    return new Promise(resolve => {
    811      browser.ownerGlobal.requestAnimationFrame(() => {
    812        buttonsPanel
    813          .querySelector("screenshots-buttons")
    814          .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD);
    815        resolve();
    816      });
    817    });
    818  },
    819 
    820  /**
    821   * Close the panel
    822   *
    823   * @param browser The current browser
    824   */
    825  closePanel(browser) {
    826    let buttonsPanel = this.panelForBrowser(browser);
    827    if (!buttonsPanel) {
    828      return;
    829    }
    830    buttonsPanel.hidden = true;
    831  },
    832 
    833  /**
    834   * If the buttons panel exists and is open we will hide both the panel
    835   * and the overlay. If the overlay is showing, we will hide the overlay.
    836   * Otherwise create or display the buttons.
    837   *
    838   * @param browser The current browser.
    839   */
    840  async showPanelAndOverlay(browser, data) {
    841    let actor = this.getActor(browser);
    842    actor.sendAsyncMessage("Screenshots:ShowOverlay");
    843    this.recordTelemetryEvent("started" + data);
    844    this.openPanel(browser);
    845  },
    846 
    847  /**
    848   * Close the overlay UI, and clear out internal state if there was an overlay selection
    849   * The overlay lives in the child document; so although closing is actually async, we assume success.
    850   *
    851   * @param browser The current browser.
    852   */
    853  closeOverlay(browser, options = {}) {
    854    // If the actor has been unregistered (e.g. if the component enabled pref is flipped false)
    855    // its possible getActor will throw an exception. That's ok.
    856    let actor;
    857    try {
    858      actor = this.getActor(browser);
    859    } catch (ex) {}
    860    actor?.sendAsyncMessage("Screenshots:HideOverlay", options);
    861 
    862    if (this.browserToScreenshotsState.has(browser)) {
    863      this.setPerBrowserState(browser, {
    864        hasOverlaySelection: false,
    865      });
    866    }
    867  },
    868 
    869  /**
    870   * Gets the screenshots dialog box
    871   *
    872   * @param browser The selected browser
    873   * @returns Screenshots dialog box if it exists otherwise null
    874   */
    875  getDialog(browser) {
    876    let currTabDialogBox = browser.tabDialogBox;
    877    let browserContextId = browser.browsingContext.id;
    878    if (currTabDialogBox) {
    879      let manager = currTabDialogBox.getTabDialogManager();
    880      let dialogs = manager.hasDialogs && manager.dialogs;
    881      if (dialogs.length) {
    882        for (let dialog of dialogs) {
    883          if (
    884            dialog._openedURL.endsWith(
    885              `browsingContextId=${browserContextId}`
    886            ) &&
    887            dialog._openedURL.includes("screenshots-preview.html")
    888          ) {
    889            return dialog;
    890          }
    891        }
    892      }
    893    }
    894    return null;
    895  },
    896 
    897  /**
    898   * Closes the dialog box it it exists
    899   *
    900   * @param browser The selected browser
    901   */
    902  closeDialogBox(browser) {
    903    let perBrowserState = this.browserToScreenshotsState.get(browser);
    904    if (perBrowserState?.previewDialog) {
    905      perBrowserState.previewDialog.close();
    906      return true;
    907    }
    908    return false;
    909  },
    910 
    911  /**
    912   * Callback fired when the preview dialog window closes
    913   * Will exit the screenshots UI if the `exitOnPreviewClose` flag is set for this browser
    914   *
    915   * @param browser The associated browser
    916   */
    917  onDialogClose(browser) {
    918    let perBrowserState = this.browserToScreenshotsState.get(browser);
    919    if (!perBrowserState) {
    920      return;
    921    }
    922    delete perBrowserState.previewDialog;
    923    if (perBrowserState?.exitOnPreviewClose) {
    924      this.exit(browser);
    925    }
    926  },
    927 
    928  /**
    929   * Gets the screenshots button if it is visible, otherwise it will get the
    930   * element that the screenshots button is nested under. If the screenshots
    931   * button doesn't exist then we will default to the navigator toolbox.
    932   *
    933   * @param browser The selected browser
    934   * @returns The anchor element for the ConfirmationHint
    935   */
    936  getWidgetAnchor(browser) {
    937    let window = browser.ownerGlobal;
    938    let widgetGroup = window.CustomizableUI.getWidget("screenshot-button");
    939    let widget = widgetGroup?.forWindow(window);
    940    let anchor = widget?.anchor;
    941 
    942    // Check if the anchor exists and is visible
    943    if (
    944      !anchor ||
    945      !anchor.isConnected ||
    946      !window.isElementVisible(anchor.parentNode)
    947    ) {
    948      // Use the hamburger button if the screenshots button isn't available
    949      anchor = browser.ownerDocument.getElementById("PanelUI-menu-button");
    950    }
    951    return anchor;
    952  },
    953 
    954  /**
    955   * Indicate that the screenshot has been copied via ConfirmationHint.
    956   *
    957   * @param browser The selected browser
    958   */
    959  showCopiedConfirmationHint(browser) {
    960    let anchor = this.getWidgetAnchor(browser);
    961 
    962    browser.ownerGlobal.ConfirmationHint.show(
    963      anchor,
    964      "confirmation-hint-screenshot-copied"
    965    );
    966  },
    967 
    968  /**
    969   * Gets the full page bounds from the screenshots child actor.
    970   *
    971   * @param browser The current browser.
    972   * @returns { object }
    973   *    Contains the full page bounds from the screenshots child actor.
    974   */
    975  fetchFullPageBounds(browser) {
    976    let actor = this.getActor(browser);
    977    return actor.sendQuery("Screenshots:getFullPageBounds");
    978  },
    979 
    980  /**
    981   * Gets the visible bounds from the screenshots child actor.
    982   *
    983   * @param browser The current browser.
    984   * @returns { object }
    985   *    Contains the visible bounds from the screenshots child actor.
    986   */
    987  fetchVisibleBounds(browser) {
    988    let actor = this.getActor(browser);
    989    return actor.sendQuery("Screenshots:getVisibleBounds");
    990  },
    991 
    992  showAlertMessage(title, message) {
    993    lazy.AlertsService.showAlert(
    994      new AlertNotification({ title, text: message })
    995    );
    996  },
    997 
    998  /**
    999   * Revoke the object url of the current browsers screenshot.
   1000   *
   1001   * @param {browser} browser The current browser
   1002   */
   1003  revokeBlobURL(browser) {
   1004    let browserState = this.browserToScreenshotsState.get(browser);
   1005    if (browserState?.blobURL) {
   1006      URL.revokeObjectURL(browserState.blobURL);
   1007    }
   1008  },
   1009 
   1010  /**
   1011   * Set the blob url on the browser state so we can revoke on exit.
   1012   *
   1013   * @param {browser} browser The current browser
   1014   * @param {string} blobURL The object url for the screenshot
   1015   */
   1016  setBlobURL(browser, blobURL) {
   1017    // We shouldn't already have a blob URL on the browser
   1018    // but let's revoke just in case.
   1019    this.revokeBlobURL(browser);
   1020 
   1021    this.setPerBrowserState(browser, { blobURL });
   1022  },
   1023 
   1024  /**
   1025   * The max dimension of any side of a canvas is 32767 and the max canvas area is
   1026   * 124925329. If the width or height is greater or equal to 32766 we will crop the
   1027   * screenshot to the max width. If the area is still too large for the canvas
   1028   * we will adjust the height so we can successfully capture the screenshot.
   1029   *
   1030   * @param {object} rect The dimensions of the screenshot. The rect will be
   1031   * modified in place
   1032   */
   1033  cropScreenshotRectIfNeeded(rect) {
   1034    let cropped = false;
   1035    let width = rect.width * rect.devicePixelRatio;
   1036    let height = rect.height * rect.devicePixelRatio;
   1037 
   1038    if (width > MAX_CAPTURE_DIMENSION) {
   1039      width = MAX_CAPTURE_DIMENSION;
   1040      cropped = true;
   1041    }
   1042    if (height > MAX_CAPTURE_DIMENSION) {
   1043      height = MAX_CAPTURE_DIMENSION;
   1044      cropped = true;
   1045    }
   1046    if (width * height > MAX_CAPTURE_AREA) {
   1047      height = Math.floor(MAX_CAPTURE_AREA / width);
   1048      cropped = true;
   1049    }
   1050 
   1051    rect.width = Math.floor(width / rect.devicePixelRatio);
   1052    rect.height = Math.floor(height / rect.devicePixelRatio);
   1053    rect.right = rect.left + rect.width;
   1054    rect.bottom = rect.top + rect.height;
   1055 
   1056    if (cropped) {
   1057      let [errorTitle, errorMessage] =
   1058        lazy.screenshotsLocalization.formatMessagesSync([
   1059          { id: "screenshots-too-large-error-title" },
   1060          { id: "screenshots-too-large-error-details" },
   1061        ]);
   1062      this.showAlertMessage(errorTitle.value, errorMessage.value);
   1063      this.recordTelemetryEvent("failedScreenshotTooLarge");
   1064    }
   1065  },
   1066 
   1067  /**
   1068   * Take the screenshot, then open and add the screenshot-ui element to the
   1069   * dialog box.
   1070   *
   1071   * @param browser The current browser.
   1072   * @param type The type of screenshot taken.
   1073   */
   1074  async takeScreenshot(browser, type) {
   1075    this.closePanel(browser);
   1076    this.closeOverlay(browser, {
   1077      doNotResetMethods: true,
   1078      highlightRegions: true,
   1079    });
   1080 
   1081    Services.focus.setFocus(browser, 0);
   1082 
   1083    let rect;
   1084    let lastUsedMethod;
   1085    if (type === "FullPage") {
   1086      rect = await this.fetchFullPageBounds(browser);
   1087      lastUsedMethod = "fullpage";
   1088    } else {
   1089      rect = await this.fetchVisibleBounds(browser);
   1090      lastUsedMethod = "visible";
   1091    }
   1092 
   1093    let canvas = await this.createCanvas(rect, browser);
   1094    let blob = await canvas.convertToBlob();
   1095 
   1096    let dialog = await this.openPreviewDialog(browser);
   1097    await dialog._dialogReady;
   1098    let screenshotsPreviewEl = dialog._frame.contentDocument.querySelector(
   1099      "screenshots-preview"
   1100    );
   1101 
   1102    let blobURL = URL.createObjectURL(blob);
   1103    this.setBlobURL(browser, blobURL);
   1104    screenshotsPreviewEl.previewImg.src = blobURL;
   1105 
   1106    screenshotsPreviewEl.focusButton(lazy.SCREENSHOTS_LAST_SAVED_METHOD);
   1107 
   1108    Services.prefs.setStringPref(
   1109      SCREENSHOTS_LAST_SCREENSHOT_METHOD_PREF,
   1110      lastUsedMethod
   1111    );
   1112    this.methodsUsed[lastUsedMethod] += 1;
   1113    this.recordTelemetryEvent("selected" + type);
   1114 
   1115    if (Cu.isInAutomation) {
   1116      Services.obs.notifyObservers(null, "screenshots-preview-ready");
   1117    }
   1118  },
   1119 
   1120  /**
   1121   * Creates a canvas and draws a snapshot of the screenshot on the canvas
   1122   *
   1123   * @param region The bounds of screenshots
   1124   * @param browser The current browser
   1125   * @returns The canvas
   1126   */
   1127  async createCanvas(region, browser) {
   1128    region.left = Math.round(region.left);
   1129    region.right = Math.round(region.right);
   1130    region.top = Math.round(region.top);
   1131    region.bottom = Math.round(region.bottom);
   1132    region.width = Math.round(region.right - region.left);
   1133    region.height = Math.round(region.bottom - region.top);
   1134 
   1135    this.cropScreenshotRectIfNeeded(region);
   1136 
   1137    let { devicePixelRatio } = region;
   1138 
   1139    let browsingContext = BrowsingContext.get(browser.browsingContext.id);
   1140 
   1141    let canvas = new OffscreenCanvas(
   1142      region.width * devicePixelRatio,
   1143      region.height * devicePixelRatio
   1144    );
   1145    let context = canvas.getContext("2d");
   1146 
   1147    const snapshotSize = Math.floor(MAX_SNAPSHOT_DIMENSION * devicePixelRatio);
   1148 
   1149    for (
   1150      let startLeft = region.left;
   1151      startLeft < region.right;
   1152      startLeft += MAX_SNAPSHOT_DIMENSION
   1153    ) {
   1154      for (
   1155        let startTop = region.top;
   1156        startTop < region.bottom;
   1157        startTop += MAX_SNAPSHOT_DIMENSION
   1158      ) {
   1159        let height =
   1160          startTop + MAX_SNAPSHOT_DIMENSION > region.bottom
   1161            ? region.bottom - startTop
   1162            : MAX_SNAPSHOT_DIMENSION;
   1163        let width =
   1164          startLeft + MAX_SNAPSHOT_DIMENSION > region.right
   1165            ? region.right - startLeft
   1166            : MAX_SNAPSHOT_DIMENSION;
   1167        let rect = new DOMRect(startLeft, startTop, width, height);
   1168 
   1169        let snapshot = await browsingContext.currentWindowGlobal.drawSnapshot(
   1170          rect,
   1171          devicePixelRatio,
   1172          "rgb(255,255,255)"
   1173        );
   1174 
   1175        // The `left` and `top` need to be a multiple of the `snapshotSize` to
   1176        // prevent gaps/lines from appearing in the screenshot.
   1177        // If devicePixelRatio is 0.3, snapshotSize would be 307 after flooring
   1178        // from 307.2. Therefore every fifth snapshot would have a start of
   1179        // 307.2 * 5 or 1536 which is not a multiple of 307 and would cause a
   1180        // gap/line in the snapshot.
   1181        let left = Math.floor((startLeft - region.left) * devicePixelRatio);
   1182        let top = Math.floor((startTop - region.top) * devicePixelRatio);
   1183        context.drawImage(
   1184          snapshot,
   1185          left - (left % snapshotSize),
   1186          top - (top % snapshotSize),
   1187          Math.floor(width * devicePixelRatio),
   1188          Math.floor(height * devicePixelRatio)
   1189        );
   1190 
   1191        snapshot.close();
   1192      }
   1193    }
   1194 
   1195    return canvas;
   1196  },
   1197 
   1198  /**
   1199   * Copy the screenshot
   1200   *
   1201   * @param region The bounds of the screenshots
   1202   * @param browser The current browser
   1203   */
   1204  async copyScreenshotFromRegion(region, browser) {
   1205    let canvas = await this.createCanvas(region, browser);
   1206    let blob = await canvas.convertToBlob();
   1207 
   1208    await this.copyScreenshot(blob, browser, "OverlayCopy");
   1209  },
   1210 
   1211  async copyScreenshotFromBlobURL(blobURL, browser, eventName) {
   1212    let blob = await fetch(blobURL).then(r => r.blob());
   1213    await this.copyScreenshot(blob, browser, eventName);
   1214  },
   1215 
   1216  /**
   1217   * Copy the image to the clipboard
   1218   * This is called from the preview dialog
   1219   *
   1220   * @param blob The image data
   1221   * @param browser The current browser
   1222   * @param eventName For telemetry
   1223   */
   1224  async copyScreenshot(blob, browser, eventName) {
   1225    // Guard against missing image data.
   1226    if (!blob) {
   1227      return;
   1228    }
   1229 
   1230    const imageTools = Cc["@mozilla.org/image/tools;1"].getService(
   1231      Ci.imgITools
   1232    );
   1233 
   1234    let buffer = await blob.arrayBuffer();
   1235    const imgDecoded = imageTools.decodeImageFromArrayBuffer(
   1236      buffer,
   1237      "image/png"
   1238    );
   1239 
   1240    const transferable = Cc[
   1241      "@mozilla.org/widget/transferable;1"
   1242    ].createInstance(Ci.nsITransferable);
   1243    transferable.init(null);
   1244    // Internal consumers expect the image data to be stored as a
   1245    // nsIInputStream. On Linux and Windows, pasted data is directly
   1246    // retrieved from the system's native clipboard, and made available
   1247    // as a nsIInputStream.
   1248    //
   1249    // On macOS, nsClipboard::GetNativeClipboardData (nsClipboard.mm) uses
   1250    // a cached copy of nsITransferable if available, e.g. when the copy
   1251    // was initiated by the same browser instance. To make sure that a
   1252    // nsIInputStream is returned instead of the cached imgIContainer,
   1253    // the image is exported as as `kNativeImageMime`. Data associated
   1254    // with this type is converted to a platform-specific image format
   1255    // when written to the clipboard. The type is not used when images
   1256    // are read from the clipboard (on all platforms, not just macOS).
   1257    // This forces nsClipboard::GetNativeClipboardData to fall back to
   1258    // the native clipboard, and return the image as a nsITransferable.
   1259    transferable.addDataFlavor("application/x-moz-nativeimage");
   1260    transferable.setTransferData("application/x-moz-nativeimage", imgDecoded);
   1261 
   1262    Services.clipboard.setData(
   1263      transferable,
   1264      null,
   1265      Services.clipboard.kGlobalClipboard
   1266    );
   1267 
   1268    this.showCopiedConfirmationHint(browser);
   1269 
   1270    let extra = await this.getActor(browser).sendQuery(
   1271      "Screenshots:GetMethodsUsed"
   1272    );
   1273    this.recordTelemetryEvent("copy" + eventName, {
   1274      ...extra,
   1275      ...this.methodsUsed,
   1276    });
   1277    this.resetMethodsUsed();
   1278 
   1279    Services.prefs.setStringPref(SCREENSHOTS_LAST_SAVED_METHOD_PREF, "copy");
   1280  },
   1281 
   1282  /**
   1283   * Download the screenshot
   1284   *
   1285   * @param title The title of the current page
   1286   * @param region The bounds of the screenshot
   1287   * @param browser The current browser
   1288   */
   1289  async downloadScreenshotFromRegion(title, region, browser) {
   1290    let canvas = await this.createCanvas(region, browser);
   1291    let blob = await canvas.convertToBlob();
   1292    let blobURL = URL.createObjectURL(blob);
   1293    this.setBlobURL(browser, blobURL);
   1294 
   1295    await this.downloadScreenshot(title, blobURL, browser, "OverlayDownload");
   1296  },
   1297 
   1298  /**
   1299   * Download the screenshot
   1300   * This is called from the preview dialog
   1301   *
   1302   * @param title The title of the current page or null and getFilename will get the title
   1303   * @param blobURL The image data
   1304   * @param browser The current browser
   1305   * @param eventName For telemetry
   1306   * @returns true if the download succeeds, otherwise false
   1307   */
   1308  async downloadScreenshot(title, blobURL, browser, eventName) {
   1309    // Guard against missing image data.
   1310    if (!blobURL) {
   1311      return false;
   1312    }
   1313 
   1314    let { filename, accepted } = await getFilename(title, browser);
   1315    if (!accepted) {
   1316      return false;
   1317    }
   1318 
   1319    const targetFile = new lazy.FileUtils.File(filename);
   1320 
   1321    // Create download and track its progress.
   1322    try {
   1323      const download = await lazy.Downloads.createDownload({
   1324        source: blobURL,
   1325        target: targetFile,
   1326      });
   1327 
   1328      let isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(
   1329        browser.ownerGlobal
   1330      );
   1331      const list = await lazy.Downloads.getList(
   1332        isPrivate ? lazy.Downloads.PRIVATE : lazy.Downloads.PUBLIC
   1333      );
   1334      // add the download to the download list in the Downloads list in the Browser UI
   1335      list.add(download);
   1336 
   1337      // Await successful completion of the save via the download manager
   1338      await download.start();
   1339    } catch (ex) {
   1340      console.error(
   1341        `Failed to create download using filename: ${filename} (length: ${
   1342          new Blob([filename]).size
   1343        })`
   1344      );
   1345 
   1346      return false;
   1347    }
   1348 
   1349    let extra = await this.getActor(browser).sendQuery(
   1350      "Screenshots:GetMethodsUsed"
   1351    );
   1352    this.recordTelemetryEvent("download" + eventName, {
   1353      ...extra,
   1354      ...this.methodsUsed,
   1355    });
   1356    this.resetMethodsUsed();
   1357 
   1358    Services.prefs.setStringPref(
   1359      SCREENSHOTS_LAST_SAVED_METHOD_PREF,
   1360      "download"
   1361    );
   1362 
   1363    return true;
   1364  },
   1365 
   1366  recordTelemetryEvent(name, args) {
   1367    Glean.screenshots[name].record(args);
   1368  },
   1369 };
   1370 
   1371 export const ScreenshotsCustomizableWidget = {
   1372  init() {
   1373    // In testing, we might call init more than once
   1374    const widgetId = "screenshot-button";
   1375    lazy.CustomizableUI.createWidget({
   1376      id: widgetId,
   1377      shortcutId: "key_screenshot",
   1378      l10nId: "screenshot-toolbar-button",
   1379      onCommand(aEvent) {
   1380        Services.obs.notifyObservers(
   1381          aEvent.currentTarget.ownerGlobal,
   1382          "menuitem-screenshot",
   1383          "ToolbarButton"
   1384        );
   1385      },
   1386    });
   1387    const maybePlaceToolbarButton = () => {
   1388      // If Nimbus tells us the widget should be placed and visible by default, first check we
   1389      // didn't already handle this
   1390      const buttonPlacedByNimbus = Services.prefs.getBoolPref(
   1391        "screenshots.browser.component.buttonOnToolbarByDefault.handled",
   1392        false
   1393      );
   1394      if (
   1395        !buttonPlacedByNimbus &&
   1396        !lazy.CustomizableUI.getPlacementOfWidget(widgetId)?.area &&
   1397        lazy.NimbusFeatures.screenshots.getVariable("buttonOnToolbarByDefault")
   1398      ) {
   1399        // We'll place the button after the urlbar if its in the nav-bar
   1400        let buttonPosition = 0;
   1401        const AREA_NAVBAR = lazy.CustomizableUI.AREA_NAVBAR;
   1402        const urlbarPlacement =
   1403          lazy.CustomizableUI.getPlacementOfWidget("urlbar-container");
   1404        if (urlbarPlacement?.area == AREA_NAVBAR) {
   1405          buttonPosition = urlbarPlacement.position + 1;
   1406          const widgetIds = lazy.CustomizableUI.getWidgetIdsInArea(AREA_NAVBAR);
   1407          // we want to go after the spring widget when there's one directly after the urlbar
   1408          if (widgetIds[buttonPosition].includes("special-spring")) {
   1409            buttonPosition++;
   1410          }
   1411        }
   1412        lazy.CustomizableUI.addWidgetToArea(
   1413          widgetId,
   1414          AREA_NAVBAR,
   1415          buttonPosition
   1416        );
   1417        Services.prefs.setBoolPref(
   1418          "screenshots.browser.component.buttonOnToolbarByDefault.handled",
   1419          true
   1420        );
   1421      }
   1422    };
   1423    // Check now and handle future Nimbus updates
   1424    maybePlaceToolbarButton();
   1425 
   1426    lazy.NimbusFeatures.screenshots.onUpdate(() => {
   1427      const enrollment =
   1428        lazy.NimbusFeatures.screenshots.getEnrollmentMetadata();
   1429      if (!enrollment) {
   1430        return;
   1431      }
   1432      maybePlaceToolbarButton();
   1433    });
   1434  },
   1435 
   1436  uninit() {
   1437    lazy.CustomizableUI.destroyWidget("screenshot-button");
   1438  },
   1439 };