From d5459ac316b2af8c2314151ab3ace8d3f78a7fd2 Mon Sep 17 00:00:00 2001 From: Matteo Ferretti Date: Wed, 30 Mar 2016 22:31:37 +0200 Subject: [PATCH] Bug 1239461 - Screenshot button for taking a screenshot of the current viewport; r=jryans MozReview-Commit-ID: AMbzmf1uO0P --HG-- rename : devtools/client/responsive.html/components/utils/l10n.js => devtools/client/responsive.html/utils/l10n.js rename : devtools/client/responsive.html/components/utils/moz.build => devtools/client/responsive.html/utils/moz.build extra : transplant_source : A%97_%C1d%AC%09%7C%E3%7F%0D%BCWl%8C%92V%09%1E%03 --- .../locales/en-US/responsive.properties | 9 ++ .../client/responsive.html/actions/index.js | 6 ++ .../client/responsive.html/actions/moz.build | 1 + .../responsive.html/actions/screenshot.js | 89 ++++++++++++++++++ devtools/client/responsive.html/app.js | 20 +++- .../responsive.html/audio/camera-click.mp3 | Bin 0 -> 27634 bytes .../client/responsive.html/audio/moz.build | 9 ++ .../components/device-selector.js | 2 +- .../components/global-toolbar.js | 18 +++- .../responsive.html/components/moz.build | 4 - .../components/resizable-viewport.js | 10 +- .../responsive.html/components/viewport.js | 3 + .../responsive.html/components/viewports.js | 3 + .../client/responsive.html/images/moz.build | 1 + .../responsive.html/images/screenshot.svg | 7 ++ devtools/client/responsive.html/index.css | 19 +++- devtools/client/responsive.html/index.js | 6 +- devtools/client/responsive.html/moz.build | 2 + devtools/client/responsive.html/reducers.js | 1 + .../client/responsive.html/reducers/moz.build | 1 + .../responsive.html/reducers/screenshot.js | 31 ++++++ .../responsive.html/test/browser/browser.ini | 1 + .../test/browser/browser_screenshot_button.js | 59 ++++++++++++ devtools/client/responsive.html/types.js | 9 ++ .../{components => }/utils/l10n.js | 0 .../{components => }/utils/moz.build | 0 26 files changed, 294 insertions(+), 17 deletions(-) create mode 100644 devtools/client/responsive.html/actions/screenshot.js create mode 100644 devtools/client/responsive.html/audio/camera-click.mp3 create mode 100644 devtools/client/responsive.html/audio/moz.build create mode 100644 devtools/client/responsive.html/images/screenshot.svg create mode 100644 devtools/client/responsive.html/reducers/screenshot.js create mode 100644 devtools/client/responsive.html/test/browser/browser_screenshot_button.js rename devtools/client/responsive.html/{components => }/utils/l10n.js (100%) rename devtools/client/responsive.html/{components => }/utils/moz.build (100%) diff --git a/devtools/client/locales/en-US/responsive.properties b/devtools/client/locales/en-US/responsive.properties index 8406987eda77..fa5965828ed9 100644 --- a/devtools/client/locales/en-US/responsive.properties +++ b/devtools/client/locales/en-US/responsive.properties @@ -21,3 +21,12 @@ 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 diff --git a/devtools/client/responsive.html/actions/index.js b/devtools/client/responsive.html/actions/index.js index de026ba7ba8e..8f5e30822c8c 100644 --- a/devtools/client/responsive.html/actions/index.js +++ b/devtools/client/responsive.html/actions/index.js @@ -32,6 +32,12 @@ createEnum([ // 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); /** diff --git a/devtools/client/responsive.html/actions/moz.build b/devtools/client/responsive.html/actions/moz.build index b64117d96201..db36543d7651 100644 --- a/devtools/client/responsive.html/actions/moz.build +++ b/devtools/client/responsive.html/actions/moz.build @@ -8,5 +8,6 @@ DevToolsModules( 'devices.js', 'index.js', 'location.js', + 'screenshot.js', 'viewports.js', ) diff --git a/devtools/client/responsive.html/actions/screenshot.js b/devtools/client/responsive.html/actions/screenshot.js new file mode 100644 index 000000000000..c913c3227501 --- /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 }); + })); + }; + } + +}; diff --git a/devtools/client/responsive.html/app.js b/devtools/client/responsive.html/app.js index c0d9487b7926..c6a6f47cefc4 100644 --- a/devtools/client/responsive.html/app.js +++ b/devtools/client/responsive.html/app.js @@ -2,6 +2,8 @@ * 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 } = @@ -13,6 +15,7 @@ const { 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")); @@ -25,13 +28,17 @@ let App = createClass({ 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)); }, @@ -40,18 +47,24 @@ let App = createClass({ 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( @@ -59,11 +72,14 @@ let App = createClass({ id: "app", }, GlobalToolbar({ + screenshot, onExit, + onScreenshot, }), Viewports({ devices, location, + screenshot, viewports, onChangeViewportDevice, onRotateViewport, diff --git a/devtools/client/responsive.html/audio/camera-click.mp3 b/devtools/client/responsive.html/audio/camera-click.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..6d9af013315d873e910ecf6e15bc298093dc1e99 GIT binary patch literal 27634 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 + + + + diff --git a/devtools/client/responsive.html/index.css b/devtools/client/responsive.html/index.css index f015e9cc7501..c65e4685a6d3 100644 --- a/devtools/client/responsive.html/index.css +++ b/devtools/client/responsive.html/index.css @@ -91,17 +91,28 @@ body { 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) */ @@ -220,6 +231,10 @@ body { cursor: se-resize; } +.viewport-resize-handle.hidden { + display: none; +} + .viewport-horizontal-resize-handle { position: absolute; width: 5px; diff --git a/devtools/client/responsive.html/index.js b/devtools/client/responsive.html/index.js index b85c84d1d2db..942079754e93 100644 --- a/devtools/client/responsive.html/index.js +++ b/devtools/client/responsive.html/index.js @@ -42,10 +42,8 @@ let bootstrap = { "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" }, "*"); diff --git a/devtools/client/responsive.html/moz.build b/devtools/client/responsive.html/moz.build index d2a6ac50ce40..3c3c62e61f70 100644 --- a/devtools/client/responsive.html/moz.build +++ b/devtools/client/responsive.html/moz.build @@ -6,9 +6,11 @@ DIRS += [ 'actions', + 'audio', 'components', 'images', 'reducers', + 'utils', ] DevToolsModules( diff --git a/devtools/client/responsive.html/reducers.js b/devtools/client/responsive.html/reducers.js index 7c31d2542dfa..86c249a23c8e 100644 --- a/devtools/client/responsive.html/reducers.js +++ b/devtools/client/responsive.html/reducers.js @@ -6,4 +6,5 @@ exports.devices = require("./reducers/devices"); exports.location = require("./reducers/location"); +exports.screenshot = require("./reducers/screenshot"); exports.viewports = require("./reducers/viewports"); diff --git a/devtools/client/responsive.html/reducers/moz.build b/devtools/client/responsive.html/reducers/moz.build index e159a0da434a..584a216187f2 100644 --- a/devtools/client/responsive.html/reducers/moz.build +++ b/devtools/client/responsive.html/reducers/moz.build @@ -7,5 +7,6 @@ DevToolsModules( 'devices.js', 'location.js', + 'screenshot.js', 'viewports.js', ) diff --git a/devtools/client/responsive.html/reducers/screenshot.js b/devtools/client/responsive.html/reducers/screenshot.js new file mode 100644 index 000000000000..7df0643887f3 --- /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); +}; diff --git a/devtools/client/responsive.html/test/browser/browser.ini b/devtools/client/responsive.html/test/browser/browser.ini index ed65c6608e92..2d826f319ad3 100644 --- a/devtools/client/responsive.html/test/browser/browser.ini +++ b/devtools/client/responsive.html/test/browser/browser.ini @@ -8,4 +8,5 @@ support-files = !/devtools/client/framework/test/shared-redux-head.js [browser_exit_button.js] +[browser_screenshot_button.js] [browser_viewport_basics.js] diff --git a/devtools/client/responsive.html/test/browser/browser_screenshot_button.js b/devtools/client/responsive.html/test/browser/browser_screenshot_button.js new file mode 100644 index 000000000000..60605c33b6d6 --- /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); +}); diff --git a/devtools/client/responsive.html/types.js b/devtools/client/responsive.html/types.js index 846f98100629..067cb8f42c91 100644 --- a/devtools/client/responsive.html/types.js +++ b/devtools/client/responsive.html/types.js @@ -70,6 +70,15 @@ exports.devices = { */ exports.location = PropTypes.string; +/** + * The progression of the screenshot + */ +exports.screenshot = { + + isCapturing: PropTypes.bool.isRequired, + +}; + /** * A single viewport displaying a document. */ diff --git a/devtools/client/responsive.html/components/utils/l10n.js b/devtools/client/responsive.html/utils/l10n.js similarity index 100% rename from devtools/client/responsive.html/components/utils/l10n.js rename to devtools/client/responsive.html/utils/l10n.js diff --git a/devtools/client/responsive.html/components/utils/moz.build b/devtools/client/responsive.html/utils/moz.build similarity index 100% rename from devtools/client/responsive.html/components/utils/moz.build rename to devtools/client/responsive.html/utils/moz.build