--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -103,16 +103,17 @@ XPCOMUtils.defineLazyScriptGetter(this,
"chrome://browser/content/browser-sync.js");
XPCOMUtils.defineLazyScriptGetter(this, "gBrowserThumbnails",
"chrome://browser/content/browser-thumbnails.js");
XPCOMUtils.defineLazyScriptGetter(this, ["setContextMenuContentData",
"openContextMenu", "nsContextMenu"],
"chrome://browser/content/nsContextMenu.js");
XPCOMUtils.defineLazyScriptGetter(this, ["DownloadsPanel",
"DownloadsOverlayLoader",
+ "DownloadsSubview",
"DownloadsView", "DownloadsViewUI",
"DownloadsViewController",
"DownloadsSummary", "DownloadsFooter",
"DownloadsBlockedSubview"],
"chrome://browser/content/downloads/downloads.js");
XPCOMUtils.defineLazyScriptGetter(this, ["DownloadsButton",
"DownloadsIndicatorView"],
"chrome://browser/content/downloads/indicator.js");
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -713,16 +713,21 @@
label="&historyMenu.label;"
closemenu="none"
oncommand="PanelUI.showSubView('PanelUI-history', this)"/>
<toolbarbutton id="appMenu-library-remotetabs-button"
class="subviewbutton subviewbutton-iconic subviewbutton-nav"
label="&appMenuRemoteTabs.label;"
closemenu="none"
oncommand="PanelUI.showSubView('PanelUI-remotetabs', this)"/>
+ <toolbarbutton id="appMenu-library-downloads-button"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ label="&libraryDownloads.label;"
+ closemenu="none"
+ oncommand="DownloadsSubview.show(this);"/>
</vbox>
</panelview>
<panelview id="PanelUI-bookmarkingTools" class="PanelUI-subView">
<vbox class="panel-subview-body">
<toolbarbutton id="panelMenu_toggleBookmarksMenu"
label="&addBookmarksMenu.label;"
label-checked="&removeBookmarksMenu.label;"
new file mode 100644
--- /dev/null
+++ b/browser/components/downloads/DownloadsSubview.jsm
@@ -0,0 +1,399 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadsSubview",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
+ "resource:///modules/DownloadsCommon.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadsViewUI",
+ "resource:///modules/DownloadsViewUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+let gPanelViewInstances = new WeakMap();
+const kEvents = ["ViewShowing", "ViewHiding", "click", "command"];
+XPCOMUtils.defineLazyGetter(this, "kButtonLabels", () => {
+ return {
+ show: DownloadsCommon.strings[AppConstants.platform == "macosx" ? "showMacLabel" : "showLabel"],
+ open: DownloadsCommon.strings.openFileLabel,
+ retry: DownloadsCommon.strings.retryLabel,
+ };
+});
+
+class DownloadsSubview extends DownloadsViewUI.BaseView {
+ constructor(panelview) {
+ super();
+ this.document = panelview.ownerDocument;
+ this.window = panelview.ownerGlobal;
+
+ this.context = "panelDownloadsContextMenu";
+
+ this.panelview = panelview;
+ this.container = this.document.getElementById("panelMenu_downloadsMenu");
+ while (this.container.lastChild) {
+ this.container.lastChild.remove();
+ }
+ this.panelview.addEventListener("click", DownloadsSubview.onClick);
+ this.panelview.addEventListener("ViewHiding", DownloadsSubview.onViewHiding);
+
+ this._viewItemsForDownloads = new WeakMap();
+
+ let contextMenu = this.document.getElementById(this.context);
+ if (!contextMenu) {
+ contextMenu = this.document.getElementById("downloadsContextMenu").cloneNode(true);
+ contextMenu.setAttribute("closemenu", "none");
+ contextMenu.setAttribute("id", this.context);
+ contextMenu.removeAttribute("onpopupshown");
+ contextMenu.setAttribute("onpopupshowing",
+ "DownloadsSubview.updateContextMenu(document.popupNode, this);");
+ contextMenu.setAttribute("onpopuphidden", "DownloadsSubview.onContextMenuHidden(this);")
+ let clearButton = contextMenu.querySelector("menuitem[command='downloadsCmd_clearDownloads'");
+ clearButton.hidden = false;
+ clearButton.previousSibling.hidden = true;
+ }
+ this.panelview.appendChild(contextMenu);
+ this.container.setAttribute("context", this.context);
+
+ this._downloadsData = DownloadsCommon.getData(this.window, true, true, true);
+ this._downloadsData.addView(this);
+ }
+
+ destructor(event) {
+ this.panelview.removeEventListener("click", DownloadsSubview.onClick);
+ this.panelview.removeEventListener("ViewHiding", DownloadsSubview.onViewHiding);
+ this._downloadsData.removeView(this);
+ gPanelViewInstances.delete(this);
+ }
+
+ /**
+ * DataView handler; invoked when a batch of downloads is being passed in -
+ * usually when this instance is added as a view in the constructor.
+ */
+ onDownloadBatchStarting() {
+ this.batchFragment = this.document.createDocumentFragment();
+ this.window.clearTimeout(this._batchTimeout);
+ }
+
+ /**
+ * DataView handler; invoked when the view stopped feeding its current list of
+ * downloads.
+ */
+ onDownloadBatchEnded() {
+ let {window} = this;
+ window.clearTimeout(this._batchTimeout);
+ let waitForMs = 200;
+ if (this.batchFragment.childElementCount) {
+ // Prepend the batch fragment.
+ this.container.insertBefore(this.batchFragment, this.container.firstChild || null);
+ waitForMs = 0;
+ }
+ // Wait a wee bit to dispatch the event, because another batch may start
+ // right away.
+ this._batchTimeout = window.setTimeout(() =>
+ this.panelview.dispatchEvent(new window.CustomEvent("DownloadsLoaded")), waitForMs);
+ this.batchFragment = null;
+ }
+
+ /**
+ * DataView handler; invoked when a new download is added to the list.
+ *
+ * @param {Download} download
+ * @param {DOMNode} [options.insertBefore]
+ */
+ onDownloadAdded(download, { insertBefore } = {}) {
+ let shell = new DownloadsSubview.Button(download, this.document);
+ this._viewItemsForDownloads.set(download, shell);
+ // Triggger the code that update all attributes to match the downloads'
+ // current state.
+ shell.onChanged();
+
+ // Since newest downloads are displayed at the top, either prepend the new
+ // element or insert it after the one indicated by the insertBefore option.
+ if (insertBefore) {
+ this._viewItemsForDownloads.get(insertBefore)
+ .element.insertAdjacentElement("afterend", shell.element);
+ } else {
+ (this.batchFragment || this.container).prepend(shell.element);
+ }
+ }
+
+ /**
+ * DataView Handler; invoked when the state of a download changed.
+ *
+ * @param {Download} download
+ */
+ onDownloadChanged(download) {
+ this._viewItemsForDownloads.get(download).onChanged();
+ }
+
+ /**
+ * DataView handler; invoked when a download is removed.
+ *
+ * @param {Download} download
+ */
+ onDownloadRemoved(download) {
+ this._viewItemsForDownloads.get(download).element.remove();
+ }
+
+ // ----- Static methods. -----
+
+ /**
+ * Perform all tasks necessary to be able to show a Downloads Subview.
+ *
+ * @param {DOMWindow} window Global window object.
+ * @return {Promise} Will resolve when all tasks are done.
+ */
+ static init(window) {
+ return new Promise(resolve =>
+ window.DownloadsOverlayLoader.ensureOverlayLoaded(window.DownloadsPanel.kDownloadsOverlay, resolve));
+ }
+
+ /**
+ * Show the Downloads subview panel and listen for events that will trigger
+ * building the dynamic part of the view.
+ *
+ * @param {DOMNode} anchor The button that was commanded to trigger this function.
+ */
+ static async show(anchor) {
+ let document = anchor.ownerDocument;
+ let window = anchor.ownerGlobal;
+ await DownloadsSubview.init(window);
+
+ let panelview = document.getElementById("PanelUI-downloads");
+ anchor.setAttribute("closemenu", "none");
+ gPanelViewInstances.set(panelview, new DownloadsSubview(panelview));
+
+ // Since the DownloadsLists are propagated asynchronously, we need to wait a
+ // little to get the view propagated.
+ panelview.addEventListener("ViewShowing", event => {
+ event.detail.addBlocker(new Promise(resolve => {
+ panelview.addEventListener("DownloadsLoaded", resolve, { once: true });
+ }));
+ }, { once: true });
+
+ window.PanelUI.showSubView("PanelUI-downloads", anchor);
+ }
+
+ /**
+ * Handler method; reveal the users' download directory using the OS specific
+ * method.
+ */
+ static async onShowDownloads() {
+ // Retrieve the user's default download directory.
+ let preferredDir = await Downloads.getPreferredDownloadsDirectory();
+ DownloadsCommon.showDirectory(new FileUtils.File(preferredDir));
+ }
+
+ /**
+ * Handler method; clear the list downloads finished and old(er) downloads,
+ * just like in the Library.
+ *
+ * @param {DOMNode} button Button that was clicked to call this method.
+ */
+ static onClearDownloads(button) {
+ let instance = gPanelViewInstances.get(button.closest("panelview"));
+ if (!instance)
+ return;
+ instance._downloadsData.removeFinished();
+ Cc["@mozilla.org/browser/download-history;1"]
+ .getService(Ci.nsIDownloadHistory)
+ .removeAllDownloads();
+ }
+
+ /**
+ * Just before showing the context menu, anchored to a download item, we need
+ * to set the right properties to make sure the right menu-items are visible.
+ *
+ * @param {DOMNode} button The Button the context menu will be anchored to.
+ * @param {DOMNode} menu The context menu.
+ */
+ static updateContextMenu(button, menu) {
+ while (!button._shell) {
+ button = button.parentNode;
+ }
+ menu.setAttribute("state", button.getAttribute("state"));
+ if (button.hasAttribute("exists"))
+ menu.setAttribute("exists", button.getAttribute("exists"));
+ else
+ menu.removeAttribute("exists");
+ menu.classList.toggle("temporary-block", button.classList.contains("temporary-block"));
+ menu.querySelector("menuitem[command='downloadsCmd_clearDownloads'").disabled =
+ !DownloadsSubview.canClearDownloads(button);
+ // The menu anchorNode property is not available long enough to be used elsewhere,
+ // so tack it another property name.
+ menu._anchorNode = button;
+ }
+
+ /**
+ * Right after the context menu was hidden, perform a bit of cleanup.
+ *
+ * @param {DOMNode} menu The context menu.
+ */
+ static onContextMenuHidden(menu) {
+ delete menu._anchorNode;
+ }
+
+ /**
+ * Static version of DownloadsSubview#canClearDownloads().
+ *
+ * @param {DOMNode} button Button that we'll use to find the right
+ * DownloadsSubview instance.
+ */
+ static canClearDownloads(button) {
+ let instance = gPanelViewInstances.get(button.closest("panelview"));
+ if (!instance)
+ return false;
+ return instance.canClearDownloads(instance.container);
+ }
+
+ /**
+ * Handler method; invoked when the Downloads panel is hidden and should be
+ * torn down & cleaned up.
+ *
+ * @param {DOMEvent} event
+ */
+ static onViewHiding(event) {
+ let instance = gPanelViewInstances.get(event.target);
+ if (!instance)
+ return;
+ instance.destructor(event);
+ }
+
+ /**
+ * Handler method; invoked when anything is clicked inside the Downloads panel.
+ * Depending on the context, it will find the appropriate command to invoke.
+ *
+ * We don't have a command dispatcher registered for this view, so we don't go
+ * through the goDoCommand path like we do for the other views.
+ *
+ * @param {DOMMouseEvent} event
+ */
+ static onClick(event) {
+ // Middle clicks fall through and are regarded as left clicks.
+ if (event.button > 1)
+ return;
+
+ let button = event.originalTarget;
+ if (!button.hasAttribute || button.classList.contains("subviewbutton-back"))
+ return;
+
+ let command = "downloadsCmd_open";
+ if (button.classList.contains("action-button")) {
+ button = button.parentNode;
+ command = button.hasAttribute("showLabel") ? "downloadsCmd_show" : "downloadsCmd_retry";
+ } else if (button.localName == "menuitem") {
+ command = button.getAttribute("command");
+ button = button.parentNode._anchorNode;
+ }
+ while (button && !button._shell && button != this.panelview &&
+ (!button.hasAttribute || !button.hasAttribute("oncommand"))) {
+ button = button.parentNode;
+ }
+
+ // We don't need to do anything when no button was clicked, like a separator
+ // or a blank panel area. Also, when 'oncommand' is set, the button will invoke
+ // its own, custom command handler.
+ if (!button || button == this.panelview || button.hasAttribute("oncommand"))
+ return;
+
+ if (command == "downloadsCmd_clearDownloads") {
+ DownloadsSubview.onClearDownloads(button);
+ } else if (button._shell.isCommandEnabled(command)) {
+ button._shell[command]();
+ }
+ }
+}
+
+DownloadsSubview.Button = class extends DownloadsViewUI.DownloadElementShell {
+ constructor(download, document) {
+ super();
+ this.download = download;
+
+ this.element = document.createElement("toolbarbutton");
+ this.element._shell = this;
+
+ this.element.classList.add("subviewbutton", "subviewbutton-iconic", "download",
+ "download-state");
+ }
+
+ get browserWindow() {
+ return this.element.ownerGlobal;
+ }
+
+ /**
+ * Handle state changes of a download.
+ */
+ onStateChanged() {
+ // Since the state changed, we may need to check the target file again.
+ this._targetFileChecked = false;
+
+ this._updateState();
+ }
+
+ /**
+ * Handler method; invoked when any state attribute of a download changed.
+ */
+ onChanged() {
+ // TODO: implement "file moved or missing" check - bug 1395615.
+ let newState = DownloadsCommon.stateOfDownload(this.download);
+ if (this._downloadState !== newState) {
+ this._downloadState = newState;
+ this.onStateChanged();
+ } else {
+ this._updateState();
+ }
+
+ // This cannot be placed within onStateChanged because when a download goes
+ // from hasBlockedData to !hasBlockedData it will still remain in the same state.
+ this.element.classList.toggle("temporary-block",
+ !!this.download.hasBlockedData);
+ }
+
+ /**
+ * Update the DOM representation of this download to match the current, recently
+ * updated, state.
+ */
+ _updateState() {
+ super._updateState();
+ this.element.setAttribute("label", this.element.getAttribute("displayName"));
+ this.element.setAttribute("tooltiptext", this.element.getAttribute("fullStatus"));
+
+ if (this.isCommandEnabled("downloadsCmd_show")) {
+ this.element.setAttribute("openLabel", kButtonLabels.open);
+ this.element.setAttribute("showLabel", kButtonLabels.show);
+ } else if (this.isCommandEnabled("downloadsCmd_retry")) {
+ this.element.setAttribute("retryLabel", kButtonLabels.retry);
+ }
+
+ this._updateVisibility();
+ }
+
+ _updateVisibility() {
+ let state = this.element.getAttribute("state");
+ // This view only show completed and failed downloads.
+ this.element.hidden = !(state == DownloadsCommon.DOWNLOAD_FINISHED ||
+ state == DownloadsCommon.DOWNLOAD_FAILED);
+ }
+
+ /**
+ * Command handler; copy the download URL to the OS general clipboard.
+ */
+ downloadsCmd_copyLocation() {
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(this.download.source.url);
+ }
+};
--- a/browser/components/downloads/DownloadsViewUI.jsm
+++ b/browser/components/downloads/DownloadsViewUI.jsm
@@ -18,31 +18,52 @@ const { classes: Cc, interfaces: Ci, uti
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
"resource://gre/modules/DownloadUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
"resource:///modules/DownloadsCommon.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
+ "resource:///modules/RecentWindow.jsm");
this.DownloadsViewUI = {
/**
* Returns true if the given string is the name of a command that can be
* handled by the Downloads user interface, including standard commands.
*/
isCommandName(name) {
return name.startsWith("cmd_") || name.startsWith("downloadsCmd_");
},
};
+this.DownloadsViewUI.BaseView = class {
+ canClearDownloads(nodeContainer) {
+ // Downloads can be cleared if there's at least one removable download in
+ // the list (either a history download or a completed session download).
+ // Because history downloads are always removable and are listed after the
+ // session downloads, check from bottom to top.
+ for (let elt = nodeContainer.lastChild; elt; elt = elt.previousSibling) {
+ // Stopped, paused, and failed downloads with partial data are removed.
+ let download = elt._shell.download;
+ if (download.stopped && !(download.canceled && download.hasPartialData)) {
+ return true;
+ }
+ }
+ return false;
+ }
+};
+
/**
* A download element shell is responsible for handling the commands and the
* displayed data for a single element that uses the "download.xml" binding.
*
* The information to display is obtained through the associated Download object
* from the JavaScript API for downloads, and commands are executed using a
* combination of Download methods and DownloadsCommon.jsm helper functions.
*
@@ -85,16 +106,20 @@ this.DownloadsViewUI.DownloadElementShel
*/
get displayName() {
if (!this.download.target.path) {
return this.download.source.url;
}
return OS.Path.basename(this.download.target.path);
},
+ get browserWindow() {
+ return RecentWindow.getMostRecentBrowserWindow();
+ },
+
/**
* The progress element for the download, or undefined in case the XBL binding
* has not been applied yet.
*/
get _progressElement() {
if (!this.__progressElement) {
// If the element is not available now, we will try again the next time.
this.__progressElement =
@@ -363,41 +388,89 @@ this.DownloadsViewUI.DownloadElementShel
case "downloadsCmd_confirmBlock":
case "downloadsCmd_chooseUnblock":
case "downloadsCmd_chooseOpen":
case "downloadsCmd_unblock":
case "downloadsCmd_unblockAndOpen":
return this.download.hasBlockedData;
case "downloadsCmd_cancel":
return this.download.hasPartialData || !this.download.stopped;
+ case "downloadsCmd_open":
+ // This property is false if the download did not succeed.
+ return this.download.target.exists;
+ case "downloadsCmd_show":
+ // TODO: Bug 827010 - Handle part-file asynchronously.
+ if (this.download.target.partFilePath) {
+ let partFile = new FileUtils.File(this.download.target.partFilePath);
+ if (partFile.exists()) {
+ return true;
+ }
+ }
+
+ // This property is false if the download did not succeed.
+ return this.download.target.exists;
+ case "cmd_delete":
+ // We don't want in-progress downloads to be removed accidentally.
+ return this.download.stopped;
}
- return false;
+ return DownloadsViewUI.isCommandName(aCommand) && !!this[aCommand];
+ },
+
+ doCommand(aCommand) {
+ if (DownloadsViewUI.isCommandName(aCommand)) {
+ this[aCommand]();
+ }
},
downloadsCmd_cancel() {
// This is the correct way to avoid race conditions when cancelling.
this.download.cancel().catch(() => {});
this.download.removePartialData().catch(Cu.reportError);
},
- downloadsCmd_retry() {
- // Errors when retrying are already reported as download failures.
- this.download.start().catch(() => {});
+ downloadsCmd_confirmBlock() {
+ this.download.confirmBlock().catch(Cu.reportError);
+ },
+
+ downloadsCmd_open() {
+ let file = new FileUtils.File(this.download.target.path);
+ DownloadsCommon.openDownloadedFile(file, null, this.element.ownerGlobal);
+ },
+
+ downloadsCmd_openReferrer() {
+ this.element.ownerGlobal.openURL(this.download.source.referrer);
},
downloadsCmd_pauseResume() {
if (this.download.stopped) {
this.download.start();
} else {
this.download.cancel();
}
},
- downloadsCmd_confirmBlock() {
- this.download.confirmBlock().catch(Cu.reportError);
+ downloadsCmd_show() {
+ let file = new FileUtils.File(this.download.target.path);
+ DownloadsCommon.showDownloadedFile(file);
+ },
+
+ downloadsCmd_retry() {
+ if (this.download.start) {
+ // Errors when retrying are already reported as download failures.
+ this.download.start().catch(() => {});
+ return;
+ }
+
+ let window = this.browserWindow || this.element.ownerGlobal;
+ let document = window.document;
+
+ // Do not suggest a file name if we don't know the original target.
+ let targetPath = this.download.target.path ?
+ OS.Path.basename(this.download.target.path) : null;
+ window.DownloadURL(this.download.source.url, targetPath, document);
},
cmd_delete() {
(async () => {
// Remove the associated history element first, if any, so that the views
// that combine history and session downloads won't resurrect the history
// download into the view just before it is deleted permanently.
try {
--- a/browser/components/downloads/content/allDownloadsViewOverlay.js
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.js
@@ -115,75 +115,20 @@ HistoryDownloadElementShell.prototype =
},
_downloadState: null,
isCommandEnabled(aCommand) {
// The only valid command for inactive elements is cmd_delete.
if (!this.active && aCommand != "cmd_delete") {
return false;
}
- switch (aCommand) {
- case "downloadsCmd_open":
- // This property is false if the download did not succeed.
- return this.download.target.exists;
- case "downloadsCmd_show":
- // TODO: Bug 827010 - Handle part-file asynchronously.
- if (this.download.target.partFilePath) {
- let partFile = new FileUtils.File(this.download.target.partFilePath);
- if (partFile.exists()) {
- return true;
- }
- }
-
- // This property is false if the download did not succeed.
- return this.download.target.exists;
- case "cmd_delete":
- // We don't want in-progress downloads to be removed accidentally.
- return this.download.stopped;
- }
return DownloadsViewUI.DownloadElementShell.prototype
.isCommandEnabled.call(this, aCommand);
},
- doCommand(aCommand) {
- if (DownloadsViewUI.isCommandName(aCommand)) {
- this[aCommand]();
- }
- },
-
- downloadsCmd_retry() {
- if (this.download.start) {
- DownloadsViewUI.DownloadElementShell.prototype
- .downloadsCmd_retry.call(this);
- return;
- }
-
- let browserWin = RecentWindow.getMostRecentBrowserWindow();
- let initiatingDoc = browserWin ? browserWin.document : document;
-
- // Do not suggest a file name if we don't know the original target.
- let targetPath = this.download.target.path ?
- OS.Path.basename(this.download.target.path) : null;
- DownloadURL(this.download.source.url, targetPath, initiatingDoc);
- },
-
- downloadsCmd_open() {
- let file = new FileUtils.File(this.download.target.path);
- DownloadsCommon.openDownloadedFile(file, null, window);
- },
-
- downloadsCmd_show() {
- let file = new FileUtils.File(this.download.target.path);
- DownloadsCommon.showDownloadedFile(file);
- },
-
- downloadsCmd_openReferrer() {
- openURL(this.download.source.referrer);
- },
-
downloadsCmd_unblock() {
this.confirmUnblock(window, "unblock");
},
downloadsCmd_chooseUnblock() {
this.confirmUnblock(window, "chooseUnblock");
},
@@ -293,16 +238,18 @@ function DownloadsPlacesView(aRichListBo
}, true);
// Resizing the window may change items visibility.
window.addEventListener("resize", () => {
this._ensureVisibleElementsAreActive();
}, true);
}
DownloadsPlacesView.prototype = {
+ __proto__: DownloadsViewUI.BaseView.prototype,
+
get associatedElement() {
return this._richlistbox;
},
get active() {
return this._active;
},
set active(val) {
@@ -571,38 +518,23 @@ DownloadsPlacesView.prototype = {
case "downloadsCmd_openReferrer":
case "downloadShowMenuItem":
return this._richlistbox.selectedItems.length == 1;
case "cmd_selectAll":
return true;
case "cmd_paste":
return this._canDownloadClipboardURL();
case "downloadsCmd_clearDownloads":
- return this._canClearDownloads();
+ return this.canClearDownloads(this._richlistbox);
default:
return Array.every(this._richlistbox.selectedItems,
element => element._shell.isCommandEnabled(aCommand));
}
},
- _canClearDownloads() {
- // Downloads can be cleared if there's at least one removable download in
- // the list (either a history download or a completed session download).
- // Because history downloads are always removable and are listed after the
- // session downloads, check from bottom to top.
- for (let elt = this._richlistbox.lastChild; elt; elt = elt.previousSibling) {
- // Stopped, paused, and failed downloads with partial data are removed.
- let download = elt._shell.download;
- if (download.stopped && !(download.canceled && download.hasPartialData)) {
- return true;
- }
- }
- return false;
- },
-
_copySelectedDownloadsToClipboard() {
let urls = Array.map(this._richlistbox.selectedItems,
element => element._shell.download.source.url);
Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper)
.copyString(urls.join("\n"));
},
--- a/browser/components/downloads/content/download.xml
+++ b/browser/components/downloads/content/download.xml
@@ -119,9 +119,25 @@
<xul:label class="toolbarbutton-badge" xbl:inherits="value=badge" top="0" end="0" crop="none"/>
</xul:stack>
<xul:label class="toolbarbutton-text" crop="right" flex="1"
xbl:inherits="value=label,accesskey,crop,wrap"/>
<xul:label class="toolbarbutton-multiline-text" flex="1"
xbl:inherits="xbl:text=label,accesskey,wrap"/>
</content>
</binding>
+
+ <binding id="download-subview-toolbarbutton"
+ extends="chrome://global/content/bindings/button.xml#menu-button-base">
+ <content>
+ <children includes="observes|template|menupopup|panel|tooltip"/>
+ <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/>
+ <xul:vbox class="toolbarbutton-text" flex="1">
+ <xul:label crop="end" xbl:inherits="value=label,accesskey,crop,wrap"/>
+ <xul:label class="status-text status-full" crop="end" xbl:inherits="value=fullStatus"/>
+ <xul:label class="status-text status-open" crop="end" xbl:inherits="value=openLabel"/>
+ <xul:label class="status-text status-retry" crop="end" xbl:inherits="value=retryLabel"/>
+ <xul:label class="status-text status-show" crop="end" xbl:inherits="value=showLabel"/>
+ </xul:vbox>
+ <xul:toolbarbutton anonid="button" class="action-button"/>
+ </content>
+ </binding>
</bindings>
--- a/browser/components/downloads/content/downloads.css
+++ b/browser/components/downloads/content/downloads.css
@@ -202,8 +202,38 @@ richlistitem.download button {
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack > .panel-mainview {
max-width: unset;
}
/* Show the "show blocked info" button. */
#downloadsPanel-mainView .download-state[state="8"] .downloadShowBlockedInfo {
display: inline;
}
+
+/* DownloadsSubview styles: */
+
+.subviewbutton.download {
+ -moz-binding: url("chrome://browser/content/downloads/download.xml#download-subview-toolbarbutton");
+}
+
+/* When a Download is hovered that has an [openLabel] attribute set, which means
+ that the file exists and can be opened, hide the status label.
+ When a Download is hovered - specifically on the secondary action button - that
+ has a [retryLabel] attribute set, which means that the file does not exist and
+ the download failed earlier, hide the status label. */
+.subviewbutton.download:hover:-moz-any([openLabel],[retryLabel][buttonover]) > .toolbarbutton-text > .status-full,
+/* When a Download is not hovered at all or the secondary action button is hovered,
+ hide the 'Open File' status label. */
+.subviewbutton.download:-moz-any(:not(:hover),[buttonover]) > .toolbarbutton-text > .status-open,
+/* When a Download is not hovered at all, or when it's hovered but specifically
+ not the secondary action button or when the [retryLabel] is not set, hide the
+ 'Retry Downloads' label. */
+.subviewbutton.download:-moz-any(:not(:hover),:hover:not([buttonover]),:not([retryLabel])) > .toolbarbutton-text > .status-retry,
+/* When a Download is not hovered at all, or when it's hovered but specifically
+ not the secondary action button or when the file does not exist, hide the
+ 'Open Containing Folder' label. */
+.subviewbutton.download:-moz-any(:not(:hover),:hover:not([buttonover]),:not([exists])) > .toolbarbutton-text > .status-show,
+/* When a Download is not hovered at all, hide the secondary action button. */
+.subviewbutton.download:not(:hover) > .action-button,
+/* Always hide the label of the secondary action button. */
+.subviewbutton.download > .action-button > .toolbarbutton-text {
+ display: none;
+}
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -63,16 +63,18 @@
"use strict";
var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsViewUI",
"resource:///modules/DownloadsViewUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadsSubview",
+ "resource:///modules/DownloadsSubview.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
--- a/browser/components/downloads/content/downloadsOverlay.xul
+++ b/browser/components/downloads/content/downloadsOverlay.xul
@@ -99,16 +99,19 @@
<menuitem command="cmd_delete"
class="downloadRemoveFromHistoryMenuItem"
label="&cmd.removeFromHistory.label;"
accesskey="&cmd.removeFromHistory.accesskey;"/>
<menuitem command="downloadsCmd_clearList"
label="&cmd.clearList2.label;"
accesskey="&cmd.clearList2.accesskey;"/>
+ <menuitem command="downloadsCmd_clearDownloads"
+ hidden="true"
+ label="&cmd.clearDownloads.label;"/>
</menupopup>
<photonpanelmultiview id="downloadsPanel-multiView"
mainViewId="downloadsPanel-mainView">
<panelview id="downloadsPanel-mainView">
<vbox class="panel-view-body-unscrollable">
<richlistbox id="downloadsListBox"
@@ -176,13 +179,35 @@
<button id="downloadsPanel-blockedSubview-deleteButton"
class="downloadsPanelFooterButton"
oncommand="DownloadsBlockedSubview.confirmBlock();"
default="true"
flex="1"/>
</hbox>
</panelview>
+ <panelview id="PanelUI-downloads" class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="appMenu-library-downloads-show-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&cmd.showDownloads.label;"
+ closemenu="none"
+ oncommand="DownloadsSubview.onShowDownloads(this);"/>
+ <toolbarseparator/>
+ <toolbaritem id="panelMenu_downloadsMenu"
+ orient="vertical"
+ smoothscroll="false"
+ flatList="true"
+ tooltip="bhTooltip">
+ <!-- downloads menu items will go here -->
+ </toolbaritem>
+ </vbox>
+ <toolbarbutton id="PanelUI-downloadsMore"
+ class="panel-subview-footer subviewbutton"
+ label="&downloadsHistory.label;"
+ oncommand="BrowserDownloadsUI(); CustomizableUI.hidePanelForNode(this);"/>
+ </panelview>
+
</photonpanelmultiview>
</panel>
</popupset>
</overlay>
--- a/browser/components/downloads/moz.build
+++ b/browser/components/downloads/moz.build
@@ -8,14 +8,15 @@ with Files('*'):
BUG_COMPONENT = ('Firefox', 'Downloads Panel')
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
JAR_MANIFESTS += ['jar.mn']
EXTRA_JS_MODULES += [
'DownloadsCommon.jsm',
+ 'DownloadsSubview.jsm',
'DownloadsTaskbar.jsm',
'DownloadsViewUI.jsm',
]
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Downloads Panel')
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -256,16 +256,19 @@ These should match what Safari and other
<!ENTITY toolsMenu.label "Tools">
<!ENTITY toolsMenu.accesskey "T">
<!ENTITY keywordfield.label "Add a Keyword for this Search…">
<!ENTITY keywordfield.accesskey "K">
<!ENTITY downloads.label "Downloads">
+<!-- LOCALIZATION NOTE (libraryDownloads.label): This label is similar to
+ - downloads.label, but used in the Library panel. -->
+<!ENTITY libraryDownloads.label "Downloads">
<!ENTITY downloads.accesskey "D">
<!ENTITY downloads.commandkey "j">
<!ENTITY downloadsUnix.commandkey "y">
<!ENTITY addons.label "Add-ons">
<!ENTITY addons.accesskey "A">
<!ENTITY addons.commandkey "A">
<!ENTITY webDeveloperMenu.label "Web Developer">
--- a/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
+++ b/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
@@ -51,16 +51,17 @@
cmd.showMac.accesskey):
The show and showMac commands are never shown together, thus they can share
the same access key (though the two access keys can also be different).
-->
<!ENTITY cmd.show.label "Open Containing Folder">
<!ENTITY cmd.show.accesskey "F">
<!ENTITY cmd.showMac.label "Show In Finder">
<!ENTITY cmd.showMac.accesskey "F">
+<!ENTITY cmd.showDownloads.label "Show Downloads Folder">
<!ENTITY cmd.retry.label "Retry">
<!ENTITY cmd.goToDownloadPage.label "Go To Download Page">
<!ENTITY cmd.goToDownloadPage.accesskey "G">
<!ENTITY cmd.copyDownloadLink.label "Copy Download Link">
<!ENTITY cmd.copyDownloadLink.accesskey "L">
<!ENTITY cmd.removeFromHistory.label "Remove From History">
<!ENTITY cmd.removeFromHistory.accesskey "e">
<!ENTITY cmd.clearList2.label "Clear Preview Panel">
--- a/browser/locales/en-US/chrome/browser/downloads/downloads.properties
+++ b/browser/locales/en-US/chrome/browser/downloads/downloads.properties
@@ -97,8 +97,22 @@ fileExecutableSecurityWarningTitle=Open
fileExecutableSecurityWarningDontAsk=Don’t ask me this again
# LOCALIZATION NOTE (otherDownloads3):
# This is displayed in an item at the bottom of the Downloads Panel when
# there are more downloads than can fit in the list in the panel. Use a
# semi-colon list of plural forms.
# See: http://developer.mozilla.org/en/Localization_and_Plurals
otherDownloads3=%1$S file downloading;%1$S files downloading
+
+# LOCALIZATION NOTE (showLabel, showMacLabel):
+# This is displayed when you hover a download item in the Library widget view.
+# showMacLabel is only shown on Mac OSX.
+showLabel=Open Containing Folder
+showMacLabel=Open In Finder
+# LOCALIZATION NOTE (openFileLabel):
+# Displayed when hovering a complete download, indicates that it's possible to
+# open the file using an app available in the system.
+openFileLabel=Open File
+# LOCALIZATION NOTE (retryLabel):
+# Displayed when hovering a download which is able to be retried by users,
+# indicates that it's possible to download this file again.
+retryLabel=Retry Download
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -2041,9 +2041,50 @@ photonpanelmultiview .cui-widget-panelvi
overflow-x: visible;
overflow-y: visible;
}
photonpanelmultiview #panelMenu_pocket {
display: none;
}
+.subviewbutton.download {
+ -moz-box-align: start;
+ min-height: 48px;
+}
+
+.subviewbutton.download > .toolbarbutton-icon,
+.subviewbutton.download > .toolbarbutton-text > label {
+ margin: 4px 0 0;
+}
+
+.subviewbutton.download > .toolbarbutton-text > .status-text {
+ color: GrayText;
+ font-size: .7em;
+}
+
+.subviewbutton.download > .action-button {
+ -moz-context-properties: fill;
+ fill: currentColor;
+ list-style-image: url("chrome://browser/skin/find.svg");
+ /* Measurement to vertically center this button: 1 line of text minus half of 4px top margin. */
+ margin: calc(1em - 2px) 0 0;
+ padding: 4px;
+}
+
+.subviewbutton.download[retryLabel] > .action-button {
+ list-style-image: url("chrome://browser/skin/reload.svg");
+}
+
+.subviewbutton.download:not([openLabel]):not([retryLabel]) > .action-button {
+ fill: GrayText;
+ opacity: .5;
+}
+
+.subviewbutton.download:-moz-any([openLabel],[retryLabel]) > .action-button@buttonStateHover@ {
+ background-color: var(--arrowpanel-dimmed-further);
+}
+
+.subviewbutton.download:-moz-any([openLabel],[retryLabel]) > .action-button@buttonStateActive@ {
+ background-color: var(--arrowpanel-dimmed-even-further);
+}
+
/* END photon adjustments */
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/icons/folder.svg
@@ -0,0 +1,6 @@
+<!-- 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" width="16" height="16" viewBox="0 0 16 16">
+ <path fill="context-fill" d="M14 3H8.151L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 -2-2zM5.219 3l1.072 1H2V3zM14 13H2V5h6v-.014c.05 0 .1.014.151.014H14z"/>
+</svg>
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -133,16 +133,17 @@
skin/classic/browser/device-tablet.svg (../shared/icons/device-tablet.svg)
skin/classic/browser/device-desktop.svg (../shared/icons/device-desktop.svg)
skin/classic/browser/edit-copy.svg (../shared/icons/edit-copy.svg)
skin/classic/browser/edit-cut.svg (../shared/icons/edit-cut.svg)
skin/classic/browser/edit-paste.svg (../shared/icons/edit-paste.svg)
skin/classic/browser/email-link.svg (../shared/icons/email-link.svg)
skin/classic/browser/feed.svg (../shared/icons/feed.svg)
skin/classic/browser/find.svg (../shared/icons/find.svg)
+ skin/classic/browser/folder.svg (../shared/icons/folder.svg)
skin/classic/browser/forget.svg (../shared/icons/forget.svg)
skin/classic/browser/forward.svg (../shared/icons/forward.svg)
skin/classic/browser/fullscreen.svg (../shared/icons/fullscreen.svg)
skin/classic/browser/fullscreen-exit.svg (../shared/icons/fullscreen-exit.svg)
skin/classic/browser/history.svg (../shared/icons/history.svg)
skin/classic/browser/home.svg (../shared/icons/home.svg)
skin/classic/browser/library.svg (../shared/icons/library.svg)
skin/classic/browser/library-bookmark-animation.svg (../shared/icons/library-bookmark-animation.svg)
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -122,8 +122,15 @@ toolbarpaletteitem[place="palette"] > #b
#appMenuRestoreLastSession {
list-style-image: url("chrome://browser/skin/restore-session.svg");
}
#appMenuRecentlyClosedWindows {
list-style-image: url(chrome://browser/skin/window.svg);
}
+#appMenu-library-downloads-button {
+ list-style-image: url("chrome://browser/skin/downloads/download-icons.svg#arrow-with-bar");
+}
+
+#appMenu-library-downloads-show-button {
+ list-style-image: url("chrome://browser/skin/folder.svg");
+}