--- a/devtools/shared/gcli/commands/screenshot.js
+++ b/devtools/shared/gcli/commands/screenshot.js
@@ -1,22 +1,26 @@
/* 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, Cu } = require("chrome");
+const { Cc, Ci, Cr } = require("chrome");
const l10n = require("gcli/l10n");
const Services = require("Services");
const { getRect } = require("devtools/shared/layout/utils");
+const promise = require("promise");
loader.lazyImporter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
loader.lazyImporter(this, "Task", "resource://gre/modules/Task.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");
const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
.getService(Ci.nsIStringBundleService)
.createBundle("chrome://branding/locale/brand.properties")
.GetStringFromName("brandShortName");
// String used as an indication to generate default file name in the following
// format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
@@ -30,17 +34,21 @@ const FILENAME_DEFAULT_VALUE = " ";
* command when the --chrome flag is *not* used.
*/
/**
* Both commands have the same initial filename parameter
*/
const filenameParam = {
name: "filename",
- type: "string",
+ type: {
+ name: "file",
+ filetype: "file",
+ existing: "maybe",
+ },
defaultValue: FILENAME_DEFAULT_VALUE,
description: l10n.lookup("screenshotFilenameDesc"),
manual: l10n.lookup("screenshotFilenameManual")
};
/**
* Both commands have the same set of standard optional parameters
*/
@@ -425,35 +433,156 @@ function uploadToImgur(reply) {
resolve();
}
};
});
}
/**
- * Save the screenshot data to disk, returning a promise which
- * is resolved on completion
+ * 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 actually 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) {
+ function makeClosure(name) {
+ return function() {
+ transfer[name].apply(transfer, arguments);
+ };
+ }
+
+ this.window = win;
+ this.transfer = transfer;
+
+ // For most method calls, forward to the transfer object.
+ for (let name in transfer) {
+ if (name != "QueryInterface" &&
+ name != "onStateChange") {
+ this[name] = makeClosure(name);
+ }
+ }
+
+ // Allow saveToFile to await completion for error handling
+ this._completedDeferred = promise.defer();
+ this.completed = this._completedDeferred.promise;
+}
+
+DownloadListener.prototype = {
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIInterfaceRequestor) ||
+ iid.equals(Ci.nsIWebProgressListener) ||
+ iid.equals(Ci.nsIWebProgressListener2) ||
+ iid.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ getInterface: function(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt) ||
+ iid.equals(Ci.nsIAuthPrompt2)) {
+ let 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.
*/
function saveToFile(context, reply) {
return Task.spawn(function*() {
- try {
- let document = context.environment.chromeDocument;
- let window = context.environment.chromeWindow;
+ let document = context.environment.chromeDocument;
+ let window = context.environment.chromeWindow;
+
+ // Check there is a .png extension to filename
+ if (!reply.filename.match(/.png$/i)) {
+ reply.filename += ".png";
+ }
- let filename = reply.filename;
- // Check there is a .png extension to filename
- if (!filename.match(/.png$/i)) {
- filename += ".png";
- }
+ let downloadsDir = yield Downloads.getPreferredDownloadsDirectory();
+ let 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);
+ }
+
+ let sourceURI = Services.io.newURI(reply.data, null, null);
+ let targetFile = new FileUtils.File(reply.filename);
+ let targetFileURI = Services.io.newFileURI(targetFile);
- window.saveURL(reply.data, filename, null,
- true /* aShouldBypassCache */, true /* aSkipPrompt */,
- document.documentURIObject, document);
+ // 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;
+ let isPrivate =
+ PrivateBrowsingUtils.isContentWindowPrivate(document.defaultView);
+ let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(Ci.nsIWebBrowserPersist);
+ persist.persistFlags = flags;
+ let tr = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
+ tr.init(sourceURI,
+ targetFileURI,
+ "",
+ null,
+ null,
+ null,
+ persist,
+ isPrivate);
+ let listener = new DownloadListener(window, tr);
+ persist.progressListener = listener;
+ persist.savePrivacyAwareURI(sourceURI,
+ null,
+ document.documentURIObject,
+ Ci.nsIHttpChannel
+ .REFERRER_POLICY_NO_REFERRER_WHEN_DOWNGRADE,
+ null,
+ null,
+ targetFileURI,
+ isPrivate);
- reply.destinations.push(l10n.lookup("screenshotSavedToFile") + " \"" + filename + "\"");
- }
- catch (ex) {
+ 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") + " " + filename);
+ reply.destinations.push(l10n.lookup("screenshotErrorSavingToFile") + " " +
+ reply.filename);
}
});
}
--- a/devtools/shared/gcli/source/lib/gcli/util/fileparser.js
+++ b/devtools/shared/gcli/source/lib/gcli/util/fileparser.js
@@ -100,17 +100,17 @@ exports.parse = function(context, typed,
});
};
var RANK_OPTIONS = { noSort: true, prefixZero: true };
/**
* We want to be able to turn predictions off in Firefox
*/
-exports.supportsPredictions = true;
+exports.supportsPredictions = false;
/**
* Get a function which creates predictions of files that match the given
* path
*/
function getPredictor(typed, options) {
if (!exports.supportsPredictions) {
return undefined;