123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584 |
- /* 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 { Ci } = require("chrome");
- const promise = require("promise");
- const { Task } = require("devtools/shared/task");
- const EventEmitter = require("devtools/shared/event-emitter");
- const { getOwnerWindow } = require("sdk/tabs/utils");
- const { startup } = require("sdk/window/helpers");
- const message = require("./utils/message");
- const { swapToInnerBrowser } = require("./browser/swap");
- const { EmulationFront } = require("devtools/shared/fronts/emulation");
- const { getStr } = require("./utils/l10n");
- const TOOL_URL = "chrome://devtools/content/responsive.html/index.xhtml";
- loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
- loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
- loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
- loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
- loader.lazyRequireGetter(this, "throttlingProfiles",
- "devtools/client/shared/network-throttling-profiles");
- /**
- * ResponsiveUIManager is the external API for the browser UI, etc. to use when
- * opening and closing the responsive UI.
- *
- * While the HTML UI is in an experimental stage, the older ResponsiveUIManager
- * from devtools/client/responsivedesign/responsivedesign.jsm delegates to this
- * object when the pref "devtools.responsive.html.enabled" is true.
- */
- const ResponsiveUIManager = exports.ResponsiveUIManager = {
- activeTabs: new Map(),
- /**
- * Toggle the responsive UI for a tab.
- *
- * @param window
- * The main browser chrome window.
- * @param tab
- * The browser tab.
- * @param options
- * Other options associated with toggling. Currently includes:
- * - `command`: Whether initiated via GCLI command bar or toolbox button
- * @return Promise
- * Resolved when the toggling has completed. If the UI has opened,
- * it is resolved to the ResponsiveUI instance for this tab. If the
- * the UI has closed, there is no resolution value.
- */
- toggle(window, tab, options) {
- let action = this.isActiveForTab(tab) ? "close" : "open";
- let completed = this[action + "IfNeeded"](window, tab, options);
- completed.catch(console.error);
- return completed;
- },
- /**
- * Opens the responsive UI, if not already open.
- *
- * @param window
- * The main browser chrome window.
- * @param tab
- * The browser tab.
- * @param options
- * Other options associated with opening. Currently includes:
- * - `command`: Whether initiated via GCLI command bar or toolbox button
- * @return Promise
- * Resolved to the ResponsiveUI instance for this tab when opening is
- * complete.
- */
- openIfNeeded: Task.async(function* (window, tab, options) {
- if (!tab.linkedBrowser.isRemoteBrowser) {
- this.showRemoteOnlyNotification(window, tab, options);
- return promise.reject(new Error("RDM only available for remote tabs."));
- }
- if (!this.isActiveForTab(tab)) {
- this.initMenuCheckListenerFor(window);
- let ui = new ResponsiveUI(window, tab);
- this.activeTabs.set(tab, ui);
- yield this.setMenuCheckFor(tab, window);
- yield ui.inited;
- this.emit("on", { tab });
- }
- return this.getResponsiveUIForTab(tab);
- }),
- /**
- * Closes the responsive UI, if not already closed.
- *
- * @param window
- * The main browser chrome window.
- * @param tab
- * The browser tab.
- * @param options
- * Other options associated with closing. Currently includes:
- * - `command`: Whether initiated via GCLI command bar or toolbox button
- * - `reason`: String detailing the specific cause for closing
- * @return Promise
- * Resolved (with no value) when closing is complete.
- */
- closeIfNeeded: Task.async(function* (window, tab, options) {
- if (this.isActiveForTab(tab)) {
- let ui = this.activeTabs.get(tab);
- let destroyed = yield ui.destroy(options);
- if (!destroyed) {
- // Already in the process of destroying, abort.
- return;
- }
- this.activeTabs.delete(tab);
- if (!this.isActiveForWindow(window)) {
- this.removeMenuCheckListenerFor(window);
- }
- this.emit("off", { tab });
- yield this.setMenuCheckFor(tab, window);
- }
- }),
- /**
- * Returns true if responsive UI is active for a given tab.
- *
- * @param tab
- * The browser tab.
- * @return boolean
- */
- isActiveForTab(tab) {
- return this.activeTabs.has(tab);
- },
- /**
- * Returns true if responsive UI is active in any tab in the given window.
- *
- * @param window
- * The main browser chrome window.
- * @return boolean
- */
- isActiveForWindow(window) {
- return [...this.activeTabs.keys()].some(t => getOwnerWindow(t) === window);
- },
- /**
- * Return the responsive UI controller for a tab.
- *
- * @param tab
- * The browser tab.
- * @return ResponsiveUI
- * The UI instance for this tab.
- */
- getResponsiveUIForTab(tab) {
- return this.activeTabs.get(tab);
- },
- /**
- * Handle GCLI commands.
- *
- * @param window
- * The main browser chrome window.
- * @param tab
- * The browser tab.
- * @param command
- * The GCLI command name.
- * @param args
- * The GCLI command arguments.
- */
- handleGcliCommand(window, tab, command, args) {
- let completed;
- switch (command) {
- case "resize to":
- completed = this.openIfNeeded(window, tab, { command: true });
- this.activeTabs.get(tab).setViewportSize(args);
- break;
- case "resize on":
- completed = this.openIfNeeded(window, tab, { command: true });
- break;
- case "resize off":
- completed = this.closeIfNeeded(window, tab, { command: true });
- break;
- case "resize toggle":
- completed = this.toggle(window, tab, { command: true });
- break;
- default:
- }
- completed.catch(e => console.error(e));
- },
- handleMenuCheck({target}) {
- ResponsiveUIManager.setMenuCheckFor(target);
- },
- initMenuCheckListenerFor(window) {
- let { tabContainer } = window.gBrowser;
- tabContainer.addEventListener("TabSelect", this.handleMenuCheck);
- },
- removeMenuCheckListenerFor(window) {
- if (window && window.gBrowser && window.gBrowser.tabContainer) {
- let { tabContainer } = window.gBrowser;
- tabContainer.removeEventListener("TabSelect", this.handleMenuCheck);
- }
- },
- setMenuCheckFor: Task.async(function* (tab, window = getOwnerWindow(tab)) {
- yield startup(window);
- let menu = window.document.getElementById("menu_responsiveUI");
- if (menu) {
- menu.setAttribute("checked", this.isActiveForTab(tab));
- }
- }),
- showRemoteOnlyNotification(window, tab, { command } = {}) {
- // Default to using the browser's per-tab notification box
- let nbox = window.gBrowser.getNotificationBox(tab.linkedBrowser);
- // If opening was initiated by GCLI command bar or toolbox button, check for an open
- // toolbox for the tab. If one exists, use the toolbox's notification box so that the
- // message is placed closer to the action taken by the user.
- if (command) {
- let target = TargetFactory.forTab(tab);
- let toolbox = gDevTools.getToolbox(target);
- if (toolbox) {
- nbox = toolbox.notificationBox;
- }
- }
- let value = "devtools-responsive-error";
- if (nbox.getNotificationWithValue(value)) {
- // Notification already displayed
- return;
- }
- nbox.appendNotification(
- msg,
- value,
- null,
- nbox.PRIORITY_CRITICAL_MEDIUM,
- []);
- },
- };
- // GCLI commands in ../responsivedesign/resize-commands.js listen for events
- // from this object to know when the UI for a tab has opened or closed.
- EventEmitter.decorate(ResponsiveUIManager);
- /**
- * 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.
- */
- function ResponsiveUI(window, tab) {
- this.browserWindow = window;
- this.tab = tab;
- this.inited = this.init();
- }
- ResponsiveUI.prototype = {
- /**
- * The main browser chrome window (that holds many tabs).
- */
- browserWindow: null,
- /**
- * The specific browser tab this responsive instance is for.
- */
- tab: null,
- /**
- * Promise resovled when the UI init has completed.
- */
- inited: null,
- /**
- * Flag set when destruction has begun.
- */
- destroying: false,
- /**
- * Flag set when destruction has ended.
- */
- destroyed: false,
- /**
- * A window reference for the chrome:// document that displays the responsive
- * design tool. It is safe to reference this window directly even with e10s,
- * as the tool UI is always loaded in the parent process. The web content
- * contained *within* the tool UI on the other hand is loaded in the child
- * process.
- */
- toolWindow: null,
- /**
- * Open RDM while preserving the state of the page. We use `swapFrameLoaders`
- * to ensure all in-page state is preserved, just like when you move a tab to
- * a new window.
- *
- * For more details, see /devtools/docs/responsive-design-mode.md.
- */
- init: Task.async(function* () {
- let ui = this;
- // Watch for tab close and window close so we can clean up RDM synchronously
- this.tab.addEventListener("TabClose", this);
- this.browserWindow.addEventListener("unload", this);
- // Swap page content from the current tab into a viewport within RDM
- this.swap = swapToInnerBrowser({
- tab: this.tab,
- containerURL: TOOL_URL,
- getInnerBrowser: Task.async(function* (containerBrowser) {
- let toolWindow = ui.toolWindow = containerBrowser.contentWindow;
- toolWindow.addEventListener("message", ui);
- yield message.request(toolWindow, "init");
- toolWindow.addInitialViewport("about:blank");
- yield message.wait(toolWindow, "browser-mounted");
- return ui.getViewportBrowser();
- })
- });
- yield this.swap.start();
- this.tab.addEventListener("BeforeTabRemotenessChange", this);
- // Notify the inner browser to start the frame script
- yield message.request(this.toolWindow, "start-frame-script");
- // Get the protocol ready to speak with emulation actor
- yield this.connectToServer();
- // Non-blocking message to tool UI to start any delayed init activities
- message.post(this.toolWindow, "post-init");
- }),
- /**
- * 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.
- */
- destroy: Task.async(function* (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 yielding when we're about to close the tab.
- let isWindowClosing = options && options.reason === "unload";
- let isTabContentDestroying =
- isWindowClosing || (options && (options.reason === "TabClose" ||
- options.reason === "BeforeTabRemotenessChange"));
- // Ensure init has finished before starting destroy
- if (!isTabContentDestroying) {
- yield this.inited;
- }
- this.tab.removeEventListener("TabClose", this);
- this.tab.removeEventListener("BeforeTabRemotenessChange", this);
- this.browserWindow.removeEventListener("unload", this);
- this.toolWindow.removeEventListener("message", this);
- if (!isTabContentDestroying) {
- // Notify the inner browser to stop the frame script
- yield message.request(this.toolWindow, "stop-frame-script");
- }
- // Destroy local state
- let swap = this.swap;
- this.browserWindow = null;
- this.tab = null;
- this.inited = null;
- this.toolWindow = null;
- this.swap = null;
- // Close the debugger client used to speak with emulation actor.
- // The actor handles clearing any overrides itself, so it's not necessary to clear
- // anything on shutdown client side.
- let clientClosed = this.client.close();
- if (!isTabContentDestroying) {
- yield clientClosed;
- }
- this.client = this.emulationFront = null;
- if (!isWindowClosing) {
- // Undo the swap and return the content back to a normal tab
- swap.stop();
- }
- this.destroyed = true;
- return true;
- }),
- connectToServer: Task.async(function* () {
- if (!DebuggerServer.initialized) {
- DebuggerServer.init();
- DebuggerServer.addBrowserActors();
- }
- this.client = new DebuggerClient(DebuggerServer.connectPipe());
- yield this.client.connect();
- let { tab } = yield this.client.getTab();
- this.emulationFront = EmulationFront(this.client, tab);
- }),
- handleEvent(event) {
- let { browserWindow, tab } = this;
- switch (event.type) {
- case "message":
- this.handleMessage(event);
- break;
- case "BeforeTabRemotenessChange":
- case "TabClose":
- case "unload":
- ResponsiveUIManager.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-throtting":
- this.onChangeNetworkThrottling(event);
- break;
- case "change-pixel-ratio":
- this.onChangePixelRatio(event);
- break;
- case "change-touch-simulation":
- this.onChangeTouchSimulation(event);
- break;
- case "content-resize":
- this.onContentResize(event);
- break;
- case "exit":
- this.onExit();
- break;
- case "remove-device":
- this.onRemoveDevice(event);
- break;
- }
- },
- onChangeDevice: Task.async(function* (event) {
- let { userAgent, pixelRatio, touch } = event.data.device;
- yield this.updateUserAgent(userAgent);
- yield this.updateDPPX(pixelRatio);
- yield this.updateTouchSimulation(touch);
- // Used by tests
- this.emit("device-changed");
- }),
- onChangeNetworkThrottling: Task.async(function* (event) {
- let { enabled, profile } = event.data;
- yield this.updateNetworkThrottling(enabled, profile);
- // Used by tests
- this.emit("network-throttling-changed");
- }),
- onChangePixelRatio(event) {
- let { pixelRatio } = event.data;
- this.updateDPPX(pixelRatio);
- },
- onChangeTouchSimulation(event) {
- let { enabled } = event.data;
- this.updateTouchSimulation(enabled);
- },
- onContentResize(event) {
- let { width, height } = event.data;
- this.emit("content-resize", {
- width,
- height,
- });
- },
- onExit() {
- let { browserWindow, tab } = this;
- ResponsiveUIManager.closeIfNeeded(browserWindow, tab);
- },
- onRemoveDevice: Task.async(function* (event) {
- yield this.updateUserAgent();
- yield this.updateDPPX();
- yield this.updateTouchSimulation();
- // Used by tests
- this.emit("device-removed");
- }),
- updateDPPX: Task.async(function* (dppx) {
- if (!dppx) {
- yield this.emulationFront.clearDPPXOverride();
- return;
- }
- yield this.emulationFront.setDPPXOverride(dppx);
- }),
- updateNetworkThrottling: Task.async(function* (enabled, profile) {
- if (!enabled) {
- yield this.emulationFront.clearNetworkThrottling();
- return;
- }
- let data = throttlingProfiles.find(({ id }) => id == profile);
- let { download, upload, latency } = data;
- yield this.emulationFront.setNetworkThrottling({
- downloadThroughput: download,
- uploadThroughput: upload,
- latency,
- });
- }),
- updateUserAgent: Task.async(function* (userAgent) {
- if (!userAgent) {
- yield this.emulationFront.clearUserAgentOverride();
- return;
- }
- yield this.emulationFront.setUserAgentOverride(userAgent);
- }),
- updateTouchSimulation: Task.async(function* (enabled) {
- if (!enabled) {
- yield this.emulationFront.clearTouchEventsOverride();
- return;
- }
- let reloadNeeded = yield this.emulationFront.setTouchEventsOverride(
- Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED
- );
- if (reloadNeeded) {
- this.getViewportBrowser().reload();
- }
- }),
- /**
- * Helper for tests. Assumes a single viewport for now.
- */
- getViewportSize() {
- return this.toolWindow.getViewportSize();
- },
- /**
- * Helper for tests, GCLI, etc. Assumes a single viewport for now.
- */
- setViewportSize: Task.async(function* (size) {
- yield this.inited;
- this.toolWindow.setViewportSize(size);
- }),
- /**
- * Helper for tests/reloading the viewport. Assumes a single viewport for now.
- */
- getViewportBrowser() {
- return this.toolWindow.getViewportBrowser();
- },
- /**
- * Helper for contacting the viewport content. Assumes a single viewport for now.
- */
- getViewportMessageManager() {
- return this.getViewportBrowser().messageManager;
- },
- };
- EventEmitter.decorate(ResponsiveUI.prototype);
|