--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -14,16 +14,17 @@ loader.lazyRequireGetter(this, "defer",
loader.lazyRequireGetter(this, "Debugger", "Debugger");
loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup");
loader.lazyRequireGetter(this, "PropTypes", "devtools/client/shared/vendor/react-prop-types");
loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
loader.lazyRequireGetter(this, "KeyCodes", "devtools/client/shared/keycodes", true);
loader.lazyRequireGetter(this, "Editor", "devtools/client/sourceeditor/editor");
loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
+loader.lazyRequireGetter(this, "processScreenshot", "devtools/shared/webconsole/screenshot-helper");
const l10n = require("devtools/client/webconsole/webconsole-l10n");
const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
function gSequenceId() {
return gSequenceId.n++;
}
@@ -262,17 +263,17 @@ class JSTerm extends Component {
*
* @private
* @param function [callback]
* Optional function to invoke when the evaluation result is added to
* the output.
* @param object response
* The message received from the server.
*/
- _executeResultCallback(callback, response) {
+ async _executeResultCallback(callback, response) {
if (!this.hud) {
return;
}
if (response.error) {
console.error("Evaluation error " + response.error + ": " + response.message);
return;
}
let errorMessage = response.exceptionMessage;
@@ -304,16 +305,22 @@ class JSTerm extends Component {
}
break;
case "help":
this.hud.owner.openLink(HELP_URL);
break;
case "copyValueToClipboard":
clipboardHelper.copyString(helperResult.value);
break;
+ case "screenshotOutput":
+ const { args, value } = helperResult;
+ const results = await processScreenshot(this.hud.window, args, value);
+ this.screenshotNotify(results);
+ // early return as screenshot notify has dispatched all necessary messages
+ return;
}
}
// Hide undefined results coming from JSTerm helper functions.
if (!errorMessage && result && typeof result == "object" &&
result.type == "undefined" &&
helperResult && !helperHasRawOutput) {
callback && callback();
@@ -330,16 +337,21 @@ class JSTerm extends Component {
helperResult: {
type: "inspectObject",
object: objectActor
}
}, true);
return this.hud.consoleOutput;
}
+ screenshotNotify(results) {
+ const wrappedResults = results.map(result => ({ result }));
+ this.hud.consoleOutput.dispatchMessagesAdd(wrappedResults);
+ }
+
/**
* Execute a string. Execution happens asynchronously in the content process.
*
* @param string [executeString]
* The string you want to execute. If this is not provided, the current
* user input is used - taken from |this.getInputValue()|.
* @param function [callback]
* Optional function to invoke when the result is displayed.
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -870,16 +870,17 @@ WebConsoleActor.prototype =
};
},
/**
* Handler for the "evaluateJSAsync" request. This method evaluates the given
* JavaScript string and sends back a packet with a unique ID.
* The result will be returned later as an unsolicited `evaluationResult`,
* that can be associated back to this request via the `resultID` field.
+ * Cannot be async, see Comment two on Bug #1452920
*
* @param object request
* The JSON request object received from the Web Console client.
* @return object
* The response packet to send to with the unique id in the
* `resultID` field.
*/
evaluateJSAsync: function(request) {
@@ -892,16 +893,44 @@ WebConsoleActor.prototype =
from: this.actorID,
resultID: resultID
});
// Then, execute the script that may pause.
const response = this.evaluateJS(request);
response.resultID = resultID;
+ this._waitForHelperResultAndSend(response);
+ },
+
+ /**
+ * In order to have asynchornous commands such as screenshot, we have to be
+ * able to handle promises in the helper result. This method handles waiting
+ * for the promise, and then dispatching the result
+ *
+ *
+ * @private
+ * @param object response
+ * The response packet to send to with the unique id in the
+ * `resultID` field, and potentially a promise in the helperResult
+ * field.
+ *
+ * @return object
+ * The response packet to send to with the unique id in the
+ * `resultID` field, with a sanitized helperResult field.
+ */
+ _waitForHelperResultAndSend: async function(response) {
+ // Wait for asynchronous command completion before sending back the response
+ if (
+ response.helperResult &&
+ typeof response.helperResult.then == "function"
+ ) {
+ response.helperResult = await response.helperResult;
+ }
+
// Finally, send an unsolicited evaluationResult packet with
// the normal return value
this.conn.sendActorEvent(this.actorID, "evaluationResult", response);
},
/**
* Handler for the "evaluateJS" request. This method evaluates the given
* JavaScript string and sends back the result.
--- a/devtools/server/actors/webconsole/moz.build
+++ b/devtools/server/actors/webconsole/moz.build
@@ -2,11 +2,12 @@
# 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(
'content-process-forward.js',
'listeners.js',
+ 'screenshot.js',
'utils.js',
'worker-listeners.js',
)
copy from devtools/shared/gcli/commands/screenshot.js
copy to devtools/server/actors/webconsole/screenshot.js
--- a/devtools/shared/gcli/commands/screenshot.js
+++ b/devtools/server/actors/webconsole/screenshot.js
@@ -1,240 +1,37 @@
/* 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 { Cc, Ci, Cr, Cu } = require("chrome");
-const ChromeUtils = require("ChromeUtils");
-const l10n = require("gcli/l10n");
-const Services = require("Services");
-const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
+const { Ci, Cu } = require("chrome");
const { getRect } = require("devtools/shared/layout/utils");
-const defer = require("devtools/shared/defer");
-const { Task } = require("devtools/shared/task");
-
-loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
-
-loader.lazyImporter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
-loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
-loader.lazyImporter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
-loader.lazyImporter(this, "PrivateBrowsingUtils",
- "resource://gre/modules/PrivateBrowsingUtils.jsm");
-
-// String used as an indication to generate default file name in the following
-// format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
-const FILENAME_DEFAULT_VALUE = " ";
-const CONTAINER_FLASHING_DURATION = 500;
-
-/*
- * There are 2 commands and 1 converter here. The 2 commands are nearly
- * identical except that one runs on the client and one in the server.
- *
- * The server command is hidden, and is designed to be called from the client
- * command.
- */
-
-/**
- * Both commands have the same initial filename parameter
- */
-const filenameParam = {
- name: "filename",
- type: "string",
- defaultValue: FILENAME_DEFAULT_VALUE,
- description: l10n.lookup("screenshotFilenameDesc"),
- manual: l10n.lookup("screenshotFilenameManual")
-};
+const { LocalizationHelper } = require("devtools/shared/L10N");
-/**
- * Both commands have almost the same set of standard optional parameters, except for the
- * type of the --selector option, which can be a node only on the server.
- */
-const getScreenshotCommandParams = function(isClient) {
- return {
- group: l10n.lookup("screenshotGroupOptions"),
- params: [
- {
- name: "clipboard",
- type: "boolean",
- description: l10n.lookup("screenshotClipboardDesc"),
- manual: l10n.lookup("screenshotClipboardManual")
- },
- {
- name: "imgur",
- type: "boolean",
- description: l10n.lookup("screenshotImgurDesc"),
- manual: l10n.lookup("screenshotImgurManual")
- },
- {
- name: "delay",
- type: { name: "number", min: 0 },
- defaultValue: 0,
- description: l10n.lookup("screenshotDelayDesc"),
- manual: l10n.lookup("screenshotDelayManual")
- },
- {
- name: "dpr",
- type: { name: "number", min: 0, allowFloat: true },
- defaultValue: 0,
- description: l10n.lookup("screenshotDPRDesc"),
- manual: l10n.lookup("screenshotDPRManual")
- },
- {
- name: "fullpage",
- type: "boolean",
- description: l10n.lookup("screenshotFullPageDesc"),
- manual: l10n.lookup("screenshotFullPageManual")
- },
- {
- name: "selector",
- // On the client side, don't try to parse the selector as a node as it will
- // trigger an unsafe CPOW.
- type: isClient ? "string" : "node",
- defaultValue: null,
- description: l10n.lookup("inspectNodeDesc"),
- manual: l10n.lookup("inspectNodeManual")
- },
- {
- name: "file",
- type: "boolean",
- description: l10n.lookup("screenshotFileDesc"),
- manual: l10n.lookup("screenshotFileManual"),
- },
- ]
- };
-};
-
-const clientScreenshotParams = getScreenshotCommandParams(true);
-const serverScreenshotParams = getScreenshotCommandParams(false);
+const CONTAINER_FLASHING_DURATION = 500;
+const STRINGS_URI = "devtools/shared/locales/screenshot.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
-exports.items = [
- {
- /**
- * Format an 'imageSummary' (as output by the screenshot command).
- * An 'imageSummary' is a simple JSON object that looks like this:
- *
- * {
- * destinations: [ "..." ], // Required array of descriptions of the
- * // locations of the result image (the command
- * // can have multiple outputs)
- * data: "...", // Optional Base64 encoded image data
- * width:1024, height:768, // Dimensions of the image data, required
- * // if data != null
- * filename: "...", // If set, clicking the image will open the
- * // folder containing the given file
- * href: "...", // If set, clicking the image will open the
- * // link in a new tab
- * }
- */
- item: "converter",
- from: "imageSummary",
- to: "dom",
- exec: function(imageSummary, context) {
- const document = context.document;
- const root = document.createElement("div");
-
- // Add a line to the result for each destination
- imageSummary.destinations.forEach(destination => {
- const title = document.createElement("div");
- title.textContent = destination;
- root.appendChild(title);
- });
-
- // Add the thumbnail image
- if (imageSummary.data != null) {
- const image = context.document.createElement("div");
- const previewHeight = parseInt(256 * imageSummary.height / imageSummary.width,
- 10);
- const style = "" +
- "width: 256px;" +
- "height: " + previewHeight + "px;" +
- "max-height: 256px;" +
- "background-image: url('" + imageSummary.data + "');" +
- "background-size: 256px " + previewHeight + "px;" +
- "margin: 4px;" +
- "display: block;";
- image.setAttribute("style", style);
- root.appendChild(image);
- }
-
- // Click handler
- if (imageSummary.href || imageSummary.filename) {
- root.style.cursor = "pointer";
- root.addEventListener("click", () => {
- if (imageSummary.href) {
- openContentLink(imageSummary.href);
- } else if (imageSummary.filename) {
- const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- file.initWithPath(imageSummary.filename);
- file.reveal();
- }
- });
- }
-
- return root;
- }
- },
- {
- item: "command",
- runAt: "client",
- name: "screenshot",
- description: l10n.lookup("screenshotDesc"),
- manual: l10n.lookup("screenshotManual"),
- returnType: "imageSummary",
- buttonId: "command-button-screenshot",
- buttonClass: "command-button",
- tooltipText: l10n.lookup("screenshotTooltipPage"),
- params: [
- filenameParam,
- clientScreenshotParams,
- ],
- exec: function(args, context) {
- // Re-execute the command on the server
- const command = context.typed.replace(/^screenshot/, "screenshot_server");
- const capture = context.updateExec(command).then(output => {
- return output.error ? Promise.reject(output.data) : output.data;
- });
-
- simulateCameraEffect(context.environment.chromeDocument, "shutter");
- return capture.then(saveScreenshot.bind(null, args, context));
- },
- },
- {
- item: "command",
- runAt: "server",
- name: "screenshot_server",
- hidden: true,
- returnType: "imageSummary",
- params: [
- filenameParam,
- serverScreenshotParams,
- ],
- exec: function(args, context) {
- return captureScreenshot(args, context.environment.document);
- },
+exports.screenshot = function takeAsyncScreenshot(owner, args = {}) {
+ if (args.help) {
+ // Early return as help will be handled on the client side.
+ return null;
}
-];
+ return captureScreenshot(args, owner.window.document);
+};
/**
* This function is called to simulate camera effects
*/
-function simulateCameraEffect(document, effect) {
+function simulateCameraFlash(document) {
const window = document.defaultView;
- if (effect === "shutter") {
- if (Services.prefs.getBoolPref("devtools.screenshot.audio.enabled")) {
- const audioCamera = new window.Audio("resource://devtools/client/themes/audio/shutter.wav");
- audioCamera.play();
- }
- }
- if (effect == "flash") {
- const frames = Cu.cloneInto({ opacity: [ 0, 1 ] }, window);
- document.documentElement.animate(frames, CONTAINER_FLASHING_DURATION);
- }
+ const frames = Cu.cloneInto({ opacity: [ 0, 1 ] }, window);
+ document.documentElement.animate(frames, CONTAINER_FLASHING_DURATION);
}
/**
* This function simply handles the --delay argument before calling
* createScreenshotData
*/
function captureScreenshot(args, document) {
if (args.delay > 0) {
@@ -243,36 +40,16 @@ function captureScreenshot(args, documen
createScreenshotData(document, args).then(resolve, reject);
}, args.delay * 1000);
});
}
return createScreenshotData(document, args);
}
/**
- * There are several possible destinations for the screenshot, SKIP is used
- * in saveScreenshot() whenever one of them is not used
- */
-const SKIP = Promise.resolve();
-
-/**
- * Save the captured screenshot to one of several destinations.
- */
-function saveScreenshot(args, context, reply) {
- const fileNeeded = args.filename != FILENAME_DEFAULT_VALUE ||
- (!args.imgur && !args.clipboard) || args.file;
-
- return Promise.all([
- args.clipboard ? saveToClipboard(context, reply) : SKIP,
- args.imgur ? uploadToImgur(reply) : SKIP,
- fileNeeded ? saveToFile(context, reply) : SKIP,
- ]).then(() => reply);
-}
-
-/**
* This does the dirty work of creating a base64 string out of an
* area of the browser window
*/
function createScreenshotData(document, args) {
const window = document.defaultView;
let left = 0;
let top = 0;
let width;
@@ -285,17 +62,18 @@ function createScreenshotData(document,
if (args.fullpage) {
// Bug 961832: GCLI screenshot shows fixed position element in wrong
// position if we don't scroll to top
window.scrollTo(0, 0);
width = window.innerWidth + window.scrollMaxX - window.scrollMinX;
height = window.innerHeight + window.scrollMaxY - window.scrollMinY;
filename = filename.replace(".png", "-fullpage.png");
} else if (args.selector) {
- ({ top, left, width, height } = getRect(window, args.selector, window));
+ const node = window.document.querySelector(args.selector);
+ ({ top, left, width, height } = getRect(window, node, window));
} else {
left = window.scrollX;
top = window.scrollY;
width = window.innerWidth;
height = window.innerHeight;
}
// Only adjust for scrollbars when considering the full window
@@ -318,278 +96,46 @@ function createScreenshotData(document,
ctx.drawWindow(window, left, top, width, height, "#fff");
const data = canvas.toDataURL("image/png", "");
// See comment above on bug 961832
if (args.fullpage) {
window.scrollTo(currentX, currentY);
}
- simulateCameraEffect(document, "flash");
+ simulateCameraFlash(document);
return Promise.resolve({
destinations: [],
data: data,
height: height,
width: width,
filename: filename,
});
}
/**
* We may have a filename specified in args, or we might have to generate
* one.
*/
function getFilename(defaultName) {
// Create a name for the file if not present
- if (defaultName != FILENAME_DEFAULT_VALUE) {
+ if (defaultName) {
return defaultName;
}
const date = new Date();
let dateString = date.getFullYear() + "-" + (date.getMonth() + 1) +
"-" + date.getDate();
dateString = dateString.split("-").map(function(part) {
if (part.length == 1) {
part = "0" + part;
}
return part;
}).join("-");
const timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
- return l10n.lookupFormat("screenshotGeneratedFilename",
- [ dateString, timeString ]) + ".png";
-}
-
-/**
- * Save the image data to the clipboard. This returns a promise, so it can
- * be treated exactly like imgur / file processing, but it's really sync
- * for now.
- */
-function saveToClipboard(context, reply) {
- return new Promise(resolve => {
- try {
- const channel = NetUtil.newChannel({
- uri: reply.data,
- loadUsingSystemPrincipal: true,
- contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE
- });
- const input = channel.open2();
-
- const loadContext = context.environment.chromeWindow
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsILoadContext);
-
- const callback = {
- onImageReady(container, status) {
- if (!container) {
- console.error("imgTools.decodeImageAsync failed");
- reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
- resolve();
- return;
- }
-
- try {
- const wrapped = Cc["@mozilla.org/supports-interface-pointer;1"]
- .createInstance(Ci.nsISupportsInterfacePointer);
- wrapped.data = container;
-
- const trans = Cc["@mozilla.org/widget/transferable;1"]
- .createInstance(Ci.nsITransferable);
- trans.init(loadContext);
- trans.addDataFlavor(channel.contentType);
- trans.setTransferData(channel.contentType, wrapped, -1);
-
- Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
-
- reply.destinations.push(l10n.lookup("screenshotCopied"));
- } catch (ex) {
- console.error(ex);
- reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
- }
- resolve();
- }
- };
-
- const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
- const imgTools = Cc["@mozilla.org/image/tools;1"]
- .getService(Ci.imgITools);
- imgTools.decodeImageAsync(input, channel.contentType, callback,
- threadManager.currentThread);
- } catch (ex) {
- console.error(ex);
- reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
- resolve();
- }
- });
-}
-
-/**
- * Upload screenshot data to Imgur, returning a promise of a URL (as a string)
- */
-function uploadToImgur(reply) {
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- const fd = new FormData();
- fd.append("image", reply.data.split(",")[1]);
- fd.append("type", "base64");
- fd.append("title", reply.filename);
-
- const postURL = Services.prefs.getCharPref("devtools.gcli.imgurUploadURL");
- const clientID = "Client-ID " +
- Services.prefs.getCharPref("devtools.gcli.imgurClientID");
-
- xhr.open("POST", postURL);
- xhr.setRequestHeader("Authorization", clientID);
- xhr.send(fd);
- xhr.responseType = "json";
-
- xhr.onreadystatechange = function() {
- if (xhr.readyState == 4) {
- if (xhr.status == 200) {
- reply.href = xhr.response.data.link;
- reply.destinations.push(l10n.lookupFormat("screenshotImgurUploaded",
- [ reply.href ]));
- } else {
- reply.destinations.push(l10n.lookup("screenshotImgurError"));
- }
-
- resolve();
- }
- };
- });
+ return L10N.getFormatStr(
+ "screenshotGeneratedFilename",
+ dateString,
+ timeString
+ ) + ".png";
}
-
-/**
- * Progress listener that forwards calls to a transfer object.
- *
- * This is used below in saveToFile to forward progress updates from the
- * nsIWebBrowserPersist object that does the actual saving to the nsITransfer
- * which just represents the operation for the Download Manager. This keeps the
- * Download Manager updated on saving progress and completion, so that it gives
- * visual feedback from the downloads toolbar button when the save is done.
- *
- * It also allows the browser window to show auth prompts if needed (should not
- * be needed for saving screenshots).
- *
- * This code is borrowed directly from contentAreaUtils.js.
- */
-function DownloadListener(win, transfer) {
- this.window = win;
- this.transfer = transfer;
-
- // For most method calls, forward to the transfer object.
- for (const name in transfer) {
- if (name != "QueryInterface" &&
- name != "onStateChange") {
- this[name] = (...args) => transfer[name].apply(transfer, args);
- }
- }
-
- // Allow saveToFile to await completion for error handling
- this._completedDeferred = defer();
- this.completed = this._completedDeferred.promise;
-}
-
-DownloadListener.prototype = {
- QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor",
- "nsIWebProgressListener",
- "nsIWebProgressListener2"]),
-
- getInterface: function(iid) {
- if (iid.equals(Ci.nsIAuthPrompt) ||
- iid.equals(Ci.nsIAuthPrompt2)) {
- const ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]
- .getService(Ci.nsIPromptFactory);
- return ww.getPrompt(this.window, iid);
- }
-
- throw Cr.NS_ERROR_NO_INTERFACE;
- },
-
- onStateChange: function(webProgress, request, state, status) {
- // Check if the download has completed
- if ((state & Ci.nsIWebProgressListener.STATE_STOP) &&
- (state & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
- if (status == Cr.NS_OK) {
- this._completedDeferred.resolve();
- } else {
- this._completedDeferred.reject();
- }
- }
-
- this.transfer.onStateChange.apply(this.transfer, arguments);
- }
-};
-
-/**
- * Save the screenshot data to disk, returning a promise which is resolved on
- * completion.
- */
-var saveToFile = Task.async(function* (context, reply) {
- const document = context.environment.chromeDocument;
- const window = context.environment.chromeWindow;
-
- // Check there is a .png extension to filename
- if (!reply.filename.match(/.png$/i)) {
- reply.filename += ".png";
- }
-
- const downloadsDir = yield Downloads.getPreferredDownloadsDirectory();
- const downloadsDirExists = yield OS.File.exists(downloadsDir);
- if (downloadsDirExists) {
- // If filename is absolute, it will override the downloads directory and
- // still be applied as expected.
- reply.filename = OS.Path.join(downloadsDir, reply.filename);
- }
-
- const sourceURI = Services.io.newURI(reply.data);
- const targetFile = new FileUtils.File(reply.filename);
- const targetFileURI = Services.io.newFileURI(targetFile);
-
- // Create download and track its progress.
- // This is adapted from saveURL in contentAreaUtils.js, but simplified greatly
- // and modified to allow saving to arbitrary paths on disk. Using these
- // objects as opposed to just writing with OS.File allows us to tie into the
- // download manager to record a download entry and to get visual feedback from
- // the downloads toolbar button when the save is done.
- const nsIWBP = Ci.nsIWebBrowserPersist;
- const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
- nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES |
- nsIWBP.PERSIST_FLAGS_BYPASS_CACHE |
- nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
- const isPrivate =
- PrivateBrowsingUtils.isContentWindowPrivate(document.defaultView);
- const persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
- .createInstance(Ci.nsIWebBrowserPersist);
- persist.persistFlags = flags;
- const tr = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
- tr.init(sourceURI,
- targetFileURI,
- "",
- null,
- null,
- null,
- persist,
- isPrivate);
- const listener = new DownloadListener(window, tr);
- persist.progressListener = listener;
- persist.savePrivacyAwareURI(sourceURI,
- 0,
- document.documentURIObject,
- Ci.nsIHttpChannel.REFERRER_POLICY_UNSET,
- null,
- null,
- targetFileURI,
- isPrivate);
-
- try {
- // Await successful completion of the save via the listener
- yield listener.completed;
- reply.destinations.push(l10n.lookup("screenshotSavedToFile") +
- ` "${reply.filename}"`);
- } catch (ex) {
- console.error(ex);
- reply.destinations.push(l10n.lookup("screenshotErrorSavingToFile") + " " +
- reply.filename);
- }
-});
--- a/devtools/server/actors/webconsole/utils.js
+++ b/devtools/server/actors/webconsole/utils.js
@@ -1,18 +1,19 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* 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/. */
"use strict";
const {Ci, Cu} = require("chrome");
+loader.lazyRequireGetter(this, "screenshot", "devtools/server/actors/webconsole/screenshot", true);
+
// Note that this is only used in WebConsoleCommands, see $0 and pprint().
if (!isWorker) {
loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm");
}
const CONSOLE_WORKER_IDS = exports.CONSOLE_WORKER_IDS = [
"SharedWorker",
"ServiceWorker",
@@ -588,16 +589,34 @@ WebConsoleCommands._registerOriginal("co
}
owner.helperResult = {
type: "copyValueToClipboard",
value: payload,
};
});
/**
+ * Take a screenshot of a page.
+ *
+ * @param object args
+ * The arguments to be passed to the screenshot
+ * @return void
+ */
+WebConsoleCommands._registerOriginal("screenshot", function(owner, args) {
+ owner.helperResult = (async () => {
+ const value = await screenshot(owner, args);
+ return {
+ type: "screenshotOutput",
+ value,
+ args
+ };
+ })();
+});
+
+/**
* (Internal only) Add the bindings to |owner.sandbox|.
* This is intended to be used by the WebConsole actor only.
*
* @param object owner
* The owning object.
*/
function addWebConsoleCommands(owner) {
// Not supporting extra commands in workers yet. This should be possible to
new file mode 100644
--- /dev/null
+++ b/devtools/shared/locales/en-US/screenshot.properties
@@ -0,0 +1,136 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside Web Console commands.
+# The Web Console command line is available from the Web Developer sub-menu
+# -> 'Web Console'.
+#
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (screenshotDesc) A very short description of the
+# 'screenshot' command. See screenshotManual for a fuller description of what
+# it does. This string is designed to be shown in a menu alongside the
+# command name, which is why it should be as short as possible.
+screenshotDesc=Save an image of the page
+
+# LOCALIZATION NOTE (screenshotFilenameDesc) A very short string to describe
+# the 'filename' parameter to the 'screenshot' command, which is displayed in
+# a dialog when the user is using this command.
+screenshotFilenameDesc=Destination filename
+
+# LOCALIZATION NOTE (screenshotFilenameManual) A fuller description of the
+# 'filename' parameter to the 'screenshot' command, displayed when the user
+# asks for help on what it does.
+screenshotFilenameManual=The name of the file (should have a ‘.png’ extension) to which we write the screenshot.
+
+# LOCALIZATION NOTE (screenshotClipboardDesc) A very short string to describe
+# the 'clipboard' parameter to the 'screenshot' command, which is displayed in
+# a dialog when the user is using this command.
+screenshotClipboardDesc=Copy screenshot to clipboard? (true/false)
+
+# LOCALIZATION NOTE (screenshotClipboardManual) A fuller description of the
+# 'clipboard' parameter to the 'screenshot' command, displayed when the user
+# asks for help on what it does.
+screenshotClipboardManual=True if you want to copy the screenshot instead of saving it to a file.
+
+# LOCALIZATION NOTE (screenshotGroupOptions) A label for the optional options of
+# the screenshot command.
+screenshotGroupOptions=Options
+
+# LOCALIZATION NOTE (screenshotDelayDesc) A very short string to describe
+# the 'delay' parameter to the 'screenshot' command, which is displayed in
+# a dialog when the user is using this command.
+screenshotDelayDesc=Delay (seconds)
+
+# LOCALIZATION NOTE (screenshotDelayManual) A fuller description of the
+# 'delay' parameter to the 'screenshot' command, displayed when the user
+# asks for help on what it does.
+screenshotDelayManual=The time to wait (in seconds) before the screenshot is taken
+
+# LOCALIZATION NOTE (screenshotDPRDesc) A very short string to describe
+# the 'dpr' parameter to the 'screenshot' command, which is displayed in
+# a dialog when the user is using this command.
+screenshotDPRDesc=Device pixel ratio
+
+# LOCALIZATION NOTE (screenshotDPRManual) A fuller description of the
+# 'dpr' parameter to the 'screenshot' command, displayed when the user
+# asks for help on what it does.
+screenshotDPRManual=The device pixel ratio to use when taking the screenshot
+
+# LOCALIZATION NOTE (screenshotFullPageDesc) A very short string to describe
+# the 'fullpage' parameter to the 'screenshot' command, which is displayed in
+# a dialog when the user is using this command.
+screenshotFullPageDesc=Entire webpage? (true/false)
+
+# LOCALIZATION NOTE (screenshotFullPageManual) A fuller description of the
+# 'fullpage' parameter to the 'screenshot' command, displayed when the user
+# asks for help on what it does.
+screenshotFullPageManual=True if the screenshot should also include parts of the webpage which are outside the current scrolled bounds.
+
+# LOCALIZATION NOTE (screenshotFileDesc) A very short string to describe
+# the 'file' parameter to the 'screenshot' command, which is displayed in
+# a dialog when the user is using this command.
+screenshotFileDesc=Save to file? (true/false)
+
+# LOCALIZATION NOTE (screenshotFileManual) A fuller description of the
+# 'file' parameter to the 'screenshot' command, displayed when the user
+# asks for help on what it does.
+screenshotFileManual=True if the screenshot should save the file even when other options are enabled (eg. clipboard).
+
+# LOCALIZATION NOTE (screenshotGeneratedFilename) The auto generated filename
+# when no file name is provided. 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. Please don't add the extension here.
+screenshotGeneratedFilename=Screen Shot %1$S at %2$S
+
+# LOCALIZATION NOTE (screenshotErrorSavingToFile) Text displayed to user upon
+# encountering error while saving the screenshot to the file specified.
+# The argument (%1$S) is the filename.
+screenshotErrorSavingToFile=Error saving to %1$S
+
+# LOCALIZATION NOTE (screenshotSavedToFile) Text displayed to user when the
+# screenshot is successfully saved to the file specified.
+# The argument (%1$S) is the filename.
+screenshotSavedToFile=Saved to %1$S
+
+# LOCALIZATION NOTE (screenshotErrorCopying) Text displayed to user upon
+# encountering error while copying the screenshot to clipboard.
+screenshotErrorCopying=Error occurred while copying screenshot to clipboard.
+
+# LOCALIZATION NOTE (screenshotCopied) Text displayed to user when the
+# screenshot is successfully copied to the clipboard.
+screenshotCopied=Screenshot copied to clipboard.
+
+# LOCALIZATION NOTE (screenshotImgurDesc) A very short string to describe
+# the 'imgur' parameter to the 'screenshot' command, which is displayed in
+# a dialog when the user is using this command.
+screenshotImgurDesc=Upload to imgur.com
+
+# LOCALIZATION NOTE (screenshotImgurManual) A fuller description of the
+# 'imgur' parameter to the 'screenshot' command, displayed when the user
+# asks for help on what it does.
+screenshotImgurManual=Use if you want to upload to imgur.com instead of saving to disk
+
+# LOCALIZATION NOTE (screenshotImgurError) Text displayed to user upon
+# encountering error while uploading the screenshot to imgur.com.
+screenshotImgurError=Could not reach imgur API
+
+# LOCALIZATION NOTE (screenshotImgurUploaded) Text displayed to user when the
+# screenshot is successfully sent to Imgur but the program is waiting on a response.
+# The argument (%1$S) is a new image URL at Imgur.
+screenshotImgurUploaded=Uploaded to %1$S
+
+# LOCALIZATION NOTE (inspectNodeDesc) A very short string to describe the
+# 'node' parameter to the 'inspect' command, which is displayed in a dialog
+# when the user is using this command.
+inspectNodeDesc=CSS selector
+
+# LOCALIZATION NOTE (inspectNodeManual) A fuller description of the 'node'
+# parameter to the 'inspect' command, displayed when the user asks for help
+# on what it does.
+inspectNodeManual=A CSS selector for use with document.querySelector which identifies a single element
--- a/devtools/shared/webconsole/moz.build
+++ b/devtools/shared/webconsole/moz.build
@@ -8,10 +8,11 @@ if CONFIG['OS_TARGET'] != 'Android':
MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
DevToolsModules(
'client.js',
'js-property-provider.js',
'network-helper.js',
'network-monitor.js',
+ 'screenshot-helper.js',
'throttle.js',
)
copy from devtools/shared/gcli/commands/screenshot.js
copy to devtools/shared/webconsole/screenshot-helper.js
--- a/devtools/shared/gcli/commands/screenshot.js
+++ b/devtools/shared/webconsole/screenshot-helper.js
@@ -1,498 +1,290 @@
/* 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 { Cc, Ci, Cr, Cu } = require("chrome");
+const { Cc, Ci, Cr } = require("chrome");
const ChromeUtils = require("ChromeUtils");
-const l10n = require("gcli/l10n");
+const { LocalizationHelper } = require("devtools/shared/l10n");
const Services = require("Services");
const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
-const { getRect } = require("devtools/shared/layout/utils");
-const defer = require("devtools/shared/defer");
-const { Task } = require("devtools/shared/task");
-
-loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
loader.lazyImporter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
loader.lazyImporter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
loader.lazyImporter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
-// String used as an indication to generate default file name in the following
-// format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
-const FILENAME_DEFAULT_VALUE = " ";
-const CONTAINER_FLASHING_DURATION = 500;
-
-/*
- * There are 2 commands and 1 converter here. The 2 commands are nearly
- * identical except that one runs on the client and one in the server.
- *
- * The server command is hidden, and is designed to be called from the client
- * command.
- */
-
-/**
- * Both commands have the same initial filename parameter
- */
-const filenameParam = {
- name: "filename",
- type: "string",
- defaultValue: FILENAME_DEFAULT_VALUE,
- description: l10n.lookup("screenshotFilenameDesc"),
- manual: l10n.lookup("screenshotFilenameManual")
-};
+const STRINGS_URI = "devtools/shared/locales/screenshot.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
-/**
- * Both commands have almost the same set of standard optional parameters, except for the
- * type of the --selector option, which can be a node only on the server.
- */
-const getScreenshotCommandParams = function(isClient) {
- return {
- group: l10n.lookup("screenshotGroupOptions"),
- params: [
- {
- name: "clipboard",
- type: "boolean",
- description: l10n.lookup("screenshotClipboardDesc"),
- manual: l10n.lookup("screenshotClipboardManual")
- },
- {
- name: "imgur",
- type: "boolean",
- description: l10n.lookup("screenshotImgurDesc"),
- manual: l10n.lookup("screenshotImgurManual")
- },
- {
- name: "delay",
- type: { name: "number", min: 0 },
- defaultValue: 0,
- description: l10n.lookup("screenshotDelayDesc"),
- manual: l10n.lookup("screenshotDelayManual")
- },
- {
- name: "dpr",
- type: { name: "number", min: 0, allowFloat: true },
- defaultValue: 0,
- description: l10n.lookup("screenshotDPRDesc"),
- manual: l10n.lookup("screenshotDPRManual")
- },
- {
- name: "fullpage",
- type: "boolean",
- description: l10n.lookup("screenshotFullPageDesc"),
- manual: l10n.lookup("screenshotFullPageManual")
- },
- {
- name: "selector",
- // On the client side, don't try to parse the selector as a node as it will
- // trigger an unsafe CPOW.
- type: isClient ? "string" : "node",
- defaultValue: null,
- description: l10n.lookup("inspectNodeDesc"),
- manual: l10n.lookup("inspectNodeManual")
- },
- {
- name: "file",
- type: "boolean",
- description: l10n.lookup("screenshotFileDesc"),
- manual: l10n.lookup("screenshotFileManual"),
- },
- ]
- };
-};
-
-const clientScreenshotParams = getScreenshotCommandParams(true);
-const serverScreenshotParams = getScreenshotCommandParams(false);
-
-exports.items = [
+const screenshotDescription = L10N.getStr("screenshotDesc");
+const screenshotGroupOptions = L10N.getStr("screenshotGroupOptions");
+const screenshotCommandParams = [
{
- /**
- * Format an 'imageSummary' (as output by the screenshot command).
- * An 'imageSummary' is a simple JSON object that looks like this:
- *
- * {
- * destinations: [ "..." ], // Required array of descriptions of the
- * // locations of the result image (the command
- * // can have multiple outputs)
- * data: "...", // Optional Base64 encoded image data
- * width:1024, height:768, // Dimensions of the image data, required
- * // if data != null
- * filename: "...", // If set, clicking the image will open the
- * // folder containing the given file
- * href: "...", // If set, clicking the image will open the
- * // link in a new tab
- * }
- */
- item: "converter",
- from: "imageSummary",
- to: "dom",
- exec: function(imageSummary, context) {
- const document = context.document;
- const root = document.createElement("div");
-
- // Add a line to the result for each destination
- imageSummary.destinations.forEach(destination => {
- const title = document.createElement("div");
- title.textContent = destination;
- root.appendChild(title);
- });
-
- // Add the thumbnail image
- if (imageSummary.data != null) {
- const image = context.document.createElement("div");
- const previewHeight = parseInt(256 * imageSummary.height / imageSummary.width,
- 10);
- const style = "" +
- "width: 256px;" +
- "height: " + previewHeight + "px;" +
- "max-height: 256px;" +
- "background-image: url('" + imageSummary.data + "');" +
- "background-size: 256px " + previewHeight + "px;" +
- "margin: 4px;" +
- "display: block;";
- image.setAttribute("style", style);
- root.appendChild(image);
- }
-
- // Click handler
- if (imageSummary.href || imageSummary.filename) {
- root.style.cursor = "pointer";
- root.addEventListener("click", () => {
- if (imageSummary.href) {
- openContentLink(imageSummary.href);
- } else if (imageSummary.filename) {
- const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- file.initWithPath(imageSummary.filename);
- file.reveal();
- }
- });
- }
-
- return root;
- }
+ name: "clipboard",
+ type: "boolean",
+ description: L10N.getStr("screenshotClipboardDesc"),
+ manual: L10N.getStr("screenshotClipboardManual")
+ },
+ {
+ name: "delay",
+ type: "number",
+ description: L10N.getStr("screenshotDelayDesc"),
+ manual: L10N.getStr("screenshotDelayManual")
},
{
- item: "command",
- runAt: "client",
- name: "screenshot",
- description: l10n.lookup("screenshotDesc"),
- manual: l10n.lookup("screenshotManual"),
- returnType: "imageSummary",
- buttonId: "command-button-screenshot",
- buttonClass: "command-button",
- tooltipText: l10n.lookup("screenshotTooltipPage"),
- params: [
- filenameParam,
- clientScreenshotParams,
- ],
- exec: function(args, context) {
- // Re-execute the command on the server
- const command = context.typed.replace(/^screenshot/, "screenshot_server");
- const capture = context.updateExec(command).then(output => {
- return output.error ? Promise.reject(output.data) : output.data;
- });
-
- simulateCameraEffect(context.environment.chromeDocument, "shutter");
- return capture.then(saveScreenshot.bind(null, args, context));
- },
+ name: "dpr",
+ type: "number",
+ description: L10N.getStr("screenshotDPRDesc"),
+ manual: L10N.getStr("screenshotDPRManual")
+ },
+ {
+ name: "fullpage",
+ type: "boolean",
+ description: L10N.getStr("screenshotFullPageDesc"),
+ manual: L10N.getStr("screenshotFullPageManual")
},
{
- item: "command",
- runAt: "server",
- name: "screenshot_server",
- hidden: true,
- returnType: "imageSummary",
- params: [
- filenameParam,
- serverScreenshotParams,
- ],
- exec: function(args, context) {
- return captureScreenshot(args, context.environment.document);
- },
+ name: "selector",
+ type: "string",
+ description: L10N.getStr("inspectNodeDesc"),
+ manual: L10N.getStr("inspectNodeManual")
+ },
+ {
+ name: "file",
+ type: "boolean",
+ description: L10N.getStr("screenshotFileDesc"),
+ manual: L10N.getStr("screenshotFileManual"),
+ },
+ {
+ name: "filename",
+ type: "string",
+ description: L10N.getStr("screenshotFilenameDesc"),
+ manual: L10N.getStr("screenshotFilenameManual")
}
];
/**
- * This function is called to simulate camera effects
+ * Creates a string from an object for use when screenshot is passed the `--help` argument
+ *
+ * @param object param
+ * The param object to be formatted.
+ * @return string
+ * The formatted information from the param object as a string
+ */
+function formatHelpField(param) {
+ const padding = " ".repeat(5);
+ return Object.entries(param).map(([key, value]) => {
+ if (key === "name") {
+ const name = `${padding}--${value}`;
+ return name;
+ }
+ return `${padding.repeat(2)}${key}: ${value}`;
+ }).join("\n");
+}
+
+/**
+ * Creates a string response from the screenshot options for use when
+ * screenshot is passed the `--help` argument
+ *
+ * @return string
+ * The formatted information from the param object as a string
*/
-function simulateCameraEffect(document, effect) {
+function getFormattedHelpData() {
+ const formattedParams = screenshotCommandParams
+ .map(formatHelpField)
+ .join("\n\n");
+
+ return `${screenshotDescription}\n${screenshotGroupOptions}\n\n${formattedParams}`;
+}
+
+/**
+ * Main entry point in this file; Takes the original arguments that `:screenshot` was
+ * called with and the image value from the server, and uses the client window to save
+ * the screenshot to the remote debugging machine's memory or clipboard.
+ *
+ * @param object args
+ * The original args with which the screenshot
+ * was called.
+ * @param object value
+ * an object with a image value and file name
+ *
+ * @param object window
+ * The Debugger Client window.
+ *
+ * @return string[]
+ * Response messages from processing the screenshot
+ */
+function processScreenshot(window, args = {}, value) {
+ if (args.help) {
+ const message = getFormattedHelpData();
+ // Wrap meesage in an array so that the return value is consistant with saveScreenshot
+ return [message];
+ }
+ simulateCameraShutter(window.document);
+ return saveScreenshot(window, args, value);
+}
+
+/**
+ * This function is called to simulate camera effects
+ *
+ * @param object document
+ * The Debugger Client document.
+ */
+function simulateCameraShutter(document) {
const window = document.defaultView;
- if (effect === "shutter") {
- if (Services.prefs.getBoolPref("devtools.screenshot.audio.enabled")) {
- const audioCamera = new window.Audio("resource://devtools/client/themes/audio/shutter.wav");
- audioCamera.play();
- }
- }
- if (effect == "flash") {
- const frames = Cu.cloneInto({ opacity: [ 0, 1 ] }, window);
- document.documentElement.animate(frames, CONTAINER_FLASHING_DURATION);
+ if (Services.prefs.getBoolPref("devtools.screenshot.audio.enabled")) {
+ const audioCamera = new window.Audio("resource://devtools/client/themes/audio/shutter.wav");
+ audioCamera.play();
}
}
/**
- * This function simply handles the --delay argument before calling
- * createScreenshotData
+ * Save the captured screenshot to one of several destinations.
+ *
+ * @param object args
+ * The original args with which the screenshot was called.
+ *
+ * @param object window
+ * The Debugger Client window.
+ *
+ * @param object image
+ * The image object that was sent from the server.
+ *
+ * @return string[]
+ * Response messages from processing the screenshot.
*/
-function captureScreenshot(args, document) {
- if (args.delay > 0) {
- return new Promise((resolve, reject) => {
- document.defaultView.setTimeout(() => {
- createScreenshotData(document, args).then(resolve, reject);
- }, args.delay * 1000);
- });
- }
- return createScreenshotData(document, args);
-}
-
-/**
- * There are several possible destinations for the screenshot, SKIP is used
- * in saveScreenshot() whenever one of them is not used
- */
-const SKIP = Promise.resolve();
-
-/**
- * Save the captured screenshot to one of several destinations.
- */
-function saveScreenshot(args, context, reply) {
- const fileNeeded = args.filename != FILENAME_DEFAULT_VALUE ||
- (!args.imgur && !args.clipboard) || args.file;
+async function saveScreenshot(window, args, image) {
+ const fileNeeded = args.filename ||
+ !args.clipboard || args.file;
+ const results = [];
- return Promise.all([
- args.clipboard ? saveToClipboard(context, reply) : SKIP,
- args.imgur ? uploadToImgur(reply) : SKIP,
- fileNeeded ? saveToFile(context, reply) : SKIP,
- ]).then(() => reply);
-}
-
-/**
- * This does the dirty work of creating a base64 string out of an
- * area of the browser window
- */
-function createScreenshotData(document, args) {
- const window = document.defaultView;
- let left = 0;
- let top = 0;
- let width;
- let height;
- const currentX = window.scrollX;
- const currentY = window.scrollY;
-
- let filename = getFilename(args.filename);
-
- if (args.fullpage) {
- // Bug 961832: GCLI screenshot shows fixed position element in wrong
- // position if we don't scroll to top
- window.scrollTo(0, 0);
- width = window.innerWidth + window.scrollMaxX - window.scrollMinX;
- height = window.innerHeight + window.scrollMaxY - window.scrollMinY;
- filename = filename.replace(".png", "-fullpage.png");
- } else if (args.selector) {
- ({ top, left, width, height } = getRect(window, args.selector, window));
- } else {
- left = window.scrollX;
- top = window.scrollY;
- width = window.innerWidth;
- height = window.innerHeight;
+ if (args.clipboard) {
+ const result = await saveToClipboard(window, image.data);
+ results.push(result);
}
- // Only adjust for scrollbars when considering the full window
- if (!args.selector) {
- const winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils);
- const scrollbarHeight = {};
- const scrollbarWidth = {};
- winUtils.getScrollbarSize(false, scrollbarWidth, scrollbarHeight);
- width -= scrollbarWidth.value;
- height -= scrollbarHeight.value;
- }
-
- const canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
- const ctx = canvas.getContext("2d");
- const ratio = args.dpr ? args.dpr : window.devicePixelRatio;
- canvas.width = width * ratio;
- canvas.height = height * ratio;
- ctx.scale(ratio, ratio);
- ctx.drawWindow(window, left, top, width, height, "#fff");
- const data = canvas.toDataURL("image/png", "");
-
- // See comment above on bug 961832
- if (args.fullpage) {
- window.scrollTo(currentX, currentY);
+ if (fileNeeded) {
+ const result = await saveToFile(window, image);
+ results.push(result);
}
-
- simulateCameraEffect(document, "flash");
-
- return Promise.resolve({
- destinations: [],
- data: data,
- height: height,
- width: width,
- filename: filename,
- });
-}
-
-/**
- * We may have a filename specified in args, or we might have to generate
- * one.
- */
-function getFilename(defaultName) {
- // Create a name for the file if not present
- if (defaultName != FILENAME_DEFAULT_VALUE) {
- return defaultName;
- }
-
- const date = new Date();
- let dateString = date.getFullYear() + "-" + (date.getMonth() + 1) +
- "-" + date.getDate();
- dateString = dateString.split("-").map(function(part) {
- if (part.length == 1) {
- part = "0" + part;
- }
- return part;
- }).join("-");
-
- const timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
- return l10n.lookupFormat("screenshotGeneratedFilename",
- [ dateString, timeString ]) + ".png";
+ return results;
}
/**
* Save the image data to the clipboard. This returns a promise, so it can
- * be treated exactly like imgur / file processing, but it's really sync
- * for now.
+ * be treated exactly like file processing.
+ *
+ * @param object window
+ * The Debugger Client window.
+ *
+ * @param string data
+ * The image data encoded in base64 that was sent from the server.
+ *
+ * @return string
+ * Response message from processing the screenshot.
*/
-function saveToClipboard(context, reply) {
+function saveToClipboard(window, data) {
return new Promise(resolve => {
try {
const channel = NetUtil.newChannel({
- uri: reply.data,
+ uri: data,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE
});
const input = channel.open2();
- const loadContext = context.environment.chromeWindow
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsILoadContext);
+ const loadContext = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext);
const callback = {
onImageReady(container, status) {
if (!container) {
console.error("imgTools.decodeImageAsync failed");
- reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
- resolve();
+ resolve(L10N.getStr("screenshotErrorCopying"));
return;
}
try {
const wrapped = Cc["@mozilla.org/supports-interface-pointer;1"]
.createInstance(Ci.nsISupportsInterfacePointer);
wrapped.data = container;
const trans = Cc["@mozilla.org/widget/transferable;1"]
.createInstance(Ci.nsITransferable);
trans.init(loadContext);
trans.addDataFlavor(channel.contentType);
trans.setTransferData(channel.contentType, wrapped, -1);
Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
- reply.destinations.push(l10n.lookup("screenshotCopied"));
+ resolve(L10N.getStr("screenshotCopied"));
} catch (ex) {
console.error(ex);
- reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
+ resolve(L10N.getStr("screenshotErrorCopying"));
}
- resolve();
}
};
const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
const imgTools = Cc["@mozilla.org/image/tools;1"]
.getService(Ci.imgITools);
imgTools.decodeImageAsync(input, channel.contentType, callback,
threadManager.currentThread);
} catch (ex) {
console.error(ex);
- reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
- resolve();
+ resolve(L10N.getStr("screenshotErrorCopying"));
}
});
}
/**
- * Upload screenshot data to Imgur, returning a promise of a URL (as a string)
- */
-function uploadToImgur(reply) {
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- const fd = new FormData();
- fd.append("image", reply.data.split(",")[1]);
- fd.append("type", "base64");
- fd.append("title", reply.filename);
-
- const postURL = Services.prefs.getCharPref("devtools.gcli.imgurUploadURL");
- const clientID = "Client-ID " +
- Services.prefs.getCharPref("devtools.gcli.imgurClientID");
-
- xhr.open("POST", postURL);
- xhr.setRequestHeader("Authorization", clientID);
- xhr.send(fd);
- xhr.responseType = "json";
-
- xhr.onreadystatechange = function() {
- if (xhr.readyState == 4) {
- if (xhr.status == 200) {
- reply.href = xhr.response.data.link;
- reply.destinations.push(l10n.lookupFormat("screenshotImgurUploaded",
- [ reply.href ]));
- } else {
- reply.destinations.push(l10n.lookup("screenshotImgurError"));
- }
-
- resolve();
- }
- };
- });
-}
-
-/**
* Progress listener that forwards calls to a transfer object.
*
* This is used below in saveToFile to forward progress updates from the
* nsIWebBrowserPersist object that does the actual saving to the nsITransfer
* which just represents the operation for the Download Manager. This keeps the
* Download Manager updated on saving progress and completion, so that it gives
* visual feedback from the downloads toolbar button when the save is done.
*
* It also allows the browser window to show auth prompts if needed (should not
* be needed for saving screenshots).
*
* This code is borrowed directly from contentAreaUtils.js.
+ *
+ * @param object win
+ * The Debugger Client window.
+ *
+ * @param object transfer
+ * The transfer object.
+ *
*/
function DownloadListener(win, transfer) {
this.window = win;
this.transfer = transfer;
// For most method calls, forward to the transfer object.
for (const name in transfer) {
if (name != "QueryInterface" &&
name != "onStateChange") {
this[name] = (...args) => transfer[name].apply(transfer, args);
}
}
// Allow saveToFile to await completion for error handling
- this._completedDeferred = defer();
- this.completed = this._completedDeferred.promise;
+ this._completedDeferred = {};
+ this.completed = new Promise((resolve, reject) => {
+ this._completedDeferred.resolve = resolve;
+ this._completedDeferred.reject = reject;
+ });
}
DownloadListener.prototype = {
QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor",
"nsIWebProgressListener",
"nsIWebProgressListener2"]),
getInterface: function(iid) {
@@ -519,36 +311,45 @@ DownloadListener.prototype = {
this.transfer.onStateChange.apply(this.transfer, arguments);
}
};
/**
* Save the screenshot data to disk, returning a promise which is resolved on
* completion.
+ *
+ * @param object window
+ * The Debugger Client window.
+ *
+ * @param object image
+ * The image object that was sent from the server.
+ *
+ * @return string
+ * Response message from processing the screenshot.
*/
-var saveToFile = Task.async(function* (context, reply) {
- const document = context.environment.chromeDocument;
- const window = context.environment.chromeWindow;
+async function saveToFile(window, image) {
+ const document = window.document;
+ let filename = image.filename;
// Check there is a .png extension to filename
- if (!reply.filename.match(/.png$/i)) {
- reply.filename += ".png";
+ if (!filename.match(/.png$/i)) {
+ filename += ".png";
}
- const downloadsDir = yield Downloads.getPreferredDownloadsDirectory();
- const downloadsDirExists = yield OS.File.exists(downloadsDir);
+ const downloadsDir = await Downloads.getPreferredDownloadsDirectory();
+ const downloadsDirExists = await OS.File.exists(downloadsDir);
if (downloadsDirExists) {
// If filename is absolute, it will override the downloads directory and
// still be applied as expected.
- reply.filename = OS.Path.join(downloadsDir, reply.filename);
+ filename = OS.Path.join(downloadsDir, filename);
}
- const sourceURI = Services.io.newURI(reply.data);
- const targetFile = new FileUtils.File(reply.filename);
+ const sourceURI = Services.io.newURI(image.data);
+ const targetFile = new FileUtils.File(filename);
const targetFileURI = Services.io.newFileURI(targetFile);
// Create download and track its progress.
// This is adapted from saveURL in contentAreaUtils.js, but simplified greatly
// and modified to allow saving to arbitrary paths on disk. Using these
// objects as opposed to just writing with OS.File allows us to tie into the
// download manager to record a download entry and to get visual feedback from
// the downloads toolbar button when the save is done.
@@ -579,17 +380,17 @@ var saveToFile = Task.async(function* (c
Ci.nsIHttpChannel.REFERRER_POLICY_UNSET,
null,
null,
targetFileURI,
isPrivate);
try {
// Await successful completion of the save via the listener
- yield listener.completed;
- reply.destinations.push(l10n.lookup("screenshotSavedToFile") +
- ` "${reply.filename}"`);
+ await listener.completed;
+ return L10N.getFormatStr("screenshotSavedToFile", filename);
} catch (ex) {
console.error(ex);
- reply.destinations.push(l10n.lookup("screenshotErrorSavingToFile") + " " +
- reply.filename);
+ return L10N.getFormatStr("screenshotErrorSavingToFile", filename);
}
-});
+}
+
+module.exports = processScreenshot;