Bug 777972 - [responsive mode] translate click events to touch events. r=mratcliffe
authorPaul Rouget <paul@mozilla.com>
Tue, 03 Sep 2013 09:15:51 +0200
changeset 145283 8b82a374ece51b3d874bbc41db97ef5b2e0c5026
parent 145282 acd1d842771828ac5546c8922740195100d6ec5c
child 145284 61a110ae7914c242fad4784f726b614d2099a7d9
push id25205
push useremorley@mozilla.com
push dateTue, 03 Sep 2013 11:14:02 +0000
treeherdermozilla-central@7ff96bd19c1c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmratcliffe
bugs777972
milestone26.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 777972 - [responsive mode] translate click events to touch events. r=mratcliffe
browser/devtools/responsivedesign/responsivedesign.jsm
browser/devtools/responsivedesign/test/Makefile.in
browser/devtools/responsivedesign/test/browser_responsiveui_touch.js
browser/devtools/responsivedesign/test/touch.html
browser/devtools/shared/touch-events.js
browser/locales/en-US/chrome/browser/devtools/responsiveUI.properties
browser/themes/linux/jar.mn
browser/themes/osx/jar.mn
browser/themes/shared/devtools/responsivedesign.inc.css
browser/themes/shared/devtools/responsiveui-touch.png
browser/themes/windows/jar.mn
--- a/browser/devtools/responsivedesign/responsivedesign.jsm
+++ b/browser/devtools/responsivedesign/responsivedesign.jsm
@@ -10,16 +10,17 @@ const Cu = Components.utils;
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 Cu.import("resource:///modules/devtools/FloatingScrollbars.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 
 var require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
 let Telemetry = require("devtools/shared/telemetry");
+let {TouchEventHandler} = require("devtools/shared/touch-events");
 
 this.EXPORTED_SYMBOLS = ["ResponsiveUIManager"];
 
 const MIN_WIDTH = 50;
 const MIN_HEIGHT = 50;
 
 const MAX_WIDTH = 10000;
 const MAX_HEIGHT = 10000;
@@ -111,16 +112,17 @@ function ResponsiveUI(aWindow, aTab)
   this.tab = aTab;
   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();
 
+
   // Try to load presets from prefs
   if (Services.prefs.prefHasUserValue("devtools.responsiveUI.presets")) {
     try {
       presets = JSON.parse(Services.prefs.getCharPref("devtools.responsiveUI.presets"));
     } catch(e) {
       // User pref is malformated.
       Cu.reportError("Could not parse pref `devtools.responsiveUI.presets`: " + e);
     }
@@ -151,21 +153,24 @@ function ResponsiveUI(aWindow, aTab)
 
     this.currentPresetKey = this.presets[1].key; // most common preset
   }
 
   this.container.setAttribute("responsivemode", "true");
   this.stack.setAttribute("responsivemode", "true");
 
   // Let's bind some callbacks.
+  this.bound_onPageLoad = this.onPageLoad.bind(this);
+  this.bound_onPageUnload = this.onPageUnload.bind(this);
   this.bound_presetSelected = this.presetSelected.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_onKeypress = this.onKeypress.bind(this);
 
   // Events
   this.tab.addEventListener("TabClose", this);
@@ -183,16 +188,28 @@ function ResponsiveUI(aWindow, aTab)
 
   if (this._floatingScrollbars)
     switchToFloatingScrollbars(this.tab);
 
   this.tab.__responsiveUI = this;
 
   this._telemetry.toolOpened("responsive");
 
+  // Touch events support
+  this.touchEnableBefore = false;
+  this.touchEventHandler = new TouchEventHandler(this.browser.contentWindow);
+
+  this.browser.addEventListener("load", this.bound_onPageLoad, true);
+  this.browser.addEventListener("unload", this.bound_onPageUnload, true);
+
+  if (this.browser.contentWindow.document &&
+      this.browser.contentWindow.document.readyState == "complete") {
+    this.onPageLoad();
+  }
+
   ResponsiveUIManager.emit("on", this.tab, this);
 }
 
 ResponsiveUI.prototype = {
   _transitionsEnabled: true,
   _floatingScrollbars: Services.appinfo.OS != "Darwin",
   get transitionsEnabled() this._transitionsEnabled,
   set transitionsEnabled(aValue) {
@@ -200,23 +217,44 @@ ResponsiveUI.prototype = {
     if (aValue && !this._resizing && this.stack.hasAttribute("responsivemode")) {
       this.stack.removeAttribute("notransition");
     } else if (!aValue) {
       this.stack.setAttribute("notransition", "true");
     }
   },
 
   /**
+   * Window onload / onunload
+   */
+   onPageLoad: function() {
+     this.touchEventHandler = new TouchEventHandler(this.browser.contentWindow);
+     if (this.touchEnableBefore) {
+       this.enableTouch();
+     }
+   },
+
+   onPageUnload: function() {
+     if (this.closing)
+       return;
+     this.touchEnableBefore = this.touchEventHandler.enabled;
+     this.disableTouch();
+     delete this.touchEventHandler;
+   },
+
+  /**
    * Destroy the nodes. Remove listeners. Reset the style.
    */
   close: function RUI_unload() {
     if (this.closing)
       return;
     this.closing = true;
 
+    this.browser.removeEventListener("load", this.bound_onPageLoad, true);
+    this.browser.removeEventListener("unload", this.bound_onPageUnload, true);
+
     if (this._floatingScrollbars)
       switchToNativeScrollbars(this.tab);
 
     this.unCheckMenus();
     // Reset style of the stack.
     let style = "max-width: none;" +
                 "min-width: 0;" +
                 "max-height: none;" +
@@ -228,31 +266,34 @@ ResponsiveUI.prototype = {
 
     // Remove listeners.
     this.mainWindow.document.removeEventListener("keypress", this.bound_onKeypress, false);
     this.menulist.removeEventListener("select", this.bound_presetSelected, true);
     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.touchbutton.removeEventListener("command", this.bound_touch, 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);
 
     // Removed elements.
     this.container.removeChild(this.toolbar);
     this.stack.removeChild(this.resizer);
     this.stack.removeChild(this.resizeBarV);
     this.stack.removeChild(this.resizeBarH);
 
     // Unset the responsive mode.
     this.container.removeAttribute("responsivemode");
     this.stack.removeAttribute("responsivemode");
 
     delete this.tab.__responsiveUI;
+    if (this.touchEventHandler)
+      this.touchEventHandler.stop();
     this._telemetry.toolClosed("responsive");
     ResponsiveUIManager.emit("off", this.tab, this);
   },
 
   /**
    * Handle keypressed.
    *
    * @param aEvent
@@ -352,25 +393,32 @@ ResponsiveUI.prototype = {
     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-toolbarbutton devtools-responsiveui-screenshot";
     this.screenshotbutton.addEventListener("command", this.bound_screenshot, true);
 
+    this.touchbutton = this.chromeDoc.createElement("toolbarbutton");
+    this.touchbutton.setAttribute("tabindex", "0");
+    this.touchbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.touch"));
+    this.touchbutton.className = "devtools-toolbarbutton devtools-responsiveui-touch";
+    this.touchbutton.addEventListener("command", this.bound_touch, true);
+
     this.closebutton = this.chromeDoc.createElement("toolbarbutton");
     this.closebutton.setAttribute("tabindex", "0");
     this.closebutton.className = "devtools-toolbarbutton devtools-responsiveui-close";
     this.closebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.close"));
     this.closebutton.addEventListener("command", this.bound_close, true);
 
     this.toolbar.appendChild(this.closebutton);
     this.toolbar.appendChild(this.menulist);
     this.toolbar.appendChild(this.rotatebutton);
+    this.toolbar.appendChild(this.touchbutton);
     this.toolbar.appendChild(this.screenshotbutton);
 
     // 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");
@@ -618,16 +666,47 @@ ResponsiveUI.prototype = {
     canvas.toBlob(blob => {
       let chromeWindow = this.chromeDoc.defaultView;
       let url = chromeWindow.URL.createObjectURL(blob);
       chromeWindow.saveURL(url, filename + ".png", null, true, true, document.documentURIObject, document);
     });
   },
 
   /**
+   * Enable/Disable mouse -> touch events translation.
+   */
+   enableTouch: function RUI_enableTouch() {
+     if (!this.touchEventHandler.enabled) {
+       let isReloadNeeded = this.touchEventHandler.start();
+       this.touchbutton.setAttribute("checked", "true");
+       return isReloadNeeded;
+     }
+     return false;
+   },
+
+   disableTouch: function RUI_disableTouch() {
+     if (this.touchEventHandler.enabled) {
+       this.touchEventHandler.stop();
+       this.touchbutton.removeAttribute("checked");
+     }
+   },
+
+   toggleTouch: function RUI_toggleTouch() {
+     if (this.touchEventHandler.enabled) {
+       this.disableTouch();
+     } else {
+       let isReloadNeeded = this.enableTouch();
+       if (isReloadNeeded) {
+         // Lightest way to reload I found:
+         this.browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+       }
+     }
+   },
+
+  /**
    * Change the size of the browser.
    *
    * @param aWidth width of the browser.
    * @param aHeight height of the browser.
    */
   setSize: function RUI_setSize(aWidth, aHeight) {
     aWidth = Math.min(Math.max(aWidth, MIN_WIDTH), MAX_WIDTH);
     aHeight = Math.min(Math.max(aHeight, MIN_HEIGHT), MAX_HEIGHT);
--- a/browser/devtools/responsivedesign/test/Makefile.in
+++ b/browser/devtools/responsivedesign/test/Makefile.in
@@ -11,12 +11,14 @@ relativesrcdir  = @relativesrcdir@
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_BROWSER_FILES := \
 		browser_responsiveui.js \
 		browser_responsiveuiaddcustompreset.js \
 		browser_responsiveruleview.js \
 		browser_responsive_cmd.js \
 		browser_responsivecomputedview.js \
+		browser_responsiveui_touch.js \
+		touch.html \
 		head.js \
 		$(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/responsivedesign/test/browser_responsiveui_touch.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+  let url = "http://mochi.test:8888/browser/browser/devtools/responsivedesign/test/touch.html";
+
+  let mgr = ResponsiveUI.ResponsiveUIManager;
+
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    waitForFocus(startTest, content);
+  }, true);
+
+  content.location = url;
+
+  function startTest() {
+    mgr.once("on", function() {executeSoon(testWithNoTouch)});
+    mgr.once("off", function() {executeSoon(finishUp)});
+    mgr.toggle(window, gBrowser.selectedTab);
+  }
+
+  function testWithNoTouch() {
+    let div = content.document.querySelector("div");
+    let x = 2, y = 2;
+    EventUtils.synthesizeMouse(div, x, y, {type: "mousedown"}, content);
+    x += 20; y += 10;
+    EventUtils.synthesizeMouse(div, x, y, {type: "mousemove"}, content);
+    is(div.style.transform, "", "touch didn't work");
+    EventUtils.synthesizeMouse(div, x, y, {type: "mouseup"}, content);
+    testWithTouch();
+  }
+
+  function testWithTouch() {
+    gBrowser.selectedTab.__responsiveUI.enableTouch();
+    let div = content.document.querySelector("div");
+    let x = 2, y = 2;
+    EventUtils.synthesizeMouse(div, x, y, {type: "mousedown"}, content);
+    x += 20; y += 10;
+    EventUtils.synthesizeMouse(div, x, y, {type: "mousemove"}, content);
+    is(div.style.transform, "translate(20px, 10px)", "touch worked");
+    EventUtils.synthesizeMouse(div, x, y, {type: "mouseup"}, content);
+    is(div.style.transform, "none", "end event worked");
+    mgr.toggle(window, gBrowser.selectedTab);
+  }
+
+  function testWithTouchAgain() {
+    gBrowser.selectedTab.__responsiveUI.disableTouch();
+    let div = content.document.querySelector("div");
+    let x = 2, y = 2;
+    EventUtils.synthesizeMouse(div, x, y, {type: "mousedown"}, content);
+    x += 20; y += 10;
+    EventUtils.synthesizeMouse(div, x, y, {type: "mousemove"}, content);
+    is(div.style.transform, "", "touch didn't work");
+    EventUtils.synthesizeMouse(div, x, y, {type: "mouseup"}, content);
+    finishUp();
+  }
+
+
+  function finishUp() {
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/responsivedesign/test/touch.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+
+<meta charset=utf-8 />
+<title>test</title>
+
+
+<style>
+  div {
+    border:1px solid red;
+    width: 100px; height: 100px;
+  }
+</style>
+
+<div></div>
+
+<script>
+  var div = document.querySelector("div");
+  var initX, initY;
+
+
+  div.addEventListener("touchstart", function(evt) {
+    var touch = evt.changedTouches[0];
+    initX = touch.pageX;
+    initY = touch.pageY;
+  }, true);
+
+  div.addEventListener("touchmove", function(evt) {
+    var touch = evt.changedTouches[0];
+    var deltaX = touch.pageX - initX;
+    var deltaY = touch.pageY - initY;
+    div.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
+  }, true);
+
+  div.addEventListener("touchend", function(evt) {
+    div.style.transform = "none";
+  }, true);
+</script>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/touch-events.js
@@ -0,0 +1,185 @@
+/* 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/. */
+
+let {CC, Cc, Ci, Cu, Cr} = require('chrome');
+
+Cu.import('resource://gre/modules/Services.jsm');
+
+let handlerCount = 0;
+
+let orig_w3c_touch_events = Services.prefs.getIntPref('dom.w3c_touch_events.enabled');
+
+// =================== Touch ====================
+// Simulate touch events on desktop
+function TouchEventHandler (window) {
+  let contextMenuTimeout = 0;
+
+  // This guard is used to not re-enter the events processing loop for
+  // self dispatched events
+  let ignoreEvents = false;
+
+  let threshold = 25;
+  try {
+    threshold = Services.prefs.getIntPref('ui.dragThresholdX');
+  } catch(e) {}
+
+  let delay = 500;
+  try {
+    delay = Services.prefs.getIntPref('ui.click_hold_context_menus.delay');
+  } catch(e) {}
+
+  let TouchEventHandler = {
+    enabled: false,
+    events: ['mousedown', 'mousemove', 'mouseup', 'click'],
+    start: function teh_start() {
+      let isReloadNeeded = Services.prefs.getIntPref('dom.w3c_touch_events.enabled') != 1;
+      handlerCount++;
+      Services.prefs.setIntPref('dom.w3c_touch_events.enabled', 1);
+      this.enabled = true;
+      this.events.forEach((function(evt) {
+        window.addEventListener(evt, this, true);
+      }).bind(this));
+      return isReloadNeeded;
+    },
+    stop: function teh_stop() {
+      handlerCount--;
+      if (handlerCount == 0)
+        Services.prefs.setIntPref('dom.w3c_touch_events.enabled', orig_w3c_touch_events);
+      this.enabled = false;
+      this.events.forEach((function(evt) {
+        window.removeEventListener(evt, this, true);
+      }).bind(this));
+    },
+    handleEvent: function teh_handleEvent(evt) {
+      if (evt.button || ignoreEvents ||
+          evt.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_UNKNOWN)
+        return;
+
+      // The gaia system window use an hybrid system even on the device which is
+      // a mix of mouse/touch events. So let's not cancel *all* mouse events
+      // if it is the current target.
+      let content = evt.target.ownerDocument.defaultView;
+      let isSystemWindow = content.location.toString().indexOf("system.gaiamobile.org") != -1;
+
+      let eventTarget = this.target;
+      let type = '';
+      switch (evt.type) {
+        case 'mousedown':
+          this.target = evt.target;
+
+          contextMenuTimeout =
+            this.sendContextMenu(evt.target, evt.pageX, evt.pageY, delay);
+
+          this.cancelClick = false;
+          this.startX = evt.pageX;
+          this.startY = evt.pageY;
+
+          // Capture events so if a different window show up the events
+          // won't be dispatched to something else.
+          evt.target.setCapture(false);
+
+          type = 'touchstart';
+          break;
+
+        case 'mousemove':
+          if (!eventTarget)
+            return;
+
+          if (!this.cancelClick) {
+            if (Math.abs(this.startX - evt.pageX) > threshold ||
+                Math.abs(this.startY - evt.pageY) > threshold) {
+              this.cancelClick = true;
+              content.clearTimeout(contextMenuTimeout);
+            }
+          }
+
+          type = 'touchmove';
+          break;
+
+        case 'mouseup':
+          if (!eventTarget)
+            return;
+          this.target = null;
+
+          content.clearTimeout(contextMenuTimeout);
+          type = 'touchend';
+          break;
+
+        case 'click':
+          // Mouse events has been cancelled so dispatch a sequence
+          // of events to where touchend has been fired
+          evt.preventDefault();
+          evt.stopImmediatePropagation();
+
+          if (this.cancelClick)
+            return;
+
+          ignoreEvents = true;
+          content.setTimeout(function dispatchMouseEvents(self) {
+            self.fireMouseEvent('mousedown', evt);
+            self.fireMouseEvent('mousemove', evt);
+            self.fireMouseEvent('mouseup', evt);
+            ignoreEvents = false;
+         }, 0, this);
+
+          return;
+      }
+
+      let target = eventTarget || this.target;
+      if (target && type) {
+        this.sendTouchEvent(evt, target, type);
+      }
+
+      if (!isSystemWindow) {
+        evt.preventDefault();
+        evt.stopImmediatePropagation();
+      }
+    },
+    fireMouseEvent: function teh_fireMouseEvent(type, evt)  {
+      let content = evt.target.ownerDocument.defaultView;
+      var utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindowUtils);
+      utils.sendMouseEvent(type, evt.clientX, evt.clientY, 0, 1, 0, true);
+    },
+    sendContextMenu: function teh_sendContextMenu(target, x, y, delay) {
+      let doc = target.ownerDocument;
+      let evt = doc.createEvent('MouseEvent');
+      evt.initMouseEvent('contextmenu', true, true, doc.defaultView,
+                         0, x, y, x, y, false, false, false, false,
+                         0, null);
+
+      let content = target.ownerDocument.defaultView;
+      let timeout = content.setTimeout((function contextMenu() {
+        target.dispatchEvent(evt);
+        this.cancelClick = true;
+      }).bind(this), delay);
+
+      return timeout;
+    },
+    sendTouchEvent: function teh_sendTouchEvent(evt, target, name) {
+      let document = target.ownerDocument;
+      let content = document.defaultView;
+
+      let touchEvent = document.createEvent('touchevent');
+      let point = document.createTouch(content, target, 0,
+                                       evt.pageX, evt.pageY,
+                                       evt.screenX, evt.screenY,
+                                       evt.clientX, evt.clientY,
+                                       1, 1, 0, 0);
+      let touches = document.createTouchList(point);
+      let targetTouches = touches;
+      let changedTouches = touches;
+      touchEvent.initTouchEvent(name, true, true, content, 0,
+                                false, false, false, false,
+                                touches, targetTouches, changedTouches);
+      target.dispatchEvent(touchEvent);
+      return touchEvent;
+    }
+  };
+
+  return TouchEventHandler;
+}
+
+exports.TouchEventHandler = TouchEventHandler;
--- a/browser/locales/en-US/chrome/browser/devtools/responsiveUI.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/responsiveUI.properties
@@ -18,16 +18,19 @@ responsiveUI.rotate2=Rotate
 # LOCALIZATION NOTE  (responsiveUI.screenshot): tooltip of the screenshot button.
 responsiveUI.screenshot=Screenshot
 
 # LOCALIZATION NOTE (responsiveUI.screenshotGeneratedFilename): The auto generated filename.
 # The first argument (%1$S) is the date string in yyyy-mm-dd format and the second
 # argument (%2$S) is the time string in HH.MM.SS format.
 responsiveUI.screenshotGeneratedFilename=Screen Shot %1$S at %2$S
 
+# LOCALIZATION NOTE  (responsiveUI.touch): tooltip of the touch button.
+responsiveUI.touch=Simulate touch events (might trigger a reload)
+
 # LOCALIZATION NOTE  (responsiveUI.addPreset): label of the add preset button.
 responsiveUI.addPreset=Add Preset
 
 # LOCALIZATION NOTE  (responsiveUI.removePreset): label of the remove preset button.
 responsiveUI.removePreset=Remove Preset
 
 # LOCALIZATION NOTE  (responsiveUI.customResolution): label of the first item
 # in the menulist at the beginning of the toolbar. For %S is replace with the
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -218,16 +218,17 @@ browser.jar:
   skin/classic/browser/devtools/close.png                 (devtools/close.png)
   skin/classic/browser/devtools/vview-delete.png          (devtools/vview-delete.png)
   skin/classic/browser/devtools/vview-edit.png            (devtools/vview-edit.png)
   skin/classic/browser/devtools/undock.png                (devtools/undock.png)
   skin/classic/browser/devtools/font-inspector.css        (devtools/font-inspector.css)
   skin/classic/browser/devtools/computedview.css          (devtools/computedview.css)
   skin/classic/browser/devtools/arrow-e.png               (devtools/arrow-e.png)
   skin/classic/browser/devtools/responsiveui-rotate.png   (../shared/devtools/responsiveui-rotate.png)
+  skin/classic/browser/devtools/responsiveui-touch.png    (../shared/devtools/responsiveui-touch.png)
   skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png)
 #ifdef MOZ_SERVICES_SYNC
   skin/classic/browser/sync-16-throbber.png
   skin/classic/browser/sync-16.png
   skin/classic/browser/sync-24-throbber.png
   skin/classic/browser/sync-32.png
   skin/classic/browser/sync-bg.png
   skin/classic/browser/sync-128.png
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -308,16 +308,17 @@ browser.jar:
   skin/classic/browser/devtools/close.png                   (devtools/close.png)
   skin/classic/browser/devtools/vview-delete.png            (devtools/vview-delete.png)
   skin/classic/browser/devtools/vview-edit.png              (devtools/vview-edit.png)
   skin/classic/browser/devtools/undock.png                  (devtools/undock.png)
   skin/classic/browser/devtools/font-inspector.css          (devtools/font-inspector.css)
   skin/classic/browser/devtools/computedview.css            (devtools/computedview.css)
   skin/classic/browser/devtools/arrow-e.png                 (devtools/arrow-e.png)
   skin/classic/browser/devtools/responsiveui-rotate.png     (../shared/devtools/responsiveui-rotate.png)
+  skin/classic/browser/devtools/responsiveui-touch.png      (../shared/devtools/responsiveui-touch.png)
   skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png)
 #ifdef MOZ_SERVICES_SYNC
   skin/classic/browser/sync-throbber.png
   skin/classic/browser/sync-16.png
   skin/classic/browser/sync-32.png
   skin/classic/browser/sync-bg.png
   skin/classic/browser/sync-128.png
   skin/classic/browser/sync-desktopIcon.png
--- a/browser/themes/shared/devtools/responsivedesign.inc.css
+++ b/browser/themes/shared/devtools/responsivedesign.inc.css
@@ -37,16 +37,25 @@
 .devtools-responsiveui-close {
   list-style-image: url("chrome://browser/skin/devtools/close.png");
 }
 
 .devtools-responsiveui-rotate {
   list-style-image: url("chrome://browser/skin/devtools/responsiveui-rotate.png");
 }
 
+.devtools-responsiveui-touch {
+  list-style-image: url("chrome://browser/skin/devtools/responsiveui-touch.png");
+  -moz-image-region: rect(0px,16px,16px,0px);
+}
+
+.devtools-responsiveui-touch[checked] {
+  -moz-image-region: rect(0px,32px,16px,16px);
+}
+
 .devtools-responsiveui-screenshot {
   list-style-image: url("chrome://browser/skin/devtools/responsiveui-screenshot.png");
 }
 
 .devtools-responsiveui-resizebarV {
   width: 7px;
   height: 24px;
   cursor: ew-resize;
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c25b44d04402adadda813d70c849c1c19507e161
GIT binary patch
literal 834
zc$@)31HJr-P)<h;3K|Lk000e1NJLTq001BW000mO1^@s6cL04^0009CNkl<ZNXL!T
zdq`7Z6aet~K#IuTKIVLtZKkE@Qr3J6si=WvVE*AFshL`tkUhvInDl^XS&f<IWAlk-
zlrqhc{$Ut}5ENOWD=Q5%+aDTj(-~jqtUDRv&2R8y=R5bFd(L*hdpVqCA3}&7H~^%e
zl!@&)l%MpM#2#b-1*l|Vd)j)=;^JcR*vIiae&D_|gt5tFN}#Pz9hlG9-#$_98ZF6l
zjgsVJ47nrJ1L*aJ;s8->-0a+31H#y7oG+%WkMEt6^Wut*B|e&(K^Vtte#xl^fTo32
zbq!nLZy!P!V~)07)G>9}FS{-$Y2Xh*ep$6Sn4=y*+0=HF8Q=v{HMei6u5Zkyt><6u
zF7nN)$&KruCkSKym9Ep&18n9A_S!je*Fy8>2xG_v)7GzX4-ojI$TDIzdV+io-jTV4
z<)-itD>hMG02<mdPeVUL0UNWDb^xr0X6X^?S%P9(zUpF{KkFb@0EE<oKw}N#8RH(J
zx(zUZ=!uRXapymgP2)O5JqR>YFVn-QK&uTPeE6MM3+<RT6D!AbVx@Kl&JnLRz+)Dj
z4124F>%kt70K!3jK+6PKNl@#gnIOHo2<t4(Y=8vRfkw>znE8ouI~&*+*-Uc>e%k%W
zz?-i|gvv3!ut95t+-*VSJA<$46JZm0eNc--n+I4}Fa^U!+dwPAoWWpJLfpazYE+hA
z?_b(i?pv=VKDDD}kW=lf`e52oGfLR(dN8s8=OtQpuB*3SNanecvNOZ)M`W1$t=HE;
zBNZDwZutjz(Pu+=iBg$YUgMDGjlpTim3hhEm^>?o2^&1VGypS%1K_Q5@{YxIkETz`
z+>#Y_s(VYU_J9pH;?xFi_{pGcp_zwZ9p!Q9=_FVFmay@-*!^Yc7A$*?ZSdR4%Ri*6
zTO);Es(^;o7P!W|v-fTx%X`2EF6`(YfBA&l1@$+VTSF%2ir2&f*N~rdPmoHMHX+*!
z1qeYZNVk&8<Zb`=6m3tsw$Vj&JeeC-aNaGVunb(mZ(L&@kK=iN0i$xzr-V<Uk^lez
M07*qoM6N<$g7?gkw*UYD
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -245,16 +245,17 @@ browser.jar:
         skin/classic/browser/devtools/close.png                     (devtools/close.png)
         skin/classic/browser/devtools/vview-delete.png              (devtools/vview-delete.png)
         skin/classic/browser/devtools/vview-edit.png                (devtools/vview-edit.png)
         skin/classic/browser/devtools/undock.png                    (devtools/undock.png)
         skin/classic/browser/devtools/font-inspector.css            (devtools/font-inspector.css)
         skin/classic/browser/devtools/computedview.css              (devtools/computedview.css)
         skin/classic/browser/devtools/arrow-e.png                   (devtools/arrow-e.png)
         skin/classic/browser/devtools/responsiveui-rotate.png       (../shared/devtools/responsiveui-rotate.png)
+        skin/classic/browser/devtools/responsiveui-touch.png        (../shared/devtools/responsiveui-touch.png)
         skin/classic/browser/devtools/responsiveui-screenshot.png   (../shared/devtools/responsiveui-screenshot.png)
 #ifdef MOZ_SERVICES_SYNC
         skin/classic/browser/sync-throbber.png
         skin/classic/browser/sync-16.png
         skin/classic/browser/sync-32.png
         skin/classic/browser/sync-128.png
         skin/classic/browser/sync-bg.png
         skin/classic/browser/sync-desktopIcon.png
@@ -506,16 +507,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/close.png                 (devtools/close.png)
         skin/classic/aero/browser/devtools/vview-delete.png          (devtools/vview-delete.png)
         skin/classic/aero/browser/devtools/vview-edit.png            (devtools/vview-edit.png)
         skin/classic/aero/browser/devtools/undock.png                (devtools/undock.png)
         skin/classic/aero/browser/devtools/font-inspector.css        (devtools/font-inspector.css)
         skin/classic/aero/browser/devtools/computedview.css          (devtools/computedview.css)
         skin/classic/aero/browser/devtools/arrow-e.png               (devtools/arrow-e.png)
         skin/classic/aero/browser/devtools/responsiveui-rotate.png   (../shared/devtools/responsiveui-rotate.png)
+        skin/classic/aero/browser/devtools/responsiveui-touch.png    (../shared/devtools/responsiveui-touch.png)
         skin/classic/aero/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png)
 #ifdef MOZ_SERVICES_SYNC
         skin/classic/aero/browser/sync-throbber.png
         skin/classic/aero/browser/sync-16.png
         skin/classic/aero/browser/sync-32.png
         skin/classic/aero/browser/sync-128.png
         skin/classic/aero/browser/sync-bg.png
         skin/classic/aero/browser/sync-desktopIcon.png