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 145230 8b82a374ece51b3d874bbc41db97ef5b2e0c5026
parent 145229 acd1d842771828ac5546c8922740195100d6ec5c
child 145231 61a110ae7914c242fad4784f726b614d2099a7d9
push id2476
push userprouget@mozilla.com
push dateTue, 03 Sep 2013 07:16:13 +0000
treeherderfx-team@61a110ae7914 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmratcliffe
bugs777972
milestone26.0a1
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