tor-browser

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

ui.js (34975B)


      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 "use strict";
      6 
      7 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
      8 const {
      9  getOrientation,
     10 } = require("resource://devtools/client/responsive/utils/orientation.js");
     11 const Constants = require("resource://devtools/client/responsive/constants.js");
     12 const {
     13  CommandsFactory,
     14 } = require("resource://devtools/shared/commands/commands-factory.js");
     15 
     16 loader.lazyRequireGetter(
     17  this,
     18  "throttlingProfiles",
     19  "resource://devtools/client/shared/components/throttling/profiles.js"
     20 );
     21 loader.lazyRequireGetter(
     22  this,
     23  "message",
     24  "resource://devtools/client/responsive/utils/message.js"
     25 );
     26 loader.lazyRequireGetter(
     27  this,
     28  "showNotification",
     29  "resource://devtools/client/responsive/utils/notification.js",
     30  true
     31 );
     32 loader.lazyRequireGetter(
     33  this,
     34  "PriorityLevels",
     35  "resource://devtools/client/shared/components/NotificationBox.js",
     36  true
     37 );
     38 loader.lazyRequireGetter(
     39  this,
     40  "l10n",
     41  "resource://devtools/client/responsive/utils/l10n.js"
     42 );
     43 loader.lazyRequireGetter(
     44  this,
     45  "asyncStorage",
     46  "resource://devtools/shared/async-storage.js"
     47 );
     48 loader.lazyRequireGetter(
     49  this,
     50  "captureAndSaveScreenshot",
     51  "resource://devtools/client/shared/screenshot.js",
     52  true
     53 );
     54 
     55 const RELOAD_CONDITION_PREF_PREFIX = "devtools.responsive.reloadConditions.";
     56 const RELOAD_NOTIFICATION_PREF =
     57  "devtools.responsive.reloadNotification.enabled";
     58 const USE_DYNAMIC_TOOLBAR_PREF = "devtools.responsive.dynamicToolbar.enabled";
     59 const DYNAMIC_TOOLBAR_MAX_HEIGHT = 40; // px
     60 
     61 function debug(_msg) {
     62  // console.log(`RDM manager: ${_msg}`);
     63 }
     64 
     65 /**
     66 * ResponsiveUI manages the responsive design tool for a specific tab.  The
     67 * actual tool itself lives in a separate chrome:// document that is loaded into
     68 * the tab upon opening responsive design.  This object acts a helper to
     69 * integrate the tool into the surrounding browser UI as needed.
     70 */
     71 class ResponsiveUI {
     72  /**
     73   * @param {ResponsiveUIManager} manager
     74   *        The ResponsiveUIManager instance.
     75   * @param {ChromeWindow} window
     76   *        The main browser chrome window (that holds many tabs).
     77   * @param {Tab} tab
     78   *        The specific browser <tab> element this responsive instance is for.
     79   */
     80  constructor(manager, window, tab) {
     81    this.manager = manager;
     82    // The main browser chrome window (that holds many tabs).
     83    this.browserWindow = window;
     84    // The specific browser tab this responsive instance is for.
     85    this.tab = tab;
     86 
     87    // Flag set when destruction has begun.
     88    this.destroying = false;
     89    // Flag set when destruction has ended.
     90    this.destroyed = false;
     91    // The iframe containing the RDM UI.
     92    this.rdmFrame = null;
     93 
     94    // Bind callbacks for resizers.
     95    this.onResizeDrag = this.onResizeDrag.bind(this);
     96    this.onResizeStart = this.onResizeStart.bind(this);
     97    this.onResizeStop = this.onResizeStop.bind(this);
     98 
     99    this.onTargetAvailable = this.onTargetAvailable.bind(this);
    100    this.onContentScrolled = this.onContentScrolled.bind(this);
    101 
    102    this.networkFront = null;
    103    // Promise resolved when the UI init has completed.
    104    const { promise, resolve } = Promise.withResolvers();
    105    this.initialized = promise;
    106    this.resolveInited = resolve;
    107 
    108    this.dynamicToolbar = null;
    109    EventEmitter.decorate(this);
    110  }
    111 
    112  get toolWindow() {
    113    return this.rdmFrame.contentWindow;
    114  }
    115 
    116  get docShell() {
    117    return this.toolWindow.docShell;
    118  }
    119 
    120  get viewportElement() {
    121    return this.browserStackEl.querySelector("browser");
    122  }
    123 
    124  get currentTarget() {
    125    return this.commands.targetCommand.targetFront;
    126  }
    127 
    128  get watcherFront() {
    129    return this.resourceCommand.watcherFront;
    130  }
    131 
    132  /**
    133   * Open RDM while preserving the state of the page.
    134   */
    135  async initialize() {
    136    debug("Init start");
    137 
    138    this.initRDMFrame();
    139 
    140    // Hide the browser content temporarily while things move around to avoid displaying
    141    // strange intermediate states.
    142    this.hideBrowserUI();
    143 
    144    // Watch for tab close and window close so we can clean up RDM synchronously
    145    this.tab.addEventListener("TabClose", this);
    146    this.browserWindow.addEventListener("unload", this);
    147    this.rdmFrame.contentWindow.addEventListener("message", this);
    148 
    149    this.tab.linkedBrowser.enterResponsiveMode();
    150 
    151    // Listen to FullZoomChange events coming from the browser window,
    152    // so that we can zoom the size of the viewport by the same amount.
    153    this.browserWindow.addEventListener("FullZoomChange", this);
    154 
    155    // Get the protocol ready to speak with responsive emulation actor
    156    debug("Wait until RDP server connect");
    157    await this.connectToServer();
    158 
    159    // Restore the previous UI state.
    160    await this.restoreUIState();
    161 
    162    // Show the browser UI now that its state is ready.
    163    this.showBrowserUI();
    164 
    165    // Non-blocking message to tool UI to start any delayed init activities
    166    message.post(this.toolWindow, "post-init");
    167 
    168    debug("Init done");
    169    this.resolveInited();
    170  }
    171 
    172  /**
    173   * Initialize the RDM iframe inside of the browser document.
    174   */
    175  initRDMFrame() {
    176    const { document: doc, gBrowser } = this.browserWindow;
    177    const rdmFrame = doc.createElement("iframe");
    178    rdmFrame.src = "chrome://devtools/content/responsive/toolbar.xhtml";
    179    rdmFrame.classList.add("rdm-toolbar");
    180 
    181    // Create dynamic toolbar
    182    this.dynamicToolbar = doc.createElement("div");
    183    this.dynamicToolbar.classList.add("rdm-dynamic-toolbar", "dynamic-toolbar");
    184    this.dynamicToolbar.style.visibility = "hidden";
    185 
    186    if (Services.prefs.getBoolPref(USE_DYNAMIC_TOOLBAR_PREF)) {
    187      this.dynamicToolbar.style.visibility = "visible";
    188      this.dynamicToolbar.style.height = DYNAMIC_TOOLBAR_MAX_HEIGHT + "px";
    189      InspectorUtils.setDynamicToolbarMaxHeight(
    190        this.tab.linkedBrowser.browsingContext,
    191        DYNAMIC_TOOLBAR_MAX_HEIGHT
    192      );
    193      InspectorUtils.setVerticalClipping(
    194        this.tab.linkedBrowser.browsingContext,
    195        0
    196      );
    197    }
    198 
    199    // Create resizer handlers
    200    const resizeHandle = doc.createElement("div");
    201    resizeHandle.classList.add(
    202      "rdm-viewport-resize-handle",
    203      "viewport-resize-handle"
    204    );
    205    const resizeHandleX = doc.createElement("div");
    206    resizeHandleX.classList.add(
    207      "rdm-viewport-resize-handle",
    208      "viewport-horizontal-resize-handle"
    209    );
    210    const resizeHandleY = doc.createElement("div");
    211    resizeHandleY.classList.add(
    212      "rdm-viewport-resize-handle",
    213      "viewport-vertical-resize-handle"
    214    );
    215 
    216    this.browserContainerEl = gBrowser.getBrowserContainer(
    217      gBrowser.getBrowserForTab(this.tab)
    218    );
    219    this.browserStackEl =
    220      this.browserContainerEl.querySelector(".browserStack");
    221 
    222    this.browserContainerEl.classList.add("responsive-mode");
    223 
    224    // Prepend the RDM iframe inside of the current tab's browser container.
    225    this.browserContainerEl.prepend(rdmFrame);
    226 
    227    this.browserStackEl.append(
    228      this.dynamicToolbar,
    229      resizeHandle,
    230      resizeHandleX,
    231      resizeHandleY
    232    );
    233 
    234    // Wait for the frame script to be loaded.
    235    message.wait(rdmFrame.contentWindow, "script-init").then(async () => {
    236      // Notify the frame window that the Resposnive UI manager has begun initializing.
    237      // At this point, we can render our React content inside the frame.
    238      message.post(rdmFrame.contentWindow, "init");
    239      // Wait for the tools to be rendered above the content. The frame script will
    240      // then dispatch the necessary actions to the Redux store to give the toolbar the
    241      // state it needs.
    242      message.wait(rdmFrame.contentWindow, "init:done").then(() => {
    243        rdmFrame.contentWindow.addInitialViewport({
    244          userContextId: this.tab.userContextId,
    245        });
    246      });
    247    });
    248 
    249    this.rdmFrame = rdmFrame;
    250 
    251    this.resizeHandle = resizeHandle;
    252    this.resizeHandle.addEventListener("mousedown", this.onResizeStart);
    253 
    254    this.resizeHandleX = resizeHandleX;
    255    this.resizeHandleX.addEventListener("mousedown", this.onResizeStart);
    256 
    257    this.resizeHandleY = resizeHandleY;
    258    this.resizeHandleY.addEventListener("mousedown", this.onResizeStart);
    259 
    260    this.resizeToolbarObserver = new this.browserWindow.ResizeObserver(
    261      entries => {
    262        for (const entry of entries) {
    263          // If the toolbar needs extra space for the UA input, then set a class
    264          // that will accomodate its height. We should also make sure to keep
    265          // the width value we're toggling against in sync with the media-query
    266          // in devtools/client/responsive/index.css
    267          this.rdmFrame.classList.toggle(
    268            "accomodate-ua",
    269            entry.contentBoxSize[0].inlineSize <= 800
    270          );
    271        }
    272      }
    273    );
    274 
    275    this.resizeToolbarObserver.observe(this.browserStackEl);
    276  }
    277 
    278  /**
    279   * Close RDM and restore page content back into a regular tab.
    280   *
    281   * @param object
    282   *        Destroy options, which currently includes a `reason` string.
    283   * @return boolean
    284   *         Whether this call is actually destroying.  False means destruction
    285   *         was already in progress.
    286   */
    287  async destroy(options) {
    288    if (this.destroying) {
    289      return false;
    290    }
    291    this.destroying = true;
    292 
    293    // If our tab is about to be closed, there's not enough time to exit
    294    // gracefully, but that shouldn't be a problem since the tab will go away.
    295    // So, skip any waiting when we're about to close the tab.
    296    const isTabDestroyed = !this.tab.linkedBrowser;
    297    const isWindowClosing = options?.reason === "unload" || isTabDestroyed;
    298    const isTabContentDestroying =
    299      isWindowClosing || options?.reason === "TabClose";
    300 
    301    // Ensure init has finished before starting destroy
    302    if (!isTabContentDestroying) {
    303      await this.initialized;
    304 
    305      // Restore screen orientation of physical device.
    306      await Promise.all([
    307        this.updateScreenOrientation("landscape-primary", 0),
    308        this.updateMaxTouchPointsEnabled(false),
    309      ]);
    310 
    311      // Hide browser UI to avoid displaying weird intermediate states while closing.
    312      this.hideBrowserUI();
    313 
    314      // Resseting the throtting needs to be done before the
    315      // network events watching is stopped.
    316      await this.updateNetworkThrottling();
    317    }
    318 
    319    this.tab.removeEventListener("TabClose", this);
    320    this.browserWindow.removeEventListener("unload", this);
    321    this.tab.linkedBrowser.leaveResponsiveMode();
    322 
    323    this.browserWindow.removeEventListener("FullZoomChange", this);
    324    this.rdmFrame.contentWindow.removeEventListener("message", this);
    325 
    326    // Remove observers on the stack.
    327    this.resizeToolbarObserver.unobserve(this.browserStackEl);
    328 
    329    // Cleanup the frame content before disconnecting the frame element.
    330    this.rdmFrame.contentWindow.destroy();
    331 
    332    this.rdmFrame.remove();
    333 
    334    // Clean up resize handlers
    335    this.resizeHandle.remove();
    336    this.resizeHandleX.remove();
    337    this.resizeHandleY.remove();
    338    this.dynamicToolbar.remove();
    339 
    340    this.browserContainerEl.classList.remove("responsive-mode");
    341    this.browserStackEl.style.removeProperty("--rdm-width");
    342    this.browserStackEl.style.removeProperty("--rdm-height");
    343    this.browserStackEl.style.removeProperty("--rdm-zoom");
    344 
    345    // Ensure the tab is reloaded if required when exiting RDM so that no emulated
    346    // settings are left in a customized state.
    347    if (!isTabContentDestroying) {
    348      let reloadNeeded = false;
    349      await this.updateDPPX(null);
    350      reloadNeeded |=
    351        (await this.updateUserAgent()) && this.reloadOnChange("userAgent");
    352 
    353      // Don't reload on the server if we're already doing a reload on the client
    354      const reloadOnTouchSimulationChange =
    355        this.reloadOnChange("touchSimulation") && !reloadNeeded;
    356      await this.updateTouchSimulation(null, reloadOnTouchSimulationChange);
    357      if (reloadNeeded) {
    358        await this.reloadBrowser();
    359      }
    360 
    361      // Unwatch targets & resources as the last step. If we are not waching for
    362      // any resource & target anymore, the JSWindowActors will be unregistered
    363      // which will trigger an early destruction of the RDM target, before we
    364      // could finalize the cleanup.
    365      this.commands.targetCommand.unwatchTargets({
    366        types: [this.commands.targetCommand.TYPES.FRAME],
    367        onAvailable: this.onTargetAvailable,
    368      });
    369 
    370      this.resourceCommand.unwatchResources(
    371        [this.resourceCommand.TYPES.NETWORK_EVENT],
    372        { onAvailable: this.onNetworkResourceAvailable }
    373      );
    374 
    375      this.commands.targetCommand.destroy();
    376    }
    377 
    378    // Show the browser UI now.
    379    this.showBrowserUI();
    380 
    381    // Destroy local state
    382    this.browserContainerEl = null;
    383    this.browserStackEl = null;
    384    this.browserWindow = null;
    385    this.tab = null;
    386    this.initialized = null;
    387    this.rdmFrame = null;
    388    this.resizeHandle = null;
    389    this.resizeHandleX = null;
    390    this.resizeHandleY = null;
    391    this.dynamicToolbar = null;
    392    this.resizeToolbarObserver = null;
    393 
    394    // Destroying the commands will close the devtools client used to speak with responsive emulation actor.
    395    // The actor handles clearing any overrides itself, so it's not necessary to clear
    396    // anything on shutdown client side.
    397    const commandsDestroyed = this.commands.destroy();
    398    if (!isTabContentDestroying) {
    399      await commandsDestroyed;
    400    }
    401    this.commands = null;
    402    this.destroyed = true;
    403 
    404    return true;
    405  }
    406 
    407  async connectToServer() {
    408    this.commands = await CommandsFactory.forTab(this.tab);
    409    this.resourceCommand = this.commands.resourceCommand;
    410 
    411    await this.commands.targetCommand.startListening();
    412 
    413    await this.commands.targetCommand.watchTargets({
    414      types: [this.commands.targetCommand.TYPES.FRAME],
    415      onAvailable: this.onTargetAvailable,
    416    });
    417 
    418    // To support network throttling the resource command
    419    // needs to be watching for network resources.
    420    await this.resourceCommand.watchResources(
    421      [this.resourceCommand.TYPES.NETWORK_EVENT],
    422      { onAvailable: this.onNetworkResourceAvailable }
    423    );
    424 
    425    this.networkFront = await this.watcherFront.getNetworkParentActor();
    426  }
    427 
    428  /**
    429   * Show one-time notification about reloads for responsive emulation.
    430   */
    431  showReloadNotification() {
    432    if (Services.prefs.getBoolPref(RELOAD_NOTIFICATION_PREF, false)) {
    433      showNotification(this.browserWindow, this.tab, {
    434        msg: l10n.getFormatStr("responsive.reloadNotification.description2"),
    435      });
    436      Services.prefs.setBoolPref(RELOAD_NOTIFICATION_PREF, false);
    437    }
    438  }
    439 
    440  reloadOnChange(id) {
    441    this.showReloadNotification();
    442    const pref = RELOAD_CONDITION_PREF_PREFIX + id;
    443    return Services.prefs.getBoolPref(pref, false);
    444  }
    445 
    446  hideBrowserUI() {
    447    this.tab.linkedBrowser.style.visibility = "hidden";
    448    this.resizeHandle.style.visibility = "hidden";
    449  }
    450 
    451  showBrowserUI() {
    452    this.tab.linkedBrowser.style.removeProperty("visibility");
    453    this.resizeHandle.style.removeProperty("visibility");
    454  }
    455 
    456  handleEvent(event) {
    457    const { browserWindow, tab } = this;
    458 
    459    switch (event.type) {
    460      case "message":
    461        this.handleMessage(event);
    462        break;
    463      case "FullZoomChange": {
    464        // Get the current device size and update to that size, which
    465        // will pick up changes to the zoom.
    466        const { width, height } = this.getViewportSize();
    467        this.updateViewportSize(width, height);
    468        break;
    469      }
    470      case "TabClose":
    471      case "unload":
    472        this.manager.closeIfNeeded(browserWindow, tab, {
    473          reason: event.type,
    474        });
    475        break;
    476    }
    477  }
    478 
    479  handleMessage(event) {
    480    if (event.origin !== "chrome://devtools") {
    481      return;
    482    }
    483 
    484    switch (event.data.type) {
    485      case "change-device":
    486        this.onChangeDevice(event);
    487        break;
    488      case "change-network-throttling":
    489        this.onChangeNetworkThrottling(event);
    490        break;
    491      case "change-pixel-ratio":
    492        this.onChangePixelRatio(event);
    493        break;
    494      case "change-touch-simulation":
    495        this.onChangeTouchSimulation(event);
    496        break;
    497      case "change-user-agent":
    498        this.onChangeUserAgent(event);
    499        break;
    500      case "exit":
    501        this.onExit();
    502        break;
    503      case "remove-device-association":
    504        this.onRemoveDeviceAssociation(event);
    505        break;
    506      case "viewport-orientation-change":
    507        this.onRotateViewport(event);
    508        break;
    509      case "viewport-resize":
    510        this.onResizeViewport(event);
    511        break;
    512      case "screenshot":
    513        this.onScreenshot();
    514        break;
    515      case "toggle-left-alignment":
    516        this.onToggleLeftAlignment(event);
    517        break;
    518      case "update-device-modal":
    519        this.onUpdateDeviceModal(event);
    520        break;
    521    }
    522  }
    523 
    524  async onChangeDevice(event) {
    525    const { pixelRatio, touch, userAgent } = event.data.device;
    526    let reloadNeeded = false;
    527    await this.updateDPPX(pixelRatio);
    528 
    529    // Get the orientation values of the device we are changing to and update.
    530    const { device, viewport } = event.data;
    531    const { type, angle } = getOrientation(device, viewport);
    532    await this.updateScreenOrientation(type, angle);
    533    await this.updateMaxTouchPointsEnabled(touch);
    534 
    535    reloadNeeded |=
    536      (await this.updateUserAgent(userAgent)) &&
    537      this.reloadOnChange("userAgent");
    538 
    539    // Don't reload on the server if we're already doing a reload on the client
    540    const reloadOnTouchSimulationChange =
    541      this.reloadOnChange("touchSimulation") && !reloadNeeded;
    542    await this.updateTouchSimulation(touch, reloadOnTouchSimulationChange);
    543 
    544    if (reloadNeeded) {
    545      this.reloadBrowser();
    546    }
    547 
    548    // Used by tests
    549    this.emitForTests("device-changed", {
    550      reloadTriggered: reloadNeeded || reloadOnTouchSimulationChange,
    551    });
    552  }
    553 
    554  async onChangeNetworkThrottling(event) {
    555    const { enabled, profile } = event.data;
    556    await this.updateNetworkThrottling(enabled, profile);
    557    // Used by tests
    558    this.emit("network-throttling-changed");
    559  }
    560 
    561  onChangePixelRatio(event) {
    562    const { pixelRatio } = event.data;
    563    this.updateDPPX(pixelRatio);
    564  }
    565 
    566  async onChangeTouchSimulation(event) {
    567    const { enabled } = event.data;
    568 
    569    await this.updateMaxTouchPointsEnabled(enabled);
    570 
    571    await this.updateTouchSimulation(
    572      enabled,
    573      this.reloadOnChange("touchSimulation")
    574    );
    575 
    576    // Used by tests
    577    this.emit("touch-simulation-changed");
    578  }
    579 
    580  async onChangeUserAgent(event) {
    581    const { userAgent } = event.data;
    582    const reloadNeeded =
    583      (await this.updateUserAgent(userAgent)) &&
    584      this.reloadOnChange("userAgent");
    585    if (reloadNeeded) {
    586      this.reloadBrowser();
    587    }
    588    this.emit("user-agent-changed");
    589  }
    590 
    591  onExit() {
    592    const { browserWindow, tab } = this;
    593    this.manager.closeIfNeeded(browserWindow, tab);
    594  }
    595 
    596  async onRemoveDeviceAssociation(event) {
    597    const { resetProfile } = event.data;
    598 
    599    if (resetProfile) {
    600      let reloadNeeded = false;
    601      await this.updateDPPX(null);
    602      reloadNeeded |=
    603        (await this.updateUserAgent()) && this.reloadOnChange("userAgent");
    604 
    605      // Don't reload on the server if we're already doing a reload on the client
    606      const reloadOnTouchSimulationChange =
    607        this.reloadOnChange("touchSimulation") && !reloadNeeded;
    608      await this.updateTouchSimulation(null, reloadOnTouchSimulationChange);
    609      if (reloadNeeded) {
    610        this.reloadBrowser();
    611      }
    612    }
    613 
    614    // Used by tests
    615    this.emitForTests("device-association-removed");
    616  }
    617 
    618  /**
    619   * Resizing the browser on mousemove
    620   */
    621  onResizeDrag({ screenX, screenY }) {
    622    if (!this.isResizing || !this.rdmFrame.contentWindow) {
    623      return;
    624    }
    625 
    626    const zoom = this.tab.linkedBrowser.fullZoom;
    627 
    628    let deltaX = (screenX - this.lastScreenX) / zoom;
    629    let deltaY = (screenY - this.lastScreenY) / zoom;
    630 
    631    const leftAlignmentEnabled = Services.prefs.getBoolPref(
    632      "devtools.responsive.leftAlignViewport.enabled",
    633      false
    634    );
    635 
    636    if (!leftAlignmentEnabled) {
    637      // The viewport is centered horizontally, so horizontal resize resizes
    638      // by twice the distance the mouse was dragged - on left and right side.
    639      deltaX = deltaX * 2;
    640    }
    641 
    642    if (this.ignoreX) {
    643      deltaX = 0;
    644    }
    645    if (this.ignoreY) {
    646      deltaY = 0;
    647    }
    648 
    649    const viewportSize = this.getViewportSize();
    650 
    651    let width = Math.round(viewportSize.width + deltaX);
    652    let height = Math.round(viewportSize.height + deltaY);
    653 
    654    if (width < Constants.MIN_VIEWPORT_DIMENSION) {
    655      width = Constants.MIN_VIEWPORT_DIMENSION;
    656    } else if (width != viewportSize.width) {
    657      this.lastScreenX = screenX;
    658    }
    659 
    660    if (height < Constants.MIN_VIEWPORT_DIMENSION) {
    661      height = Constants.MIN_VIEWPORT_DIMENSION;
    662    } else if (height != viewportSize.height) {
    663      this.lastScreenY = screenY;
    664    }
    665 
    666    // Update the RDM store and viewport size with the new width and height.
    667    this.rdmFrame.contentWindow.setViewportSize({ width, height });
    668    this.updateViewportSize(width, height);
    669 
    670    // Change the device selector back to an unselected device
    671    if (this.rdmFrame.contentWindow.getAssociatedDevice()) {
    672      this.rdmFrame.contentWindow.clearDeviceAssociation();
    673    }
    674  }
    675 
    676  /**
    677   * Start the process of resizing the browser.
    678   */
    679  onResizeStart({ target, screenX, screenY }) {
    680    this.browserWindow.addEventListener("mousemove", this.onResizeDrag, true);
    681    this.browserWindow.addEventListener("mouseup", this.onResizeStop, true);
    682 
    683    this.isResizing = true;
    684    this.lastScreenX = screenX;
    685    this.lastScreenY = screenY;
    686    this.ignoreX = target === this.resizeHandleY;
    687    this.ignoreY = target === this.resizeHandleX;
    688  }
    689 
    690  /**
    691   * Stop the process of resizing the browser.
    692   */
    693  onResizeStop() {
    694    this.browserWindow.removeEventListener(
    695      "mousemove",
    696      this.onResizeDrag,
    697      true
    698    );
    699    this.browserWindow.removeEventListener("mouseup", this.onResizeStop, true);
    700 
    701    this.isResizing = false;
    702    this.lastScreenX = 0;
    703    this.lastScreenY = 0;
    704    this.ignoreX = false;
    705    this.ignoreY = false;
    706 
    707    // Used by tests.
    708    this.emit("viewport-resize-dragend");
    709  }
    710 
    711  onResizeViewport(event) {
    712    const { width, height } = event.data;
    713    this.updateViewportSize(width, height);
    714    this.emit("viewport-resize", {
    715      width,
    716      height,
    717    });
    718  }
    719 
    720  async onRotateViewport(event) {
    721    const { orientationType: type, angle } = event.data;
    722    await this.updateScreenOrientation(type, angle);
    723  }
    724 
    725  async onScreenshot() {
    726    const messages = await captureAndSaveScreenshot(
    727      this.currentTarget,
    728      this.browserWindow
    729    );
    730 
    731    const priorityMap = {
    732      error: PriorityLevels.PRIORITY_CRITICAL_HIGH,
    733      warn: PriorityLevels.PRIORITY_WARNING_HIGH,
    734    };
    735    for (const { text, level } of messages) {
    736      // captureAndSaveScreenshot returns "saved" messages, that indicate where the
    737      // screenshot was saved. We don't want to display them as the download UI can be
    738      // used to open the file.
    739      if (level !== "warn" && level !== "error") {
    740        continue;
    741      }
    742 
    743      showNotification(this.browserWindow, this.tab, {
    744        msg: text,
    745        priority: priorityMap[level],
    746      });
    747    }
    748 
    749    message.post(this.rdmFrame.contentWindow, "screenshot-captured");
    750  }
    751 
    752  onToggleLeftAlignment(event) {
    753    this.updateUIAlignment(event.data.leftAlignmentEnabled);
    754  }
    755 
    756  onUpdateDeviceModal(event) {
    757    this.rdmFrame.classList.toggle("device-modal-opened", event.data.isOpen);
    758  }
    759 
    760  async hasDeviceState() {
    761    const deviceState = await asyncStorage.getItem(
    762      "devtools.responsive.deviceState"
    763    );
    764    return !!deviceState;
    765  }
    766 
    767  /**
    768   * Restores the previous UI state.
    769   */
    770  async restoreUIState() {
    771    const leftAlignmentEnabled = Services.prefs.getBoolPref(
    772      "devtools.responsive.leftAlignViewport.enabled",
    773      false
    774    );
    775 
    776    this.updateUIAlignment(leftAlignmentEnabled);
    777 
    778    const height = Services.prefs.getIntPref(
    779      "devtools.responsive.viewport.height",
    780      0
    781    );
    782    const width = Services.prefs.getIntPref(
    783      "devtools.responsive.viewport.width",
    784      0
    785    );
    786    this.updateViewportSize(width, height);
    787  }
    788 
    789  /**
    790   * Restores the previous actor state.
    791   *
    792   * @param {boolean} isTargetSwitching
    793   */
    794  async restoreActorState(isTargetSwitching) {
    795    // It's possible the target will switch to a page loaded in the
    796    // parent-process (i.e: about:robots). When this happens, the values set
    797    // on the BrowsingContext by RDM are not preserved. So we need to call
    798    // enterResponsiveMode whenever there is a target switch.
    799    this.tab.linkedBrowser.enterResponsiveMode();
    800 
    801    // If the target follows the window global lifecycle, the configuration was already
    802    // restored from the server during target switch, so we can stop here.
    803    // This function is still called at startup to restore potential state from previous
    804    // RDM session so we only stop here during target switching.
    805    if (
    806      isTargetSwitching &&
    807      this.commands.targetCommand.targetFront.targetForm
    808        .followWindowGlobalLifeCycle
    809    ) {
    810      return;
    811    }
    812 
    813    const hasDeviceState = await this.hasDeviceState();
    814    if (hasDeviceState) {
    815      // Return if there is a device state to restore, this will be done when the
    816      // device list is loaded after the post-init.
    817      return;
    818    }
    819 
    820    const height = Services.prefs.getIntPref(
    821      "devtools.responsive.viewport.height",
    822      0
    823    );
    824    const pixelRatio = Services.prefs.getIntPref(
    825      "devtools.responsive.viewport.pixelRatio",
    826      0
    827    );
    828    const touchSimulationEnabled = Services.prefs.getBoolPref(
    829      "devtools.responsive.touchSimulation.enabled",
    830      false
    831    );
    832    const userAgent = Services.prefs.getCharPref(
    833      "devtools.responsive.userAgent",
    834      ""
    835    );
    836    const width = Services.prefs.getIntPref(
    837      "devtools.responsive.viewport.width",
    838      0
    839    );
    840 
    841    // Restore the previously set orientation, or get it from the initial viewport if it
    842    // wasn't set yet.
    843    const { type, angle } =
    844      this.commands.targetConfigurationCommand.configuration
    845        .rdmPaneOrientation ||
    846      this.getInitialViewportOrientation({
    847        width,
    848        height,
    849      });
    850 
    851    await this.updateDPPX(pixelRatio);
    852    await this.updateScreenOrientation(type, angle);
    853    await this.updateMaxTouchPointsEnabled(touchSimulationEnabled);
    854 
    855    if (touchSimulationEnabled) {
    856      await this.updateTouchSimulation(touchSimulationEnabled);
    857    }
    858 
    859    let reloadNeeded = false;
    860    if (userAgent) {
    861      reloadNeeded |=
    862        (await this.updateUserAgent(userAgent)) &&
    863        this.reloadOnChange("userAgent");
    864    }
    865    if (reloadNeeded) {
    866      await this.reloadBrowser();
    867    }
    868  }
    869 
    870  /**
    871   * Set or clear the emulated device pixel ratio.
    872   *
    873   * @param {number | null} dppx: The ratio to simulate. Set to null to disable the
    874   *                      simulation and roll back to the original ratio
    875   */
    876  async updateDPPX(dppx = null) {
    877    await this.commands.targetConfigurationCommand.updateConfiguration({
    878      overrideDPPX: dppx,
    879    });
    880  }
    881 
    882  /**
    883   * Set or clear network throttling.
    884   *
    885   * @return boolean
    886   *         Whether a reload is needed to apply the change.
    887   *         (This is always immediate, so it's always false.)
    888   */
    889  async updateNetworkThrottling(enabled, profile) {
    890    if (!enabled) {
    891      await this.networkFront.clearNetworkThrottling();
    892      await this.commands.targetConfigurationCommand.updateConfiguration({
    893        setTabOffline: false,
    894      });
    895      return false;
    896    }
    897    const data = throttlingProfiles.profiles.find(({ id }) => id == profile);
    898    const { download, upload, latency, id } = data;
    899 
    900    // Update offline mode
    901    await this.commands.targetConfigurationCommand.updateConfiguration({
    902      setTabOffline: id === throttlingProfiles.PROFILE_CONSTANTS.OFFLINE,
    903    });
    904 
    905    await this.networkFront.setNetworkThrottling({
    906      downloadThroughput: download,
    907      uploadThroughput: upload,
    908      latency,
    909    });
    910    return false;
    911  }
    912 
    913  /**
    914   * Set or clear the emulated user agent.
    915   *
    916   * @param {string | null} userAgent: The user agent to set on the page. Set to null to revert
    917   *                      the user agent to its original value
    918   * @return {boolean} Whether a reload is needed to apply the change.
    919   */
    920  async updateUserAgent(userAgent) {
    921    const getConfigurationCustomUserAgent = () =>
    922      this.commands.targetConfigurationCommand.configuration.customUserAgent ||
    923      "";
    924    const previousCustomUserAgent = getConfigurationCustomUserAgent();
    925    await this.commands.targetConfigurationCommand.updateConfiguration({
    926      customUserAgent: userAgent,
    927    });
    928 
    929    const updatedUserAgent = getConfigurationCustomUserAgent();
    930    return previousCustomUserAgent !== updatedUserAgent;
    931  }
    932 
    933  /**
    934   * Set or clear touch simulation. When setting to true, this method will
    935   * additionally set meta viewport override.
    936   * When setting to false, this method will clear all touch simulation and meta viewport
    937   * overrides, returning to default behavior for both settings.
    938   *
    939   * @param {boolean} enabled
    940   * @param {boolean} reloadOnTouchSimulationToggle: Set to true to trigger a page reload
    941   *        if the touch simulation state changes.
    942   */
    943  async updateTouchSimulation(enabled, reloadOnTouchSimulationToggle) {
    944    await this.commands.targetConfigurationCommand.updateConfiguration({
    945      touchEventsOverride: enabled ? "enabled" : null,
    946      reloadOnTouchSimulationToggle,
    947    });
    948  }
    949 
    950  /**
    951   * Sets the screen orientation values of the simulated device.
    952   *
    953   * @param {string} type
    954   *        The orientation type to update the current device screen to.
    955   * @param {number} angle
    956   *        The rotation angle to update the current device screen to.
    957   */
    958  async updateScreenOrientation(type, angle) {
    959    // We need to call the method on the parent process
    960    await this.commands.targetConfigurationCommand.updateConfiguration({
    961      rdmPaneOrientation: { type, angle },
    962    });
    963  }
    964 
    965  /**
    966   * Sets whether or not maximum touch points are supported for the simulated device.
    967   *
    968   * @param {boolean} touchSimulationEnabled
    969   *        Whether or not touch is enabled for the simulated device.
    970   */
    971  async updateMaxTouchPointsEnabled(touchSimulationEnabled) {
    972    return this.commands.targetConfigurationCommand.updateConfiguration({
    973      rdmPaneMaxTouchPoints: touchSimulationEnabled ? 1 : 0,
    974    });
    975  }
    976 
    977  /**
    978   * Sets whether or not the RDM UI should be left-aligned.
    979   *
    980   * @param {boolean} leftAlignmentEnabled
    981   *        Whether or not the UI is left-aligned.
    982   */
    983  updateUIAlignment(leftAlignmentEnabled) {
    984    this.browserContainerEl.classList.toggle(
    985      "left-aligned",
    986      leftAlignmentEnabled
    987    );
    988  }
    989 
    990  /**
    991   * Sets the browser element to be the given width and height.
    992   *
    993   * @param {number} width
    994   *        The viewport's width.
    995   * @param {number} height
    996   *        The viewport's height.
    997   */
    998  updateViewportSize(width, height) {
    999    const zoom = this.tab.linkedBrowser.fullZoom;
   1000 
   1001    // Setting this with a variable on the stack instead of directly as width/height
   1002    // on the <browser> because we'll need to use this for the alert dialog as well.
   1003    this.browserStackEl.style.setProperty("--rdm-width", `${width}px`);
   1004    this.browserStackEl.style.setProperty("--rdm-height", `${height}px`);
   1005    this.browserStackEl.style.setProperty("--rdm-zoom", zoom);
   1006 
   1007    // This is a bit premature, but we emit a content-resize event here. It
   1008    // would be preferrable to wait until the viewport is actually resized,
   1009    // but the "resize" event is not triggered by this style change. The
   1010    // content-resize message is only used by tests, and if needed those tests
   1011    // can use the testing function setViewportSizeAndAwaitReflow to ensure
   1012    // the viewport has had time to reach this size.
   1013    this.emit("content-resize", {
   1014      width,
   1015      height,
   1016    });
   1017  }
   1018 
   1019  /**
   1020   * Helper for tests. Assumes a single viewport for now.
   1021   */
   1022  getViewportSize() {
   1023    // The getViewportSize function is loaded in index.js, and might not be
   1024    // available yet.
   1025    if (this.toolWindow.getViewportSize) {
   1026      return this.toolWindow.getViewportSize();
   1027    }
   1028 
   1029    return { width: 0, height: 0 };
   1030  }
   1031 
   1032  /**
   1033   * Helper for tests, etc. Assumes a single viewport for now.
   1034   */
   1035  async setViewportSize(size) {
   1036    await this.initialized;
   1037 
   1038    // Ensure that width and height are valid.
   1039    let { width, height } = size;
   1040    if (!size.width) {
   1041      width = this.getViewportSize().width;
   1042    }
   1043 
   1044    if (!size.height) {
   1045      height = this.getViewportSize().height;
   1046    }
   1047 
   1048    this.rdmFrame.contentWindow.setViewportSize({ width, height });
   1049    this.updateViewportSize(width, height);
   1050  }
   1051 
   1052  /**
   1053   * Helper for tests/reloading the viewport. Assumes a single viewport for now.
   1054   */
   1055  getViewportBrowser() {
   1056    return this.tab.linkedBrowser;
   1057  }
   1058 
   1059  /**
   1060   * Helper for contacting the viewport content. Assumes a single viewport for now.
   1061   */
   1062  getViewportMessageManager() {
   1063    return this.getViewportBrowser().messageManager;
   1064  }
   1065 
   1066  /**
   1067   * Helper for getting the initial viewport orientation.
   1068   */
   1069  getInitialViewportOrientation(viewport) {
   1070    return getOrientation(viewport, viewport);
   1071  }
   1072 
   1073  /**
   1074   * Helper for tests to get the browser's window.
   1075   */
   1076  getBrowserWindow() {
   1077    return this.browserWindow;
   1078  }
   1079 
   1080  clamp(min, max, value) {
   1081    return Math.min(Math.max(value, min), max);
   1082  }
   1083 
   1084  onContentScrolled(deltaY) {
   1085    const currentHeight = parseInt(this.dynamicToolbar.style.height, 10);
   1086    const newHeight = this.clamp(
   1087      0,
   1088      DYNAMIC_TOOLBAR_MAX_HEIGHT,
   1089      currentHeight + deltaY
   1090    );
   1091    this.dynamicToolbar.style.height = newHeight + "px";
   1092    const offset = newHeight - DYNAMIC_TOOLBAR_MAX_HEIGHT;
   1093    InspectorUtils.setVerticalClipping(
   1094      this.tab.linkedBrowser.browsingContext,
   1095      offset
   1096    );
   1097  }
   1098 
   1099  async onTargetAvailable({ targetFront, isTargetSwitching }) {
   1100    if (this.destroying) {
   1101      return;
   1102    }
   1103 
   1104    if (targetFront.isTopLevel) {
   1105      await this.restoreActorState(isTargetSwitching);
   1106      this.emitForTests("responsive-ui-target-switch-done");
   1107    }
   1108 
   1109    if (Services.prefs.getBoolPref(USE_DYNAMIC_TOOLBAR_PREF)) {
   1110      targetFront.on("contentScrolled", this.onContentScrolled);
   1111    }
   1112  }
   1113 
   1114  async setElementPickerState(state, pickerType) {
   1115    this.commands.responsiveCommand.setElementPickerState(state, pickerType);
   1116  }
   1117 
   1118  // This just needed to setup watching for network resources,
   1119  // to support network throttling.
   1120  onNetworkResourceAvailable() {}
   1121 
   1122  /**
   1123   * Reload the current tab.
   1124   */
   1125  async reloadBrowser() {
   1126    await this.commands.targetCommand.reloadTopLevelTarget();
   1127  }
   1128 }
   1129 
   1130 module.exports = ResponsiveUI;