summaryrefslogtreecommitdiffstats
path: root/devtools/client/responsivedesign/responsivedesign.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/responsivedesign/responsivedesign.jsm')
-rw-r--r--devtools/client/responsivedesign/responsivedesign.jsm1193
1 files changed, 1193 insertions, 0 deletions
diff --git a/devtools/client/responsivedesign/responsivedesign.jsm b/devtools/client/responsivedesign/responsivedesign.jsm
new file mode 100644
index 000000000..f67d1912c
--- /dev/null
+++ b/devtools/client/responsivedesign/responsivedesign.jsm
@@ -0,0 +1,1193 @@
+/* -*- 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 <meta viewport>
+ {key: "360x640", width: 360, height: 640}, // Android 4, phones, with <meta viewport>
+
+ // Tablets
+ {key: "768x1024", width: 768, height: 1024}, // iPad, with <meta viewport>
+ {key: "800x1280", width: 800, height: 1280}, // Android 4, Tablet, with <meta viewport>
+
+ // Default width for mobile browsers, no <meta viewport>
+ {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.
+ *
+ * <vbox class="browserContainer"> From tabbrowser.xml
+ * <toolbar class="devtools-responsiveui-toolbar">
+ * <menulist class="devtools-responsiveui-menulist"/> // presets
+ * <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="rotate"/> // rotate
+ * <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="screenshot"/> // screenshot
+ * <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="Leave Responsive Design Mode"/> // close
+ * </toolbar>
+ * <stack class="browserStack"> From tabbrowser.xml
+ * <browser/>
+ * <box class="devtools-responsiveui-resizehandle" bottom="0" right="0"/>
+ * <box class="devtools-responsiveui-resizebarV" top="0" right="0"/>
+ * <box class="devtools-responsiveui-resizebarH" bottom="0" left="0"/>
+ * // Additional button in FxOS mode:
+ * <button class="devtools-responsiveui-sleep-button" />
+ * <vbox class="devtools-responsiveui-volume-buttons">
+ * <button class="devtools-responsiveui-volume-up-button" />
+ * <button class="devtools-responsiveui-volume-down-button" />
+ * </vbox>
+ * </stack>
+ * <toolbar class="devtools-responsiveui-hardware-button">
+ * <toolbarbutton class="devtools-responsiveui-home-button" />
+ * </toolbar>
+ * </vbox>
+ */
+ buildUI: function RUI_buildUI() {
+ // Toolbar
+ this.toolbar = this.chromeDoc.createElement("toolbar");
+ this.toolbar.className = "devtools-responsiveui-toolbar";
+ this.toolbar.setAttribute("fullscreentoolbar", "true");
+
+ this.menulist = this.chromeDoc.createElement("menulist");
+ this.menulist.className = "devtools-responsiveui-menulist";
+ this.menulist.setAttribute("editable", "true");
+
+ this.menulist.addEventListener("select", this.bound_presetSelected, true);
+ this.menulist.addEventListener("change", this.bound_handleManualInput, true);
+
+ this.menuitems = new Map();
+
+ let menupopup = this.chromeDoc.createElement("menupopup");
+ this.registerPresets(menupopup);
+ this.menulist.appendChild(menupopup);
+
+ this.addbutton = this.chromeDoc.createElement("menuitem");
+ this.addbutton.setAttribute("label", this.strings.GetStringFromName("responsiveUI.addPreset"));
+ this.addbutton.addEventListener("command", this.bound_addPreset, true);
+
+ this.removebutton = this.chromeDoc.createElement("menuitem");
+ this.removebutton.setAttribute("label", this.strings.GetStringFromName("responsiveUI.removePreset"));
+ this.removebutton.addEventListener("command", this.bound_removePreset, true);
+
+ menupopup.appendChild(this.chromeDoc.createElement("menuseparator"));
+ menupopup.appendChild(this.addbutton);
+ menupopup.appendChild(this.removebutton);
+
+ this.rotatebutton = this.chromeDoc.createElement("toolbarbutton");
+ this.rotatebutton.setAttribute("tabindex", "0");
+ this.rotatebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.rotate2"));
+ this.rotatebutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-rotate";
+ this.rotatebutton.addEventListener("command", this.bound_rotate, true);
+
+ this.screenshotbutton = this.chromeDoc.createElement("toolbarbutton");
+ this.screenshotbutton.setAttribute("tabindex", "0");
+ this.screenshotbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.screenshot"));
+ this.screenshotbutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-screenshot";
+ this.screenshotbutton.addEventListener("command", this.bound_screenshot, true);
+
+ this.closebutton = this.chromeDoc.createElement("toolbarbutton");
+ this.closebutton.setAttribute("tabindex", "0");
+ this.closebutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-close";
+ this.closebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.close1"));
+ this.closebutton.addEventListener("command", this.bound_close, true);
+
+ this.toolbar.appendChild(this.closebutton);
+ this.toolbar.appendChild(this.menulist);
+ this.toolbar.appendChild(this.rotatebutton);
+
+ this.touchbutton = this.chromeDoc.createElement("toolbarbutton");
+ this.touchbutton.setAttribute("tabindex", "0");
+ this.touchbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.touch"));
+ this.touchbutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-touch";
+ this.touchbutton.addEventListener("command", this.bound_touch, true);
+ this.toolbar.appendChild(this.touchbutton);
+
+ this.toolbar.appendChild(this.screenshotbutton);
+
+ this.userAgentInput = this.chromeDoc.createElement("textbox");
+ this.userAgentInput.className = "devtools-responsiveui-textinput";
+ this.userAgentInput.setAttribute("placeholder",
+ this.strings.GetStringFromName("responsiveUI.userAgentPlaceholder"));
+ this.userAgentInput.addEventListener("blur", this.bound_changeUA, true);
+ this.userAgentInput.hidden = true;
+ this.toolbar.appendChild(this.userAgentInput);
+
+ // Resizers
+ let resizerTooltip = this.strings.GetStringFromName("responsiveUI.resizerTooltip");
+ this.resizer = this.chromeDoc.createElement("box");
+ this.resizer.className = "devtools-responsiveui-resizehandle";
+ this.resizer.setAttribute("right", "0");
+ this.resizer.setAttribute("bottom", "0");
+ this.resizer.setAttribute("tooltiptext", resizerTooltip);
+ this.resizer.onmousedown = this.bound_startResizing;
+
+ this.resizeBarV = this.chromeDoc.createElement("box");
+ this.resizeBarV.className = "devtools-responsiveui-resizebarV";
+ this.resizeBarV.setAttribute("top", "0");
+ this.resizeBarV.setAttribute("right", "0");
+ this.resizeBarV.setAttribute("tooltiptext", resizerTooltip);
+ this.resizeBarV.onmousedown = this.bound_startResizing;
+
+ this.resizeBarH = this.chromeDoc.createElement("box");
+ this.resizeBarH.className = "devtools-responsiveui-resizebarH";
+ this.resizeBarH.setAttribute("bottom", "0");
+ this.resizeBarH.setAttribute("left", "0");
+ this.resizeBarH.setAttribute("tooltiptext", resizerTooltip);
+ this.resizeBarH.onmousedown = this.bound_startResizing;
+
+ this.container.insertBefore(this.toolbar, this.stack);
+ this.stack.appendChild(this.resizer);
+ this.stack.appendChild(this.resizeBarV);
+ this.stack.appendChild(this.resizeBarH);
+ },
+
+ // FxOS custom controls
+ buildPhoneUI: function () {
+ this.stack.classList.add("fxos-mode");
+
+ let sleepButton = this.chromeDoc.createElement("button");
+ sleepButton.className = "devtools-responsiveui-sleep-button";
+ sleepButton.setAttribute("top", 0);
+ sleepButton.setAttribute("right", 0);
+ sleepButton.addEventListener("mousedown", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "Power"});
+ });
+ sleepButton.addEventListener("mouseup", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "Power"});
+ });
+ this.stack.appendChild(sleepButton);
+
+ let volumeButtons = this.chromeDoc.createElement("vbox");
+ volumeButtons.className = "devtools-responsiveui-volume-buttons";
+ volumeButtons.setAttribute("top", 0);
+ volumeButtons.setAttribute("left", 0);
+
+ let volumeUp = this.chromeDoc.createElement("button");
+ volumeUp.className = "devtools-responsiveui-volume-up-button";
+ volumeUp.addEventListener("mousedown", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "AudioVolumeUp"});
+ });
+ volumeUp.addEventListener("mouseup", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "AudioVolumeUp"});
+ });
+
+ let volumeDown = this.chromeDoc.createElement("button");
+ volumeDown.className = "devtools-responsiveui-volume-down-button";
+ volumeDown.addEventListener("mousedown", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "AudioVolumeDown"});
+ });
+ volumeDown.addEventListener("mouseup", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "AudioVolumeDown"});
+ });
+
+ volumeButtons.appendChild(volumeUp);
+ volumeButtons.appendChild(volumeDown);
+ this.stack.appendChild(volumeButtons);
+
+ let bottomToolbar = this.chromeDoc.createElement("toolbar");
+ bottomToolbar.className = "devtools-responsiveui-hardware-buttons";
+ bottomToolbar.setAttribute("align", "center");
+ bottomToolbar.setAttribute("pack", "center");
+
+ let homeButton = this.chromeDoc.createElement("toolbarbutton");
+ homeButton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-home-button";
+ homeButton.addEventListener("mousedown", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "Home"});
+ });
+ homeButton.addEventListener("mouseup", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "Home"});
+ });
+ bottomToolbar.appendChild(homeButton);
+ this.bottomToolbar = bottomToolbar;
+ this.container.appendChild(bottomToolbar);
+ },
+
+ /**
+ * Validate and apply any user input on the editable menulist
+ */
+ handleManualInput: function RUI_handleManualInput() {
+ let userInput = this.menulist.inputField.value;
+ let value = INPUT_PARSER.exec(userInput);
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+
+ // In case of an invalide value, we show back the last preset
+ if (!value || value.length < 3) {
+ this.setMenuLabel(this.selectedItem, selectedPreset);
+ return;
+ }
+
+ this.rotateValue = false;
+
+ if (!selectedPreset.custom) {
+ let menuitem = this.customMenuitem;
+ this.currentPresetKey = this.customPreset.key;
+ this.menulist.selectedItem = menuitem;
+ }
+
+ let w = this.customPreset.width = parseInt(value[1], 10);
+ let h = this.customPreset.height = parseInt(value[2], 10);
+
+ this.saveCustomSize();
+ this.setViewportSize({
+ width: w,
+ height: h,
+ });
+ },
+
+ /**
+ * Build the presets list and append it to the menupopup.
+ *
+ * @param aParent menupopup.
+ */
+ registerPresets: function RUI_registerPresets(aParent) {
+ let fragment = this.chromeDoc.createDocumentFragment();
+ let doc = this.chromeDoc;
+
+ for (let preset of this.presets) {
+ let menuitem = doc.createElement("menuitem");
+ menuitem.setAttribute("ispreset", true);
+ this.menuitems.set(menuitem, preset);
+
+ if (preset.key === this.currentPresetKey) {
+ menuitem.setAttribute("selected", "true");
+ this.selectedItem = menuitem;
+ }
+
+ if (preset.custom) {
+ this.customMenuitem = menuitem;
+ }
+
+ this.setMenuLabel(menuitem, preset);
+ fragment.appendChild(menuitem);
+ }
+ aParent.appendChild(fragment);
+ },
+
+ /**
+ * Set the menuitem label of a preset.
+ *
+ * @param aMenuitem menuitem to edit.
+ * @param aPreset associated preset.
+ */
+ setMenuLabel: function RUI_setMenuLabel(aMenuitem, aPreset) {
+ let size = SHARED_L10N.getFormatStr("dimensions",
+ Math.round(aPreset.width), Math.round(aPreset.height));
+
+ // .inputField might be not reachable yet (async XBL loading)
+ if (this.menulist.inputField) {
+ this.menulist.inputField.value = size;
+ }
+
+ if (aPreset.custom) {
+ size = this.strings.formatStringFromName("responsiveUI.customResolution", [size], 1);
+ } else if (aPreset.name != null && aPreset.name !== "") {
+ size = this.strings.formatStringFromName("responsiveUI.namedResolution", [size, aPreset.name], 2);
+ }
+
+ aMenuitem.setAttribute("label", size);
+ },
+
+ /**
+ * When a preset is selected, apply it.
+ */
+ presetSelected: function RUI_presetSelected() {
+ if (this.menulist.selectedItem.getAttribute("ispreset") === "true") {
+ this.selectedItem = this.menulist.selectedItem;
+
+ this.rotateValue = false;
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+ this.loadPreset(selectedPreset);
+ this.currentPresetKey = selectedPreset.key;
+ this.saveCurrentPreset();
+
+ // Update the buttons hidden status according to the new selected preset
+ if (selectedPreset == this.customPreset) {
+ this.addbutton.hidden = false;
+ this.removebutton.hidden = true;
+ } else {
+ this.addbutton.hidden = true;
+ this.removebutton.hidden = false;
+ }
+ }
+ },
+
+ /**
+ * Apply a preset.
+ */
+ loadPreset(preset) {
+ this.setViewportSize(preset);
+ },
+
+ /**
+ * Add a preset to the list and the memory
+ */
+ addPreset: function RUI_addPreset() {
+ let w = this.customPreset.width;
+ let h = this.customPreset.height;
+ let newName = {};
+
+ let title = this.strings.GetStringFromName("responsiveUI.customNamePromptTitle1");
+ let message = this.strings.formatStringFromName("responsiveUI.customNamePromptMsg", [w, h], 2);
+ let promptOk = Services.prompt.prompt(null, title, message, newName, null, {});
+
+ if (!promptOk) {
+ // Prompt has been cancelled
+ this.menulist.selectedItem = this.selectedItem;
+ return;
+ }
+
+ let newPreset = {
+ key: w + "x" + h,
+ name: newName.value,
+ width: w,
+ height: h
+ };
+
+ this.presets.push(newPreset);
+
+ // Sort the presets according to width/height ascending order
+ this.presets.sort(function RUI_sortPresets(aPresetA, aPresetB) {
+ // We keep custom preset at first
+ if (aPresetA.custom && !aPresetB.custom) {
+ return 1;
+ }
+ if (!aPresetA.custom && aPresetB.custom) {
+ return -1;
+ }
+
+ if (aPresetA.width === aPresetB.width) {
+ if (aPresetA.height === aPresetB.height) {
+ return 0;
+ } else {
+ return aPresetA.height > aPresetB.height;
+ }
+ } else {
+ return aPresetA.width > aPresetB.width;
+ }
+ });
+
+ this.savePresets();
+
+ let newMenuitem = this.chromeDoc.createElement("menuitem");
+ newMenuitem.setAttribute("ispreset", true);
+ this.setMenuLabel(newMenuitem, newPreset);
+
+ this.menuitems.set(newMenuitem, newPreset);
+ let idx = this.presets.indexOf(newPreset);
+ let beforeMenuitem = this.menulist.firstChild.childNodes[idx + 1];
+ this.menulist.firstChild.insertBefore(newMenuitem, beforeMenuitem);
+
+ this.menulist.selectedItem = newMenuitem;
+ this.currentPresetKey = newPreset.key;
+ this.saveCurrentPreset();
+ },
+
+ /**
+ * remove a preset from the list and the memory
+ */
+ removePreset: function RUI_removePreset() {
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+ let w = selectedPreset.width;
+ let h = selectedPreset.height;
+
+ this.presets.splice(this.presets.indexOf(selectedPreset), 1);
+ this.menulist.firstChild.removeChild(this.selectedItem);
+ this.menuitems.delete(this.selectedItem);
+
+ this.customPreset.width = w;
+ this.customPreset.height = h;
+ let menuitem = this.customMenuitem;
+ this.setMenuLabel(menuitem, this.customPreset);
+ this.menulist.selectedItem = menuitem;
+ this.currentPresetKey = this.customPreset.key;
+
+ this.setViewportSize({
+ width: w,
+ height: h,
+ });
+
+ this.savePresets();
+ },
+
+ /**
+ * Swap width and height.
+ */
+ rotate: function RUI_rotate() {
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+ let width = this.rotateValue ? selectedPreset.height : selectedPreset.width;
+ let height = this.rotateValue ? selectedPreset.width : selectedPreset.height;
+
+ this.setViewportSize({
+ width: height,
+ height: width,
+ });
+
+ if (selectedPreset.custom) {
+ this.saveCustomSize();
+ } else {
+ this.rotateValue = !this.rotateValue;
+ this.saveCurrentPreset();
+ }
+ },
+
+ /**
+ * Take a screenshot of the page.
+ *
+ * @param aFileName name of the screenshot file (used for tests).
+ */
+ screenshot: function RUI_screenshot(aFileName) {
+ let filename = aFileName;
+ if (!filename) {
+ let date = new Date();
+ let month = ("0" + (date.getMonth() + 1)).substr(-2, 2);
+ let day = ("0" + date.getDate()).substr(-2, 2);
+ let dateString = [date.getFullYear(), month, day].join("-");
+ let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
+ filename = this.strings.formatStringFromName("responsiveUI.screenshotGeneratedFilename", [dateString, timeString], 2);
+ }
+ let mm = this.tab.linkedBrowser.messageManager;
+ let chromeWindow = this.chromeDoc.defaultView;
+ let doc = chromeWindow.document;
+ function onScreenshot(aMessage) {
+ mm.removeMessageListener("ResponsiveMode:RequestScreenshot:Done", onScreenshot);
+ chromeWindow.saveURL(aMessage.data, filename + ".png", null, true, true, doc.documentURIObject, doc);
+ }
+ mm.addMessageListener("ResponsiveMode:RequestScreenshot:Done", onScreenshot);
+ mm.sendAsyncMessage("ResponsiveMode:RequestScreenshot");
+ },
+
+ /**
+ * Enable/Disable mouse -> touch events translation.
+ */
+ enableTouch: function RUI_enableTouch() {
+ this.touchbutton.setAttribute("checked", "true");
+ return this.touchEventSimulator.start();
+ },
+
+ disableTouch: function RUI_disableTouch() {
+ this.touchbutton.removeAttribute("checked");
+ return this.touchEventSimulator.stop();
+ },
+
+ hideTouchNotification: function RUI_hideTouchNotification() {
+ let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
+ let n = nbox.getNotificationWithValue("responsive-ui-need-reload");
+ if (n) {
+ n.close();
+ }
+ },
+
+ toggleTouch: Task.async(function* () {
+ this.hideTouchNotification();
+ if (this.touchEventSimulator.enabled) {
+ this.disableTouch();
+ } else {
+ let isReloadNeeded = yield this.enableTouch();
+ if (isReloadNeeded) {
+ if (Services.prefs.getBoolPref("devtools.responsiveUI.no-reload-notification")) {
+ return;
+ }
+
+ let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
+
+ var buttons = [{
+ label: this.strings.GetStringFromName("responsiveUI.notificationReload"),
+ callback: () => {
+ this.browser.reload();
+ },
+ accessKey: this.strings.GetStringFromName("responsiveUI.notificationReload_accesskey"),
+ }, {
+ label: this.strings.GetStringFromName("responsiveUI.dontShowReloadNotification"),
+ callback: function () {
+ Services.prefs.setBoolPref("devtools.responsiveUI.no-reload-notification", true);
+ },
+ accessKey: this.strings.GetStringFromName("responsiveUI.dontShowReloadNotification_accesskey"),
+ }];
+
+ nbox.appendNotification(
+ this.strings.GetStringFromName("responsiveUI.needReload"),
+ "responsive-ui-need-reload",
+ null,
+ nbox.PRIORITY_INFO_LOW,
+ buttons);
+ }
+ }
+ }),
+
+ waitForReload() {
+ let navigatedDeferred = promise.defer();
+ let onNavigated = (_, { state }) => {
+ if (state != "stop") {
+ return;
+ }
+ this.client.removeListener("tabNavigated", onNavigated);
+ navigatedDeferred.resolve();
+ };
+ this.client.addListener("tabNavigated", onNavigated);
+ return navigatedDeferred.promise;
+ },
+
+ /**
+ * Change the user agent string
+ */
+ changeUA: Task.async(function* () {
+ let value = this.userAgentInput.value;
+ let changed;
+ if (value) {
+ changed = yield this.emulationFront.setUserAgentOverride(value);
+ this.userAgentInput.setAttribute("attention", "true");
+ } else {
+ changed = yield this.emulationFront.clearUserAgentOverride();
+ this.userAgentInput.removeAttribute("attention");
+ }
+ if (changed) {
+ let reloaded = this.waitForReload();
+ this.tab.linkedBrowser.reload();
+ yield reloaded;
+ }
+ ResponsiveUIManager.emit("userAgentChanged", { tab: this.tab });
+ }),
+
+ /**
+ * Get the current width and height.
+ */
+ getSize() {
+ let width = Number(this.stack.style.minWidth.replace("px", ""));
+ let height = Number(this.stack.style.minHeight.replace("px", ""));
+ return {
+ width,
+ height,
+ };
+ },
+
+ /**
+ * Change the size of the viewport.
+ */
+ setViewportSize({ width, height }) {
+ debug(`SET SIZE TO ${width} x ${height}`);
+ if (width) {
+ this.setWidth(width);
+ }
+ if (height) {
+ this.setHeight(height);
+ }
+ },
+
+ setWidth: function RUI_setWidth(aWidth) {
+ aWidth = Math.min(Math.max(aWidth, MIN_WIDTH), MAX_WIDTH);
+ this.stack.style.maxWidth = this.stack.style.minWidth = aWidth + "px";
+
+ if (!this.ignoreX)
+ this.resizeBarH.setAttribute("left", Math.round(aWidth / 2));
+
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+
+ if (selectedPreset.custom) {
+ selectedPreset.width = aWidth;
+ this.setMenuLabel(this.selectedItem, selectedPreset);
+ }
+ },
+
+ setHeight: function RUI_setHeight(aHeight) {
+ aHeight = Math.min(Math.max(aHeight, MIN_HEIGHT), MAX_HEIGHT);
+ this.stack.style.maxHeight = this.stack.style.minHeight = aHeight + "px";
+
+ if (!this.ignoreY)
+ this.resizeBarV.setAttribute("top", Math.round(aHeight / 2));
+
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+ if (selectedPreset.custom) {
+ selectedPreset.height = aHeight;
+ this.setMenuLabel(this.selectedItem, selectedPreset);
+ }
+ },
+ /**
+ * Start the process of resizing the browser.
+ *
+ * @param aEvent
+ */
+ startResizing: function RUI_startResizing(aEvent) {
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+
+ if (!selectedPreset.custom) {
+ this.customPreset.width = this.rotateValue ? selectedPreset.height : selectedPreset.width;
+ this.customPreset.height = this.rotateValue ? selectedPreset.width : selectedPreset.height;
+
+ let menuitem = this.customMenuitem;
+ this.setMenuLabel(menuitem, this.customPreset);
+
+ this.currentPresetKey = this.customPreset.key;
+ this.menulist.selectedItem = menuitem;
+ }
+ this.mainWindow.addEventListener("mouseup", this.bound_stopResizing, true);
+ this.mainWindow.addEventListener("mousemove", this.bound_onDrag, true);
+ this.container.style.pointerEvents = "none";
+
+ this._resizing = true;
+ this.stack.setAttribute("notransition", "true");
+
+ this.lastScreenX = aEvent.screenX;
+ this.lastScreenY = aEvent.screenY;
+
+ this.ignoreY = (aEvent.target === this.resizeBarV);
+ this.ignoreX = (aEvent.target === this.resizeBarH);
+
+ this.isResizing = true;
+ },
+
+ /**
+ * Resizing on mouse move.
+ *
+ * @param aEvent
+ */
+ onDrag: function RUI_onDrag(aEvent) {
+ let shift = aEvent.shiftKey;
+ let ctrl = !aEvent.shiftKey && aEvent.ctrlKey;
+
+ let screenX = aEvent.screenX;
+ let screenY = aEvent.screenY;
+
+ let deltaX = screenX - this.lastScreenX;
+ let deltaY = screenY - this.lastScreenY;
+
+ if (this.ignoreY)
+ deltaY = 0;
+ if (this.ignoreX)
+ deltaX = 0;
+
+ if (ctrl) {
+ deltaX /= SLOW_RATIO;
+ deltaY /= SLOW_RATIO;
+ }
+
+ let width = this.customPreset.width + deltaX;
+ let height = this.customPreset.height + deltaY;
+
+ if (shift) {
+ let roundedWidth, roundedHeight;
+ roundedWidth = 10 * Math.floor(width / ROUND_RATIO);
+ roundedHeight = 10 * Math.floor(height / ROUND_RATIO);
+ screenX += roundedWidth - width;
+ screenY += roundedHeight - height;
+ width = roundedWidth;
+ height = roundedHeight;
+ }
+
+ if (width < MIN_WIDTH) {
+ width = MIN_WIDTH;
+ } else {
+ this.lastScreenX = screenX;
+ }
+
+ if (height < MIN_HEIGHT) {
+ height = MIN_HEIGHT;
+ } else {
+ this.lastScreenY = screenY;
+ }
+
+ this.setViewportSize({ width, height });
+ },
+
+ /**
+ * Stop End resizing
+ */
+ stopResizing: function RUI_stopResizing() {
+ this.container.style.pointerEvents = "auto";
+
+ this.mainWindow.removeEventListener("mouseup", this.bound_stopResizing, true);
+ this.mainWindow.removeEventListener("mousemove", this.bound_onDrag, true);
+
+ this.saveCustomSize();
+
+ delete this._resizing;
+ if (this.transitionsEnabled) {
+ this.stack.removeAttribute("notransition");
+ }
+ this.ignoreY = false;
+ this.ignoreX = false;
+ this.isResizing = false;
+ },
+
+ /**
+ * Store the custom size as a pref.
+ */
+ saveCustomSize: function RUI_saveCustomSize() {
+ Services.prefs.setIntPref("devtools.responsiveUI.customWidth", this.customPreset.width);
+ Services.prefs.setIntPref("devtools.responsiveUI.customHeight", this.customPreset.height);
+ },
+
+ /**
+ * Store the current preset as a pref.
+ */
+ saveCurrentPreset: function RUI_saveCurrentPreset() {
+ Services.prefs.setCharPref("devtools.responsiveUI.currentPreset", this.currentPresetKey);
+ Services.prefs.setBoolPref("devtools.responsiveUI.rotate", this.rotateValue);
+ },
+
+ /**
+ * Store the list of all registered presets as a pref.
+ */
+ savePresets: function RUI_savePresets() {
+ // We exclude the custom one
+ let registeredPresets = this.presets.filter(function (aPreset) {
+ return !aPreset.custom;
+ });
+
+ Services.prefs.setCharPref("devtools.responsiveUI.presets", JSON.stringify(registeredPresets));
+ },
+};
+
+loader.lazyGetter(ResponsiveUI.prototype, "strings", function () {
+ return Services.strings.createBundle("chrome://devtools/locale/responsiveUI.properties");
+});