/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* 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/. */ const Ci = Components.interfaces; const Cu = Components.utils; var {loader, require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); var Telemetry = require("devtools/client/shared/telemetry"); var {showDoorhanger} = require("devtools/client/shared/doorhanger"); var {TouchEventSimulator} = require("devtools/shared/touch/simulator"); var {Task} = require("devtools/shared/task"); var promise = require("promise"); var DevToolsUtils = require("devtools/shared/DevToolsUtils"); var flags = require("devtools/shared/flags"); var Services = require("Services"); var EventEmitter = require("devtools/shared/event-emitter"); var {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers"); var { LocalizationHelper } = require("devtools/shared/l10n"); var { EmulationFront } = require("devtools/shared/fronts/emulation"); loader.lazyImporter(this, "SystemAppProxy", "resource://gre/modules/SystemAppProxy.jsm"); loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true); loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true); this.EXPORTED_SYMBOLS = ["ResponsiveUIManager"]; const MIN_WIDTH = 50; const MIN_HEIGHT = 50; const MAX_WIDTH = 10000; const MAX_HEIGHT = 10000; const SLOW_RATIO = 6; const ROUND_RATIO = 10; const INPUT_PARSER = /(\d+)[^\d]+(\d+)/; const SHARED_L10N = new LocalizationHelper("devtools/client/locales/shared.properties"); function debug(msg) { // dump(`RDM UI: ${msg}\n`); } var ActiveTabs = new Map(); var Manager = { /** * Check if the a tab is in a responsive mode. * Leave the responsive mode if active, * active the responsive mode if not active. * * @param aWindow the main window. * @param aTab the tab targeted. */ toggle: function (aWindow, aTab) { if (this.isActiveForTab(aTab)) { ActiveTabs.get(aTab).close(); } else { this.openIfNeeded(aWindow, aTab); } }, /** * Launches the responsive mode. * * @param aWindow the main window. * @param aTab the tab targeted. * @returns {ResponsiveUI} the instance of ResponsiveUI for the current tab. */ openIfNeeded: Task.async(function* (aWindow, aTab) { let ui; if (!this.isActiveForTab(aTab)) { ui = new ResponsiveUI(aWindow, aTab); yield ui.inited; } else { ui = this.getResponsiveUIForTab(aTab); } return ui; }), /** * Returns true if responsive view is active for the provided tab. * * @param aTab the tab targeted. */ isActiveForTab: function (aTab) { return ActiveTabs.has(aTab); }, /** * Return the responsive UI controller for a tab. */ getResponsiveUIForTab: function (aTab) { return ActiveTabs.get(aTab); }, /** * Handle gcli commands. * * @param aWindow the browser window. * @param aTab the tab targeted. * @param aCommand the command name. * @param aArgs command arguments. */ handleGcliCommand: Task.async(function* (aWindow, aTab, aCommand, aArgs) { switch (aCommand) { case "resize to": let ui = yield this.openIfNeeded(aWindow, aTab); ui.setViewportSize(aArgs); break; case "resize on": this.openIfNeeded(aWindow, aTab); break; case "resize off": if (this.isActiveForTab(aTab)) { yield ActiveTabs.get(aTab).close(); } break; case "resize toggle": this.toggle(aWindow, aTab); default: } }) }; EventEmitter.decorate(Manager); // If the new HTML RDM UI is enabled and e10s is enabled by default (e10s is required for // the new HTML RDM UI to function), delegate the ResponsiveUIManager API over to that // tool instead. Performing this delegation here allows us to contain the pref check to a // single place. if (Services.prefs.getBoolPref("devtools.responsive.html.enabled") && Services.appinfo.browserTabsRemoteAutostart) { let { ResponsiveUIManager } = require("devtools/client/responsive.html/manager"); this.ResponsiveUIManager = ResponsiveUIManager; } else { this.ResponsiveUIManager = Manager; } var defaultPresets = [ // Phones {key: "320x480", width: 320, height: 480}, // iPhone, B2G, with {key: "360x640", width: 360, height: 640}, // Android 4, phones, with // Tablets {key: "768x1024", width: 768, height: 1024}, // iPad, with {key: "800x1280", width: 800, height: 1280}, // Android 4, Tablet, with // Default width for mobile browsers, no {key: "980x1280", width: 980, height: 1280}, // Computer {key: "1280x600", width: 1280, height: 600}, {key: "1920x900", width: 1920, height: 900}, ]; function ResponsiveUI(aWindow, aTab) { this.mainWindow = aWindow; this.tab = aTab; this.mm = this.tab.linkedBrowser.messageManager; this.tabContainer = aWindow.gBrowser.tabContainer; this.browser = aTab.linkedBrowser; this.chromeDoc = aWindow.document; this.container = aWindow.gBrowser.getBrowserContainer(this.browser); this.stack = this.container.querySelector(".browserStack"); this._telemetry = new Telemetry(); // Let's bind some callbacks. this.bound_presetSelected = this.presetSelected.bind(this); this.bound_handleManualInput = this.handleManualInput.bind(this); this.bound_addPreset = this.addPreset.bind(this); this.bound_removePreset = this.removePreset.bind(this); this.bound_rotate = this.rotate.bind(this); this.bound_screenshot = () => this.screenshot(); this.bound_touch = this.toggleTouch.bind(this); this.bound_close = this.close.bind(this); this.bound_startResizing = this.startResizing.bind(this); this.bound_stopResizing = this.stopResizing.bind(this); this.bound_onDrag = this.onDrag.bind(this); this.bound_changeUA = this.changeUA.bind(this); this.bound_onContentResize = this.onContentResize.bind(this); this.mm.addMessageListener("ResponsiveMode:OnContentResize", this.bound_onContentResize); // We must be ready to handle window or tab close now that we have saved // ourselves in ActiveTabs. Otherwise we risk leaking the window. this.mainWindow.addEventListener("unload", this); this.tab.addEventListener("TabClose", this); this.tabContainer.addEventListener("TabSelect", this); ActiveTabs.set(this.tab, this); this.inited = this.init(); } ResponsiveUI.prototype = { _transitionsEnabled: true, get transitionsEnabled() { return this._transitionsEnabled; }, set transitionsEnabled(aValue) { this._transitionsEnabled = aValue; if (aValue && !this._resizing && this.stack.hasAttribute("responsivemode")) { this.stack.removeAttribute("notransition"); } else if (!aValue) { this.stack.setAttribute("notransition", "true"); } }, init: Task.async(function* () { debug("INIT BEGINS"); let ready = this.waitForMessage("ResponsiveMode:ChildScriptReady"); this.mm.loadFrameScript("resource://devtools/client/responsivedesign/responsivedesign-child.js", true); yield ready; let requiresFloatingScrollbars = !this.mainWindow.matchMedia("(-moz-overlay-scrollbars)").matches; let started = this.waitForMessage("ResponsiveMode:Start:Done"); debug("SEND START"); this.mm.sendAsyncMessage("ResponsiveMode:Start", { requiresFloatingScrollbars, // Tests expect events on resize to yield on various size changes notifyOnResize: flags.testing, }); yield started; // Load Presets this.loadPresets(); // Setup the UI this.container.setAttribute("responsivemode", "true"); this.stack.setAttribute("responsivemode", "true"); this.buildUI(); this.checkMenus(); // Rotate the responsive mode if needed try { if (Services.prefs.getBoolPref("devtools.responsiveUI.rotate")) { this.rotate(); } } catch (e) {} // Touch events support this.touchEnableBefore = false; this.touchEventSimulator = new TouchEventSimulator(this.browser); yield this.connectToServer(); this.userAgentInput.hidden = false; // Hook to display promotional Developer Edition doorhanger. // Only displayed once. showDoorhanger({ window: this.mainWindow, type: "deveditionpromo", anchor: this.chromeDoc.querySelector("#content") }); // Notify that responsive mode is on. this._telemetry.toolOpened("responsive"); ResponsiveUIManager.emit("on", { tab: this.tab }); }), 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(); yield this.client.attachTab(tab.actor); this.emulationFront = EmulationFront(this.client, tab); }), loadPresets: function () { // Try to load presets from prefs let presets = defaultPresets; if (Services.prefs.prefHasUserValue("devtools.responsiveUI.presets")) { try { presets = JSON.parse(Services.prefs.getCharPref("devtools.responsiveUI.presets")); } catch (e) { // User pref is malformated. console.error("Could not parse pref `devtools.responsiveUI.presets`: " + e); } } this.customPreset = {key: "custom", custom: true}; if (Array.isArray(presets)) { this.presets = [this.customPreset].concat(presets); } else { console.error("Presets value (devtools.responsiveUI.presets) is malformated."); this.presets = [this.customPreset]; } try { let width = Services.prefs.getIntPref("devtools.responsiveUI.customWidth"); let height = Services.prefs.getIntPref("devtools.responsiveUI.customHeight"); this.customPreset.width = Math.min(MAX_WIDTH, width); this.customPreset.height = Math.min(MAX_HEIGHT, height); this.currentPresetKey = Services.prefs.getCharPref("devtools.responsiveUI.currentPreset"); } catch (e) { // Default size. The first preset (custom) is the one that will be used. let bbox = this.stack.getBoundingClientRect(); this.customPreset.width = bbox.width - 40; // horizontal padding of the container this.customPreset.height = bbox.height - 80; // vertical padding + toolbar height this.currentPresetKey = this.presets[1].key; // most common preset } }, /** * Destroy the nodes. Remove listeners. Reset the style. */ close: Task.async(function* () { debug("CLOSE BEGINS"); if (this.closing) { debug("ALREADY CLOSING, ABORT"); return; } this.closing = true; // If we're closing very fast (in tests), ensure init has finished. debug("CLOSE: WAIT ON INITED"); yield this.inited; debug("CLOSE: INITED DONE"); this.unCheckMenus(); // Reset style of the stack. debug(`CURRENT SIZE: ${this.stack.getAttribute("style")}`); let style = "max-width: none;" + "min-width: 0;" + "max-height: none;" + "min-height: 0;"; debug("RESET STACK SIZE"); this.stack.setAttribute("style", style); // Wait for resize message before stopping in the child when testing, // but only if we should expect to still get a message. if (flags.testing && this.tab.linkedBrowser.messageManager) { yield this.waitForMessage("ResponsiveMode:OnContentResize"); } if (this.isResizing) this.stopResizing(); // Remove listeners. this.menulist.removeEventListener("select", this.bound_presetSelected, true); this.menulist.removeEventListener("change", this.bound_handleManualInput, true); this.mainWindow.removeEventListener("unload", this); this.tab.removeEventListener("TabClose", this); this.tabContainer.removeEventListener("TabSelect", this); this.rotatebutton.removeEventListener("command", this.bound_rotate, true); this.screenshotbutton.removeEventListener("command", this.bound_screenshot, true); this.closebutton.removeEventListener("command", this.bound_close, true); this.addbutton.removeEventListener("command", this.bound_addPreset, true); this.removebutton.removeEventListener("command", this.bound_removePreset, true); this.touchbutton.removeEventListener("command", this.bound_touch, true); this.userAgentInput.removeEventListener("blur", this.bound_changeUA, true); // Removed elements. this.container.removeChild(this.toolbar); if (this.bottomToolbar) { this.bottomToolbar.remove(); delete this.bottomToolbar; } this.stack.removeChild(this.resizer); this.stack.removeChild(this.resizeBarV); this.stack.removeChild(this.resizeBarH); this.stack.classList.remove("fxos-mode"); // Unset the responsive mode. this.container.removeAttribute("responsivemode"); this.stack.removeAttribute("responsivemode"); ActiveTabs.delete(this.tab); if (this.touchEventSimulator) { this.touchEventSimulator.stop(); } yield this.client.close(); this.client = this.emulationFront = null; this._telemetry.toolClosed("responsive"); if (this.tab.linkedBrowser.messageManager) { let stopped = this.waitForMessage("ResponsiveMode:Stop:Done"); this.tab.linkedBrowser.messageManager.sendAsyncMessage("ResponsiveMode:Stop"); yield stopped; } this.inited = null; ResponsiveUIManager.emit("off", { tab: this.tab }); }), waitForMessage(message) { return new Promise(resolve => { let listener = () => { this.mm.removeMessageListener(message, listener); resolve(); }; this.mm.addMessageListener(message, listener); }); }, /** * Emit an event when the content has been resized. Only used in tests. */ onContentResize: function (msg) { ResponsiveUIManager.emit("content-resize", { tab: this.tab, width: msg.data.width, height: msg.data.height, }); }, /** * Handle events */ handleEvent: function (aEvent) { switch (aEvent.type) { case "TabClose": case "unload": this.close(); break; case "TabSelect": if (this.tab.selected) { this.checkMenus(); } else if (!this.mainWindow.gBrowser.selectedTab.responsiveUI) { this.unCheckMenus(); } break; } }, getViewportBrowser() { return this.browser; }, /** * Check the menu items. */ checkMenus: function RUI_checkMenus() { this.chromeDoc.getElementById("menu_responsiveUI").setAttribute("checked", "true"); }, /** * Uncheck the menu items. */ unCheckMenus: function RUI_unCheckMenus() { let el = this.chromeDoc.getElementById("menu_responsiveUI"); if (el) { el.setAttribute("checked", "false"); } }, /** * Build the toolbar and the resizers. * * From tabbrowser.xml * * // presets * // rotate * // screenshot * // close * * From tabbrowser.xml * * * * * // Additional button in FxOS mode: *