/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const EventEmitter = require("resource://devtools/shared/event-emitter.js"); const { getOrientation, } = require("resource://devtools/client/responsive/utils/orientation.js"); const Constants = require("resource://devtools/client/responsive/constants.js"); const { CommandsFactory, } = require("resource://devtools/shared/commands/commands-factory.js"); loader.lazyRequireGetter( this, "throttlingProfiles", "resource://devtools/client/shared/components/throttling/profiles.js" ); loader.lazyRequireGetter( this, "message", "resource://devtools/client/responsive/utils/message.js" ); loader.lazyRequireGetter( this, "showNotification", "resource://devtools/client/responsive/utils/notification.js", true ); loader.lazyRequireGetter( this, "PriorityLevels", "resource://devtools/client/shared/components/NotificationBox.js", true ); loader.lazyRequireGetter( this, "l10n", "resource://devtools/client/responsive/utils/l10n.js" ); loader.lazyRequireGetter( this, "asyncStorage", "resource://devtools/shared/async-storage.js" ); loader.lazyRequireGetter( this, "captureAndSaveScreenshot", "resource://devtools/client/shared/screenshot.js", true ); const RELOAD_CONDITION_PREF_PREFIX = "devtools.responsive.reloadConditions."; const RELOAD_NOTIFICATION_PREF = "devtools.responsive.reloadNotification.enabled"; function debug(msg) { // console.log(`RDM manager: ${msg}`); } /** * ResponsiveUI manages the responsive design tool for a specific tab. The * actual tool itself lives in a separate chrome:// document that is loaded into * the tab upon opening responsive design. This object acts a helper to * integrate the tool into the surrounding browser UI as needed. */ class ResponsiveUI { /** * @param {ResponsiveUIManager} manager * The ResponsiveUIManager instance. * @param {ChromeWindow} window * The main browser chrome window (that holds many tabs). * @param {Tab} tab * The specific browser element this responsive instance is for. */ constructor(manager, window, tab) { this.manager = manager; // The main browser chrome window (that holds many tabs). this.browserWindow = window; // The specific browser tab this responsive instance is for. this.tab = tab; // Flag set when destruction has begun. this.destroying = false; // Flag set when destruction has ended. this.destroyed = false; // The iframe containing the RDM UI. this.rdmFrame = null; // Bind callbacks for resizers. this.onResizeDrag = this.onResizeDrag.bind(this); this.onResizeStart = this.onResizeStart.bind(this); this.onResizeStop = this.onResizeStop.bind(this); this.onTargetAvailable = this.onTargetAvailable.bind(this); this.networkFront = null; // Promise resovled when the UI init has completed. this.inited = this.init(); EventEmitter.decorate(this); } get toolWindow() { return this.rdmFrame.contentWindow; } get docShell() { return this.toolWindow.docShell; } get viewportElement() { return this.browserStackEl.querySelector("browser"); } get currentTarget() { return this.commands.targetCommand.targetFront; } get watcherFront() { return this.resourceCommand.watcherFront; } /** * Open RDM while preserving the state of the page. */ async init() { debug("Init start"); this.initRDMFrame(); // Hide the browser content temporarily while things move around to avoid displaying // strange intermediate states. this.hideBrowserUI(); // Watch for tab close and window close so we can clean up RDM synchronously this.tab.addEventListener("TabClose", this); this.browserWindow.addEventListener("unload", this); this.rdmFrame.contentWindow.addEventListener("message", this); this.tab.linkedBrowser.enterResponsiveMode(); // Listen to FullZoomChange events coming from the browser window, // so that we can zoom the size of the viewport by the same amount. this.browserWindow.addEventListener("FullZoomChange", this); // Get the protocol ready to speak with responsive emulation actor debug("Wait until RDP server connect"); await this.connectToServer(); // Restore the previous UI state. await this.restoreUIState(); // Show the browser UI now that its state is ready. this.showBrowserUI(); // Non-blocking message to tool UI to start any delayed init activities message.post(this.toolWindow, "post-init"); debug("Init done"); } /** * Initialize the RDM iframe inside of the browser document. */ initRDMFrame() { const { document: doc, gBrowser } = this.browserWindow; const rdmFrame = doc.createElement("iframe"); rdmFrame.src = "chrome://devtools/content/responsive/toolbar.xhtml"; rdmFrame.classList.add("rdm-toolbar"); // Create resizer handlers const resizeHandle = doc.createElement("div"); resizeHandle.classList.add( "rdm-viewport-resize-handle", "viewport-resize-handle" ); const resizeHandleX = doc.createElement("div"); resizeHandleX.classList.add( "rdm-viewport-resize-handle", "viewport-horizontal-resize-handle" ); const resizeHandleY = doc.createElement("div"); resizeHandleY.classList.add( "rdm-viewport-resize-handle", "viewport-vertical-resize-handle" ); this.browserContainerEl = gBrowser.getBrowserContainer( gBrowser.getBrowserForTab(this.tab) ); this.browserStackEl = this.browserContainerEl.querySelector(".browserStack"); this.browserContainerEl.classList.add("responsive-mode"); // Prepend the RDM iframe inside of the current tab's browser container. this.browserContainerEl.prepend(rdmFrame); this.browserStackEl.append(resizeHandle); this.browserStackEl.append(resizeHandleX); this.browserStackEl.append(resizeHandleY); // Wait for the frame script to be loaded. message.wait(rdmFrame.contentWindow, "script-init").then(async () => { // Notify the frame window that the Resposnive UI manager has begun initializing. // At this point, we can render our React content inside the frame. message.post(rdmFrame.contentWindow, "init"); // Wait for the tools to be rendered above the content. The frame script will // then dispatch the necessary actions to the Redux store to give the toolbar the // state it needs. message.wait(rdmFrame.contentWindow, "init:done").then(() => { rdmFrame.contentWindow.addInitialViewport({ userContextId: this.tab.userContextId, }); }); }); this.rdmFrame = rdmFrame; this.resizeHandle = resizeHandle; this.resizeHandle.addEventListener("mousedown", this.onResizeStart); this.resizeHandleX = resizeHandleX; this.resizeHandleX.addEventListener("mousedown", this.onResizeStart); this.resizeHandleY = resizeHandleY; this.resizeHandleY.addEventListener("mousedown", this.onResizeStart); this.resizeToolbarObserver = new this.browserWindow.ResizeObserver( entries => { for (const entry of entries) { // If the toolbar needs extra space for the UA input, then set a class // that will accomodate its height. We should also make sure to keep // the width value we're toggling against in sync with the media-query // in devtools/client/responsive/index.css this.rdmFrame.classList.toggle( "accomodate-ua", entry.contentBoxSize[0].inlineSize < 520 ); } } ); this.resizeToolbarObserver.observe(this.browserStackEl); } /** * Close RDM and restore page content back into a regular tab. * * @param object * Destroy options, which currently includes a `reason` string. * @return boolean * Whether this call is actually destroying. False means destruction * was already in progress. */ async destroy(options) { if (this.destroying) { return false; } this.destroying = true; // If our tab is about to be closed, there's not enough time to exit // gracefully, but that shouldn't be a problem since the tab will go away. // So, skip any waiting when we're about to close the tab. const isTabDestroyed = !this.tab.linkedBrowser || this.responsiveFront.isDestroyed(); const isWindowClosing = options?.reason === "unload" || isTabDestroyed; const isTabContentDestroying = isWindowClosing || options?.reason === "TabClose"; // Ensure init has finished before starting destroy if (!isTabContentDestroying) { await this.inited; // Restore screen orientation of physical device. await Promise.all([ this.updateScreenOrientation("landscape-primary", 0), this.updateMaxTouchPointsEnabled(false), ]); // Hide browser UI to avoid displaying weird intermediate states while closing. this.hideBrowserUI(); // Resseting the throtting needs to be done before the // network events watching is stopped. await this.updateNetworkThrottling(); } this.tab.removeEventListener("TabClose", this); this.browserWindow.removeEventListener("unload", this); this.tab.linkedBrowser.leaveResponsiveMode(); this.browserWindow.removeEventListener("FullZoomChange", this); this.rdmFrame.contentWindow.removeEventListener("message", this); // Remove observers on the stack. this.resizeToolbarObserver.unobserve(this.browserStackEl); // Cleanup the frame content before disconnecting the frame element. this.rdmFrame.contentWindow.destroy(); this.rdmFrame.remove(); // Clean up resize handlers this.resizeHandle.remove(); this.resizeHandleX.remove(); this.resizeHandleY.remove(); this.browserContainerEl.classList.remove("responsive-mode"); this.browserStackEl.style.removeProperty("--rdm-width"); this.browserStackEl.style.removeProperty("--rdm-height"); this.browserStackEl.style.removeProperty("--rdm-zoom"); // Ensure the tab is reloaded if required when exiting RDM so that no emulated // settings are left in a customized state. if (!isTabContentDestroying) { let reloadNeeded = false; await this.updateDPPX(null); reloadNeeded |= (await this.updateUserAgent()) && this.reloadOnChange("userAgent"); // Don't reload on the server if we're already doing a reload on the client const reloadOnTouchSimulationChange = this.reloadOnChange("touchSimulation") && !reloadNeeded; await this.updateTouchSimulation(null, reloadOnTouchSimulationChange); if (reloadNeeded) { await this.reloadBrowser(); } // Unwatch targets & resources as the last step. If we are not waching for // any resource & target anymore, the JSWindowActors will be unregistered // which will trigger an early destruction of the RDM target, before we // could finalize the cleanup. this.commands.targetCommand.unwatchTargets({ types: [this.commands.targetCommand.TYPES.FRAME], onAvailable: this.onTargetAvailable, }); this.resourceCommand.unwatchResources( [this.resourceCommand.TYPES.NETWORK_EVENT], { onAvailable: this.onNetworkResourceAvailable } ); this.commands.targetCommand.destroy(); } // Show the browser UI now. this.showBrowserUI(); // Destroy local state this.browserContainerEl = null; this.browserStackEl = null; this.browserWindow = null; this.tab = null; this.inited = null; this.rdmFrame = null; this.resizeHandle = null; this.resizeHandleX = null; this.resizeHandleY = null; this.resizeToolbarObserver = null; // Destroying the commands will close the devtools client used to speak with responsive emulation actor. // The actor handles clearing any overrides itself, so it's not necessary to clear // anything on shutdown client side. const commandsDestroyed = this.commands.destroy(); if (!isTabContentDestroying) { await commandsDestroyed; } this.commands = this.responsiveFront = null; this.destroyed = true; return true; } async connectToServer() { this.commands = await CommandsFactory.forTab(this.tab); this.resourceCommand = this.commands.resourceCommand; await this.commands.targetCommand.startListening(); await this.commands.targetCommand.watchTargets({ types: [this.commands.targetCommand.TYPES.FRAME], onAvailable: this.onTargetAvailable, }); // To support network throttling the resource command // needs to be watching for network resources. await this.resourceCommand.watchResources( [this.resourceCommand.TYPES.NETWORK_EVENT], { onAvailable: this.onNetworkResourceAvailable } ); this.networkFront = await this.watcherFront.getNetworkParentActor(); } /** * Show one-time notification about reloads for responsive emulation. */ showReloadNotification() { if (Services.prefs.getBoolPref(RELOAD_NOTIFICATION_PREF, false)) { showNotification(this.browserWindow, this.tab, { msg: l10n.getFormatStr("responsive.reloadNotification.description2"), }); Services.prefs.setBoolPref(RELOAD_NOTIFICATION_PREF, false); } } reloadOnChange(id) { this.showReloadNotification(); const pref = RELOAD_CONDITION_PREF_PREFIX + id; return Services.prefs.getBoolPref(pref, false); } hideBrowserUI() { this.tab.linkedBrowser.style.visibility = "hidden"; this.resizeHandle.style.visibility = "hidden"; } showBrowserUI() { this.tab.linkedBrowser.style.removeProperty("visibility"); this.resizeHandle.style.removeProperty("visibility"); } handleEvent(event) { const { browserWindow, tab } = this; switch (event.type) { case "message": this.handleMessage(event); break; case "FullZoomChange": // Get the current device size and update to that size, which // will pick up changes to the zoom. const { width, height } = this.getViewportSize(); this.updateViewportSize(width, height); break; case "TabClose": case "unload": this.manager.closeIfNeeded(browserWindow, tab, { reason: event.type, }); break; } } handleMessage(event) { if (event.origin !== "chrome://devtools") { return; } switch (event.data.type) { case "change-device": this.onChangeDevice(event); break; case "change-network-throttling": this.onChangeNetworkThrottling(event); break; case "change-pixel-ratio": this.onChangePixelRatio(event); break; case "change-touch-simulation": this.onChangeTouchSimulation(event); break; case "change-user-agent": this.onChangeUserAgent(event); break; case "exit": this.onExit(); break; case "remove-device-association": this.onRemoveDeviceAssociation(); break; case "viewport-orientation-change": this.onRotateViewport(event); break; case "viewport-resize": this.onResizeViewport(event); break; case "screenshot": this.onScreenshot(); break; case "toggle-left-alignment": this.onToggleLeftAlignment(event); break; case "update-device-modal": this.onUpdateDeviceModal(event); break; } } async onChangeDevice(event) { const { pixelRatio, touch, userAgent } = event.data.device; let reloadNeeded = false; await this.updateDPPX(pixelRatio); // Get the orientation values of the device we are changing to and update. const { device, viewport } = event.data; const { type, angle } = getOrientation(device, viewport); await this.updateScreenOrientation(type, angle); await this.updateMaxTouchPointsEnabled(touch); reloadNeeded |= (await this.updateUserAgent(userAgent)) && this.reloadOnChange("userAgent"); // Don't reload on the server if we're already doing a reload on the client const reloadOnTouchSimulationChange = this.reloadOnChange("touchSimulation") && !reloadNeeded; await this.updateTouchSimulation(touch, reloadOnTouchSimulationChange); if (reloadNeeded) { this.reloadBrowser(); } // Used by tests this.emitForTests("device-changed", { reloadTriggered: reloadNeeded || reloadOnTouchSimulationChange, }); } async onChangeNetworkThrottling(event) { const { enabled, profile } = event.data; await this.updateNetworkThrottling(enabled, profile); // Used by tests this.emit("network-throttling-changed"); } onChangePixelRatio(event) { const { pixelRatio } = event.data; this.updateDPPX(pixelRatio); } async onChangeTouchSimulation(event) { const { enabled } = event.data; await this.updateMaxTouchPointsEnabled(enabled); await this.updateTouchSimulation( enabled, this.reloadOnChange("touchSimulation") ); // Used by tests this.emit("touch-simulation-changed"); } async onChangeUserAgent(event) { const { userAgent } = event.data; const reloadNeeded = (await this.updateUserAgent(userAgent)) && this.reloadOnChange("userAgent"); if (reloadNeeded) { this.reloadBrowser(); } this.emit("user-agent-changed"); } onExit() { const { browserWindow, tab } = this; this.manager.closeIfNeeded(browserWindow, tab); } async onRemoveDeviceAssociation() { let reloadNeeded = false; await this.updateDPPX(null); reloadNeeded |= (await this.updateUserAgent()) && this.reloadOnChange("userAgent"); // Don't reload on the server if we're already doing a reload on the client const reloadOnTouchSimulationChange = this.reloadOnChange("touchSimulation") && !reloadNeeded; await this.updateTouchSimulation(null, reloadOnTouchSimulationChange); if (reloadNeeded) { this.reloadBrowser(); } // Used by tests this.emitForTests("device-association-removed", { reloadTriggered: reloadNeeded || reloadOnTouchSimulationChange, }); } /** * Resizing the browser on mousemove */ onResizeDrag({ screenX, screenY }) { if (!this.isResizing || !this.rdmFrame.contentWindow) { return; } const zoom = this.tab.linkedBrowser.fullZoom; let deltaX = (screenX - this.lastScreenX) / zoom; let deltaY = (screenY - this.lastScreenY) / zoom; const leftAlignmentEnabled = Services.prefs.getBoolPref( "devtools.responsive.leftAlignViewport.enabled", false ); if (!leftAlignmentEnabled) { // The viewport is centered horizontally, so horizontal resize resizes // by twice the distance the mouse was dragged - on left and right side. deltaX = deltaX * 2; } if (this.ignoreX) { deltaX = 0; } if (this.ignoreY) { deltaY = 0; } const viewportSize = this.getViewportSize(); let width = Math.round(viewportSize.width + deltaX); let height = Math.round(viewportSize.height + deltaY); if (width < Constants.MIN_VIEWPORT_DIMENSION) { width = Constants.MIN_VIEWPORT_DIMENSION; } else if (width != viewportSize.width) { this.lastScreenX = screenX; } if (height < Constants.MIN_VIEWPORT_DIMENSION) { height = Constants.MIN_VIEWPORT_DIMENSION; } else if (height != viewportSize.height) { this.lastScreenY = screenY; } // Update the RDM store and viewport size with the new width and height. this.rdmFrame.contentWindow.setViewportSize({ width, height }); this.updateViewportSize(width, height); // Change the device selector back to an unselected device if (this.rdmFrame.contentWindow.getAssociatedDevice()) { this.rdmFrame.contentWindow.clearDeviceAssociation(); } } /** * Start the process of resizing the browser. */ onResizeStart({ target, screenX, screenY }) { this.browserWindow.addEventListener("mousemove", this.onResizeDrag, true); this.browserWindow.addEventListener("mouseup", this.onResizeStop, true); this.isResizing = true; this.lastScreenX = screenX; this.lastScreenY = screenY; this.ignoreX = target === this.resizeHandleY; this.ignoreY = target === this.resizeHandleX; } /** * Stop the process of resizing the browser. */ onResizeStop() { this.browserWindow.removeEventListener( "mousemove", this.onResizeDrag, true ); this.browserWindow.removeEventListener("mouseup", this.onResizeStop, true); this.isResizing = false; this.lastScreenX = 0; this.lastScreenY = 0; this.ignoreX = false; this.ignoreY = false; // Used by tests. this.emit("viewport-resize-dragend"); } onResizeViewport(event) { const { width, height } = event.data; this.updateViewportSize(width, height); this.emit("viewport-resize", { width, height, }); } async onRotateViewport(event) { const { orientationType: type, angle, isViewportRotated } = event.data; await this.updateScreenOrientation(type, angle, isViewportRotated); } async onScreenshot() { const messages = await captureAndSaveScreenshot( this.currentTarget, this.browserWindow ); const priorityMap = { error: PriorityLevels.PRIORITY_CRITICAL_HIGH, warn: PriorityLevels.PRIORITY_WARNING_HIGH, }; for (const { text, level } of messages) { // captureAndSaveScreenshot returns "saved" messages, that indicate where the // screenshot was saved. We don't want to display them as the download UI can be // used to open the file. if (level !== "warn" && level !== "error") { continue; } showNotification(this.browserWindow, this.tab, { msg: text, priority: priorityMap[level], }); } message.post(this.rdmFrame.contentWindow, "screenshot-captured"); } onToggleLeftAlignment(event) { this.updateUIAlignment(event.data.leftAlignmentEnabled); } onUpdateDeviceModal(event) { this.rdmFrame.classList.toggle("device-modal-opened", event.data.isOpen); } async hasDeviceState() { const deviceState = await asyncStorage.getItem( "devtools.responsive.deviceState" ); return !!deviceState; } /** * Restores the previous UI state. */ async restoreUIState() { const leftAlignmentEnabled = Services.prefs.getBoolPref( "devtools.responsive.leftAlignViewport.enabled", false ); this.updateUIAlignment(leftAlignmentEnabled); const height = Services.prefs.getIntPref( "devtools.responsive.viewport.height", 0 ); const width = Services.prefs.getIntPref( "devtools.responsive.viewport.width", 0 ); this.updateViewportSize(width, height); } /** * Restores the previous actor state. * * @param {Boolean} isTargetSwitching */ async restoreActorState(isTargetSwitching) { // It's possible the target will switch to a page loaded in the // parent-process (i.e: about:robots). When this happens, the values set // on the BrowsingContext by RDM are not preserved. So we need to call // enterResponsiveMode whenever there is a target switch. this.tab.linkedBrowser.enterResponsiveMode(); // If the target follows the window global lifecycle, the configuration was already // restored from the server during target switch, so we can stop here. // This function is still called at startup to restore potential state from previous // RDM session so we only stop here during target switching. if ( isTargetSwitching && this.commands.targetCommand.targetFront.targetForm .followWindowGlobalLifeCycle ) { return; } const hasDeviceState = await this.hasDeviceState(); if (hasDeviceState) { // Return if there is a device state to restore, this will be done when the // device list is loaded after the post-init. return; } const height = Services.prefs.getIntPref( "devtools.responsive.viewport.height", 0 ); const pixelRatio = Services.prefs.getIntPref( "devtools.responsive.viewport.pixelRatio", 0 ); const touchSimulationEnabled = Services.prefs.getBoolPref( "devtools.responsive.touchSimulation.enabled", false ); const userAgent = Services.prefs.getCharPref( "devtools.responsive.userAgent", "" ); const width = Services.prefs.getIntPref( "devtools.responsive.viewport.width", 0 ); // Restore the previously set orientation, or get it from the initial viewport if it // wasn't set yet. const { type, angle } = this.commands.targetConfigurationCommand.configuration .rdmPaneOrientation || this.getInitialViewportOrientation({ width, height, }); await this.updateDPPX(pixelRatio); await this.updateScreenOrientation(type, angle); await this.updateMaxTouchPointsEnabled(touchSimulationEnabled); if (touchSimulationEnabled) { await this.updateTouchSimulation(touchSimulationEnabled); } let reloadNeeded = false; if (userAgent) { reloadNeeded |= (await this.updateUserAgent(userAgent)) && this.reloadOnChange("userAgent"); } if (reloadNeeded) { await this.reloadBrowser(); } } /** * Set or clear the emulated device pixel ratio. * * @param {Number|null} dppx: The ratio to simulate. Set to null to disable the * simulation and roll back to the original ratio */ async updateDPPX(dppx = null) { await this.commands.targetConfigurationCommand.updateConfiguration({ overrideDPPX: dppx, }); } /** * Set or clear network throttling. * * @return boolean * Whether a reload is needed to apply the change. * (This is always immediate, so it's always false.) */ async updateNetworkThrottling(enabled, profile) { if (!enabled) { await this.networkFront.clearNetworkThrottling(); return false; } const data = throttlingProfiles.find(({ id }) => id == profile); const { download, upload, latency } = data; await this.networkFront.setNetworkThrottling({ downloadThroughput: download, uploadThroughput: upload, latency, }); return false; } /** * Set or clear the emulated user agent. * * @param {String|null} userAgent: The user agent to set on the page. Set to null to revert * the user agent to its original value * @return {Boolean} Whether a reload is needed to apply the change. */ async updateUserAgent(userAgent) { const getConfigurationCustomUserAgent = () => this.commands.targetConfigurationCommand.configuration.customUserAgent || ""; const previousCustomUserAgent = getConfigurationCustomUserAgent(); await this.commands.targetConfigurationCommand.updateConfiguration({ customUserAgent: userAgent, }); const updatedUserAgent = getConfigurationCustomUserAgent(); return previousCustomUserAgent !== updatedUserAgent; } /** * Set or clear touch simulation. When setting to true, this method will * additionally set meta viewport override. * When setting to false, this method will clear all touch simulation and meta viewport * overrides, returning to default behavior for both settings. * * @param {boolean} enabled * @param {boolean} reloadOnTouchSimulationToggle: Set to true to trigger a page reload * if the touch simulation state changes. */ async updateTouchSimulation(enabled, reloadOnTouchSimulationToggle) { await this.commands.targetConfigurationCommand.updateConfiguration({ touchEventsOverride: enabled ? "enabled" : null, reloadOnTouchSimulationToggle, }); } /** * Sets the screen orientation values of the simulated device. * * @param {String} type * The orientation type to update the current device screen to. * @param {Number} angle * The rotation angle to update the current device screen to. * @param {Boolean} isViewportRotated * Whether or not the reason for updating the screen orientation is a result * of actually rotating the device via the RDM toolbar. If true, then an * "orientationchange" event is simulated. Otherwise, the screen orientation is * updated because of changing devices, opening RDM, or the page has been * reloaded/navigated to, so we should not be simulating "orientationchange". */ async updateScreenOrientation(type, angle, isViewportRotated = false) { await this.commands.targetConfigurationCommand.simulateScreenOrientationChange( { type, angle, isViewportRotated, } ); } /** * Sets whether or not maximum touch points are supported for the simulated device. * * @param {Boolean} touchSimulationEnabled * Whether or not touch is enabled for the simulated device. */ async updateMaxTouchPointsEnabled(touchSimulationEnabled) { return this.commands.targetConfigurationCommand.updateConfiguration({ rdmPaneMaxTouchPoints: touchSimulationEnabled ? 1 : 0, }); } /** * Sets whether or not the RDM UI should be left-aligned. * * @param {Boolean} leftAlignmentEnabled * Whether or not the UI is left-aligned. */ updateUIAlignment(leftAlignmentEnabled) { this.browserContainerEl.classList.toggle( "left-aligned", leftAlignmentEnabled ); } /** * Sets the browser element to be the given width and height. * * @param {Number} width * The viewport's width. * @param {Number} height * The viewport's height. */ updateViewportSize(width, height) { const zoom = this.tab.linkedBrowser.fullZoom; // Setting this with a variable on the stack instead of directly as width/height // on the because we'll need to use this for the alert dialog as well. this.browserStackEl.style.setProperty("--rdm-width", `${width}px`); this.browserStackEl.style.setProperty("--rdm-height", `${height}px`); this.browserStackEl.style.setProperty("--rdm-zoom", zoom); // This is a bit premature, but we emit a content-resize event here. It // would be preferrable to wait until the viewport is actually resized, // but the "resize" event is not triggered by this style change. The // content-resize message is only used by tests, and if needed those tests // can use the testing function setViewportSizeAndAwaitReflow to ensure // the viewport has had time to reach this size. this.emit("content-resize", { width, height, }); } /** * Helper for tests. Assumes a single viewport for now. */ getViewportSize() { // The getViewportSize function is loaded in index.js, and might not be // available yet. if (this.toolWindow.getViewportSize) { return this.toolWindow.getViewportSize(); } return { width: 0, height: 0 }; } /** * Helper for tests, etc. Assumes a single viewport for now. */ async setViewportSize(size) { await this.inited; // Ensure that width and height are valid. let { width, height } = size; if (!size.width) { width = this.getViewportSize().width; } if (!size.height) { height = this.getViewportSize().height; } this.rdmFrame.contentWindow.setViewportSize({ width, height }); this.updateViewportSize(width, height); } /** * Helper for tests/reloading the viewport. Assumes a single viewport for now. */ getViewportBrowser() { return this.tab.linkedBrowser; } /** * Helper for contacting the viewport content. Assumes a single viewport for now. */ getViewportMessageManager() { return this.getViewportBrowser().messageManager; } /** * Helper for getting the initial viewport orientation. */ getInitialViewportOrientation(viewport) { return getOrientation(viewport, viewport); } /** * Helper for tests to get the browser's window. */ getBrowserWindow() { return this.browserWindow; } async onTargetAvailable({ targetFront, isTargetSwitching }) { if (this.destroying) { return; } if (targetFront.isTopLevel) { this.responsiveFront = await targetFront.getFront("responsive"); if (this.destroying) { return; } await this.restoreActorState(isTargetSwitching); this.emitForTests("responsive-ui-target-switch-done"); } } // This just needed to setup watching for network resources, // to support network throttling. onNetworkResourceAvailable() {} /** * Reload the current tab. */ async reloadBrowser() { await this.commands.targetCommand.reloadTopLevelTarget(); } } module.exports = ResponsiveUI;