browser/devtools/responsivedesign/responsivedesign.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/responsivedesign/responsivedesign.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,927 @@
     1.4 +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +const Ci = Components.interfaces;
    1.11 +const Cu = Components.utils;
    1.12 +
    1.13 +Cu.import("resource://gre/modules/Services.jsm");
    1.14 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.15 +Cu.import("resource:///modules/devtools/gDevTools.jsm");
    1.16 +Cu.import("resource:///modules/devtools/FloatingScrollbars.jsm");
    1.17 +Cu.import("resource://gre/modules/devtools/event-emitter.js");
    1.18 +
    1.19 +var require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
    1.20 +let Telemetry = require("devtools/shared/telemetry");
    1.21 +let {TouchEventHandler} = require("devtools/touch-events");
    1.22 +
    1.23 +this.EXPORTED_SYMBOLS = ["ResponsiveUIManager"];
    1.24 +
    1.25 +const MIN_WIDTH = 50;
    1.26 +const MIN_HEIGHT = 50;
    1.27 +
    1.28 +const MAX_WIDTH = 10000;
    1.29 +const MAX_HEIGHT = 10000;
    1.30 +
    1.31 +const SLOW_RATIO = 6;
    1.32 +const ROUND_RATIO = 10;
    1.33 +
    1.34 +this.ResponsiveUIManager = {
    1.35 +  /**
    1.36 +   * Check if the a tab is in a responsive mode.
    1.37 +   * Leave the responsive mode if active,
    1.38 +   * active the responsive mode if not active.
    1.39 +   *
    1.40 +   * @param aWindow the main window.
    1.41 +   * @param aTab the tab targeted.
    1.42 +   */
    1.43 +  toggle: function(aWindow, aTab) {
    1.44 +    if (aTab.__responsiveUI) {
    1.45 +      aTab.__responsiveUI.close();
    1.46 +    } else {
    1.47 +      new ResponsiveUI(aWindow, aTab);
    1.48 +    }
    1.49 +  },
    1.50 +
    1.51 +  /**
    1.52 +   * Returns true if responsive view is active for the provided tab.
    1.53 +   *
    1.54 +   * @param aTab the tab targeted.
    1.55 +   */
    1.56 +  isActiveForTab: function(aTab) {
    1.57 +    return !!aTab.__responsiveUI;
    1.58 +  },
    1.59 +
    1.60 +  /**
    1.61 +   * Handle gcli commands.
    1.62 +   *
    1.63 +   * @param aWindow the browser window.
    1.64 +   * @param aTab the tab targeted.
    1.65 +   * @param aCommand the command name.
    1.66 +   * @param aArgs command arguments.
    1.67 +   */
    1.68 +  handleGcliCommand: function(aWindow, aTab, aCommand, aArgs) {
    1.69 +    switch (aCommand) {
    1.70 +      case "resize to":
    1.71 +        if (!aTab.__responsiveUI) {
    1.72 +          new ResponsiveUI(aWindow, aTab);
    1.73 +        }
    1.74 +        aTab.__responsiveUI.setSize(aArgs.width, aArgs.height);
    1.75 +        break;
    1.76 +      case "resize on":
    1.77 +        if (!aTab.__responsiveUI) {
    1.78 +          new ResponsiveUI(aWindow, aTab);
    1.79 +        }
    1.80 +        break;
    1.81 +      case "resize off":
    1.82 +        if (aTab.__responsiveUI) {
    1.83 +          aTab.__responsiveUI.close();
    1.84 +        }
    1.85 +        break;
    1.86 +      case "resize toggle":
    1.87 +          this.toggle(aWindow, aTab);
    1.88 +      default:
    1.89 +    }
    1.90 +  }
    1.91 +}
    1.92 +
    1.93 +EventEmitter.decorate(ResponsiveUIManager);
    1.94 +
    1.95 +let presets = [
    1.96 +  // Phones
    1.97 +  {key: "320x480", width: 320, height: 480},    // iPhone, B2G, with <meta viewport>
    1.98 +  {key: "360x640", width: 360, height: 640},    // Android 4, phones, with <meta viewport>
    1.99 +
   1.100 +  // Tablets
   1.101 +  {key: "768x1024", width: 768, height: 1024},   // iPad, with <meta viewport>
   1.102 +  {key: "800x1280", width: 800, height: 1280},   // Android 4, Tablet, with <meta viewport>
   1.103 +
   1.104 +  // Default width for mobile browsers, no <meta viewport>
   1.105 +  {key: "980x1280", width: 980, height: 1280},
   1.106 +
   1.107 +  // Computer
   1.108 +  {key: "1280x600", width: 1280, height: 600},
   1.109 +  {key: "1920x900", width: 1920, height: 900},
   1.110 +];
   1.111 +
   1.112 +function ResponsiveUI(aWindow, aTab)
   1.113 +{
   1.114 +  this.mainWindow = aWindow;
   1.115 +  this.tab = aTab;
   1.116 +  this.tabContainer = aWindow.gBrowser.tabContainer;
   1.117 +  this.browser = aTab.linkedBrowser;
   1.118 +  this.chromeDoc = aWindow.document;
   1.119 +  this.container = aWindow.gBrowser.getBrowserContainer(this.browser);
   1.120 +  this.stack = this.container.querySelector(".browserStack");
   1.121 +  this._telemetry = new Telemetry();
   1.122 +  this._floatingScrollbars = !this.mainWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
   1.123 +
   1.124 +
   1.125 +  // Try to load presets from prefs
   1.126 +  if (Services.prefs.prefHasUserValue("devtools.responsiveUI.presets")) {
   1.127 +    try {
   1.128 +      presets = JSON.parse(Services.prefs.getCharPref("devtools.responsiveUI.presets"));
   1.129 +    } catch(e) {
   1.130 +      // User pref is malformated.
   1.131 +      Cu.reportError("Could not parse pref `devtools.responsiveUI.presets`: " + e);
   1.132 +    }
   1.133 +  }
   1.134 +
   1.135 +  this.customPreset = {key: "custom", custom: true};
   1.136 +
   1.137 +  if (Array.isArray(presets)) {
   1.138 +    this.presets = [this.customPreset].concat(presets);
   1.139 +  } else {
   1.140 +    Cu.reportError("Presets value (devtools.responsiveUI.presets) is malformated.");
   1.141 +    this.presets = [this.customPreset];
   1.142 +  }
   1.143 +
   1.144 +  try {
   1.145 +    let width = Services.prefs.getIntPref("devtools.responsiveUI.customWidth");
   1.146 +    let height = Services.prefs.getIntPref("devtools.responsiveUI.customHeight");
   1.147 +    this.customPreset.width = Math.min(MAX_WIDTH, width);
   1.148 +    this.customPreset.height = Math.min(MAX_HEIGHT, height);
   1.149 +
   1.150 +    this.currentPresetKey = Services.prefs.getCharPref("devtools.responsiveUI.currentPreset");
   1.151 +  } catch(e) {
   1.152 +    // Default size. The first preset (custom) is the one that will be used.
   1.153 +    let bbox = this.stack.getBoundingClientRect();
   1.154 +
   1.155 +    this.customPreset.width = bbox.width - 40; // horizontal padding of the container
   1.156 +    this.customPreset.height = bbox.height - 80; // vertical padding + toolbar height
   1.157 +
   1.158 +    this.currentPresetKey = this.presets[1].key; // most common preset
   1.159 +  }
   1.160 +
   1.161 +  this.container.setAttribute("responsivemode", "true");
   1.162 +  this.stack.setAttribute("responsivemode", "true");
   1.163 +
   1.164 +  // Let's bind some callbacks.
   1.165 +  this.bound_onPageLoad = this.onPageLoad.bind(this);
   1.166 +  this.bound_onPageUnload = this.onPageUnload.bind(this);
   1.167 +  this.bound_presetSelected = this.presetSelected.bind(this);
   1.168 +  this.bound_addPreset = this.addPreset.bind(this);
   1.169 +  this.bound_removePreset = this.removePreset.bind(this);
   1.170 +  this.bound_rotate = this.rotate.bind(this);
   1.171 +  this.bound_screenshot = () => this.screenshot();
   1.172 +  this.bound_touch = this.toggleTouch.bind(this);
   1.173 +  this.bound_close = this.close.bind(this);
   1.174 +  this.bound_startResizing = this.startResizing.bind(this);
   1.175 +  this.bound_stopResizing = this.stopResizing.bind(this);
   1.176 +  this.bound_onDrag = this.onDrag.bind(this);
   1.177 +  this.bound_onKeypress = this.onKeypress.bind(this);
   1.178 +
   1.179 +  // Events
   1.180 +  this.tab.addEventListener("TabClose", this);
   1.181 +  this.tabContainer.addEventListener("TabSelect", this);
   1.182 +  this.mainWindow.document.addEventListener("keypress", this.bound_onKeypress, false);
   1.183 +
   1.184 +  this.buildUI();
   1.185 +  this.checkMenus();
   1.186 +
   1.187 +  this.docShell = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   1.188 +                      .getInterface(Ci.nsIWebNavigation)
   1.189 +                      .QueryInterface(Ci.nsIDocShell);
   1.190 +
   1.191 +  this._deviceSizeWasPageSize = this.docShell.deviceSizeIsPageSize;
   1.192 +  this.docShell.deviceSizeIsPageSize = true;
   1.193 +
   1.194 +  try {
   1.195 +    if (Services.prefs.getBoolPref("devtools.responsiveUI.rotate")) {
   1.196 +      this.rotate();
   1.197 +    }
   1.198 +  } catch(e) {}
   1.199 +
   1.200 +  if (this._floatingScrollbars)
   1.201 +    switchToFloatingScrollbars(this.tab);
   1.202 +
   1.203 +  this.tab.__responsiveUI = this;
   1.204 +
   1.205 +  this._telemetry.toolOpened("responsive");
   1.206 +
   1.207 +  // Touch events support
   1.208 +  this.touchEnableBefore = false;
   1.209 +  this.touchEventHandler = new TouchEventHandler(this.browser);
   1.210 +
   1.211 +  this.browser.addEventListener("load", this.bound_onPageLoad, true);
   1.212 +  this.browser.addEventListener("unload", this.bound_onPageUnload, true);
   1.213 +
   1.214 +  if (this.browser.contentWindow.document &&
   1.215 +      this.browser.contentWindow.document.readyState == "complete") {
   1.216 +    this.onPageLoad();
   1.217 +  }
   1.218 +
   1.219 +  ResponsiveUIManager.emit("on", this.tab, this);
   1.220 +}
   1.221 +
   1.222 +ResponsiveUI.prototype = {
   1.223 +  _transitionsEnabled: true,
   1.224 +  get transitionsEnabled() this._transitionsEnabled,
   1.225 +  set transitionsEnabled(aValue) {
   1.226 +    this._transitionsEnabled = aValue;
   1.227 +    if (aValue && !this._resizing && this.stack.hasAttribute("responsivemode")) {
   1.228 +      this.stack.removeAttribute("notransition");
   1.229 +    } else if (!aValue) {
   1.230 +      this.stack.setAttribute("notransition", "true");
   1.231 +    }
   1.232 +  },
   1.233 +
   1.234 +  /**
   1.235 +   * Window onload / onunload
   1.236 +   */
   1.237 +   onPageLoad: function() {
   1.238 +     this.touchEventHandler = new TouchEventHandler(this.browser);
   1.239 +     if (this.touchEnableBefore) {
   1.240 +       this.enableTouch();
   1.241 +     }
   1.242 +   },
   1.243 +
   1.244 +   onPageUnload: function(evt) {
   1.245 +     // Ignore sub frames unload events
   1.246 +     if (evt.target != this.browser.contentDocument)
   1.247 +       return;
   1.248 +     if (this.closing)
   1.249 +       return;
   1.250 +     if (this.touchEventHandler) {
   1.251 +       this.touchEnableBefore = this.touchEventHandler.enabled;
   1.252 +       this.disableTouch();
   1.253 +       delete this.touchEventHandler;
   1.254 +     }
   1.255 +   },
   1.256 +
   1.257 +  /**
   1.258 +   * Destroy the nodes. Remove listeners. Reset the style.
   1.259 +   */
   1.260 +  close: function RUI_unload() {
   1.261 +    if (this.closing)
   1.262 +      return;
   1.263 +    this.closing = true;
   1.264 +
   1.265 +    this.docShell.deviceSizeIsPageSize = this._deviceSizeWasPageSize;
   1.266 +
   1.267 +    this.browser.removeEventListener("load", this.bound_onPageLoad, true);
   1.268 +    this.browser.removeEventListener("unload", this.bound_onPageUnload, true);
   1.269 +
   1.270 +    if (this._floatingScrollbars)
   1.271 +      switchToNativeScrollbars(this.tab);
   1.272 +
   1.273 +    this.unCheckMenus();
   1.274 +    // Reset style of the stack.
   1.275 +    let style = "max-width: none;" +
   1.276 +                "min-width: 0;" +
   1.277 +                "max-height: none;" +
   1.278 +                "min-height: 0;";
   1.279 +    this.stack.setAttribute("style", style);
   1.280 +
   1.281 +    if (this.isResizing)
   1.282 +      this.stopResizing();
   1.283 +
   1.284 +    // Remove listeners.
   1.285 +    this.mainWindow.document.removeEventListener("keypress", this.bound_onKeypress, false);
   1.286 +    this.menulist.removeEventListener("select", this.bound_presetSelected, true);
   1.287 +    this.tab.removeEventListener("TabClose", this);
   1.288 +    this.tabContainer.removeEventListener("TabSelect", this);
   1.289 +    this.rotatebutton.removeEventListener("command", this.bound_rotate, true);
   1.290 +    this.screenshotbutton.removeEventListener("command", this.bound_screenshot, true);
   1.291 +    this.touchbutton.removeEventListener("command", this.bound_touch, true);
   1.292 +    this.closebutton.removeEventListener("command", this.bound_close, true);
   1.293 +    this.addbutton.removeEventListener("command", this.bound_addPreset, true);
   1.294 +    this.removebutton.removeEventListener("command", this.bound_removePreset, true);
   1.295 +
   1.296 +    // Removed elements.
   1.297 +    this.container.removeChild(this.toolbar);
   1.298 +    this.stack.removeChild(this.resizer);
   1.299 +    this.stack.removeChild(this.resizeBarV);
   1.300 +    this.stack.removeChild(this.resizeBarH);
   1.301 +
   1.302 +    // Unset the responsive mode.
   1.303 +    this.container.removeAttribute("responsivemode");
   1.304 +    this.stack.removeAttribute("responsivemode");
   1.305 +
   1.306 +    delete this.docShell;
   1.307 +    delete this.tab.__responsiveUI;
   1.308 +    if (this.touchEventHandler)
   1.309 +      this.touchEventHandler.stop();
   1.310 +    this._telemetry.toolClosed("responsive");
   1.311 +    ResponsiveUIManager.emit("off", this.tab, this);
   1.312 +  },
   1.313 +
   1.314 +  /**
   1.315 +   * Handle keypressed.
   1.316 +   *
   1.317 +   * @param aEvent
   1.318 +   */
   1.319 +  onKeypress: function RUI_onKeypress(aEvent) {
   1.320 +    if (aEvent.keyCode == this.mainWindow.KeyEvent.DOM_VK_ESCAPE &&
   1.321 +        this.mainWindow.gBrowser.selectedBrowser == this.browser) {
   1.322 +
   1.323 +      aEvent.preventDefault();
   1.324 +      aEvent.stopPropagation();
   1.325 +      this.close();
   1.326 +    }
   1.327 +  },
   1.328 +
   1.329 +  /**
   1.330 +   * Handle events
   1.331 +   */
   1.332 +  handleEvent: function (aEvent) {
   1.333 +    switch (aEvent.type) {
   1.334 +      case "TabClose":
   1.335 +        this.close();
   1.336 +        break;
   1.337 +      case "TabSelect":
   1.338 +        if (this.tab.selected) {
   1.339 +          this.checkMenus();
   1.340 +        } else if (!this.mainWindow.gBrowser.selectedTab.responsiveUI) {
   1.341 +          this.unCheckMenus();
   1.342 +        }
   1.343 +        break;
   1.344 +    }
   1.345 +  },
   1.346 +
   1.347 +  /**
   1.348 +   * Check the menu items.
   1.349 +   */
   1.350 +   checkMenus: function RUI_checkMenus() {
   1.351 +     this.chromeDoc.getElementById("Tools:ResponsiveUI").setAttribute("checked", "true");
   1.352 +   },
   1.353 +
   1.354 +  /**
   1.355 +   * Uncheck the menu items.
   1.356 +   */
   1.357 +   unCheckMenus: function RUI_unCheckMenus() {
   1.358 +     this.chromeDoc.getElementById("Tools:ResponsiveUI").setAttribute("checked", "false");
   1.359 +   },
   1.360 +
   1.361 +  /**
   1.362 +   * Build the toolbar and the resizers.
   1.363 +   *
   1.364 +   * <vbox class="browserContainer"> From tabbrowser.xml
   1.365 +   *  <toolbar class="devtools-responsiveui-toolbar">
   1.366 +   *    <menulist class="devtools-responsiveui-menulist"/> // presets
   1.367 +   *    <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="rotate"/> // rotate
   1.368 +   *    <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="screenshot"/> // screenshot
   1.369 +   *    <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="Leave Responsive Design View"/> // close
   1.370 +   *  </toolbar>
   1.371 +   *  <stack class="browserStack"> From tabbrowser.xml
   1.372 +   *    <browser/>
   1.373 +   *    <box class="devtools-responsiveui-resizehandle" bottom="0" right="0"/>
   1.374 +   *    <box class="devtools-responsiveui-resizebarV" top="0" right="0"/>
   1.375 +   *    <box class="devtools-responsiveui-resizebarH" bottom="0" left="0"/>
   1.376 +   *  </stack>
   1.377 +   * </vbox>
   1.378 +   */
   1.379 +  buildUI: function RUI_buildUI() {
   1.380 +    // Toolbar
   1.381 +    this.toolbar = this.chromeDoc.createElement("toolbar");
   1.382 +    this.toolbar.className = "devtools-responsiveui-toolbar";
   1.383 +
   1.384 +    this.menulist = this.chromeDoc.createElement("menulist");
   1.385 +    this.menulist.className = "devtools-responsiveui-menulist";
   1.386 +
   1.387 +    this.menulist.addEventListener("select", this.bound_presetSelected, true);
   1.388 +
   1.389 +    this.menuitems = new Map();
   1.390 +
   1.391 +    let menupopup = this.chromeDoc.createElement("menupopup");
   1.392 +    this.registerPresets(menupopup);
   1.393 +    this.menulist.appendChild(menupopup);
   1.394 +
   1.395 +    this.addbutton = this.chromeDoc.createElement("menuitem");
   1.396 +    this.addbutton.setAttribute("label", this.strings.GetStringFromName("responsiveUI.addPreset"));
   1.397 +    this.addbutton.addEventListener("command", this.bound_addPreset, true);
   1.398 +
   1.399 +    this.removebutton = this.chromeDoc.createElement("menuitem");
   1.400 +    this.removebutton.setAttribute("label", this.strings.GetStringFromName("responsiveUI.removePreset"));
   1.401 +    this.removebutton.addEventListener("command", this.bound_removePreset, true);
   1.402 +
   1.403 +    menupopup.appendChild(this.chromeDoc.createElement("menuseparator"));
   1.404 +    menupopup.appendChild(this.addbutton);
   1.405 +    menupopup.appendChild(this.removebutton);
   1.406 +
   1.407 +    this.rotatebutton = this.chromeDoc.createElement("toolbarbutton");
   1.408 +    this.rotatebutton.setAttribute("tabindex", "0");
   1.409 +    this.rotatebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.rotate2"));
   1.410 +    this.rotatebutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-rotate";
   1.411 +    this.rotatebutton.addEventListener("command", this.bound_rotate, true);
   1.412 +
   1.413 +    this.screenshotbutton = this.chromeDoc.createElement("toolbarbutton");
   1.414 +    this.screenshotbutton.setAttribute("tabindex", "0");
   1.415 +    this.screenshotbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.screenshot"));
   1.416 +    this.screenshotbutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-screenshot";
   1.417 +    this.screenshotbutton.addEventListener("command", this.bound_screenshot, true);
   1.418 +
   1.419 +    this.touchbutton = this.chromeDoc.createElement("toolbarbutton");
   1.420 +    this.touchbutton.setAttribute("tabindex", "0");
   1.421 +    this.touchbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.touch"));
   1.422 +    this.touchbutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-touch";
   1.423 +    this.touchbutton.addEventListener("command", this.bound_touch, true);
   1.424 +
   1.425 +    this.closebutton = this.chromeDoc.createElement("toolbarbutton");
   1.426 +    this.closebutton.setAttribute("tabindex", "0");
   1.427 +    this.closebutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-close";
   1.428 +    this.closebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.close"));
   1.429 +    this.closebutton.addEventListener("command", this.bound_close, true);
   1.430 +
   1.431 +    this.toolbar.appendChild(this.closebutton);
   1.432 +    this.toolbar.appendChild(this.menulist);
   1.433 +    this.toolbar.appendChild(this.rotatebutton);
   1.434 +    this.toolbar.appendChild(this.touchbutton);
   1.435 +    this.toolbar.appendChild(this.screenshotbutton);
   1.436 +
   1.437 +    // Resizers
   1.438 +    let resizerTooltip = this.strings.GetStringFromName("responsiveUI.resizerTooltip");
   1.439 +    this.resizer = this.chromeDoc.createElement("box");
   1.440 +    this.resizer.className = "devtools-responsiveui-resizehandle";
   1.441 +    this.resizer.setAttribute("right", "0");
   1.442 +    this.resizer.setAttribute("bottom", "0");
   1.443 +    this.resizer.setAttribute("tooltiptext", resizerTooltip);
   1.444 +    this.resizer.onmousedown = this.bound_startResizing;
   1.445 +
   1.446 +    this.resizeBarV =  this.chromeDoc.createElement("box");
   1.447 +    this.resizeBarV.className = "devtools-responsiveui-resizebarV";
   1.448 +    this.resizeBarV.setAttribute("top", "0");
   1.449 +    this.resizeBarV.setAttribute("right", "0");
   1.450 +    this.resizeBarV.setAttribute("tooltiptext", resizerTooltip);
   1.451 +    this.resizeBarV.onmousedown = this.bound_startResizing;
   1.452 +
   1.453 +    this.resizeBarH =  this.chromeDoc.createElement("box");
   1.454 +    this.resizeBarH.className = "devtools-responsiveui-resizebarH";
   1.455 +    this.resizeBarH.setAttribute("bottom", "0");
   1.456 +    this.resizeBarH.setAttribute("left", "0");
   1.457 +    this.resizeBarH.setAttribute("tooltiptext", resizerTooltip);
   1.458 +    this.resizeBarH.onmousedown = this.bound_startResizing;
   1.459 +
   1.460 +    this.container.insertBefore(this.toolbar, this.stack);
   1.461 +    this.stack.appendChild(this.resizer);
   1.462 +    this.stack.appendChild(this.resizeBarV);
   1.463 +    this.stack.appendChild(this.resizeBarH);
   1.464 +  },
   1.465 +
   1.466 +  /**
   1.467 +   * Build the presets list and append it to the menupopup.
   1.468 +   *
   1.469 +   * @param aParent menupopup.
   1.470 +   */
   1.471 +  registerPresets: function RUI_registerPresets(aParent) {
   1.472 +    let fragment = this.chromeDoc.createDocumentFragment();
   1.473 +    let doc = this.chromeDoc;
   1.474 +
   1.475 +    for (let preset of this.presets) {
   1.476 +      let menuitem = doc.createElement("menuitem");
   1.477 +      menuitem.setAttribute("ispreset", true);
   1.478 +      this.menuitems.set(menuitem, preset);
   1.479 +
   1.480 +      if (preset.key === this.currentPresetKey) {
   1.481 +        menuitem.setAttribute("selected", "true");
   1.482 +        this.selectedItem = menuitem;
   1.483 +      }
   1.484 +
   1.485 +      if (preset.custom)
   1.486 +        this.customMenuitem = menuitem;
   1.487 +
   1.488 +      this.setMenuLabel(menuitem, preset);
   1.489 +      fragment.appendChild(menuitem);
   1.490 +    }
   1.491 +    aParent.appendChild(fragment);
   1.492 +  },
   1.493 +
   1.494 +  /**
   1.495 +   * Set the menuitem label of a preset.
   1.496 +   *
   1.497 +   * @param aMenuitem menuitem to edit.
   1.498 +   * @param aPreset associated preset.
   1.499 +   */
   1.500 +  setMenuLabel: function RUI_setMenuLabel(aMenuitem, aPreset) {
   1.501 +    let size = Math.round(aPreset.width) + "x" + Math.round(aPreset.height);
   1.502 +    if (aPreset.custom) {
   1.503 +      let str = this.strings.formatStringFromName("responsiveUI.customResolution", [size], 1);
   1.504 +      aMenuitem.setAttribute("label", str);
   1.505 +    } else if (aPreset.name != null && aPreset.name !== "") {
   1.506 +      let str = this.strings.formatStringFromName("responsiveUI.namedResolution", [size, aPreset.name], 2);
   1.507 +      aMenuitem.setAttribute("label", str);
   1.508 +    } else {
   1.509 +      aMenuitem.setAttribute("label", size);
   1.510 +    }
   1.511 +  },
   1.512 +
   1.513 +  /**
   1.514 +   * When a preset is selected, apply it.
   1.515 +   */
   1.516 +  presetSelected: function RUI_presetSelected() {
   1.517 +    if (this.menulist.selectedItem.getAttribute("ispreset") === "true") {
   1.518 +      this.selectedItem = this.menulist.selectedItem;
   1.519 +
   1.520 +      this.rotateValue = false;
   1.521 +      let selectedPreset = this.menuitems.get(this.selectedItem);
   1.522 +      this.loadPreset(selectedPreset);
   1.523 +      this.currentPresetKey = selectedPreset.key;
   1.524 +      this.saveCurrentPreset();
   1.525 +
   1.526 +      // Update the buttons hidden status according to the new selected preset
   1.527 +      if (selectedPreset == this.customPreset) {
   1.528 +        this.addbutton.hidden = false;
   1.529 +        this.removebutton.hidden = true;
   1.530 +      } else {
   1.531 +        this.addbutton.hidden = true;
   1.532 +        this.removebutton.hidden = false;
   1.533 +      }
   1.534 +    }
   1.535 +  },
   1.536 +
   1.537 +  /**
   1.538 +   * Apply a preset.
   1.539 +   *
   1.540 +   * @param aPreset preset to apply.
   1.541 +   */
   1.542 +  loadPreset: function RUI_loadPreset(aPreset) {
   1.543 +    this.setSize(aPreset.width, aPreset.height);
   1.544 +  },
   1.545 +
   1.546 +  /**
   1.547 +   * Add a preset to the list and the memory
   1.548 +   */
   1.549 +  addPreset: function RUI_addPreset() {
   1.550 +    let w = this.customPreset.width;
   1.551 +    let h = this.customPreset.height;
   1.552 +    let newName = {};
   1.553 +
   1.554 +    let title = this.strings.GetStringFromName("responsiveUI.customNamePromptTitle");
   1.555 +    let message = this.strings.formatStringFromName("responsiveUI.customNamePromptMsg", [w, h], 2);
   1.556 +    let promptOk = Services.prompt.prompt(null, title, message, newName, null, {});
   1.557 +
   1.558 +    if (!promptOk) {
   1.559 +      // Prompt has been cancelled
   1.560 +      let menuitem = this.customMenuitem;
   1.561 +      this.menulist.selectedItem = menuitem;
   1.562 +      this.currentPresetKey = this.customPreset.key;
   1.563 +      return;
   1.564 +    }
   1.565 +
   1.566 +    let newPreset = {
   1.567 +      key: w + "x" + h,
   1.568 +      name: newName.value,
   1.569 +      width: w,
   1.570 +      height: h
   1.571 +    };
   1.572 +
   1.573 +    this.presets.push(newPreset);
   1.574 +
   1.575 +    // Sort the presets according to width/height ascending order
   1.576 +    this.presets.sort(function RUI_sortPresets(aPresetA, aPresetB) {
   1.577 +      // We keep custom preset at first
   1.578 +      if (aPresetA.custom && !aPresetB.custom) {
   1.579 +        return 1;
   1.580 +      }
   1.581 +      if (!aPresetA.custom && aPresetB.custom) {
   1.582 +        return -1;
   1.583 +      }
   1.584 +
   1.585 +      if (aPresetA.width === aPresetB.width) {
   1.586 +        if (aPresetA.height === aPresetB.height) {
   1.587 +          return 0;
   1.588 +        } else {
   1.589 +          return aPresetA.height > aPresetB.height;
   1.590 +        }
   1.591 +      } else {
   1.592 +        return aPresetA.width > aPresetB.width;
   1.593 +      }
   1.594 +    });
   1.595 +
   1.596 +    this.savePresets();
   1.597 +
   1.598 +    let newMenuitem = this.chromeDoc.createElement("menuitem");
   1.599 +    newMenuitem.setAttribute("ispreset", true);
   1.600 +    this.setMenuLabel(newMenuitem, newPreset);
   1.601 +
   1.602 +    this.menuitems.set(newMenuitem, newPreset);
   1.603 +    let idx = this.presets.indexOf(newPreset);
   1.604 +    let beforeMenuitem = this.menulist.firstChild.childNodes[idx + 1];
   1.605 +    this.menulist.firstChild.insertBefore(newMenuitem, beforeMenuitem);
   1.606 +
   1.607 +    this.menulist.selectedItem = newMenuitem;
   1.608 +    this.currentPresetKey = newPreset.key;
   1.609 +    this.saveCurrentPreset();
   1.610 +  },
   1.611 +
   1.612 +  /**
   1.613 +   * remove a preset from the list and the memory
   1.614 +   */
   1.615 +  removePreset: function RUI_removePreset() {
   1.616 +    let selectedPreset = this.menuitems.get(this.selectedItem);
   1.617 +    let w = selectedPreset.width;
   1.618 +    let h = selectedPreset.height;
   1.619 +
   1.620 +    this.presets.splice(this.presets.indexOf(selectedPreset), 1);
   1.621 +    this.menulist.firstChild.removeChild(this.selectedItem);
   1.622 +    this.menuitems.delete(this.selectedItem);
   1.623 +
   1.624 +    this.customPreset.width = w;
   1.625 +    this.customPreset.height = h;
   1.626 +    let menuitem = this.customMenuitem;
   1.627 +    this.setMenuLabel(menuitem, this.customPreset);
   1.628 +    this.menulist.selectedItem = menuitem;
   1.629 +    this.currentPresetKey = this.customPreset.key;
   1.630 +
   1.631 +    this.setSize(w, h);
   1.632 +
   1.633 +    this.savePresets();
   1.634 +  },
   1.635 +
   1.636 +  /**
   1.637 +   * Swap width and height.
   1.638 +   */
   1.639 +  rotate: function RUI_rotate() {
   1.640 +    let selectedPreset = this.menuitems.get(this.selectedItem);
   1.641 +    let width = this.rotateValue ? selectedPreset.height : selectedPreset.width;
   1.642 +    let height = this.rotateValue ? selectedPreset.width : selectedPreset.height;
   1.643 +
   1.644 +    this.setSize(height, width);
   1.645 +
   1.646 +    if (selectedPreset.custom) {
   1.647 +      this.saveCustomSize();
   1.648 +    } else {
   1.649 +      this.rotateValue = !this.rotateValue;
   1.650 +      this.saveCurrentPreset();
   1.651 +    }
   1.652 +  },
   1.653 +
   1.654 +  /**
   1.655 +   * Take a screenshot of the page.
   1.656 +   *
   1.657 +   * @param aFileName name of the screenshot file (used for tests).
   1.658 +   */
   1.659 +  screenshot: function RUI_screenshot(aFileName) {
   1.660 +    let window = this.browser.contentWindow;
   1.661 +    let document = window.document;
   1.662 +    let canvas = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
   1.663 +
   1.664 +    let width = window.innerWidth;
   1.665 +    let height = window.innerHeight;
   1.666 +
   1.667 +    canvas.width = width;
   1.668 +    canvas.height = height;
   1.669 +
   1.670 +    let ctx = canvas.getContext("2d");
   1.671 +    ctx.drawWindow(window, window.scrollX, window.scrollY, width, height, "#fff");
   1.672 +
   1.673 +    let filename = aFileName;
   1.674 +
   1.675 +    if (!filename) {
   1.676 +      let date = new Date();
   1.677 +      let month = ("0" + (date.getMonth() + 1)).substr(-2, 2);
   1.678 +      let day = ("0" + (date.getDay() + 1)).substr(-2, 2);
   1.679 +      let dateString = [date.getFullYear(), month, day].join("-");
   1.680 +      let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
   1.681 +      filename = this.strings.formatStringFromName("responsiveUI.screenshotGeneratedFilename", [dateString, timeString], 2);
   1.682 +    }
   1.683 +
   1.684 +    canvas.toBlob(blob => {
   1.685 +      let chromeWindow = this.chromeDoc.defaultView;
   1.686 +      let url = chromeWindow.URL.createObjectURL(blob);
   1.687 +      chromeWindow.saveURL(url, filename + ".png", null, true, true, document.documentURIObject, document);
   1.688 +    });
   1.689 +  },
   1.690 +
   1.691 +  /**
   1.692 +   * Enable/Disable mouse -> touch events translation.
   1.693 +   */
   1.694 +   enableTouch: function RUI_enableTouch() {
   1.695 +     if (!this.touchEventHandler.enabled) {
   1.696 +       let isReloadNeeded = this.touchEventHandler.start();
   1.697 +       this.touchbutton.setAttribute("checked", "true");
   1.698 +       return isReloadNeeded;
   1.699 +     }
   1.700 +     return false;
   1.701 +   },
   1.702 +
   1.703 +   disableTouch: function RUI_disableTouch() {
   1.704 +     if (this.touchEventHandler.enabled) {
   1.705 +       this.touchEventHandler.stop();
   1.706 +       this.touchbutton.removeAttribute("checked");
   1.707 +     }
   1.708 +   },
   1.709 +
   1.710 +   hideTouchNotification: function RUI_hideTouchNotification() {
   1.711 +     let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
   1.712 +     let n = nbox.getNotificationWithValue("responsive-ui-need-reload");
   1.713 +     if (n) {
   1.714 +       n.close();
   1.715 +     }
   1.716 +   },
   1.717 +
   1.718 +   toggleTouch: function RUI_toggleTouch() {
   1.719 +     this.hideTouchNotification();
   1.720 +     if (this.touchEventHandler.enabled) {
   1.721 +       this.disableTouch();
   1.722 +     } else {
   1.723 +       let isReloadNeeded = this.enableTouch();
   1.724 +       if (isReloadNeeded) {
   1.725 +         if (Services.prefs.getBoolPref("devtools.responsiveUI.no-reload-notification")) {
   1.726 +           return;
   1.727 +         }
   1.728 +
   1.729 +         let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
   1.730 +
   1.731 +         var buttons = [{
   1.732 +           label: this.strings.GetStringFromName("responsiveUI.notificationReload"),
   1.733 +           callback: () => {
   1.734 +             this.browser.reload();
   1.735 +           },
   1.736 +           accessKey: this.strings.GetStringFromName("responsiveUI.notificationReload_accesskey"),
   1.737 +         }, {
   1.738 +           label: this.strings.GetStringFromName("responsiveUI.dontShowReloadNotification"),
   1.739 +           callback: function() {
   1.740 +             Services.prefs.setBoolPref("devtools.responsiveUI.no-reload-notification", true);
   1.741 +           },
   1.742 +           accessKey: this.strings.GetStringFromName("responsiveUI.dontShowReloadNotification_accesskey"),
   1.743 +         }];
   1.744 +
   1.745 +         nbox.appendNotification(
   1.746 +           this.strings.GetStringFromName("responsiveUI.needReload"),
   1.747 +           "responsive-ui-need-reload",
   1.748 +           null,
   1.749 +           nbox.PRIORITY_INFO_LOW,
   1.750 +           buttons);
   1.751 +       }
   1.752 +     }
   1.753 +   },
   1.754 +
   1.755 +  /**
   1.756 +   * Change the size of the browser.
   1.757 +   *
   1.758 +   * @param aWidth width of the browser.
   1.759 +   * @param aHeight height of the browser.
   1.760 +   */
   1.761 +  setSize: function RUI_setSize(aWidth, aHeight) {
   1.762 +    aWidth = Math.min(Math.max(aWidth, MIN_WIDTH), MAX_WIDTH);
   1.763 +    aHeight = Math.min(Math.max(aHeight, MIN_HEIGHT), MAX_HEIGHT);
   1.764 +
   1.765 +    // We resize the containing stack.
   1.766 +    let style = "max-width: %width;" +
   1.767 +                "min-width: %width;" +
   1.768 +                "max-height: %height;" +
   1.769 +                "min-height: %height;";
   1.770 +
   1.771 +    style = style.replace(/%width/g, aWidth + "px");
   1.772 +    style = style.replace(/%height/g, aHeight + "px");
   1.773 +
   1.774 +    this.stack.setAttribute("style", style);
   1.775 +
   1.776 +    if (!this.ignoreY)
   1.777 +      this.resizeBarV.setAttribute("top", Math.round(aHeight / 2));
   1.778 +    if (!this.ignoreX)
   1.779 +      this.resizeBarH.setAttribute("left", Math.round(aWidth / 2));
   1.780 +
   1.781 +    let selectedPreset = this.menuitems.get(this.selectedItem);
   1.782 +
   1.783 +    // We uptate the custom menuitem if we are using it
   1.784 +    if (selectedPreset.custom) {
   1.785 +      selectedPreset.width = aWidth;
   1.786 +      selectedPreset.height = aHeight;
   1.787 +
   1.788 +      this.setMenuLabel(this.selectedItem, selectedPreset);
   1.789 +    }
   1.790 +  },
   1.791 +
   1.792 +  /**
   1.793 +   * Start the process of resizing the browser.
   1.794 +   *
   1.795 +   * @param aEvent
   1.796 +   */
   1.797 +  startResizing: function RUI_startResizing(aEvent) {
   1.798 +    let selectedPreset = this.menuitems.get(this.selectedItem);
   1.799 +
   1.800 +    if (!selectedPreset.custom) {
   1.801 +      this.customPreset.width = this.rotateValue ? selectedPreset.height : selectedPreset.width;
   1.802 +      this.customPreset.height = this.rotateValue ? selectedPreset.width : selectedPreset.height;
   1.803 +
   1.804 +      let menuitem = this.customMenuitem;
   1.805 +      this.setMenuLabel(menuitem, this.customPreset);
   1.806 +
   1.807 +      this.currentPresetKey = this.customPreset.key;
   1.808 +      this.menulist.selectedItem = menuitem;
   1.809 +    }
   1.810 +    this.mainWindow.addEventListener("mouseup", this.bound_stopResizing, true);
   1.811 +    this.mainWindow.addEventListener("mousemove", this.bound_onDrag, true);
   1.812 +    this.container.style.pointerEvents = "none";
   1.813 +
   1.814 +    this._resizing = true;
   1.815 +    this.stack.setAttribute("notransition", "true");
   1.816 +
   1.817 +    this.lastScreenX = aEvent.screenX;
   1.818 +    this.lastScreenY = aEvent.screenY;
   1.819 +
   1.820 +    this.ignoreY = (aEvent.target === this.resizeBarV);
   1.821 +    this.ignoreX = (aEvent.target === this.resizeBarH);
   1.822 +
   1.823 +    this.isResizing = true;
   1.824 +  },
   1.825 +
   1.826 +  /**
   1.827 +   * Resizing on mouse move.
   1.828 +   *
   1.829 +   * @param aEvent
   1.830 +   */
   1.831 +  onDrag: function RUI_onDrag(aEvent) {
   1.832 +    let shift = aEvent.shiftKey;
   1.833 +    let ctrl = !aEvent.shiftKey && aEvent.ctrlKey;
   1.834 +
   1.835 +    let screenX = aEvent.screenX;
   1.836 +    let screenY = aEvent.screenY;
   1.837 +
   1.838 +    let deltaX = screenX - this.lastScreenX;
   1.839 +    let deltaY = screenY - this.lastScreenY;
   1.840 +
   1.841 +    if (this.ignoreY)
   1.842 +      deltaY = 0;
   1.843 +    if (this.ignoreX)
   1.844 +      deltaX = 0;
   1.845 +
   1.846 +    if (ctrl) {
   1.847 +      deltaX /= SLOW_RATIO;
   1.848 +      deltaY /= SLOW_RATIO;
   1.849 +    }
   1.850 +
   1.851 +    let width = this.customPreset.width + deltaX;
   1.852 +    let height = this.customPreset.height + deltaY;
   1.853 +
   1.854 +    if (shift) {
   1.855 +      let roundedWidth, roundedHeight;
   1.856 +      roundedWidth = 10 * Math.floor(width / ROUND_RATIO);
   1.857 +      roundedHeight = 10 * Math.floor(height / ROUND_RATIO);
   1.858 +      screenX += roundedWidth - width;
   1.859 +      screenY += roundedHeight - height;
   1.860 +      width = roundedWidth;
   1.861 +      height = roundedHeight;
   1.862 +    }
   1.863 +
   1.864 +    if (width < MIN_WIDTH) {
   1.865 +        width = MIN_WIDTH;
   1.866 +    } else {
   1.867 +        this.lastScreenX = screenX;
   1.868 +    }
   1.869 +
   1.870 +    if (height < MIN_HEIGHT) {
   1.871 +        height = MIN_HEIGHT;
   1.872 +    } else {
   1.873 +        this.lastScreenY = screenY;
   1.874 +    }
   1.875 +
   1.876 +    this.setSize(width, height);
   1.877 +  },
   1.878 +
   1.879 +  /**
   1.880 +   * Stop End resizing
   1.881 +   */
   1.882 +  stopResizing: function RUI_stopResizing() {
   1.883 +    this.container.style.pointerEvents = "auto";
   1.884 +
   1.885 +    this.mainWindow.removeEventListener("mouseup", this.bound_stopResizing, true);
   1.886 +    this.mainWindow.removeEventListener("mousemove", this.bound_onDrag, true);
   1.887 +
   1.888 +    this.saveCustomSize();
   1.889 +
   1.890 +    delete this._resizing;
   1.891 +    if (this.transitionsEnabled) {
   1.892 +      this.stack.removeAttribute("notransition");
   1.893 +    }
   1.894 +    this.ignoreY = false;
   1.895 +    this.ignoreX = false;
   1.896 +    this.isResizing = false;
   1.897 +  },
   1.898 +
   1.899 +  /**
   1.900 +   * Store the custom size as a pref.
   1.901 +   */
   1.902 +   saveCustomSize: function RUI_saveCustomSize() {
   1.903 +     Services.prefs.setIntPref("devtools.responsiveUI.customWidth", this.customPreset.width);
   1.904 +     Services.prefs.setIntPref("devtools.responsiveUI.customHeight", this.customPreset.height);
   1.905 +   },
   1.906 +
   1.907 +  /**
   1.908 +   * Store the current preset as a pref.
   1.909 +   */
   1.910 +   saveCurrentPreset: function RUI_saveCurrentPreset() {
   1.911 +     Services.prefs.setCharPref("devtools.responsiveUI.currentPreset", this.currentPresetKey);
   1.912 +     Services.prefs.setBoolPref("devtools.responsiveUI.rotate", this.rotateValue);
   1.913 +   },
   1.914 +
   1.915 +  /**
   1.916 +   * Store the list of all registered presets as a pref.
   1.917 +   */
   1.918 +  savePresets: function RUI_savePresets() {
   1.919 +    // We exclude the custom one
   1.920 +    let registeredPresets = this.presets.filter(function (aPreset) {
   1.921 +      return !aPreset.custom;
   1.922 +    });
   1.923 +
   1.924 +    Services.prefs.setCharPref("devtools.responsiveUI.presets", JSON.stringify(registeredPresets));
   1.925 +  },
   1.926 +}
   1.927 +
   1.928 +XPCOMUtils.defineLazyGetter(ResponsiveUI.prototype, "strings", function () {
   1.929 +  return Services.strings.createBundle("chrome://browser/locale/devtools/responsiveUI.properties");
   1.930 +});

mercurial