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
This commit is contained in:
Matteo Ferretti 2016-03-30 22:31:37 +02:00
Родитель c6425cbdee
Коммит d5459ac316
26 изменённых файлов: 294 добавлений и 17 удалений

Просмотреть файл

@ -21,3 +21,12 @@ responsive.exit=Close Responsive Design Mode
# LOCALIZATION NOTE (responsive.noDeviceSelected): placeholder text for the # LOCALIZATION NOTE (responsive.noDeviceSelected): placeholder text for the
# device selector # device selector
responsive.noDeviceSelected=no device selected 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

Просмотреть файл

@ -32,6 +32,12 @@ createEnum([
// Rotate the viewport. // Rotate the viewport.
"ROTATE_VIEWPORT", "ROTATE_VIEWPORT",
// Take a screenshot of the viewport.
"TAKE_SCREENSHOT_START",
// Indicates when the screenshot action ends.
"TAKE_SCREENSHOT_END",
], module.exports); ], module.exports);
/** /**

Просмотреть файл

@ -8,5 +8,6 @@ DevToolsModules(
'devices.js', 'devices.js',
'index.js', 'index.js',
'location.js', 'location.js',
'screenshot.js',
'viewports.js', 'viewports.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 });
}));
};
}
};

Просмотреть файл

@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-env browser */
"use strict"; "use strict";
const { createClass, createFactory, PropTypes, DOM: dom } = const { createClass, createFactory, PropTypes, DOM: dom } =
@ -13,6 +15,7 @@ const {
resizeViewport, resizeViewport,
rotateViewport rotateViewport
} = require("./actions/viewports"); } = require("./actions/viewports");
const { takeScreenshot } = require("./actions/screenshot");
const Types = require("./types"); const Types = require("./types");
const Viewports = createFactory(require("./components/viewports")); const Viewports = createFactory(require("./components/viewports"));
const GlobalToolbar = createFactory(require("./components/global-toolbar")); const GlobalToolbar = createFactory(require("./components/global-toolbar"));
@ -25,13 +28,17 @@ let App = createClass({
devices: PropTypes.shape(Types.devices).isRequired, devices: PropTypes.shape(Types.devices).isRequired,
location: Types.location.isRequired, location: Types.location.isRequired,
viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired, viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
onExit: PropTypes.func.isRequired, screenshot: PropTypes.shape(Types.screenshot).isRequired,
}, },
onChangeViewportDevice(id, device) { onChangeViewportDevice(id, device) {
this.props.dispatch(changeDevice(id, device)); this.props.dispatch(changeDevice(id, device));
}, },
onExit() {
window.postMessage({ type: "exit" }, "*");
},
onResizeViewport(id, width, height) { onResizeViewport(id, width, height) {
this.props.dispatch(resizeViewport(id, width, height)); this.props.dispatch(resizeViewport(id, width, height));
}, },
@ -40,18 +47,24 @@ let App = createClass({
this.props.dispatch(rotateViewport(id)); this.props.dispatch(rotateViewport(id));
}, },
onScreenshot() {
this.props.dispatch(takeScreenshot());
},
render() { render() {
let { let {
devices, devices,
location, location,
screenshot,
viewports, viewports,
onExit,
} = this.props; } = this.props;
let { let {
onChangeViewportDevice, onChangeViewportDevice,
onExit,
onResizeViewport, onResizeViewport,
onRotateViewport, onRotateViewport,
onScreenshot,
} = this; } = this;
return dom.div( return dom.div(
@ -59,11 +72,14 @@ let App = createClass({
id: "app", id: "app",
}, },
GlobalToolbar({ GlobalToolbar({
screenshot,
onExit, onExit,
onScreenshot,
}), }),
Viewports({ Viewports({
devices, devices,
location, location,
screenshot,
viewports, viewports,
onChangeViewportDevice, onChangeViewportDevice,
onRotateViewport, onRotateViewport,

Двоичные данные
devtools/client/responsive.html/audio/camera-click.mp3 Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -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',
)

Просмотреть файл

@ -4,7 +4,7 @@
"use strict"; "use strict";
const { getStr } = require("./utils/l10n"); const { getStr } = require("../utils/l10n");
const { DOM: dom, createClass, PropTypes, addons } = const { DOM: dom, createClass, PropTypes, addons } =
require("devtools/client/shared/vendor/react"); require("devtools/client/shared/vendor/react");

Просмотреть файл

@ -4,23 +4,28 @@
"use strict"; "use strict";
const { getStr } = require("./utils/l10n"); const { getStr } = require("../utils/l10n");
const { DOM: dom, createClass, PropTypes, addons } = const { DOM: dom, createClass, PropTypes, addons } =
require("devtools/client/shared/vendor/react"); require("devtools/client/shared/vendor/react");
const Types = require("../types");
module.exports = createClass({ module.exports = createClass({
displayName: "GlobalToolbar", displayName: "GlobalToolbar",
mixins: [ addons.PureRenderMixin ],
propTypes: { propTypes: {
onExit: PropTypes.func.isRequired, onExit: PropTypes.func.isRequired,
onScreenshot: PropTypes.func.isRequired,
screenshot: PropTypes.shape(Types.screenshot).isRequired,
}, },
mixins: [ addons.PureRenderMixin ],
render() { render() {
let { let {
onExit, onExit,
onScreenshot,
screenshot,
} = this.props; } = this.props;
return dom.header( return dom.header(
@ -33,6 +38,13 @@ module.exports = createClass({
className: "title", className: "title",
}, },
getStr("responsive.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({ dom.button({
id: "global-exit-button", id: "global-exit-button",
className: "toolbar-button devtools-button", className: "toolbar-button devtools-button",

Просмотреть файл

@ -4,10 +4,6 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this # 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/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += [
'utils',
]
DevToolsModules( DevToolsModules(
'browser.js', 'browser.js',
'device-selector.js', 'device-selector.js',

Просмотреть файл

@ -24,6 +24,7 @@ module.exports = createClass({
propTypes: { propTypes: {
devices: PropTypes.shape(Types.devices).isRequired, devices: PropTypes.shape(Types.devices).isRequired,
location: Types.location.isRequired, location: Types.location.isRequired,
screenshot: PropTypes.shape(Types.screenshot).isRequired,
viewport: PropTypes.shape(Types.viewport).isRequired, viewport: PropTypes.shape(Types.viewport).isRequired,
onChangeViewportDevice: PropTypes.func.isRequired, onChangeViewportDevice: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired, onResizeViewport: PropTypes.func.isRequired,
@ -112,12 +113,19 @@ module.exports = createClass({
let { let {
devices, devices,
location, location,
screenshot,
viewport, viewport,
onChangeViewportDevice, onChangeViewportDevice,
onResizeViewport, onResizeViewport,
onRotateViewport, onRotateViewport,
} = this.props; } = this.props;
let resizeHandleClass = "viewport-resize-handle";
if (screenshot.isCapturing) {
resizeHandleClass += " hidden";
}
return dom.div( return dom.div(
{ {
className: "resizable-viewport", className: "resizable-viewport",
@ -136,7 +144,7 @@ module.exports = createClass({
isResizing: this.state.isResizing isResizing: this.state.isResizing
}), }),
dom.div({ dom.div({
className: "viewport-resize-handle", className: resizeHandleClass,
onMouseDown: this.onResizeStart, onMouseDown: this.onResizeStart,
}), }),
dom.div({ dom.div({

Просмотреть файл

@ -18,6 +18,7 @@ module.exports = createClass({
propTypes: { propTypes: {
devices: PropTypes.shape(Types.devices).isRequired, devices: PropTypes.shape(Types.devices).isRequired,
location: Types.location.isRequired, location: Types.location.isRequired,
screenshot: PropTypes.shape(Types.screenshot).isRequired,
viewport: PropTypes.shape(Types.viewport).isRequired, viewport: PropTypes.shape(Types.viewport).isRequired,
onChangeViewportDevice: PropTypes.func.isRequired, onChangeViewportDevice: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired, onResizeViewport: PropTypes.func.isRequired,
@ -55,6 +56,7 @@ module.exports = createClass({
let { let {
devices, devices,
location, location,
screenshot,
viewport, viewport,
} = this.props; } = this.props;
@ -71,6 +73,7 @@ module.exports = createClass({
ResizableViewport({ ResizableViewport({
devices, devices,
location, location,
screenshot,
viewport, viewport,
onChangeViewportDevice, onChangeViewportDevice,
onResizeViewport, onResizeViewport,

Просмотреть файл

@ -17,6 +17,7 @@ module.exports = createClass({
propTypes: { propTypes: {
devices: PropTypes.shape(Types.devices).isRequired, devices: PropTypes.shape(Types.devices).isRequired,
location: Types.location.isRequired, location: Types.location.isRequired,
screenshot: PropTypes.shape(Types.screenshot).isRequired,
viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired, viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
onChangeViewportDevice: PropTypes.func.isRequired, onChangeViewportDevice: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired, onResizeViewport: PropTypes.func.isRequired,
@ -27,6 +28,7 @@ module.exports = createClass({
let { let {
devices, devices,
location, location,
screenshot,
viewports, viewports,
onChangeViewportDevice, onChangeViewportDevice,
onResizeViewport, onResizeViewport,
@ -42,6 +44,7 @@ module.exports = createClass({
key: viewport.id, key: viewport.id,
devices, devices,
location, location,
screenshot,
viewport, viewport,
onChangeViewportDevice, onChangeViewportDevice,
onResizeViewport, onResizeViewport,

Просмотреть файл

@ -8,5 +8,6 @@ DevToolsModules(
'close.svg', 'close.svg',
'grippers.svg', 'grippers.svg',
'rotate-viewport.svg', 'rotate-viewport.svg',
'screenshot.svg',
'select-arrow.svg', 'select-arrow.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>

После

Ширина:  |  Высота:  |  Размер: 674 B

Просмотреть файл

@ -91,17 +91,28 @@ body {
margin: 0 0 0 5px; margin: 0 0 0 5px;
} }
#global-exit-button, #global-toolbar .toolbar-button,
#global-exit-button::before { #global-toolbar .toolbar-button::before {
width: 12px; width: 12px;
height: 12px; height: 12px;
} }
#global-screenshot-button::before {
background-image: url("./images/screenshot.svg");
margin: -6px 0 0 -6px;
}
#global-exit-button::before { #global-exit-button::before {
background-image: url("./images/close.svg"); background-image: url("./images/close.svg");
margin: -6px 0 0 -6px; margin: -6px 0 0 -6px;
} }
#global-screenshot-button:disabled {
filter: url("chrome://devtools/skin/images/filters.svg#checked-icon-state");
opacity: 1 !important;
}
#viewports { #viewports {
/* Snap to the top of the app when there isn't enough vertical space anymore /* 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) */ to center the viewports (so we don't loose the toolbar) */
@ -220,6 +231,10 @@ body {
cursor: se-resize; cursor: se-resize;
} }
.viewport-resize-handle.hidden {
display: none;
}
.viewport-horizontal-resize-handle { .viewport-horizontal-resize-handle {
position: absolute; position: absolute;
width: 5px; width: 5px;

Просмотреть файл

@ -42,10 +42,8 @@ let bootstrap = {
"agent"); "agent");
this.telemetry.toolOpened("responsive"); this.telemetry.toolOpened("responsive");
let store = this.store = Store(); let store = this.store = Store();
let app = App({ let provider = createElement(Provider, { store }, App());
onExit: () => window.postMessage({ type: "exit" }, "*"),
});
let provider = createElement(Provider, { store }, app);
ReactDOM.render(provider, document.querySelector("#root")); ReactDOM.render(provider, document.querySelector("#root"));
this.initDevices(); this.initDevices();
window.postMessage({ type: "init" }, "*"); window.postMessage({ type: "init" }, "*");

Просмотреть файл

@ -6,9 +6,11 @@
DIRS += [ DIRS += [
'actions', 'actions',
'audio',
'components', 'components',
'images', 'images',
'reducers', 'reducers',
'utils',
] ]
DevToolsModules( DevToolsModules(

Просмотреть файл

@ -6,4 +6,5 @@
exports.devices = require("./reducers/devices"); exports.devices = require("./reducers/devices");
exports.location = require("./reducers/location"); exports.location = require("./reducers/location");
exports.screenshot = require("./reducers/screenshot");
exports.viewports = require("./reducers/viewports"); exports.viewports = require("./reducers/viewports");

Просмотреть файл

@ -7,5 +7,6 @@
DevToolsModules( DevToolsModules(
'devices.js', 'devices.js',
'location.js', 'location.js',
'screenshot.js',
'viewports.js', 'viewports.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);
};

Просмотреть файл

@ -8,4 +8,5 @@ support-files =
!/devtools/client/framework/test/shared-redux-head.js !/devtools/client/framework/test/shared-redux-head.js
[browser_exit_button.js] [browser_exit_button.js]
[browser_screenshot_button.js]
[browser_viewport_basics.js] [browser_viewport_basics.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);
});

Просмотреть файл

@ -70,6 +70,15 @@ exports.devices = {
*/ */
exports.location = PropTypes.string; exports.location = PropTypes.string;
/**
* The progression of the screenshot
*/
exports.screenshot = {
isCapturing: PropTypes.bool.isRequired,
};
/** /**
* A single viewport displaying a document. * A single viewport displaying a document.
*/ */