diff options
Diffstat (limited to 'devtools/client/responsivedesign/responsivedesign.jsm')
-rw-r--r-- | devtools/client/responsivedesign/responsivedesign.jsm | 1193 |
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 0000000000..f67d1912c2 --- /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"); +}); |