Bug 1239461 - Screenshot button for taking a screenshot of the current viewport; r=jryans draft
authorMatteo Ferretti <mferretti@mozilla.com>
Wed, 30 Mar 2016 22:31:37 +0200
changeset 348309 cfb3701b5edd3d377439ddda60aa3997b94d0183
parent 348308 4e9643a989b9bec3014c76de9d7cdce21b224e81
child 348310 af8eb32cce92e6e52850eff559bbf79aa1dbbbec
push id14805
push userbmo:jacheng@mozilla.com
push dateThu, 07 Apr 2016 07:37:41 +0000
reviewersjryans
bugs1239461
milestone48.0a1
Bug 1239461 - Screenshot button for taking a screenshot of the current viewport; r=jryans MozReview-Commit-ID: AMbzmf1uO0P
devtools/client/locales/en-US/responsive.properties
devtools/client/responsive.html/actions/index.js
devtools/client/responsive.html/actions/moz.build
devtools/client/responsive.html/actions/screenshot.js
devtools/client/responsive.html/app.js
devtools/client/responsive.html/audio/camera-click.mp3
devtools/client/responsive.html/audio/moz.build
devtools/client/responsive.html/components/device-selector.js
devtools/client/responsive.html/components/global-toolbar.js
devtools/client/responsive.html/components/moz.build
devtools/client/responsive.html/components/resizable-viewport.js
devtools/client/responsive.html/components/utils/l10n.js
devtools/client/responsive.html/components/utils/moz.build
devtools/client/responsive.html/components/viewport.js
devtools/client/responsive.html/components/viewports.js
devtools/client/responsive.html/images/moz.build
devtools/client/responsive.html/images/screenshot.svg
devtools/client/responsive.html/index.css
devtools/client/responsive.html/index.js
devtools/client/responsive.html/moz.build
devtools/client/responsive.html/reducers.js
devtools/client/responsive.html/reducers/moz.build
devtools/client/responsive.html/reducers/screenshot.js
devtools/client/responsive.html/test/browser/browser.ini
devtools/client/responsive.html/test/browser/browser_screenshot_button.js
devtools/client/responsive.html/types.js
devtools/client/responsive.html/utils/l10n.js
devtools/client/responsive.html/utils/moz.build
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -16,8 +16,17 @@
 responsive.title=Responsive Design Mode
 
 # LOCALIZATION NOTE (responsive.exit): tooltip text of the exit button.
 responsive.exit=Close Responsive Design Mode
 
 # LOCALIZATION NOTE (responsive.noDeviceSelected): placeholder text for the
 # device selector
 responsive.noDeviceSelected=no device selected
