--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -481,17 +481,18 @@ toolbar:not(#TabsToolbar) > #personal-bo
#reload-button[disabled]:not(:-moz-window-inactive) > .toolbarbutton-icon {
opacity: 1 !important;
}
#PanelUI-feeds > .feed-toolbarbutton:-moz-locale-dir(rtl) {
direction: rtl;
}
-#panelMenu_bookmarksMenu > .bookmark-item {
+#panelMenu_bookmarksMenu > .bookmark-item,
+#panelMenu_downloadsMenu > .bookmark-item {
max-width: none;
}
#main-window:-moz-lwtheme {
background-repeat: no-repeat;
background-position: top right;
}
--- a/browser/components/customizableui/content/panelUI.css
+++ b/browser/components/customizableui/content/panelUI.css
@@ -55,9 +55,21 @@ photonpanelmultiview[transitioning] {
}
.panel-viewcontainer.offscreen,
.panel-viewcontainer.offscreen > .panel-viewstack {
margin: 0;
padding: 0;
}
+.subviewbutton.download {
+ -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-download");
+}
+
+.subviewbutton.download[openLabel]:hover > .toolbarbutton-text > .status-full,
+.subviewbutton.download:-moz-any(:not(:hover),[buttonover]) > .toolbarbutton-text > .status-open,
+.subviewbutton.download:-moz-any(:not(:hover),:hover:not([buttonover])) > .toolbarbutton-text > .status-show,
+.subviewbutton.download:not(:hover) > .show-button,
+.subviewbutton.download > .show-button > .toolbarbutton-text {
+ display: none;
+}
+
/* END photon adjustments */
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -742,12 +742,40 @@
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="&downloads.label;"
+ closemenu="none"
+ oncommand="DownloadsPanelSubView.show(this);"/>
+ </vbox>
+ </panelview>
+ <panelview id="PanelUI-downloads" class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <commandset id="downloadCommands"/>
+ <toolbarbutton id="appMenu-library-downloads-show-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="Show Downloads Folder"
+ closemenu="none"
+ oncommand="DownloadsPanelSubView.onShowDownloads(this);"/>
+ <toolbarbutton id="appMenu-library-downloads-clear-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="Clear Downloads"
+ closemenu="none"
+ command="downloadsCmd_clearDownloads"/>
+ <toolbarseparator/>
+ <toolbaritem id="panelMenu_downloadsMenu"
+ orient="vertical"
+ smoothscroll="false"
+ flatList="true"
+ tooltip="bhTooltip">
+ <!-- downloads menu items will go here -->
+ </toolbaritem>
</vbox>
</panelview>
</photonpanelmultiview>
</panel>
new file mode 100644
--- /dev/null
+++ b/browser/components/downloads/DownloadsPanelSubView.jsm
@@ -0,0 +1,193 @@
+/* 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 = [
+ "DownloadsPanelSubView",
+];
+
+const { interfaces: Ci, utils: Cu } = Components;
+
+// DownloadsViewUI is used right away, so no need to lazify the import.
+Cu.import("resource:///modules/DownloadsViewUI.jsm");
+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, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+let gPanelViewInstances = new WeakMap();
+
+class DownloadsPanelSubView extends DownloadsViewUI.BaseDownloadsPlacesView {
+ constructor(event) {
+ let document = event.target.ownerDocument
+ let window = document.defaultView;
+ super(window);
+
+ this.context = "panelDownloadsContextMenu";
+ this.controller = new DownloadsViewUI.DownloadsPlacesViewController(this);
+ this.showLabel = DownloadsCommon.strings[AppConstants.platform == "macosx" ?
+ "showMacLabel" : "showLabel"];
+ this.openLabel = DownloadsCommon.strings.openFileLabel;
+
+ let query = "place:transition=" + Ci.nsINavHistoryService.TRANSITION_DOWNLOAD +
+ "&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
+ let panelview = event.target;
+ this._panelMenuView = new window.PlacesPanelview(document.getElementById("panelMenu_downloadsMenu"),
+ panelview, query, this);
+ this.controller.register();
+ window.DownloadsView.goUpdateCommands();
+
+ event.detail.addBlocker(new Promise(resolve => {
+ window.DownloadsOverlayLoader.ensureOverlayLoaded(window.DownloadsPanel.kDownloadsOverlay, () => {
+ let contextMenu = document.getElementById(this.context);
+ if (!contextMenu) {
+ contextMenu = document.getElementById("downloadsContextMenu").cloneNode(true);
+ contextMenu.setAttribute("closemenu", "none");
+ contextMenu.setAttribute("id", this.context);
+ contextMenu.setAttribute("onpopupshowing",
+ "this._view = PlacesUIUtils.getViewForNode(document.popupNode);" +
+ "return this._view.buildContextMenu(this);");
+ contextMenu.setAttribute("onpopuphiding", "this._view.destroyContextMenu();");
+ let clearButton = contextMenu.querySelector("menuitem[command='downloadsCmd_clearDownloads'");
+ clearButton.hidden = false;
+ clearButton.previousSibling.hidden = true;
+ }
+ panelview.appendChild(contextMenu);
+ resolve();
+ });
+ }));
+ }
+
+ destructor(event) {
+ if (!this._panelMenuView)
+ return;
+
+ this.controller.terminate();
+ this._panelMenuView.uninit();
+ delete this._panelMenuView;
+ event.target.removeEventListener("ViewHiding", DownloadsPanelSubView.handleEvent);
+ super.destructor();
+ gPanelViewInstances.delete(this);
+ }
+
+ get result() {
+ return this._panelMenuView.result;
+ }
+
+ get selectedNode() {
+ let node = this._panelMenuView._contextMenuShown ?
+ this._panelMenuView._contextMenuShown.triggerNode : null;
+ if (node && node._placesNode)
+ return node;
+ return null;
+ }
+
+ get selectedNodes() {
+ let selectedNode = this.selectedNode;
+ return selectedNode ? [selectedNode] : [];
+ }
+
+ onInsertElement(element, placesNode) {
+ this._addDownloadData(null, placesNode, false, null, element);
+
+ element._shell.element = element;
+ element._shell.ensureActive();
+ element.classList.add("download", "download-state");
+ element.setAttribute("image", element._shell.image);
+ let fullStatus = element._shell.statusLabels.fullStatus;
+ element.setAttribute("fullStatus", fullStatus);
+ element.setAttribute("tooltiptext", fullStatus);
+ if (element._shell.isCommandEnabled("downloadsCmd_show")) {
+ element.setAttribute("openLabel", this.openLabel);
+ element.setAttribute("showLabel", this.showLabel);
+ }
+ }
+
+ onRemoveElement(element, placesNode) {
+ this._removeHistoryDownloadFromView(placesNode);
+ }
+
+ _removeElement(element) {
+ element.remove();
+ this.window.goUpdateCommand("downloadsCmd_clearDownloads");
+ }
+
+ onCommand(button) {
+ // This will only be invoked when the button is selected using keyboard
+ // navigation.
+ if (!button.classList.contains("show-button"))
+ return;
+
+ let command = "downloadsCmd_open";
+ if (button._shell.isCommandEnabled(command))
+ button._shell.doCommand(command);
+ }
+
+ onClick(target, button) {
+ let command = "downloadsCmd_open";
+ let retval = true;
+ if (target.classList.contains("show-button")) {
+ command = "downloadsCmd_show";
+ retval = false;
+ }
+
+ if (button._shell.isCommandEnabled(command))
+ button._shell.doCommand(command);
+ return retval;
+ }
+
+ onBuildContextMenu() {
+ this.window.DownloadsView.goUpdateCommands();
+ }
+
+ downloadsCmd_clearDownloads() {}
+
+ static show(anchor) {
+ let document = anchor.ownerDocument;
+ let window = document.defaultView;
+ let view = document.getElementById("PanelUI-downloads");
+ view.addEventListener("ViewShowing", DownloadsPanelSubView.handleEvent);
+ view.addEventListener("ViewHiding", DownloadsPanelSubView.handleEvent);
+ view.addEventListener("destructed", DownloadsPanelSubView.handleEvent);
+ anchor.setAttribute("closemenu", "none");
+ window.PanelUI.showSubView("PanelUI-downloads", anchor);
+ }
+
+ static async onShowDownloads() {
+ // Retrieve the user's default download directory.
+ let preferredDir = await Downloads.getPreferredDownloadsDirectory();
+ DownloadsCommon.showDirectory(new FileUtils.File(preferredDir));
+ }
+
+ static handleEvent(event) {
+ switch (event.type) {
+ case "ViewShowing":
+ DownloadsPanelSubView.onShowing(event);
+ break;
+ case "ViewHiding":
+ DownloadsPanelSubView.onHiding(event);
+ break;
+ }
+ }
+
+ static onShowing(event) {
+ let panelview = event.target;
+ panelview.removeEventListener("ViewShowing", DownloadsPanelSubView.handleEvent);
+ gPanelViewInstances.set(panelview, new DownloadsPanelSubView(event));
+ }
+
+ static onHiding(event) {
+ let instance = gPanelViewInstances.get(event.target);
+ if (!instance)
+ return;
+ instance.destructor(event);
+ }
+}
--- a/browser/components/downloads/DownloadsViewUI.jsm
+++ b/browser/components/downloads/DownloadsViewUI.jsm
@@ -183,19 +183,21 @@ this.DownloadsViewUI.BaseDownloadsPlaces
* @param [optional] aNewest
* @see onDownloadAdded. Ignored for history downloads.
* @param [optional] aDocumentFragment
* To speed up the appending of multiple elements to the end of the
* list which are coming in a single batch (i.e. invalidateContainer),
* a document fragment may be passed to which the new elements would
* be appended. It's the caller's job to ensure the fragment is merged
* to the richlistbox at the end.
+ * @param [optional] element
+ * Allow an existing element to be (re-)used, instead of creating a new
+ * richlistitem.
*/
- _addDownloadData(sessionDownload, aPlacesNode, aNewest = false,
- aDocumentFragment = null) {
+ _addDownloadData(sessionDownload, aPlacesNode, aNewest = false, aDocumentFragment = null, element = null) {
let downloadURI = aPlacesNode ? aPlacesNode.uri
: sessionDownload.source.url;
let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI);
if (!shellsForURI) {
shellsForURI = new Set();
this._downloadElementsShellsForURI.set(downloadURI, shellsForURI);
}
@@ -255,17 +257,17 @@ this.DownloadsViewUI.BaseDownloadsPlaces
let historyDownload = null;
if (aPlacesNode) {
let metaData = this._cachedPlacesMetaData.get(aPlacesNode.uri) ||
this._getPlacesMetaDataFor(aPlacesNode.uri);
historyDownload = new DownloadsViewUI.HistoryDownload(aPlacesNode);
historyDownload.updateFromMetaData(metaData);
}
let shell = new DownloadsViewUI.HistoryDownloadElementShell(sessionDownload,
- historyDownload, this.window);
+ historyDownload, this.window, element);
shell.element._placesNode = aPlacesNode;
newOrUpdatedShell = shell;
shellsForURI.add(shell);
if (sessionDownload) {
this._viewItemsForDownloads.set(sessionDownload, shell);
}
} else if (aPlacesNode) {
// We are updating information for a history download for which we have
@@ -278,52 +280,68 @@ this.DownloadsViewUI.BaseDownloadsPlaces
// download with no associated session download. We have exactly one
// download element shell in this case, but the metdata cannot have
// changed, just the reference to the Places node object is different.
// So, we update all the node references and keep the metadata intact.
for (let shell of shellsForURI) {
if (!shell.historyDownload) {
// Create the element to host the metadata when needed.
shell.historyDownload = new DownloadsViewUI.HistoryDownload(aPlacesNode);
+ // If this shell was created with a placeholder element, which is how
+ // it's used by the 'DownloadsPanelSubView', swap it out with the element
+ // that's passed in.
+ if (!shell.element.parentNode && element) {
+ // Make sure the clean up the placeholder element.
+ if (this._lastSessionDownloadElement == shell.element)
+ this._lastSessionDownloadElement = element;
+ shell.element.remove();
+ shell.element = element;
+ element._shell = shell;
+ }
}
shell.element._placesNode = aPlacesNode;
}
}
if (newOrUpdatedShell) {
if (aNewest) {
- this._richlistbox.insertBefore(newOrUpdatedShell.element,
- this._richlistbox.firstChild);
+ if (this._richlistbox) {
+ this._richlistbox.insertBefore(newOrUpdatedShell.element,
+ this._richlistbox.firstChild);
+ // Some operations like retrying an history download move an element to
+ // the top of the richlistbox, along with other session downloads.
+ // More generally, if a new download is added, should be made visible.
+ this._richlistbox.ensureElementIsVisible(newOrUpdatedShell.element);
+ }
if (!this._lastSessionDownloadElement) {
this._lastSessionDownloadElement = newOrUpdatedShell.element;
}
- // Some operations like retrying an history download move an element to
- // the top of the richlistbox, along with other session downloads.
- // More generally, if a new download is added, should be made visible.
- this._richlistbox.ensureElementIsVisible(newOrUpdatedShell.element);
} else if (sessionDownload) {
- let before = this._lastSessionDownloadElement ?
- this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild;
- this._richlistbox.insertBefore(newOrUpdatedShell.element, before);
+ if (this._richlistbox) {
+ let before = this._lastSessionDownloadElement ?
+ this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild;
+ this._richlistbox.insertBefore(newOrUpdatedShell.element, before);
+ }
this._lastSessionDownloadElement = newOrUpdatedShell.element;
- } else {
+ } else if (this._richlistbox) {
let appendTo = aDocumentFragment || this._richlistbox;
appendTo.appendChild(newOrUpdatedShell.element);
}
if (this.searchTerm) {
newOrUpdatedShell.element.hidden =
!newOrUpdatedShell.element._shell.matchesSearchTerm(this.searchTerm);
}
}
// If aDocumentFragment is defined this is a batch change, so it's up to
// the caller to append the fragment and activate the visible shells.
if (!aDocumentFragment) {
- this._ensureVisibleElementsAreActive();
+ if (this._ensureVisibleElementsAreActive)
+ this._ensureVisibleElementsAreActive();
this.window.goUpdateCommand("downloadsCmd_clearDownloads");
}
}
_removeHistoryDownloadFromView(aPlacesNode) {
let downloadURI = aPlacesNode.uri;
let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI);
if (shellsForURI) {
@@ -371,19 +389,25 @@ this.DownloadsViewUI.BaseDownloadsPlaces
let url = shell.historyDownload.source.url;
let metaData = this._getPlacesMetaDataFor(url);
shell.historyDownload.updateFromMetaData(metaData);
shell.sessionDownload = null;
// Move it below the session-download items;
if (this._lastSessionDownloadElement == shell.element) {
this._lastSessionDownloadElement = shell.element.previousSibling;
} else {
- let before = this._lastSessionDownloadElement ?
- this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild;
- this._richlistbox.insertBefore(shell.element, before);
+ let before = null;
+ if (this._lastSessionDownloadElement) {
+ before = this._lastSessionDownloadElement.nextSibling;
+ } else if (this._richlistbox) {
+ before = this._richlistbox.firstChild;
+ }
+ if (before) {
+ this._richlistbox.insertBefore(shell.element, before);
+ }
}
}
}
get searchTerm() {
return this._searchTerm;
}
@@ -394,17 +418,18 @@ this.DownloadsViewUI.BaseDownloadsPlaces
}
this._ensureVisibleElementsAreActive();
}
return this._searchTerm = aValue;
}
onDataLoadStarting() {}
onDataLoadCompleted() {
- this._ensureInitialSelection();
+ if (this._ensureInitialSelection)
+ this._ensureInitialSelection();
}
onDownloadAdded(download, newest) {
this._addDownloadData(download, null, newest);
}
onDownloadStateChanged(download) {
this._viewItemsForDownloads.get(download).onStateChanged();
@@ -928,19 +953,24 @@ this.DownloadsViewUI.HistoryDownload.pro
*
* The caller is also responsible for forwarding status notifications for
* session downloads, calling the onStateChanged and onChanged methods.
*
* @param [optional] aSessionDownload
* The session download, required if aHistoryDownload is not set.
* @param [optional] aHistoryDownload
* The history download, required if aSessionDownload is not set.
+ * @param aWindow
+ * Window object that this shell is created in.
+ * @param [optional] aElement
+ * Allow an existing element to be (re-)used, instead of creating a new
+ * richlistitem.
*/
-this.DownloadsViewUI.HistoryDownloadElementShell = function(aSessionDownload, aHistoryDownload, aWindow) {
- this.element = aWindow.document.createElement("richlistitem");
+this.DownloadsViewUI.HistoryDownloadElementShell = function(aSessionDownload, aHistoryDownload, aWindow, aElement) {
+ this.element = aElement || aWindow.document.createElement("richlistitem");
this.element._shell = this;
this.element.classList.add("download");
this.element.classList.add("download-state");
if (aSessionDownload) {
this.sessionDownload = aSessionDownload;
}
@@ -1259,54 +1289,61 @@ this.DownloadsViewUI.DownloadsPlacesView
}
// If this function returns true, other controllers won't get a chance to
// process the command even if isCommandEnabled returns false, so it's
// important to check if the list is focused here to handle common commands
// like copy and paste correctly. The clear downloads command, instead, is
// specific to the downloads list but can be invoked from the toolbar, so we
// can just return true unconditionally.
return aCommand == "downloadsCmd_clearDownloads" ||
- this.view.window.document.activeElement == this.view._richlistbox;
+ !this.view._richlistbox || this.view.window.document.activeElement == this.view._richlistbox;
}
isCommandEnabled(aCommand) {
switch (aCommand) {
case "cmd_copy":
case "downloadsCmd_openReferrer":
case "downloadShowMenuItem":
- return this.view._richlistbox.selectedItems.length == 1;
+ return this.view._richlistbox ? this.view._richlistbox.selectedItems.length == 1 :
+ this.view.selectedNodes.length == 1;
case "cmd_selectAll":
- return true;
+ return !!this.view._richlistbox;
case "cmd_paste":
return this._canDownloadClipboardURL();
case "downloadsCmd_clearDownloads":
return this._canClearDownloads();
default:
- return Array.every(this.view._richlistbox.selectedItems,
- element => element._shell.isCommandEnabled(aCommand));
+ return Array.every(this.view._richlistbox ? this.view._richlistbox.selectedItems :
+ this.view.selectedNodes, 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.view._richlistbox.lastChild; elt; elt = elt.previousSibling) {
+ let elements = this.view._richlistbox ? this.view._richlistbox.childNodes :
+ this.view._panelMenuView._rootElt.childNodes;
+ for (let i = elements.length - 1; i >= 0; --i) {
+ let elt = elements[i];
+ if (!elt._shell)
+ continue;
// 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 = this.view._richlistbox.selectedItems.map(element => element._shell.download.source.url);
+ let urls = (this.view._richlistbox ? this.view._richlistbox.selectedItems :
+ this.view.selectedNodes).map(element => element._shell.download.source.url);
Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper)
.copyString(urls.join("\n"));
}
_getURLFromClipboardData() {
let trans = Cc["@mozilla.org/widget/transferable;1"].
@@ -1354,31 +1391,33 @@ this.DownloadsViewUI.DownloadsPlacesView
this[aCommand]();
return;
}
// Cloning the nodelist into an array to get a frozen list of selected items.
// Otherwise, the selectedItems nodelist is live and doCommand may alter the
// selection while we are trying to do one particular action, like removing
// items from history.
- let selectedElements = [...this.view._richlistbox.selectedItems];
+ let selectedElements = [...this.view._richlistbox ?
+ this.view._richlistbox.selectedItems : this.view.selectedNodes];
for (let element of selectedElements) {
element._shell.doCommand(aCommand);
}
}
// nsIController
onEvent() {}
cmd_copy() {
this._copySelectedDownloadsToClipboard();
}
cmd_selectAll() {
- this.view._richlistbox.selectAll();
+ if (this.view._richlistbox)
+ this.view._richlistbox.selectAll();
}
cmd_paste() {
this._downloadURLFromClipboard();
}
downloadsCmd_clearDownloads() {
this.view._downloadsData.removeFinished();
@@ -1388,33 +1427,38 @@ this.DownloadsViewUI.DownloadsPlacesView
.removeAllDownloads();
}
// There may be no selection or focus change as a result
// of these change, and we want the command updated immediately.
this.view.window.goUpdateCommand("downloadsCmd_clearDownloads");
}
buildContextMenu(contextMenu, download) {
+ if (!download) {
+ download = contextMenu.triggerNode._shell.download;
+ }
+
contextMenu.setAttribute("state",
DownloadsCommon.stateOfDownload(download));
contextMenu.setAttribute("exists", "true");
contextMenu.classList.toggle("temporary-block",
!!download.hasBlockedData);
if (!download.stopped) {
// The hasPartialData property of a download may change at any time after
// it has started, so ensure we update the related command now.
this.view.window.goUpdateCommand("downloadsCmd_pauseResume");
}
}
setDataTransfer(aEvent) {
// TODO Bug 831358: Support d&d for multiple selection.
// For now, we just drag the first element.
- let selectedItem = this.view._richlistbox.selectedItem;
+ let selectedItem = this.view._richlistbox ? this.view._richlistbox.selectedItem :
+ this.view.selectedNode;
if (!selectedItem) {
return;
}
let targetPath = selectedItem._shell.download.target.path;
if (!targetPath) {
return;
}
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -65,16 +65,18 @@
var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
"resource:///modules/DownloadsCommon.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsViewUI",
"resource:///modules/DownloadsViewUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadsPanelSubView",
+ "resource:///modules/DownloadsPanelSubView.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
@@ -71,16 +71,20 @@
<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;"
+ accesskey="&cmd.clearDownloads.accesskey;"/>
</menupopup>
<panelmultiview id="downloadsPanel-multiView"
mainViewId="downloadsPanel-mainView">
<panelview id="downloadsPanel-mainView">
<vbox class="panel-view-body-unscrollable">
<richlistbox id="downloadsListBox"
--- 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',
+ 'DownloadsPanelSubView.jsm',
'DownloadsTaskbar.jsm',
'DownloadsViewUI.jsm',
]
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Downloads Panel')
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -13,17 +13,17 @@ Components.utils.import("resource://gre/
* menu views.
*/
function PlacesViewBase(aPlace, aOptions = {}) {
if ("rootElt" in aOptions)
this._rootElt = aOptions.rootElt;
if ("viewElt" in aOptions)
this._viewElt = aOptions.viewElt;
this.options = aOptions;
- this._controller = new PlacesController(this);
+ this._controller = aOptions.controller || new PlacesController(this);
this.place = aPlace;
this._viewElt.controllers.appendController(this._controller);
}
PlacesViewBase.prototype = {
// The xul element that holds the entire view.
_viewElt: null,
get viewElt() {
@@ -224,16 +224,18 @@ PlacesViewBase.prototype = {
return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
index, orientation, tagName);
},
buildContextMenu: function PVB_buildContextMenu(aPopup) {
this._contextMenuShown = aPopup;
window.updateCommands("places");
+ if (this.options.onBuildContextMenu)
+ this.options.onBuildContextMenu();
return this.controller.buildContextMenu(aPopup);
},
destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
this._contextMenuShown = null;
},
_cleanPopup: function PVB_cleanPopup(aPopup, aDelay) {
@@ -586,16 +588,19 @@ PlacesViewBase.prototype = {
parentElt.removeChild(elt);
// Figure out if we need to show the "<Empty>" menu-item.
// TODO Bug 517701: This doesn't seem to handle the case of an empty
// root.
if (parentElt._startMarker.nextSibling == parentElt._endMarker)
this._setEmptyPopupStatus(parentElt, true);
}
+
+ if (this.options.onRemoveElement)
+ this.options.onRemoveElement(elt, aPlacesNode);
},
nodeHistoryDetailsChanged:
function PVB_nodeHistoryDetailsChanged(aPlacesNode, aTime, aCount) {
if (aPlacesNode.parent &&
this.controller.hasCachedLivemarkInfo(aPlacesNode.parent)) {
// Find the node in the parent.
let popup = this._getDOMNodeForPlacesNode(aPlacesNode.parent);
@@ -1972,49 +1977,52 @@ PlacesPanelMenuView.prototype = {
}
for (let i = 0; i < this._resultNode.childCount; ++i) {
this._insertNewItem(this._resultNode.getChild(i), null);
}
}
};
-class PlacesPanelview extends PlacesViewBase {
+this.PlacesPanelview = class extends PlacesViewBase {
constructor(container, panelview, place, options = {}) {
options.rootElt = container;
options.viewElt = panelview;
super(place, options);
this._viewElt._placesView = this;
// We're simulating a popup show, because a panelview may only be shown when
// its containing popup is already shown.
this._onPopupShowing({ originalTarget: this._viewElt });
this._addEventListeners(window, ["unload"]);
- this._rootElt.setAttribute("context", "placesContext");
+ this._rootElt.setAttribute("context", this.options.context || "placesContext");
}
get events() {
if (this._events)
return this._events;
- return this._events = ["command", "destructed", "dragend", "dragstart",
+ return this._events = ["command", "click", "destructed", "dragend", "dragstart",
"ViewHiding", "ViewShowing", "ViewShown"];
}
get panel() {
return this.panelMultiView.parentNode;
}
get panelMultiView() {
return this._viewElt.panelMultiView;
}
handleEvent(event) {
switch (event.type) {
case "command":
this._onCommand(event);
break;
+ case "click":
+ this._onClick(event);
+ break;
case "destructed":
this._onDestructed(event);
break;
case "dragend":
this._onDragEnd(event);
break;
case "dragstart":
this._onDragStart(event);
@@ -2031,20 +2039,46 @@ class PlacesPanelview extends PlacesView
case "ViewShown":
this._onViewShown(event);
break;
}
}
_onCommand(event) {
let button = event.originalTarget;
- if (!button._placesNode)
+ let placesNode = button._placesNode || button.parentNode._placesNode
+ if (!placesNode)
+ return;
+
+ if (this.options.onCommand) {
+ this.options.onCommand(button, placesNode, event);
+ } else {
+ PlacesUIUtils.openNodeWithEvent(placesNode, event);
+ }
+ }
+
+ _onClick(event) {
+ if (!this.options.onClick || event.button !== 0)
return;
- PlacesUIUtils.openNodeWithEvent(button._placesNode, event);
+ let target = event.originalTarget;
+ let button = target;
+ // Ignore clicks from context menus.
+ if (this._contextMenuShown && button.localName == "menuitem")
+ return;
+
+ while (button.parentNode && button.parentNode != this._rootElt && !button.classList.contains("bookmark-item"))
+ button = button.parentNode;
+ if (!button.parentNode || !button.classList.contains("bookmark-item"))
+ return;
+ if (this.options.onClick(target, button, event) === false) {
+ event.stopPropagation();
+ event.preventDefault();
+ this.panel.hidePopup();
+ }
}
_onDestructed(event) {
// The panelmultiview is ephemeral, so let's keep an eye out when the root
// element is showing again.
this._removeEventListeners(event.target, this.events);
this._addEventListeners(this._viewElt, ["ViewShowing"]);
}
@@ -2066,16 +2100,17 @@ class PlacesPanelview extends PlacesView
event.stopPropagation();
}
uninit(event) {
this._removeEventListeners(this.panelMultiView, this.events);
this._removeEventListeners(this._viewElt, ["ViewShowing"]);
this._removeEventListeners(window, ["unload"]);
super.uninit(event);
+ this._viewElt = this._rootElt = null;
}
_createDOMNodeForPlacesNode(placesNode) {
this._domNodes.delete(placesNode);
let element;
let type = placesNode.type;
if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
@@ -2093,16 +2128,19 @@ class PlacesPanelview extends PlacesView
if (icon)
element.setAttribute("image", icon);
}
element._placesNode = placesNode;
if (!this._domNodes.has(placesNode))
this._domNodes.set(placesNode, element);
+ if (this.options.onInsertElement)
+ this.options.onInsertElement(element, placesNode);
+
return element;
}
_setEmptyPopupStatus(panelview, empty = false) {
if (!panelview._emptyMenuitem) {
let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
panelview._emptyMenuitem = document.createElement("toolbarbutton");
panelview._emptyMenuitem.setAttribute("label", label);
--- a/browser/locales/en-US/chrome/browser/downloads/downloads.properties
+++ b/browser/locales/en-US/chrome/browser/downloads/downloads.properties
@@ -97,8 +97,18 @@ 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
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -2132,13 +2132,52 @@ photonpanelmultiview#customizationui-wid
}
/* This is explicitly overriding the overflow properties set above. */
photonpanelmultiview .cui-widget-panelview {
overflow-x: visible;
overflow-y: visible;
}
+#customizationui-widget-multiview #appMenu-libraryView {
+ min-width: 320px !important;
+}
+
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 > .show-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 and half of 4px top margin. */
+ margin: calc(1em + 2px) 0 0;
+ padding: 0;
+}
+
+.subviewbutton.download:not([openLabel]) > .show-button {
+ fill: GrayText;
+ opacity: .5;
+}
+
+.subviewbutton.download[openLabel] > .show-button@buttonStateHover@ {
+ fill: #0a84ff;
+ outline: 1px solid #0a84ff;
+ -moz-outline-radius: 3px;
+}
+
/* END photon adjustments */
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/icons/delete.svg
@@ -0,0 +1,9 @@
+<!-- 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="M6.5 12a.5.5 0 0 0 .5-.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 0 .5.5z"/>
+ <path fill="context-fill" d="M8.5 12a.5.5 0 0 0 .5-.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 0 .5.5z"/>
+ <path fill="context-fill" d="M10.5 12a.5.5 0 0 0 .5-.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 0 .5.5z"/>
+ <path fill="context-fill" d="M14 2h-3.05a2.5 2.5 0 0 0-4.9 0H3a1 1 0 0 0 0 2v9a3 3 0 0 0 3 3h5a3 3 0 0 0 3-3V4a1 1 0 0 0 0-2zM8.5 1a1.489 1.489 0 0 1 1.391 1H7.109A1.489 1.489 0 0 1 8.5 1zM12 13a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4h7z"/>
+</svg>
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
@@ -144,19 +144,23 @@
#ifndef MOZ_PHOTON_THEME
skin/classic/browser/download.svg (../shared/icons/download.svg)
#endif
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)
#ifdef MOZ_PHOTON_THEME
skin/classic/browser/email-link.svg (../shared/icons/email-link.svg)
+ skin/classic/browser/delete.svg (../shared/icons/delete.svg)
#endif
skin/classic/browser/feed.svg (../shared/icons/feed.svg)
skin/classic/browser/find.svg (../shared/icons/find.svg)
+#ifdef MOZ_PHOTON_THEME
+ skin/classic/browser/folder.svg (../shared/icons/folder.svg)
+#endif
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-enter.svg (../shared/icons/fullscreen-enter.svg)
skin/classic/browser/fullscreen-exit.svg (../shared/icons/fullscreen-exit.svg)
skin/classic/browser/help.svg (../shared/icons/help.svg)
skin/classic/browser/history.svg (../shared/icons/history.svg)
skin/classic/browser/home.svg (../shared/icons/home.svg)
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -285,8 +285,20 @@ toolbarpaletteitem[place="palette"] > #z
}
%ifdef MOZ_PHOTON_THEME
#bookmarks-menu-button[cui-areatype="menu-panel"],
toolbarpaletteitem[place="palette"] > #bookmarks-menu-button {
list-style-image: url("chrome://browser/skin/bookmark-star-on-tray.svg");
}
%endif
+
+#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");
+}
+
+#appMenu-library-downloads-clear-button {
+ list-style-image: url("chrome://browser/skin/delete.svg");
+}
--- a/toolkit/content/widgets/toolbarbutton.xml
+++ b/toolkit/content/widgets/toolbarbutton.xml
@@ -108,9 +108,24 @@
<xul:label class="toolbarbutton-text" crop="right" flex="1"
xbl:inherits="value=label,accesskey,crop,dragover-top,wrap"/>
<xul:label class="toolbarbutton-multiline-text" flex="1"
xbl:inherits="xbl:text=label,accesskey,wrap"/>
<xul:dropmarker anonid="dropmarker" type="menu"
class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
</content>
</binding>
+
+ <binding id="toolbarbutton-download"
+ 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-show" crop="end" xbl:inherits="value=showLabel"/>
+ </xul:vbox>
+ <xul:toolbarbutton anonid="button" class="show-button" xbl:inherits="tooltiptext=showLabel"/>
+ </content>
+ </binding>
</bindings>