+
+# LOCALIZATION NOTE  (responsive.screenshot): tooltip of the screenshot button.
+responsive.screenshot=Take a screenshot of the viewport
+
+# LOCALIZATION NOTE (responsive.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.
+responsive.screenshotGeneratedFilename=Screen Shot %1$S at %2$S
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -27,16 +27,22 @@ createEnum([
   "CHANGE_LOCATION",
 
   // Resize the viewport.
   "RESIZE_VIEWPORT",
 
   // Rotate the viewport.
   "ROTATE_VIEWPORT",
 
+  // Take a screenshot of the viewport.
+  "TAKE_SCREENSHOT_START",
+
+  // Indicates when the screenshot action ends.
+  "TAKE_SCREENSHOT_END",
+
 ], module.exports);
 
 /**
  * Create a simple enum-like object with keys mirrored to values from an array.
  * This makes comparison to a specfic value simpler without having to repeat and
  * mis-type the value.
  */
 function createEnum(array, target) {
--- a/devtools/client/responsive.html/actions/moz.build
+++ b/devtools/client/responsive.html/actions/moz.build
@@ -3,10 +3,11 @@
 # 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/.
 
 DevToolsModules(
     'devices.js',
     'index.js',
     'location.js',
+    'screenshot.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/actions/screenshot.js
@@ -0,0 +1,89 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const {
+  TAKE_SCREENSHOT_START,
+  TAKE_SCREENSHOT_END,
+} = require("./index");
+
+const { getRect } = require("devtools/shared/layout/utils");
+const { getFormatStr } = require("../utils/l10n");
+const { getToplevelWindow } = require("sdk/window/utils");
+const { Task: { spawn, async } } = require("resource://gre/modules/Task.jsm");
+
+const BASE_URL = "resource://devtools/client/responsive.html";
+const audioCamera = new window.Audio(`${BASE_URL}/audio/camera-click.mp3`);
+
+function getFileName() {
+  let date = new Date();
+  let month = ("0" + (date.getMonth() + 1)).substr(-2);
+  let day = ("0" + date.getDate()).substr(-2);
+  let dateString = [date.getFullYear(), month, day].join("-");
+  let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
+
+  return getFormatStr("responsive.screenshotGeneratedFilename", dateString,
+                      timeString);
+}
+
+function createScreenshotFor(node) {
+  let { top, left, width, height } = getRect(window, node, window);
+
+  const canvas = document.createElementNS(HTML_NS, "canvas");
+  const ctx = canvas.getContext("2d");
+  const ratio = window.devicePixelRatio;
+  canvas.width = width * ratio;
+  canvas.height = height * ratio;
+  ctx.scale(ratio, ratio);
+  ctx.drawWindow(window, left, top, width, height, "#fff");
+
+  return canvas.toDataURL("image/png", "");
+}
+
+function saveToFile(data, filename) {
+  return spawn(function* () {
+    const chromeWindow = getToplevelWindow(window);
+    const chromeDocument = chromeWindow.document;
+
+    // append .png extension to filename if it doesn't exist
+    filename = filename.replace(/\.png$|$/i, ".png");
+
+    chromeWindow.saveURL(data, filename, null,
+                         true, true,
+                         chromeDocument.documentURIObject, chromeDocument);
+  });
+}
+
+function simulateCameraEffects(node) {
+  audioCamera.play();
+  node.animate({ opacity: [ 0, 1 ] }, 500);
+}
+
+module.exports = {
+
+  takeScreenshot() {
+    return function* (dispatch, getState) {
+      yield dispatch({ type: TAKE_SCREENSHOT_START });
+
+      // Waiting the next repaint, to ensure the react components
+      // can be properly render after the action dispatched above
+      window.requestAnimationFrame(async(function* () {
+        let iframe = document.querySelector("iframe");
+        let data = createScreenshotFor(iframe);
+
+        simulateCameraEffects(iframe);
+
+        yield saveToFile(data, getFileName());
+
+        dispatch({ type: TAKE_SCREENSHOT_END });
+      }));
+    };
+  }
+
+};
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -1,74 +1,90 @@
 /* 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/. */
 
+ /* eslint-env browser */
+
 "use strict";
 
 const { createClass, createFactory, PropTypes, DOM: dom } =
   require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const {
   changeDevice,
   resizeViewport,
   rotateViewport
 } = require("./actions/viewports");
+const { takeScreenshot } = require("./actions/screenshot");
 const Types = require("./types");
 const Viewports = createFactory(require("./components/viewports"));
 const GlobalToolbar = createFactory(require("./components/global-toolbar"));
 
 let App = createClass({
 
   displayName: "App",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
-    onExit: PropTypes.func.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
   },
 
   onChangeViewportDevice(id, device) {
     this.props.dispatch(changeDevice(id, device));
   },
 
+  onExit() {
+    window.postMessage({ type: "exit" }, "*");
+  },
+
   onResizeViewport(id, width, height) {
     this.props.dispatch(resizeViewport(id, width, height));
   },
 
   onRotateViewport(id) {
     this.props.dispatch(rotateViewport(id));
   },
 
+  onScreenshot() {
+    this.props.dispatch(takeScreenshot());
+  },
+
   render() {
     let {
       devices,
       location,
+      screenshot,
       viewports,
-      onExit,
     } = this.props;
 
     let {
       onChangeViewportDevice,
+      onExit,
       onResizeViewport,
       onRotateViewport,
+      onScreenshot,
     } = this;
 
     return dom.div(
       {
         id: "app",
       },
       GlobalToolbar({
+        screenshot,
         onExit,
+        onScreenshot,
       }),
       Viewports({
         devices,
         location,
+        screenshot,
         viewports,
         onChangeViewportDevice,
         onRotateViewport,
         onResizeViewport,
       })
     );
   },
 
new file mode 100644
index 0000000000000000000000000000000000000000..6d9af013315d873e910ecf6e15bc298093dc1e99
GIT binary patch
literal 27634
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
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/audio/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+    'camera-click.mp3',
+)
--- a/devtools/client/responsive.html/components/device-selector.js
+++ b/devtools/client/responsive.html/components/device-selector.js
@@ -1,15 +1,15 @@
 /* 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/. */
 
 "use strict";
 
-const { getStr } = require("./utils/l10n");
+const { getStr } = require("../utils/l10n");
 const { DOM: dom, createClass, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
 
 const Types = require("../types");
 
 module.exports = createClass({
 
   displayName: "DeviceSelector",
--- a/devtools/client/responsive.html/components/global-toolbar.js
+++ b/devtools/client/responsive.html/components/global-toolbar.js
@@ -1,44 +1,56 @@
 /* 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/. */
 
 "use strict";
 
-const { getStr } = require("./utils/l10n");
+const { getStr } = require("../utils/l10n");
 const { DOM: dom, createClass, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
+const Types = require("../types");
 
 module.exports = createClass({
 
   displayName: "GlobalToolbar",
 
-  mixins: [ addons.PureRenderMixin ],
-
   propTypes: {
     onExit: PropTypes.func.isRequired,
+    onScreenshot: PropTypes.func.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
   },
 
+  mixins: [ addons.PureRenderMixin ],
+
   render() {
     let {
       onExit,
+      onScreenshot,
+      screenshot,
     } = this.props;
 
     return dom.header(
       {
         id: "global-toolbar",
         className: "toolbar",
       },
       dom.span(
         {
           className: "title",
         },
         getStr("responsive.title")),
       dom.button({
+        id: "global-screenshot-button",
+        className: "toolbar-button devtools-button",
+        title: getStr("responsive.screenshot"),
+        onClick: onScreenshot,
+        disabled: screenshot.isCapturing,
+      }),
+      dom.button({
         id: "global-exit-button",
         className: "toolbar-button devtools-button",
         title: getStr("responsive.exit"),
         onClick: onExit,
       })
     );
   },
 
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -1,18 +1,14 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
-DIRS += [
-    'utils',
-]
-
 DevToolsModules(
     'browser.js',
     'device-selector.js',
     'global-toolbar.js',
     'resizable-viewport.js',
     'viewport-dimension.js',
     'viewport-toolbar.js',
     'viewport.js',
--- a/devtools/client/responsive.html/components/resizable-viewport.js
+++ b/devtools/client/responsive.html/components/resizable-viewport.js
@@ -19,16 +19,17 @@ const VIEWPORT_MIN_HEIGHT = Constants.MI
 
 module.exports = createClass({
 
   displayName: "ResizableViewport",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
   getInitialState() {
     return {
@@ -107,22 +108,29 @@ module.exports = createClass({
       lastClientY
     });
   },
 
   render() {
     let {
       devices,
       location,
+      screenshot,
       viewport,
       onChangeViewportDevice,
       onResizeViewport,
       onRotateViewport,
     } = this.props;
 
+    let resizeHandleClass = "viewport-resize-handle";
+
+    if (screenshot.isCapturing) {
+      resizeHandleClass += " hidden";
+    }
+
     return dom.div(
       {
         className: "resizable-viewport",
       },
       ViewportToolbar({
         devices,
         selectedDevice: viewport.device,
         onChangeViewportDevice,
@@ -131,17 +139,17 @@ module.exports = createClass({
       }),
       Browser({
         location,
         width: viewport.width,
         height: viewport.height,
         isResizing: this.state.isResizing
       }),
       dom.div({
-        className: "viewport-resize-handle",
+        className: resizeHandleClass,
         onMouseDown: this.onResizeStart,
       }),
       dom.div({
         ref: "resizeBarX",
         className: "viewport-horizontal-resize-handle",
         onMouseDown: this.onResizeStart,
       }),
       dom.div({
--- a/devtools/client/responsive.html/components/viewport.js
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -13,16 +13,17 @@ const ViewportDimension = createFactory(
 
 module.exports = createClass({
 
   displayName: "Viewport",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
   onChangeViewportDevice(device) {
     let {
@@ -50,32 +51,34 @@ module.exports = createClass({
 
     onRotateViewport(viewport.id);
   },
 
   render() {
     let {
       devices,
       location,
+      screenshot,
       viewport,
     } = this.props;
 
     let {
       onChangeViewportDevice,
       onRotateViewport,
       onResizeViewport,
     } = this;
 
     return dom.div(
       {
         className: "viewport",
       },
       ResizableViewport({
         devices,
         location,
+        screenshot,
         viewport,
         onChangeViewportDevice,
         onResizeViewport,
         onRotateViewport,
       }),
       ViewportDimension({
         viewport,
         onChangeViewportDevice,
--- a/devtools/client/responsive.html/components/viewports.js
+++ b/devtools/client/responsive.html/components/viewports.js
@@ -12,41 +12,44 @@ const Viewport = createFactory(require("
 
 module.exports = createClass({
 
   displayName: "Viewports",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
   render() {
     let {
       devices,
       location,
+      screenshot,
       viewports,
       onChangeViewportDevice,
       onResizeViewport,
       onRotateViewport,
     } = this.props;
 
     return dom.div(
       {
         id: "viewports",
       },
       viewports.map(viewport => {
         return Viewport({
           key: viewport.id,
           devices,
           location,
+          screenshot,
           viewport,
           onChangeViewportDevice,
           onResizeViewport,
           onRotateViewport,
         });
       })
     );
   },
--- a/devtools/client/responsive.html/images/moz.build
+++ b/devtools/client/responsive.html/images/moz.build
@@ -3,10 +3,11 @@
 # 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/.
 
 DevToolsModules(
     'close.svg',
     'grippers.svg',
     'rotate-viewport.svg',
+    'screenshot.svg',
     'select-arrow.svg',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/images/screenshot.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#babec3">
+  <path d="M15.5 14H.5c-.3 0-.5-.2-.5-.5v-8c0-.3.2-.5.5-.5H4V2.5c0-.3.2-.5.5-.5h7c.3 0 .5.2.5.5V5h3.5c.3 0 .5.2.5.5v8c0 .3-.2.5-.5.5zM1 13h14V6h-3.5c-.3 0-.5-.2-.5-.5V3H5v2.5c0 .3-.2.5-.5.5H1v7z"/>
+  <path d="M8 12c-1.6 0-2.9-1.3-2.9-2.9S6.4 6.2 8 6.2c1.6 0 2.9 1.3 2.9 2.9S9.6 12 8 12zm0-4.8c-1.1 0-1.9.8-1.9 1.9 0 1.1.8 1.9 1.9 1.9 1.1 0 1.9-.9 1.9-1.9C9.9 8 9.1 7.2 8 7.2z"/>
+</svg>
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -86,27 +86,38 @@ body {
   border-right: 1px solid var(--theme-splitter-color);
   padding: 1px 6px 0 2px;
 }
 
 #global-toolbar .toolbar-button {
   margin: 0 0 0 5px;
 }
 
-#global-exit-button,
-#global-exit-button::before {
+#global-toolbar .toolbar-button,
+#global-toolbar .toolbar-button::before {
   width: 12px;
   height: 12px;
 }
 
+#global-screenshot-button::before {
+  background-image: url("./images/screenshot.svg");
+  margin: -6px 0 0 -6px;
+}
+
 #global-exit-button::before {
   background-image: url("./images/close.svg");
   margin: -6px 0 0 -6px;
 }
 
+#global-screenshot-button:disabled {
+  filter: url("chrome://devtools/skin/images/filters.svg#checked-icon-state");
+  opacity: 1 !important;
+}
+
+
 #viewports {
   /* Snap to the top of the app when there isn't enough vertical space anymore
      to center the viewports (so we don't loose the toolbar) */
   position: sticky;
   top: 0;
   /* Make sure left-most viewport is visible when there's horizontal overflow.
      That is, when the horizontal space become smaller than the viewports and a
      scrollbar appears, then the first viewport will still be visible */
@@ -215,16 +226,20 @@ body {
   background-image: url("./images/grippers.svg");
   background-position: bottom right;
   padding: 0 1px 1px 0;
   background-repeat: no-repeat;
   background-origin: content-box;
   cursor: se-resize;
 }
 
+.viewport-resize-handle.hidden {
+  display: none;
+}
+
 .viewport-horizontal-resize-handle {
   position: absolute;
   width: 5px;
   height: calc(100% - 16px);
   right: -4px;
   top: 0;
   cursor: e-resize;
 }
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -37,20 +37,18 @@ let bootstrap = {
   init() {
     // Load a special UA stylesheet to reset certain styles such as dropdown
     // lists.
     loadSheet(window,
               "resource://devtools/client/responsive.html/responsive-ua.css",
               "agent");
     this.telemetry.toolOpened("responsive");
     let store = this.store = Store();
-    let app = App({
-      onExit: () => window.postMessage({ type: "exit" }, "*"),
-    });
-    let provider = createElement(Provider, { store }, app);
+    let provider = createElement(Provider, { store }, App());
+
     ReactDOM.render(provider, document.querySelector("#root"));
     this.initDevices();
     window.postMessage({ type: "init" }, "*");
   },
 
   destroy() {
     this.store = null;
     this.telemetry.toolClosed("responsive");
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/moz.build
@@ -1,19 +1,21 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DIRS += [
     'actions',
+    'audio',
     'components',
     'images',
     'reducers',
+    'utils',
 ]
 
 DevToolsModules(
     'app.js',
     'constants.js',
     'index.css',
     'manager.js',
     'reducers.js',
--- a/devtools/client/responsive.html/reducers.js
+++ b/devtools/client/responsive.html/reducers.js
@@ -1,9 +1,10 @@
 /* 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/. */
 
 "use strict";
 
 exports.devices = require("./reducers/devices");
 exports.location = require("./reducers/location");
+exports.screenshot = require("./reducers/screenshot");
 exports.viewports = require("./reducers/viewports");
--- a/devtools/client/responsive.html/reducers/moz.build
+++ b/devtools/client/responsive.html/reducers/moz.build
@@ -2,10 +2,11 @@
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
     'devices.js',
     'location.js',
+    'screenshot.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/screenshot.js
@@ -0,0 +1,31 @@
+/* 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/. */
+
+"use strict";
+
+const {
+  TAKE_SCREENSHOT_END,
+  TAKE_SCREENSHOT_START,
+} = require("../actions/index");
+
+const INITIAL_SCREENSHOT = { isCapturing: false };
+
+let reducers = {
+
+  [TAKE_SCREENSHOT_END](screenshot, action) {
+    return Object.assign({}, screenshot, { isCapturing: false });
+  },
+
+  [TAKE_SCREENSHOT_START](screenshot, action) {
+    return Object.assign({}, screenshot, { isCapturing: true });
+  },
+};
+
+module.exports = function(screenshot = INITIAL_SCREENSHOT, action) {
+  let reducer = reducers[action.type];
+  if (!reducer) {
+    return screenshot;
+  }
+  return reducer(screenshot, action);
+};
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -3,9 +3,10 @@ tags = devtools
 subsuite = devtools
 support-files =
   devices.json
   head.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/framework/test/shared-redux-head.js
 
 [browser_exit_button.js]
+[browser_screenshot_button.js]
 [browser_viewport_basics.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_screenshot_button.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test global exit button
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+const { OS } = require("resource://gre/modules/osfile.jsm");
+
+function* waitUntilScreenshot() {
+  return new Promise(Task.async(function* (resolve) {
+    let { Downloads } = require("resource://gre/modules/Downloads.jsm");
+    let list = yield Downloads.getList(Downloads.ALL);
+
+    let view = {
+      onDownloadAdded: download => {
+        download.whenSucceeded().then(() => {
+          resolve(download.target.path);
+          list.removeView(view);
+        });
+      }
+    };
+
+    yield list.addView(view);
+  }));
+}
+
+addRDMTask(TEST_URL, function* ({ ui: {toolWindow} }) {
+  let { store, document } = toolWindow;
+
+  // Wait until the viewport has been added
+  yield waitUntilState(store, state => state.viewports.length == 1);
+
+  info("Click the screenshot button");
+  let screenshotButton = document.getElementById("global-screenshot-button");
+  screenshotButton.click();
+
+  let whenScreenshotSucceeded = waitUntilScreenshot();
+
+  let filePath = yield whenScreenshotSucceeded;
+  let image = new Image();
+  image.src = OS.Path.toFileURI(filePath);
+
+  yield once(image, "load");
+
+  // We have only one viewport at the moment
+  let viewport = store.getState().viewports[0];
+  let ratio = window.devicePixelRatio;
+
+  is(image.width, viewport.width * ratio,
+    "screenshot width has the expected width");
+
+  is(image.height, viewport.height * ratio,
+    "screenshot width has the expected height");
+
+  yield OS.File.remove(filePath);
+});
--- a/devtools/client/responsive.html/types.js
+++ b/devtools/client/responsive.html/types.js
@@ -66,16 +66,25 @@ exports.devices = {
 };
 
 /**
  * The location of the document displayed in the viewport(s).
  */
 exports.location = PropTypes.string;
 
 /**
+ * The progression of the screenshot
+ */
+exports.screenshot = {
+
+  isCapturing: PropTypes.bool.isRequired,
+
+};
+
+/**
  * A single viewport displaying a document.
  */
 exports.viewport = {
 
   // The id of the viewport
   id: PropTypes.number.isRequired,
 
   // The currently selected device applied to the viewport.
rename from devtools/client/responsive.html/components/utils/l10n.js
rename to devtools/client/responsive.html/utils/l10n.js
rename from devtools/client/responsive.html/components/utils/moz.build
rename to devtools/client/responsive.html/utils/moz.build