diff --git a/browser/components/downloads/content/downloads.js b/browser/components/downloads/content/downloads.js
index f69dea0481a9..fd6efefec163 100644
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -1187,12 +1187,6 @@ function DownloadsViewItemController(aElement) {
}
DownloadsViewItemController.prototype = {
- //////////////////////////////////////////////////////////////////////////////
- //// Constants
-
- get kPrefBdmAlertOnExeOpen() "browser.download.manager.alertOnEXEOpen",
- get kPrefBdmScanWhenDone() "browser.download.manager.scanWhenDone",
-
//////////////////////////////////////////////////////////////////////////////
//// Command dispatching
@@ -1244,87 +1238,17 @@ DownloadsViewItemController.prototype = {
commands: {
cmd_delete: function DVIC_cmd_delete()
{
- this.dataItem.getDownload(function (aDownload) {
- if (this.dataItem.inProgress) {
- aDownload.cancel();
- this._ensureLocalFileRemoved();
- }
- aDownload.remove();
- }.bind(this));
+ this.dataItem.remove();
},
downloadsCmd_cancel: function DVIC_downloadsCmd_cancel()
{
- if (this.dataItem.inProgress) {
- this.dataItem.getDownload(function (aDownload) {
- aDownload.cancel();
- this._ensureLocalFileRemoved();
- }.bind(this));
- }
+ this.dataItem.cancel();
},
downloadsCmd_open: function DVIC_downloadsCmd_open()
{
- // Confirm opening executable files if required.
- let localFile = this.dataItem.localFile;
- if (localFile.isExecutable()) {
- let showAlert = true;
- try {
- showAlert = Services.prefs.getBoolPref(this.kPrefBdmAlertOnExeOpen);
- } catch (ex) { }
-
- // On Vista and above, we rely on native security prompting for
- // downloaded content unless it's disabled.
- if (DownloadsCommon.isWinVistaOrHigher) {
- try {
- if (Services.prefs.getBoolPref(this.kPrefBdmScanWhenDone)) {
- showAlert = false;
- }
- } catch (ex) { }
- }
-
- if (showAlert) {
- let name = this.dataItem.target;
- let message =
- DownloadsCommon.strings.fileExecutableSecurityWarning(name, name);
- let title =
- DownloadsCommon.strings.fileExecutableSecurityWarningTitle;
- let dontAsk =
- DownloadsCommon.strings.fileExecutableSecurityWarningDontAsk;
-
- let checkbox = { value: false };
- let open = Services.prompt.confirmCheck(window, title, message,
- dontAsk, checkbox);
- if (!open) {
- return;
- }
-
- Services.prefs.setBoolPref(this.kPrefBdmAlertOnExeOpen,
- !checkbox.value);
- }
- }
-
- // Actually open the file.
- this.dataItem.getDownload(function (aDownload) {
- try {
- let launched = false;
- try {
- let mimeInfo = aDownload.MIMEInfo;
- if (mimeInfo.preferredAction == mimeInfo.useHelperApp) {
- mimeInfo.launchWithFile(localFile);
- launched = true;
- }
- } catch (ex) { }
- if (!launched) {
- localFile.launch();
- }
- } catch (ex) {
- // If launch fails, try sending it through the system's external "file:"
- // URL handler.
- this._openExternal(localFile);
- }
- }.bind(this));
-
+ this.dataItem.openLocalFile();
// We explicitly close the panel here to give the user the feedback that
// their click has been received, and we're handling the action.
// Otherwise, we'd have to wait for the file-type handler to execute
@@ -1335,26 +1259,7 @@ DownloadsViewItemController.prototype = {
downloadsCmd_show: function DVIC_downloadsCmd_show()
{
- let localFile = this.dataItem.localFile;
-
- try {
- // Show the directory containing the file and select the file.
- localFile.reveal();
- } catch (ex) {
- // If reveal fails for some reason (e.g., it's not implemented on unix
- // or the file doesn't exist), try using the parent if we have it.
- let parent = localFile.parent.QueryInterface(Ci.nsILocalFile);
- if (parent) {
- try {
- // Open the parent directory to show where the file should be.
- parent.launch();
- } catch (ex) {
- // If launch also fails (probably because it's not implemented), let
- // the OS handler try to open the parent.
- this._openExternal(parent);
- }
- }
- }
+ this.dataItem.showLocalFile();
// We explicitly close the panel here to give the user the feedback that
// their click has been received, and we're handling the action.
@@ -1366,20 +1271,12 @@ DownloadsViewItemController.prototype = {
downloadsCmd_pauseResume: function DVIC_downloadsCmd_pauseResume()
{
- this.dataItem.getDownload(function (aDownload) {
- if (this.dataItem.paused) {
- aDownload.resume();
- } else {
- aDownload.pause();
- }
- }.bind(this));
+ this.dataItem.togglePauseResume();
},
downloadsCmd_retry: function DVIC_downloadsCmd_retry()
{
- this.dataItem.getDownload(function (aDownload) {
- aDownload.retry();
- });
+ this.dataItem.retry();
},
downloadsCmd_openReferrer: function DVIC_downloadsCmd_openReferrer()
@@ -1418,31 +1315,6 @@ DownloadsViewItemController.prototype = {
// Invoke the command.
this.doCommand(defaultCommand);
}
- },
-
- /**
- * Support function to open the specified nsIFile.
- */
- _openExternal: function DVIC_openExternal(aFile)
- {
- let protocolSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
- .getService(Ci.nsIExternalProtocolService);
- protocolSvc.loadUrl(makeFileURI(aFile));
- },
-
- /**
- * Support function that deletes the local file for a download. This is
- * used in cases where the Download Manager service doesn't delete the file
- * from disk when cancelling. See bug 732924.
- */
- _ensureLocalFileRemoved: function DVIC_ensureLocalFileRemoved()
- {
- try {
- let localFile = this.dataItem.localFile;
- if (localFile.exists()) {
- localFile.remove(false);
- }
- } catch (ex) { }
}
};
diff --git a/browser/components/downloads/src/DownloadsCommon.jsm b/browser/components/downloads/src/DownloadsCommon.jsm
index af7d82862c2a..48c3a5653ec1 100644
--- a/browser/components/downloads/src/DownloadsCommon.jsm
+++ b/browser/components/downloads/src/DownloadsCommon.jsm
@@ -63,6 +63,9 @@ const nsIDM = Ci.nsIDownloadManager;
const kDownloadsStringBundleUrl =
"chrome://browser/locale/downloads/downloads.properties";
+const kPrefBdmScanWhenDone = "browser.download.manager.scanWhenDone";
+const kPrefBdmAlertOnExeOpen = "browser.download.manager.alertOnEXEOpen";
+
const kDownloadsStringsRequiringFormatting = {
sizeWithUnits: true,
shortTimeLeftSeconds: true,
@@ -396,6 +399,117 @@ this.DownloadsCommon = {
// never adding to the time left. Ensure that we never fall below one
// second left until all downloads are actually finished.
return aLastSeconds = Math.max(aSeconds, 1);
+ },
+
+ /**
+ * Opens a downloaded file.
+ * If you've a dataItem, you should call dataItem.openLocalFile.
+ * @param aFile
+ * the downloaded file to be opened.
+ * @param aMimeInfo
+ * the mime type info object. May be null.
+ * @param aOwnerWindow
+ * the window with which this action is associated.
+ */
+ openDownloadedFile: function DC_openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) {
+ if (!(aFile instanceof Ci.nsIFile))
+ throw new Error("aFile must be a nsIFile object");
+ if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo))
+ throw new Error("Invalid value passed for aMimeInfo");
+ if (!(aOwnerWindow instanceof Ci.nsIDOMWindow))
+ throw new Error("aOwnerWindow must be a dom-window object");
+
+ // Confirm opening executable files if required.
+ if (aFile.isExecutable()) {
+ let showAlert = true;
+ try {
+ showAlert = Services.prefs.getBoolPref(kPrefBdmAlertOnExeOpen);
+ } catch (ex) { }
+
+ // On Vista and above, we rely on native security prompting for
+ // downloaded content unless it's disabled.
+ if (DownloadsCommon.isWinVistaOrHigher) {
+ try {
+ if (Services.prefs.getBoolPref(kPrefBdmScanWhenDone)) {
+ showAlert = false;
+ }
+ } catch (ex) { }
+ }
+
+ if (showAlert) {
+ let name = this.dataItem.target;
+ let message =
+ DownloadsCommon.strings.fileExecutableSecurityWarning(name, name);
+ let title =
+ DownloadsCommon.strings.fileExecutableSecurityWarningTitle;
+ let dontAsk =
+ DownloadsCommon.strings.fileExecutableSecurityWarningDontAsk;
+
+ let checkbox = { value: false };
+ let open = Services.prompt.confirmCheck(aOwnerWindow, title, message,
+ dontAsk, checkbox);
+ if (!open) {
+ return;
+ }
+
+ Services.prefs.setBoolPref(kPrefBdmAlertOnExeOpen,
+ !checkbox.value);
+ }
+ }
+
+ // Actually open the file.
+ try {
+ if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
+ aMimeInfo.launchWithFile(aFile);
+ return;
+ }
+ }
+ catch(ex) { }
+
+ // If either we don't have the mime info, or the preferred action failed,
+ // attempt to launch the file directly.
+ try {
+ aFile.launch();
+ }
+ catch(ex) {
+ // If launch fails, try sending it through the system's external "file:"
+ // URL handler.
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadUrl(NetUtil.newURI(aFile));
+ }
+ },
+
+ /**
+ * Show a donwloaded file in the system file manager.
+ * If you have a dataItem, use dataItem.showLocalFile.
+ *
+ * @param aFile
+ * a downloaded file.
+ */
+ showDownloadedFile: function DC_showDownloadedFile(aFile) {
+ if (!(aFile instanceof Ci.nsIFile))
+ throw new Error("aFile must be a nsIFile object");
+ try {
+ // Show the directory containing the file and select the file.
+ aFile.reveal();
+ } catch (ex) {
+ // If reveal fails for some reason (e.g., it's not implemented on unix
+ // or the file doesn't exist), try using the parent if we have it.
+ let parent = aFile.parent;
+ if (parent) {
+ try {
+ // Open the parent directory to show where the file should be.
+ parent.launch();
+ } catch (ex) {
+ // If launch also fails (probably because it's not implemented), let
+ // the OS handler try to open the parent.
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadUrl(NetUtil.newURI(parent));
+ }
+ }
+ }
}
};
@@ -1212,6 +1326,101 @@ DownloadsDataItem.prototype = {
// file, though this may throw an exception if the path is invalid.
return new DownloadsLocalFileCtor(aFilename);
}
+ },
+
+ /**
+ * Open the target file for this download.
+ *
+ * @param aOwnerWindow
+ * The window with which the required action is associated.
+ * @throws if the file cannot be opened.
+ */
+ openLocalFile: function DDI_openLocalFile(aOwnerWindow) {
+ this.getDownload(function(aDownload) {
+ DownloadsCommon.openDownloadedFile(this.localFile,
+ aDownload.MIMEInfo,
+ aOwnerWindow);
+ }.bind(this));
+ },
+
+ /**
+ * Show the downloaded file in the system file manager.
+ */
+ showLocalFile: function DDI_showLocalFile() {
+ DownloadsCommon.showDownloadedFile(this.localFile);
+ },
+
+ /**
+ * Resumes the download if paused, pauses it if active.
+ * @throws if the download is not resumable or if has already done.
+ */
+ togglePauseResume: function DDI_togglePauseResume() {
+ if (!this.inProgress || !this.resumable)
+ throw new Error("The given download cannot be paused or resumed");
+
+ this.getDownload(function(aDownload) {
+ if (this.inProgress) {
+ if (this.paused)
+ aDownload.resume();
+ else
+ aDownload.pause();
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Attempts to retry the download.
+ * @throws if we cannot.
+ */
+ retry: function DDI_retry() {
+ if (!this.canRetry)
+ throw new Error("Cannot rerty this download");
+
+ this.getDownload(function(aDownload) {
+ aDownload.retry();
+ });
+ },
+
+ /**
+ * Support function that deletes the local file for a download. This is
+ * used in cases where the Download Manager service doesn't delete the file
+ * from disk when cancelling. See bug 732924.
+ */
+ _ensureLocalFileRemoved: function DDI__ensureLocalFileRemoved()
+ {
+ try {
+ let localFile = this.localFile;
+ if (localFile.exists()) {
+ localFile.remove(false);
+ }
+ } catch (ex) { }
+ },
+
+ /**
+ * Cancels the download.
+ * @throws if the download is already done.
+ */
+ cancel: function() {
+ if (!this.inProgress)
+ throw new Error("Cannot cancel this download");
+
+ this.getDownload(function (aDownload) {
+ aDownload.cancel();
+ this._ensureLocalFileRemoved();
+ }.bind(this));
+ },
+
+ /**
+ * Remove the download.
+ */
+ remove: function DDI_remove() {
+ this.getDownload(function (aDownload) {
+ if (this.inProgress) {
+ aDownload.cancel();
+ this._ensureLocalFileRemoved();
+ }
+ aDownload.remove();
+ }.bind(this));
}
};
diff --git a/browser/components/places/content/browserPlacesViews.js b/browser/components/places/content/browserPlacesViews.js
index 789fcd554831..e64404e465c6 100644
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -20,6 +20,8 @@ PlacesViewBase.prototype = {
_viewElt: null,
get viewElt() this._viewElt,
+ get associatedElement() this._viewElt,
+
get controllers() this._viewElt.controllers,
// The xul element that represents the root container.
diff --git a/browser/components/places/content/download.css b/browser/components/places/content/download.css
new file mode 100644
index 000000000000..bec39ca02ed3
--- /dev/null
+++ b/browser/components/places/content/download.css
@@ -0,0 +1,45 @@
+/* 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/. */
+
+richlistitem.download button {
+ /* These buttons should never get focus, as that would "disable"
+ the downloads view controller (it's only used when the richlistbox
+ is focused). */
+ -moz-user-focus: none;
+}
+
+/*** Visibility of controls inside download items ***/
+
+.download-state:-moz-any( [state="6"], /* Blocked (parental) */
+ [state="8"], /* Blocked (dirty) */
+ [state="9"]) /* Blocked (policy) */
+ .downloadTypeIcon:not(.blockedIcon),
+
+.download-state:not(:-moz-any([state="6"], /* Blocked (parental) */
+ [state="8"], /* Blocked (dirty) */
+ [state="9"]) /* Blocked (policy) */)
+ .downloadTypeIcon.blockedIcon,
+
+.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */
+ [state="5"], /* Starting (queued) */
+ [state="0"], /* Downloading */
+ [state="4"], /* Paused */
+ [state="7"]) /* Scanning */)
+ .downloadProgress,
+
+.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */
+ [state="5"], /* Starting (queued) */
+ [state="0"], /* Downloading */
+ [state="4"]) /* Paused */)
+ .downloadCancel,
+
+.download-state:not(:-moz-any([state="2"], /* Failed */
+ [state="3"]) /* Canceled */)
+ .downloadRetry,
+
+.download-state:not( [state="1"] /* Finished */)
+ .downloadShow
+{
+ visibility: hidden;
+}
diff --git a/browser/components/places/content/download.xml b/browser/components/places/content/download.xml
new file mode 100644
index 000000000000..6033c226009d
--- /dev/null
+++ b/browser/components/places/content/download.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/components/places/content/downloadsView.js b/browser/components/places/content/downloadsView.js
new file mode 100644
index 000000000000..89edd02b25b1
--- /dev/null
+++ b/browser/components/places/content/downloadsView.js
@@ -0,0 +1,1024 @@
+/* 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/. */
+
+/**
+ * THE PLACES VIEW IMPLEMENTED IN THIS FILE HAS A VERY PARTICULAR USE CASE.
+ * IT IS HIGHLY RECOMMENDED NOT TO EXTEND IT FOR ANY OTHER USE CASES OR RELY
+ * ON IT AS AN API.
+ */
+
+let Cu = Components.utils;
+let Ci = Components.interfaces;
+let Cc = Components.classes;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/DownloadUtils.jsm");
+Cu.import("resource:///modules/DownloadsCommon.jsm");
+
+const nsIDM = Ci.nsIDownloadManager;
+
+const DESTINATION_FILE_URI_ANNO = "downloads/destinationFileURI";
+const DESTINATION_FILE_NAME_ANNO = "downloads/destinationFileName";
+const DOWNLOAD_STATE_ANNO = "downloads/state";
+
+const DOWNLOAD_VIEW_SUPPORTED_COMMANDS =
+ ["cmd_delete", "cmd_copy", "cmd_paste", "cmd_selectAll",
+ "downloadsCmd_pauseResume", "downloadsCmd_cancel",
+ "downloadsCmd_open", "downloadsCmd_show", "downloadsCmd_retry",
+ "downloadsCmd_openReferrer"];
+
+function GetFileForFileURI(aFileURI)
+ Cc["@mozilla.org/network/protocol;1?name=file"]
+ .getService(Ci.nsIFileProtocolHandler)
+ .getFileFromURLSpec(aFileURI);
+
+/**
+ * A download element shell is responsible for handling the commands and the
+ * displayed data for a single download view element. The download element
+ * could represent either a past download (for which we get data from places) or
+ * a "session" download (using a data-item object. See DownloadsCommon.jsm), or both.
+ *
+ * Once initialized with either a data item or a places node, the created richlistitem
+ * can be accessed through the |element| getter, and can then be inserted/removed from
+ * a richlistbox.
+ *
+ * The shell doesn't take care of inserting the item, or removing it when it's no longer
+ * valid. That's the caller (a DownloadsPlacesView object) responsibility.
+ *
+ * The caller is also responsible for "passing over" notification from both the
+ * download-view and the places-result-observer, in the following manner:
+ * - The DownloadsPlacesView object implements getViewItem of the download-view
+ * pseudo interface. It returns this object (therefore we implement
+ * onStateChangea and onProgressChange here).
+ * - The DownloadsPlacesView object adds itself as a places result observer and
+ * calls this object's placesNodeIconChanged, placesNodeTitleChanged and
+ * placeNodeAnnotationChanged from its callbacks.
+ *
+ * @param [optional] aDataItem
+ * The data item of a the session download. Required if aPlacesNode is not set
+ * @param [optional] aPlacesNode
+ * The places node for a past download. Required if aDataItem is not set.
+ */
+function DownloadElementShell(aDataItem, aPlacesNode) {
+ this._element = document.createElement("richlistitem");
+ this._element._shell = this;
+
+ this._element.classList.add("download");
+ this._element.classList.add("download-state");
+
+ if (aDataItem)
+ this.dataItem = aDataItem;
+ if (aPlacesNode)
+ this.placesNode = aPlacesNode;
+}
+
+DownloadElementShell.prototype = {
+ // The richlistitem for the download
+ get element() this._element,
+
+ // The data item for the download
+ get dataItem() this._dataItem,
+
+ set dataItem(aValue) {
+ if (this._dataItem = aValue) {
+ this._wasDone = this._dataItem.done;
+ this._wasInProgress = this._dataItem.inProgress;
+ }
+ else if (this._placesNode) {
+ this._wasInProgress = false;
+ this._wasDone = this._state == nsIDM.DOWNLOAD_FINISHED;
+ }
+
+ this._updateStatusUI();
+ return aValue;
+ },
+
+ get placesNode() this._placesNode,
+ set placesNode(aNode) {
+ if (this._placesNode != aNode) {
+ this._annotations = new Map();
+ this._placesNode = aNode;
+ if (!this._dataItem && this._placesNode) {
+ this._wasInProgress = false;
+ this._wasDone = this._state == nsIDM.DOWNLOAD_FINISHED;
+ this._updateStatusUI();
+ }
+ }
+ return aNode;
+ },
+
+ // The download uri (as a string)
+ get downloadURI() {
+ if (this._dataItem)
+ return this._dataItem.uri;
+ if (this._placesNode)
+ return this._placesNode.uri;
+ throw new Error("Unexpected download element state");
+ },
+
+ get _icon() {
+ if (this._targetFileURI)
+ return "moz-icon://" + this._targetFileURI + "?size=32";
+ if (this._placesNode)
+ return this.placesNode.icon;
+ if (this._dataItem)
+ throw new Error("Session-download items should always have a target file uri");
+ throw new Error("Unexpected download element state");
+ },
+
+ // Helper for getting a places annotation set for the download.
+ _getAnnotation: function DES__getAnnotation(aAnnotation, aDefaultValue) {
+ if (this._annotations.has(aAnnotation))
+ return this._annotations.get(aAnnotation);
+
+ let value;
+ try {
+ value = PlacesUtils.annotations.getPageAnnotation(
+ NetUtil.newURI(this.downloadURI), aAnnotation);
+ }
+ catch(ex) {
+ if (aDefaultValue === undefined) {
+ throw new Error("Could not get required annotation '" + aAnnotation +
+ "' for download with url '" + this.downloadURI + "'");
+ }
+ value = aDefaultValue;
+ }
+ this._annotations.set(aAnnotation, value);
+ return value;
+ },
+
+ // The uri (as a string) of the target file, if any.
+ get _targetFileURI() {
+ if (this._dataItem)
+ return this._dataItem.file;
+
+ return this._getAnnotation(DESTINATION_FILE_URI_ANNO, "");
+ },
+
+ // The label for the download
+ get _displayName() {
+ if (this._dataItem)
+ return this._dataItem.target;
+
+ try {
+ return this._getAnnotation(DESTINATION_FILE_NAME_ANNO, "");
+ }
+ catch(ex) { }
+
+ // Fallback to the places title, or, at last, to the download uri.
+ return this._placesNode.title || this.downloadURI;
+ },
+
+ // If there's a target file for the download, this is its nsIFile object.
+ get _file() {
+ if (!("__file" in this)) {
+ if (this._dataItem) {
+ this.__file = this._dataItem.localFile;
+ }
+ else {
+ this.__file = this._targetFileURI ?
+ GetFileForFileURI(this._targetFileURI) : null;
+ }
+ }
+ return this.__file;
+ },
+
+ // The target's file size in bytes. If there's no target file, or If we
+ // cannot determine its size, 0 is returned.
+ get _fileSize() {
+ if (!this._file || !this._file.exists())
+ return 0;
+ try {
+ return this._file.fileSize;
+ }
+ catch(ex) {
+ Cu.reportError(ex);
+ return 0;
+ }
+ },
+
+ // The download state (see nsIDownloadManager).
+ get _state() {
+ if (this._dataItem)
+ return this._dataItem.state;
+
+ let state = -1;
+ try {
+ return this._getAnnotation(DOWNLOAD_STATE_ANNO);
+ }
+ catch (ex) {
+ // The state annotation didn't exist in past releases.
+ if (!this._file) {
+ state = nsIDM.DOWNLOAD_FAILED;
+ }
+ else if (this._file.exists()) {
+ state = this._fileSize > 0 ?
+ nsIDM.DOWNLOAD_FINISHED : nsIDM.DOWNLOAD_FAILED;
+ }
+ else {
+ // XXXmano I'm not sure if this right. We should probably show no
+ // status text at all in this case.
+ state = nsIDM.DOWNLOAD_CANCELED;
+ }
+ }
+ return state;
+ },
+
+ // The status text for the download
+ get _statusText() {
+ let s = DownloadsCommon.strings;
+ if (this._dataItem && this._dataItem.inProgress) {
+ if (this._dataItem.paused) {
+ let transfer =
+ DownloadUtils.getTransferTotal(this._dataItem.currBytes,
+ this._dataItem.maxBytes);
+
+ // We use the same XUL label to display both the state and the amount
+ // transferred, for example "Paused - 1.1 MB".
+ return s.statusSeparatorBeforeNumber(s.statePaused, transfer);
+ }
+ if (this._dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) {
+ let [status, newEstimatedSecondsLeft] =
+ DownloadUtils.getDownloadStatus(this.dataItem.currBytes,
+ this.dataItem.maxBytes,
+ this.dataItem.speed,
+ this._lastEstimatedSecondsLeft);
+ this._lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
+ return status;
+ }
+ if (this._dataItem.starting) {
+ return s.stateStarting;
+ }
+ if (this._dataItem.state == nsIDM.DOWNLOAD_SCANNING) {
+ return s.stateScanning;
+ }
+
+ let [displayHost, fullHost] =
+ DownloadUtils.getURIHost(this._dataItem.referrer ||
+ this._dataItem.uri);
+
+ let end = new Date(this.dataItem.endTime);
+ let [displayDate, fullDate] = DownloadUtils.getReadableDates(end);
+ return s.statusSeparator(fullHost, fullDate);
+ }
+
+ switch (this._state) {
+ case nsIDM.DOWNLOAD_FAILED:
+ return s.stateFailed;
+ case nsIDM.DOWNLOAD_CANCELED:
+ return s.stateCanceled;
+ case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
+ return s.stateBlockedParentalControls;
+ case nsIDM.DOWNLOAD_BLOCKED_POLICY:
+ return s.stateBlockedPolicy;
+ case nsIDM.DOWNLOAD_DIRTY:
+ return s.stateDirty;
+ case nsIDM.DOWNLOAD_FINISHED:{
+ // For completed downloads, show the file size (e.g. "1.5 MB")
+ if (this._fileSize > 0) {
+ let [size, unit] = DownloadUtils.convertByteUnits(this._fileSize);
+ return s.sizeWithUnits(size, unit);
+ }
+ break;
+ }
+ }
+
+ return "";
+ },
+
+ // The progressmeter element for the download
+ get _progressElement() {
+ let progressElement = document.getAnonymousElementByAttribute(
+ this._element, "anonid", "progressmeter");
+ if (progressElement) {
+ delete this._progressElement;
+ return this._progressElement = progressElement;
+ }
+ return null;
+ },
+
+ // Updates the download state attribute (and by that hide/unhide the
+ // appropriate buttons and context menu items), the status text label,
+ // and the progress meter.
+ _updateDownloadStatusUI: function DES__updateDownloadStatusUI() {
+ this._element.setAttribute("state", this._state);
+ this._element.setAttribute("status", this._statusText);
+
+ // For past-downloads, we're done. For session-downloads, we may also need
+ // to update the progress-meter.
+ if (!this._dataItem)
+ return;
+
+ // Copied from updateProgress in downloads.js.
+ if (this._dataItem.starting) {
+ // Before the download starts, the progress meter has its initial value.
+ this._element.setAttribute("progressmode", "normal");
+ this._element.setAttribute("progress", "0");
+ }
+ else if (this._dataItem.state == nsIDM.DOWNLOAD_SCANNING ||
+ this._dataItem.percentComplete == -1) {
+ // We might not know the progress of a running download, and we don't know
+ // the remaining time during the malware scanning phase.
+ this._element.setAttribute("progressmode", "undetermined");
+ }
+ else {
+ // This is a running download of which we know the progress.
+ this._element.setAttribute("progressmode", "normal");
+ this._element.setAttribute("progress", this._dataItem.percentComplete);
+ }
+
+ // Dispatch the ValueChange event for accessibility, if possible.
+ if (this._progressElement) {
+ let event = document.createEvent("Events");
+ event.initEvent("ValueChange", true, true);
+ this._progressElement.dispatchEvent(event);
+ }
+
+ goUpdateDownloadCommands();
+ },
+
+ _updateStatusUI: function DES__updateStatusUI() {
+ this._updateDownloadStatusUI();
+ this._element.setAttribute("image", this._icon);
+ this._element.setAttribute("displayName", this._displayName);
+ },
+
+ placesNodeIconChanged: function DES_placesNodeIconChanged() {
+ if (!this._dataItem)
+ this._element.setAttribute("image", this._icon);
+ },
+
+ placesNodeTitleChanged: function DES_placesNodeTitleChanged() {
+ if (!this._dataItem)
+ this._element.setAttribute("displayName", this._displayName);
+ },
+
+ placesNodeAnnotationChanged: function DES_placesNodeAnnotationChanged(aAnnoName) {
+ this._annotations.delete(aAnnoName);
+ if (!this._dataItem) {
+ if (aAnnoName == DESTINATION_FILE_URI_ANNO) {
+ this._element.setAttribute("image", this._icon);
+ this._updateDownloadStatusUI();
+ }
+ else if (aAnnoName == DESTINATION_FILE_NAME_ANNO) {
+ this._element.setAttribute("displayName", this._displayName);
+ }
+ else if (aAnnoName == DOWNLOAD_STATE_ANNO) {
+ this._updateDownloadStatusUI();
+ }
+ }
+ },
+
+ /* DownloadView */
+ onStateChange: function DES_onStateChange() {
+ // See comment in DVI_onStateChange in downloads.js (the panel-view)
+ if (!this._wasDone && this._dataItem.done)
+ this._element.setAttribute("image", this._icon + "&state=normal");
+
+ this._wasDone = this._dataItem.done;
+
+ // Update the end time using the current time if required.
+ if (this._wasInProgress && !this._dataItem.inProgress) {
+ this._endTime = Date.now();
+ }
+
+ this._wasDone = this._dataItem.done;
+ this._wasInProgress = this._dataItem.inProgress;
+
+ this._updateDownloadStatusUI();
+ },
+
+ /* DownloadView */
+ onProgressChange: function DES_onProgressChange() {
+ this._updateDownloadStatusUI();
+ },
+
+ /* nsIController */
+ isCommandEnabled: function DES_isCommandEnabled(aCommand) {
+ switch (aCommand) {
+ case "downloadsCmd_open": {
+ return this._file.exists() &&
+ ((this._dataItem && this._dataItem.openable) ||
+ (this._state == nsIDM.DOWNLOAD_FINISHED));
+ }
+ case "downloadsCmd_show": {
+ return this._getTargetFileOrPartFileIfExists() != null;
+ }
+ case "downloadsCmd_pauseResume":
+ return this._dataItem && this._dataItem.inProgress && this._dataItem.resumable;
+ case "downloadsCmd_retry":
+ return ((this._dataItem && this._dataItem.canRetry) ||
+ (!this._dataItem && this._file))
+ case "downloadsCmd_openReferrer":
+ return this._dataItem && !!this._dataItem.referrer;
+ case "cmd_delete":
+ // The behavior in this case is somewhat unexpected, so we disallow that.
+ if (this._placesNode && this._dataItem && this._dataItem.inProgress)
+ return false;
+ return true;
+ case "downloadsCmd_cancel":
+ return this._dataItem != null;
+ }
+ },
+
+ _getTargetFileOrPartFileIfExists: function DES__getTargetFileOrPartFileIfExists() {
+ if (this._file && this._file.exists())
+ return this._file;
+ if (this._dataItem &&
+ this._dataItem.partFile && this._dataItem.partFile.exists())
+ return this._dataItem.partFile;
+ return null;
+ },
+
+ _retryAsHistoryDownload: function DES__retryAsHistoryDownload() {
+ // TODO: save in the right location (the current saveURL api does not allow this)
+ saveURL(this.downloadURI, this._displayName, null, true, true, undefined, document);
+ },
+
+ /* nsIController */
+ doCommand: function DES_doCommand(aCommand) {
+ switch (aCommand) {
+ case "downloadsCmd_open": {
+ if (this._dateItem)
+ this._dataItem.openLocalFile(window);
+ else
+ DownloadsCommon.openDownloadedFile(this._file, null, window);
+ break;
+ }
+ case "downloadsCmd_show": {
+ if (this._dataItem)
+ this._dataItem.showLocalFile();
+ else
+ DownloadsCommon.showDownloadedFile(this._getTargetFileOrPartFileIfExists());
+ break;
+ }
+ case "downloadsCmd_openReferrer": {
+ openURL(this._dataItem.referrer);
+ break;
+ }
+ case "downloadsCmd_cancel": {
+ this._dataItem.cancel();
+ break;
+ }
+ case "cmd_delete": {
+ if (this._dataItem)
+ this._dataItem.remove();
+ if (this._placesNode)
+ PlacesUtils.bhistory.removePage(NetUtil.newURI(this.downloadURI));
+ break;
+ }
+ case "downloadsCmd_retry": {
+ if (this._dataItem)
+ this._dataItem.retry();
+ else
+ this._retryAsHistoryDownload();
+ break;
+ }
+ case "downloadsCmd_pauseResume": {
+ this._dataItem.togglePauseResume();
+ break;
+ }
+ }
+ },
+
+ // Returns whether or not the download handled by this shell should
+ // show up in the search results for the given term. Both the display
+ // name for the download and the url are searched.
+ matchesSearchTerm: function DES_matchesSearchTerm(aTerm) {
+ // Stub implemention until we figure out something better
+ aTerm = aTerm.toLowerCase();
+ return this._displayName.toLowerCase().indexOf(aTerm) != -1 ||
+ this.downloadURI.toLowerCase().indexOf(aTerm) != -1;
+ },
+
+ // Handles return kepress on the element (the keypress listener is
+ // set in the DownloadsPlacesView object).
+ doDefaultCommand: function DES_doDefaultCommand() {
+ function getDefaultCommandForState(aState) {
+ switch (aState) {
+ case nsIDM.DOWNLOAD_FINISHED:
+ return "downloadsCmd_open";
+ case nsIDM.DOWNLOAD_PAUSED:
+ return "downloadsCmd_pauseResume";
+ case nsIDM.DOWNLOAD_NOTSTARTED:
+ case nsIDM.DOWNLOAD_QUEUED:
+ return "downloadsCmd_cancel";
+ case nsIDM.DOWNLOAD_FAILED:
+ case nsIDM.DOWNLOAD_CANCELED:
+ return "downloadsCmd_retry";
+ case nsIDM.DOWNLOAD_DOWNLOADING:
+ case nsIDM.DOWNLOAD_SCANNING:
+ return "downloadsCmd_show";
+ case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
+ case nsIDM.DOWNLOAD_DIRTY:
+ case nsIDM.DOWNLOAD_BLOCKED_POLICY:
+ return "downloadsCmd_openReferrer";
+ }
+ }
+ let command = getDefaultCommandForState(this._state);
+ if (this.isCommandEnabled(command))
+ this.doCommand(command);
+ }
+};
+
+/**
+ * A Downloads Places View is a places view designed to show a places query
+ * for history donwloads alongside the current "session"-downloads.
+ *
+ * As we don't use the places controller, some methods implemented by other
+ * places views are not implemented by this view.
+ *
+ * A richlistitem in this view can represent either a past download or a session
+ * download, or both. Session downloads are shown first in the view, and as long
+ * as they exist they "collapses" their history "counterpart" (So we don't show two
+ * items for every download).
+ */
+function DownloadsPlacesView(aRichListBox, aPlace) {
+ this._richlistbox = aRichListBox;
+ this._richlistbox._placesView = this;
+ this._downloadElementsShellsForURI = new Map();
+ this._viewItemsForDataItems = new WeakMap();
+ this._shells = new Set();
+ this._lastSessionDownloadElement = null;
+ this.place = aPlace;
+ this._richlistbox.controllers.appendController(this);
+}
+
+DownloadsPlacesView.prototype = {
+ _registerAsDownloadsView: function DPV__registerAsDownloadsView() {
+ let downloadsData = DownloadsCommon.getData(window.opener || window);
+ downloadsData.addView(this);
+ // Make sure to unregister the view if the window is closed.
+ window.addEventListener("unload", function() {
+ if (this._result) {
+ downloadsData.removeView(this);
+ }
+ }.bind(this), true);
+ },
+
+ get associatedElement() this._richlistbox,
+
+ _forEachDownloadElementShellForURI:
+ function DPV__forEachDownloadElementShellForURI(aURI, aCallback) {
+ if (this._downloadElementsShellsForURI.has(aURI)) {
+ let downloadElementShells = this._downloadElementsShellsForURI.get(aURI);
+ for (let des of downloadElementShells) {
+ aCallback(des);
+ }
+ }
+ },
+
+ // Given a data item for a session download, or a places node for a past
+ // download, updates the view as necessary.
+ // 1. If the given data is a places node, we check whether there are any
+ // element for the same download url. If there are, then we just reset
+ // their places node. Otherwise we add a new download element.
+ // 2. If the given data is a data item, we first check if there's an history
+ // download in the list that is not associated with a data item. If we found
+ // one, we use it for the data item as well and reposition it alongside the
+ // other session downloads. If we don't, then we go ahead and create a new
+ // element for the download.
+ _addDownloadData:
+ function DPV_addDownload(aDataItem, aPlacesNode, aNewest) {
+ let downloadURI = aPlacesNode ? aPlacesNode.uri : aDataItem.uri;
+ let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI, null);
+ if (!shellsForURI) {
+ shellsForURI = new Set();
+ this._downloadElementsShellsForURI.set(downloadURI, shellsForURI);
+ }
+
+ let newOrUpdatedShell = null;
+
+ // Trivial: if there are no shells for this download URI, we always
+ // need to create one.
+ let shouldCreateShell = shellsForURI.size == 0;
+
+ // However, if we do have shells for this download uri, there are
+ // few options:
+ // 1) There's only one shell and it's for a history download (it has
+ // no data item). In this case, we update this shell and move it
+ // if necessary
+ // 2) There are multiple shells, indicicating multiple downloads for
+ // the same download uri are running. In this case we create
+ // anoter shell for the download (so we have one shell for each data
+ // item).
+ //
+ // Note: If a cancelled session download is already in the list, and the
+ // download is retired, onDataItemAdded is called again for the same
+ // data item. Thus, we also check that we make sure we don't have a view item
+ // already.
+ if (!shouldCreateShell &&
+ aDataItem && this.getViewItem(aDataItem) == null) {
+ // If there's a past-download-only shell for this download-uri with no
+ // associated data item, use it for the new data item. Otherwise, go ahead
+ // and create another shell.
+ shouldCreateShell = true;
+ for (let shell of shellsForURI) {
+ if (!shell.dataItem) {
+ shouldCreateShell = false;
+ shell.dataItem = aDataItem;
+ newOrUpdatedShell = shell;
+ this._viewItemsForDataItems.set(aDataItem, shell);
+ break;
+ }
+ }
+ }
+
+ if (shouldCreateShell) {
+ let shell = new DownloadElementShell(aDataItem, aPlacesNode);
+ newOrUpdatedShell = shell;
+ element = shell.element;
+ shellsForURI.add(shell);
+ if (aDataItem)
+ this._viewItemsForDataItems.set(aDataItem, shell);
+ }
+ else if (aPlacesNode) {
+ for (let shell of shellsForURI) {
+ if (shell.placesNode != aPlacesNode)
+ shell.placesNode = aPlacesNode;
+ }
+ }
+
+ if (newOrUpdatedShell) {
+ if (aNewest) {
+ this._richlistbox.insertBefore(newOrUpdatedShell.element,
+ this._richlistbox.firstChild);
+ if (!this._lastSessionDownloadElement) {
+ this._lastSessionDownloadElement = newOrUpdatedShell.element;
+ }
+ }
+ else if (aDataItem) {
+ let before = this._lastSessionDownloadElement ?
+ this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild
+ this._richlistbox.insertBefore(newOrUpdatedShell.element, before)
+ this._lastSessionDownloadElement = newOrUpdatedShell.element;
+ }
+ else {
+ this._richlistbox.appendChild(newOrUpdatedShell.element);
+ }
+ }
+ },
+
+ _removeElement: function DPV__removeElement(aElement) {
+ // If the element was selected exclusively, select its next
+ // sibling first, if any.
+ if (aElement.nextSibling &&
+ this._richlistbox.selectedItems &&
+ this._richlistbox.selectedItems[0] == aElement) {
+ this._richlistbox.selectItem(aElement.nextSibling);
+ }
+ this._richlistbox.removeChild(aElement);
+ },
+
+ _historyDownloadRemoved:
+ function DPV__historyDownloadRemoved(aPlacesNode) {
+ let downloadURI = aPlacesNode.uri;
+ let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI, null);
+ if (shellsForURI) {
+ for (let shell of shellsForURI) {
+ if (shell.dataItem) {
+ shell.placesNode = null;
+ }
+ else {
+ this._removeElement(shell.element);
+ shellsForURI.delete(shell);
+ if (shellsForURI.size == 0)
+ this._downloadElementsShellsForURI.delete(downloadURI);
+ }
+ }
+ }
+ },
+
+ _sessionDownloadRemoved:
+ function DPV__sessionDownloadRemoved(aDataItem) {
+ let shells = this._downloadElementsShellsForURI.get(aDataItem.uri, null);
+ if (shells.size == 0)
+ throw new Error("Should have had at leaat one shell for this uri");
+
+ let shell = this.getViewItem(aDataItem);
+ if (!shells.has(shell))
+ throw new Error("Missing download element shell in shells list for url");
+
+ // If there's more than one item for this download uri, we can let the
+ // view item for this this particular data item go away.
+ // If there's only one item for this download uri, we should only
+ // keep it if it is associated with a history download.
+ if (shells.size > 1 || !shell.placesNode) {
+ this._removeElement(shell.element);
+ shells.delete(shell);
+ if (shells.size == 0)
+ this._downloadElementsShellsForURI.delete(aDataItem.uri);
+ return;
+ }
+ else {
+ shell.dataItem = null;
+ // Move it below the session-download items;
+ if (this._lastSessionDownloadElement == shell.dataItem) {
+ this._lastSessionDownloadElement = shell.dataItem.previousSibling;
+ }
+ else {
+ let before = this._lastSessionDownloadElement ?
+ this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstchild;
+ this._richlistbox.insertBefore(shell.element, before);
+ }
+ }
+ },
+
+ get place() this._place,
+
+ set place(val) {
+ // Cleanup
+ this.result = null;
+
+ this._searchTerm = "";
+ this._place = val;
+
+ let history = PlacesUtils.history;
+ let queries = { }, options = { };
+ history.queryStringToQueries(val, queries, { }, options);
+ if (!queries.value.length)
+ queries.value = [history.getNewQuery()];
+
+ let result = history.executeQueries(queries.value, queries.value.length,
+ options.value);
+ result.addObserver(this, false);
+
+ this._registerAsDownloadsView();
+
+ return val;
+ },
+
+ _cleanUp: function() {
+ while (this._richlistbox.firstChild) {
+ this._richlistbox.removeChild(this._richlistbox.firstChild);
+ }
+ this._searchTerm = "";
+ DownloadsCommon.getData(window.opener || window).removeView();
+ this._downloadElementsShellsForURI = new Map();
+ this._viewItemsForDataItems = new WeakMap();
+ this._shells = new Set();
+ this._resultNode = null;
+ this._result = null;
+ this._lastSessionDownloadElement = null;
+ },
+
+ _result: null,
+ get result() this._result,
+ set result(val) {
+ if (this._result == val)
+ return val;
+
+ if (this._result) {
+ this._result.removeObserver(this);
+ this._resultNode.containerOpen = false;
+ }
+
+ this._result = val;
+ if (val) {
+ this._resultNode = val.root;
+ this._resultNode.containerOpen = true;
+ }
+ else {
+ this._cleanUp();
+ }
+
+ return val;
+ },
+
+ get selectedNodes() {
+ let placesNodes = [];
+ let selectedElements = this._richlistbox.selectedItems;
+ for (let elt of selectedElements) {
+ if (elt.placesNode)
+ placesNodes.push(elt.placesNode);
+ }
+ return placesNodes;
+ },
+
+ get selectedNode() {
+ let selectedNodes = this.selectedNodes;
+ return selectedNodes.length == 1 ? selectedNodes[0] : null;
+ },
+
+ get hasSelection() this.selectedNodes.length > 0,
+
+ containerStateChanged:
+ function DPV_containerStateChanged(aNode, aOldState, aNewState) {
+ this.invalidateContainer(aNode)
+ },
+
+ invalidateContainer:
+ function DPV_invalidateContainer(aContainer) {
+ if (aContainer != this._resultNode)
+ throw new Error("Unexpected container node");
+
+ if (aContainer.containerOpen) {
+ for (let i = 0; i < aContainer.childCount; i++) {
+ try {
+ this._addDownloadData(null, aContainer.getChild(i), false)
+ }
+ catch(ex) {
+ Cu.reportError(ex);
+ }
+ }
+ }
+ else {
+ throw new Error("Root container for the downloads query cannot be closed");
+ }
+ },
+
+ nodeInserted: function DPV_nodeInserted(aParent, aPlacesNode) {
+ this._addDownloadData(null, aPlacesNode, false);
+ },
+
+ nodeRemoved: function DPV_nodeRemoved(aParent, aPlacesNode, aOldIndex) {
+ this._historyDownloadRemoved(aPlacesNode);
+ },
+
+ nodeIconChanged: function DPV_nodeIconChanged(aNode) {
+ this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) {
+ aDownloadElementShell.placesNodeIconChanged();
+ });
+ },
+
+ nodeAnnotationChanged: function DPV_nodeAnnotationChanged(aNode, aAnnoName) {
+ this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) {
+ aDownloadElementShell.placesNodeAnnotationChanged(aAnnoName);
+ });
+ },
+
+ nodeTitleChanged: function DPV_nodeTitleChanged(aNode, aNewTitle) {
+ this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) {
+ aDownloadElementShell.placesNodeTitleChanged();
+ });
+ },
+
+ nodeKeywordChanged: function() {},
+ nodeDateAddedChanged: function() {},
+ nodeLastModifiedChanged: function() {},
+ nodeReplaced: function() {},
+ nodeHistoryDetailsChanged: function() {},
+ nodeTagsChanged: function() {},
+ sortingChanged: function() {},
+ nodeMoved: function() {},
+ nodeURIChanged: function() {},
+
+ get controller() this._richlistbox.controller,
+
+ get searchTerm() this._searchTerm,
+ set searchTerm(aValue) {
+ for (let element of this._richlistbox.childNodes) {
+ element.hidden = !element._shell.matchesSearchTerm(aValue);
+ }
+ return aValue;
+ },
+
+ applyFilter: function() {
+ throw new Error("applyFilter is not implemented by the DownloadsView")
+ },
+
+ load: function(aQueries, aOptions) {
+ throw new Error("|load| is not implemented by the Downloads View");
+ },
+
+ onDataLoadStarting: function() { },
+ onDataLoadCompleted: function() { },
+
+ onDataItemAdded: function DPV_onDataItemAdded(aDataItem, aNewest) {
+ this._addDownloadData(aDataItem, null, aNewest);
+ },
+
+ onDataItemRemoved: function DPV_onDataItemRemoved(aDataItem) {
+ this._sessionDownloadRemoved(aDataItem)
+ },
+
+ getViewItem: function(aDataItem)
+ this._viewItemsForDataItems.get(aDataItem, null),
+
+ supportsCommand: function(aCommand)
+ DOWNLOAD_VIEW_SUPPORTED_COMMANDS.indexOf(aCommand) != -1,
+
+ isCommandEnabled: function DPV_isCommandEnabled(aCommand) {
+ let selectedElements = this._richlistbox.selectedItems;
+ switch (aCommand) {
+ case "cmd_copy":
+ return selectedElements && selectedElements.length > 0;
+ case "cmd_selectAll":
+ return true;
+ case "cmd_paste":
+ return this._canDownloadClipboardURL();
+ default:
+ return Array.every(selectedElements, function(element) {
+ return element._shell.isCommandEnabled(aCommand);
+ });
+ }
+ },
+
+ _copySelectedDownloadsToClipboard:
+ function DPV__copySelectedDownloadsToClipboard() {
+ let selectedElements = this._richlistbox.selectedItems;
+ let urls = [e._shell.downloadURI for each (e in selectedElements)];
+
+ Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper).copyString(urls.join("\n"), document);
+ },
+
+ _getURLFromClipboardData: function DPV__getURLFromClipboardData() {
+ let trans = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ trans.init(null);
+
+ let flavors = ["text/x-moz-url", "text/unicode"];
+ flavors.forEach(trans.addDataFlavor);
+
+ Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
+
+ // Getting the data or creating the nsIURI might fail
+ try {
+ let data = {};
+ trans.getAnyTransferData({}, data, {});
+ let [url, name] = data.value.QueryInterface(Ci.nsISupportsString)
+ .data.split("\n");
+ if (url)
+ return [NetUtil.newURI(url, null, null).spec, name];
+ }
+ catch(ex) { }
+
+ return ["", ""];
+ },
+
+ _canDownloadClipboardURL: function DPV__canDownloadClipboardURL() {
+ let [url, name] = this._getURLFromClipboardData();
+ return url != "";
+ },
+
+ _downloadURLFromClipboard: function DPV__downloadURLFromClipboard() {
+ let [url, name] = this._getURLFromClipboardData();
+ saveURL(url, name || url, null, true, true, undefined, document);
+ },
+
+ doCommand: function DPV_doCommand(aCommand) {
+ switch (aCommand) {
+ case "cmd_copy":
+ this._copySelectedDownloadsToClipboard();
+ break;
+ case "cmd_selectAll":
+ this._richlistbox.selectAll();
+ break;
+ case "cmd_paste":
+ this._downloadURLFromClipboard();
+ break;
+ default: {
+ let selectedElements = this._richlistbox.selectedItems;
+ for (let element of selectedElements) {
+ element._shell.doCommand(aCommand);
+ }
+ }
+ }
+ },
+
+ onEvent: function() { },
+
+ onContextMenu: function DPV_onContextMenu(aEvent)
+ {
+ let element = this._richlistbox.selectedItem;
+ if (!element || !element._shell)
+ return false;
+
+ // Set the state attribute so that only the appropriate items are displayed.
+ let contextMenu = document.getElementById("downloadsContextMenu");
+ contextMenu.setAttribute("state", element._shell._state);
+ },
+
+ onKeyPress: function DPV_onKeyPress(aEvent) {
+ let selectedElements = this._richlistbox.selectedItems;
+ if (!selectedElements)
+ return;
+
+ if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
+ // In the content tree, opening bookmarks by pressing return is only
+ // supported when a single item is selected. To be consistent, do the
+ // same here.
+ if (selectedElements.length == 1) {
+ let element = selectedElements[0];
+ if (element._shell)
+ element._shell.doDefaultCommand();
+ }
+ }
+ else if (aEvent.charCode == " ".charCodeAt(0)) {
+ // Pausue/Resume every selected download
+ for (let element of selectedElements) {
+ if (element._shell.isCommandEnabled("downloadsCmd_pauseResume"))
+ element._shell.doCommand("downloadsCmd_pauseResume");
+ }
+ }
+ }
+};
+
+function goUpdateDownloadCommands() {
+ for (let command of DOWNLOAD_VIEW_SUPPORTED_COMMANDS) {
+ goUpdateCommand(command);
+ }
+}
diff --git a/browser/components/places/content/places.css b/browser/components/places/content/places.css
index 5151cca8254b..41c5afef3016 100644
--- a/browser/components/places/content/places.css
+++ b/browser/components/places/content/places.css
@@ -14,3 +14,33 @@ tree[type="places"] {
menupopup[placespopup="true"] {
-moz-binding: url("chrome://browser/content/places/menu.xml#places-popup-base");
}
+
+richlistitem.download {
+ -moz-binding: url('chrome://browser/content/places/download.xml#download');
+}
+
+.download-state:not( [state="0"] /* Downloading */)
+ .downloadPauseMenuItem,
+.download-state:not( [state="4"] /* Paused */)
+ .downloadResumeMenuItem,
+.download-state:not(:-moz-any([state="2"], /* Failed */
+ [state="4"]) /* Paused */)
+ .downloadCancelMenuItem,
+.download-state:not(:-moz-any([state="1"], /* Finished */
+ [state="2"], /* Failed */
+ [state="3"], /* Canceled */
+ [state="6"], /* Blocked (parental) */
+ [state="8"], /* Blocked (dirty) */
+ [state="9"]) /* Blocked (policy) */)
+ .downloadRemoveFromListMenuItem,
+.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */
+ [state="5"], /* Starting (queued) */
+ [state="0"], /* Downloading */
+ [state="4"]) /* Paused */)
+ .downloadShowMenuItem,
+
+.download-state[state="7"] .downloadCommandsSeparator
+
+{
+ display: none;
+}
diff --git a/browser/components/places/content/places.js b/browser/components/places/content/places.js
index bd9a0bacfd1d..69586c59aac7 100644
--- a/browser/components/places/content/places.js
+++ b/browser/components/places/content/places.js
@@ -5,9 +5,13 @@
Components.utils.import("resource:///modules/MigrationUtils.jsm");
+const DOWNLOADS_QUERY = "place:transition=" +
+ Components.interfaces.nsINavHistoryService.TRANSITION_DOWNLOAD +
+ "&sort=" +
+ Components.interfaces.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
+
var PlacesOrganizer = {
_places: null,
- _content: null,
// IDs of fields from editBookmarkOverlay that should be hidden when infoBox
// is minimal. IDs should be kept in sync with the IDs of the elements
@@ -32,8 +36,9 @@ var PlacesOrganizer = {
},
init: function PO_init() {
+ ContentArea.init();
+
this._places = document.getElementById("placesList");
- this._content = document.getElementById("placeContent");
this._initFolderTree();
var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks
@@ -45,12 +50,6 @@ var PlacesOrganizer = {
this._backHistory.splice(0, this._backHistory.length);
document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
- var view = this._content.treeBoxObject.view;
- if (view.rowCount > 0)
- view.selection.select(0);
-
- this._content.focus();
-
// Set up the search UI.
PlacesSearchBox.init();
@@ -84,6 +83,13 @@ var PlacesOrganizer = {
#ifndef MOZ_PER_WINDOW_PRIVATE_BROWSING
gPrivateBrowsingListener.init();
#endif
+
+ // Select the first item in the content area view.
+ let view = ContentArea.currentView;
+ let root = view.result ? view.result.root : null;
+ if (root && root.containerOpen && root.childCount >= 0)
+ view.selectNode(root.getChild(0));
+ ContentArea.focus();
},
QueryInterface: function PO_QueryInterface(aIID) {
@@ -139,9 +145,9 @@ var PlacesOrganizer = {
if (!this._places.hasSelection) {
// If no node was found for the given place: uri, just load it directly
- this._content.place = aLocation;
+ ContentArea.currentPlace = aLocation;
}
- this.onContentTreeSelect();
+ this.updateDetailsPane();
// update navigation commands
if (this._backHistory.length == 0)
@@ -201,8 +207,8 @@ var PlacesOrganizer = {
// If either the place of the content tree in the right pane has changed or
// the user cleared the search box, update the place, hide the search UI,
// and update the back/forward buttons by setting location.
- if (this._content.place != placeURI || !resetSearchBox) {
- this._content.place = placeURI;
+ if (ContentArea.currentPlace != placeURI || !resetSearchBox) {
+ ContentArea.currentPlace = placeURI;
this.location = node.uri;
}
@@ -221,8 +227,7 @@ var PlacesOrganizer = {
PlacesSearchBox.searchFilter.reset();
this._setSearchScopeForNode(node);
- if (this._places.treeBoxObject.focused)
- this._fillDetailsPane([node]);
+ this.updateDetailsPane();
},
/**
@@ -247,50 +252,39 @@ var PlacesOrganizer = {
},
/**
- * Handle clicks on the tree.
+ * Handle clicks on the places list.
* Single Left click, right click or modified click do not result in any
* special action, since they're related to selection.
* @param aEvent
* The mouse event.
*/
- onTreeClick: function PO_onTreeClick(aEvent) {
+ onPlacesListClick: function PO_onPlacesListClick(aEvent) {
// Only handle clicks on tree children.
if (aEvent.target.localName != "treechildren")
return;
- var currentView = aEvent.currentTarget;
- var selectedNode = currentView.selectedNode;
- if (selectedNode) {
- var doubleClickOnFlatList = (aEvent.button == 0 && aEvent.detail == 2 &&
- aEvent.target.parentNode.flatList);
- var middleClick = (aEvent.button == 1 && aEvent.detail == 1);
-
- if (PlacesUtils.nodeIsURI(selectedNode) &&
- (doubleClickOnFlatList || middleClick)) {
- // Open associated uri in the browser.
- PlacesOrganizer.openSelectedNode(aEvent);
- }
- else if (middleClick &&
- PlacesUtils.nodeIsContainer(selectedNode)) {
+ let node = this._places.selectedNode;
+ if (node) {
+ let middleClick = aEvent.button == 1 && aEvent.detail == 1;
+ if (middleClick && PlacesUtils.nodeIsContainer(node)) {
// The command execution function will take care of seeing if the
// selection is a folder or a different container type, and will
// load its contents in tabs.
- PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent, currentView);
+ PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent, this._places);
}
}
},
/**
- * Handle focus changes on the trees.
- * When moving focus between panes we should update the details pane contents.
- * @param aEvent
- * The mouse event.
+ * Handle focus changes on the places list and the current content view.
*/
- onTreeFocus: function PO_onTreeFocus(aEvent) {
- var currentView = aEvent.currentTarget;
- var selectedNodes = currentView.selectedNode ? [currentView.selectedNode] :
- this._content.selectedNodes;
- this._fillDetailsPane(selectedNodes);
+ updateDetailsPane: function PO_updateDetailsPane() {
+ let view = PlacesUIUtils.getViewForNode(document.activeElement);
+ if (view) {
+ let selectedNodes = view.selectedNode ?
+ [view.selectedNode] : view.selectedNodes;
+ this._fillDetailsPane(selectedNodes);
+ }
},
openFlatContainer: function PO_openFlatContainerFlatContainer(aContainer) {
@@ -300,17 +294,12 @@ var PlacesOrganizer = {
this._places.selectPlaceURI(aContainer.uri);
},
- openSelectedNode: function PO_openSelectedNode(aEvent) {
- PlacesUIUtils.openNodeWithEvent(this._content.selectedNode, aEvent,
- this._content);
- },
-
/**
* Returns the options associated with the query currently loaded in the
* main places pane.
*/
getCurrentOptions: function PO_getCurrentOptions() {
- return PlacesUtils.asQuery(this._content.result.root).queryOptions;
+ return PlacesUtils.asQuery(ContentArea.currentView.result.root).queryOptions;
},
/**
@@ -318,7 +307,7 @@ var PlacesOrganizer = {
* main places pane.
*/
getCurrentQueries: function PO_getCurrentQueries() {
- return PlacesUtils.asQuery(this._content.result.root).getQueries();
+ return PlacesUtils.asQuery(ContentArea.currentView.result.root).getQueries();
},
/**
@@ -564,11 +553,6 @@ var PlacesOrganizer = {
canvas.height = height;
},
- onContentTreeSelect: function PO_onContentTreeSelect() {
- if (this._content.treeBoxObject.focused)
- this._fillDetailsPane(this._content.selectedNodes);
- },
-
_fillDetailsPane: function PO__fillDetailsPane(aNodeList) {
var infoBox = document.getElementById("infoBox");
var detailsDeck = document.getElementById("detailsDeck");
@@ -671,10 +655,15 @@ var PlacesOrganizer = {
else {
detailsDeck.selectedIndex = 0;
infoBox.hidden = true;
- var selectItemDesc = document.getElementById("selectItemDescription");
- var itemsCountLabel = document.getElementById("itemsCountText");
- var rowCount = this._content.treeBoxObject.view.rowCount;
- if (rowCount == 0) {
+ let selectItemDesc = document.getElementById("selectItemDescription");
+ let itemsCountLabel = document.getElementById("itemsCountText");
+ let itemsCount = 0;
+ if (ContentArea.currentView.result) {
+ let rootNode = ContentArea.currentView.result.root;
+ if (rootNode.containerOpen)
+ itemsCount = rootNode.childCount;
+ }
+ if (itemsCount == 0) {
selectItemDesc.hidden = true;
itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems");
}
@@ -682,7 +671,7 @@ var PlacesOrganizer = {
selectItemDesc.hidden = false;
itemsCountLabel.value =
PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
- rowCount, [rowCount]);
+ itemsCount, [itemsCount]);
}
}
},
@@ -779,14 +768,14 @@ var PlacesSearchBox = {
return;
}
- var currentOptions = PO.getCurrentOptions();
- var content = PO._content;
+ let currentView = ContentArea.currentView;
+ let currentOptions = PO.getCurrentOptions();
// Search according to the current scope, which was set by
// PQB_setScope()
switch (PlacesSearchBox.filterCollection) {
case "bookmarks":
- content.applyFilter(filterString, this.folders);
+ currentView.applyFilter(filterString, this.folders);
break;
case "history":
if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
@@ -797,13 +786,14 @@ var PlacesSearchBox = {
options.resultType = currentOptions.RESULT_TYPE_URI;
options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
options.includeHidden = true;
- content.load([query], options);
+ currentView.load([query], options);
}
else {
- content.applyFilter(filterString, null, true);
+ currentView.applyFilter(filterString, null, true);
}
break;
- case "downloads": {
+ case "downloads":
+ if (currentView == ContentTree.view) {
let query = PlacesUtils.history.getNewQuery();
query.searchTerms = filterString;
query.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], 1);
@@ -812,16 +802,19 @@ var PlacesSearchBox = {
options.resultType = currentOptions.RESULT_TYPE_URI;
options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
options.includeHidden = true;
- content.load([query], options);
+ currentView.load([query], options);
+ }
+ else {
+ // The new downloads view doesn't use places for searching downloads.
+ currentView.searchTerm = filterString;
+ }
break;
- }
- default:
- throw "Invalid filterCollection on search";
- break;
+ default:
+ throw "Invalid filterCollection on search";
}
// Update the details panel
- PlacesOrganizer.onContentTreeSelect();
+ PlacesOrganizer.updateDetailsPane();
},
/**
@@ -1251,3 +1244,94 @@ let gPrivateBrowsingListener = {
}
};
#endif
+
+let ContentArea = {
+ init: function CA_init() {
+ this._deck = document.getElementById("placesViewsDeck");
+ this._specialViews = new Map();
+ ContentTree.init();
+ },
+
+ _shouldUseNewDownloadsView: function CA_shouldUseNewDownloadsView() {
+ try {
+ return Services.prefs.getBoolPref("browser.library.useNewDownloadsView");
+ }
+ catch(ex) { }
+ return false;
+ },
+
+ getContentViewForQueryString:
+ function CA_getContentViewForQueryString(aQueryString) {
+ if (this._specialViews.has(aQueryString))
+ return this._specialViews.get(aQueryString);
+ if (aQueryString == DOWNLOADS_QUERY && this._shouldUseNewDownloadsView()) {
+ let view = new DownloadsPlacesView(document.getElementById("downloadsRichListBox"), aQueryString);
+ this.setContentViewForQueryString(aQueryString, view);
+ return view;
+ }
+ return ContentTree.view;
+ },
+
+ setContentViewForQueryString:
+ function CA_setContentViewForQueryString(aQueryString, aView) {
+ this._specialViews.set(aQueryString, aView);
+ },
+
+ get currentView() PlacesUIUtils.getViewForNode(this._deck.selectedPanel),
+ set currentView(aView) {
+ if (this.currentView != aView)
+ this._deck.selectedPanel = aView.associatedElement;
+ return aView;
+ },
+
+ get currentPlace() this.currentView.place,
+ set currentPlace(aQueryString) {
+ this.currentView = this.getContentViewForQueryString(aQueryString);
+ this.currentView.place = aQueryString;
+ return aQueryString;
+ },
+
+ focus: function() {
+ this._deck.selectedPanel.focus();
+ }
+};
+
+let ContentTree = {
+ init: function CT_init() {
+ this._view = document.getElementById("placeContent");
+ },
+
+ get view() this._view,
+
+ openSelectedNode: function CT_openSelectedNode(aEvent) {
+ let view = this.view;
+ PlacesUIUtils.openNodeWithEvent(view.selectedNode, aEvent, view);
+ },
+
+ onClick: function CT_onClick(aEvent) {
+ // Only handle clicks on tree children.
+ if (aEvent.target.localName != "treechildren")
+ return;
+
+ let node = this.view.selectedNode;
+ if (node) {
+ let doubleClick = aEvent.button == 0 && aEvent.detail == 2;
+ let middleClick = aEvent.button == 1 && aEvent.detail == 1;
+ if (PlacesUtils.nodeIsURI(node) && (doubleClick || middleClick)) {
+ // Open associated uri in the browser.
+ this.openSelectedNode(aEvent);
+ }
+ else if (middleClick && PlacesUtils.nodeIsContainer(node)) {
+ // The command execution function will take care of seeing if the
+ // selection is a folder or a different container type, and will
+ // load its contents in tabs.
+ PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this.view);
+ }
+ }
+ },
+
+ onKeyPress: function CT_onKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN)
+ this.openSelectedNode(aEvent);
+ }
+};
diff --git a/browser/components/places/content/places.xul b/browser/components/places/content/places.xul
index 77e94c0a60a8..e98088b74668 100644
--- a/browser/components/places/content/places.xul
+++ b/browser/components/places/content/places.xul
@@ -10,6 +10,7 @@
+
@@ -28,6 +29,8 @@
%editMenuOverlayDTD;
%browserDTD;
+
+%downloadsDTD;
]>
+
+
@@ -344,8 +351,8 @@
type="places"
hidecolumnpicker="true" context="placesContext"
onselect="PlacesOrganizer.onPlaceSelected(true);"
- onclick="PlacesOrganizer.onTreeClick(event);"
- onfocus="PlacesOrganizer.onTreeFocus(event);"
+ onclick="PlacesOrganizer.onPlacesListClick(event);"
+ onfocus="PlacesOrganizer.updateDetailsPane(event);"
seltype="single"
persist="width"
width="200"
@@ -358,49 +365,58 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -434,4 +450,61 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/components/places/content/tree.xml b/browser/components/places/content/tree.xml
index e69c8d15b080..235f4bc68bf1 100644
--- a/browser/components/places/content/tree.xml
+++ b/browser/components/places/content/tree.xml
@@ -56,6 +56,10 @@
]]>
+
+
diff --git a/browser/components/places/jar.mn b/browser/components/places/jar.mn
index e8ab80c5246c..fd18bea17b95 100644
--- a/browser/components/places/jar.mn
+++ b/browser/components/places/jar.mn
@@ -8,6 +8,9 @@ browser.jar:
content/browser/places/bookmarkProperties2.xul (content/bookmarkProperties.xul)
* content/browser/places/places.xul (content/places.xul)
* content/browser/places/places.js (content/places.js)
+ content/browser/places/downloadsView.js (content/downloadsView.js)
+ content/browser/places/download.xml (content/download.xml)
+ content/browser/places/download.css (content/download.css)
content/browser/places/places.css (content/places.css)
content/browser/places/organizer.css (content/organizer.css)
content/browser/places/bookmarkProperties.xul (content/bookmarkProperties.xul)
diff --git a/browser/components/places/tests/browser/browser_410196_paste_into_tags.js b/browser/components/places/tests/browser/browser_410196_paste_into_tags.js
index 9d523c8c1bc1..8a7ff8c66fc0 100644
--- a/browser/components/places/tests/browser/browser_410196_paste_into_tags.js
+++ b/browser/components/places/tests/browser/browser_410196_paste_into_tags.js
@@ -21,6 +21,7 @@ const MOZURISPEC = "http://mozilla.com/";
let gLibrary;
let PlacesOrganizer;
+let ContentTree;
function test() {
waitForExplicitFinish();
@@ -34,6 +35,9 @@ function onLibraryReady() {
PlacesOrganizer = gLibrary.PlacesOrganizer;
ok(PlacesOrganizer, "Places organizer in scope");
+ ContentTree = gLibrary.ContentTree;
+ ok(ContentTree, "ContentTree is in scope");
+
tests.makeHistVisit();
tests.makeTag();
tests.focusTag();
@@ -101,12 +105,12 @@ let tests = {
PlacesUtils.asContainer(histContainer);
histContainer.containerOpen = true;
PlacesOrganizer._places.selectNode(histContainer.getChild(0));
- let histNode = PlacesOrganizer._content.view.nodeForTreeIndex(0);
- PlacesOrganizer._content.selectNode(histNode);
+ let histNode = ContentTree.view.view.nodeForTreeIndex(0);
+ ContentTree.view.selectNode(histNode);
is(histNode.uri, MOZURISPEC,
"historyNode exists: " + histNode.uri);
// copy the history node
- PlacesOrganizer._content.controller.copy();
+ ContentTree.view.controller.copy();
},
historyNode: function (){
@@ -116,7 +120,7 @@ let tests = {
PlacesUtils.asContainer(histContainer);
histContainer.containerOpen = true;
PlacesOrganizer._places.selectNode(histContainer.getChild(0));
- let histNode = PlacesOrganizer._content.view.nodeForTreeIndex(0);
+ let histNode = ContentTree.view.view.nodeForTreeIndex(0);
ok(histNode, "histNode exists: " + histNode.title);
// check to see if the history node is tagged!
let tags = PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(MOZURISPEC));
@@ -133,8 +137,8 @@ let tests = {
// is the bookmark visible in the UI?
// get the Unsorted Bookmarks node
PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
- // now we can see what is in the _content tree
- let unsortedNode = PlacesOrganizer._content.view.nodeForTreeIndex(1);
+ // now we can see what is in the ContentTree tree
+ let unsortedNode = ContentTree.view.view.nodeForTreeIndex(1);
ok(unsortedNode, "unsortedNode is not null: " + unsortedNode.uri);
is(unsortedNode.uri, MOZURISPEC, "node uri's are the same");
},
diff --git a/browser/components/places/tests/browser/browser_416459_cut.js b/browser/components/places/tests/browser/browser_416459_cut.js
index 00cdbd5ea741..ec78df1b797c 100644
--- a/browser/components/places/tests/browser/browser_416459_cut.js
+++ b/browser/components/places/tests/browser/browser_416459_cut.js
@@ -7,6 +7,7 @@ const TEST_URL = "http://example.com/";
let gLibrary;
let gItemId;
let PlacesOrganizer;
+let ContentTree;
function test() {
waitForExplicitFinish();
@@ -15,11 +16,13 @@ function test() {
function onLibraryReady() {
PlacesOrganizer = gLibrary.PlacesOrganizer;
+ ContentTree = gLibrary.ContentTree;
// Sanity checks.
ok(PlacesUtils, "PlacesUtils in scope");
ok(PlacesUIUtils, "PlacesUIUtils in scope");
ok(PlacesOrganizer, "PlacesOrganizer in scope");
+ ok(ContentTree, "ContentTree is in scope");
gItemId = PlacesUtils.bookmarks.insertBookmark(
PlacesUtils.toolbarFolderId, NetUtil.newURI(TEST_URL),
@@ -41,21 +44,21 @@ function selectBookmarkIn(aLeftPaneQuery) {
is(PlacesUtils.bookmarks.getFolderIdForItem(gItemId), rootId,
"Bookmark has the right parent");
info("Selecting the bookmark in the right pane");
- PlacesOrganizer._content.selectItems([gItemId]);
- let bookmarkNode = PlacesOrganizer._content.selectedNode;
+ ContentTree.view.selectItems([gItemId]);
+ let bookmarkNode = ContentTree.view.selectedNode;
is(bookmarkNode.uri, TEST_URL, "Found the expected bookmark");
}
function cutSelection() {
info("Cutting selection");
- PlacesOrganizer._content.controller.cut();
+ ContentTree.view.controller.cut();
}
function pasteClipboard(aLeftPaneQuery) {
info("Selecting " + aLeftPaneQuery + " in the left pane");
PlacesOrganizer.selectLeftPaneQuery(aLeftPaneQuery);
info("Pasting clipboard");
- PlacesOrganizer._content.controller.paste();
+ ContentTree.view.controller.paste();
}
function onClipboardReady() {
diff --git a/browser/components/places/tests/browser/browser_library_batch_delete.js b/browser/components/places/tests/browser/browser_library_batch_delete.js
index 2839c81fce76..2a65b6b90675 100644
--- a/browser/components/places/tests/browser/browser_library_batch_delete.js
+++ b/browser/components/places/tests/browser/browser_library_batch_delete.js
@@ -69,19 +69,20 @@ gTests.push({
desc: "Ensure correct selection and functionality in Library",
run: function() {
let PO = gLibrary.PlacesOrganizer;
+ let ContentTree = gLibrary.ContentTree;
// Move selection forth and back.
PO.selectLeftPaneQuery("History");
PO.selectLeftPaneQuery("UnfiledBookmarks");
// Now select the "keepme" folder in the right pane and delete it.
- PO._content.selectNode(PO._content.result.root.getChild(0));
- is(PO._content.selectedNode.title, "keepme",
+ ContentTree.view.selectNode(ContentTree.view.result.root.getChild(0));
+ is(ContentTree.view.selectedNode.title, "keepme",
"Found folder in content pane");
// Test live update.
PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
makeURI(TEST_URL),
PlacesUtils.bookmarks.DEFAULT_INDEX,
"bm");
- is(PO._content.result.root.childCount, 2,
+ is(ContentTree.view.result.root.childCount, 2,
"Right pane was correctly updated");
nextTest();
}
diff --git a/browser/components/places/tests/browser/browser_library_infoBox.js b/browser/components/places/tests/browser/browser_library_infoBox.js
index 731de809d3ff..6a9eb924b36d 100644
--- a/browser/components/places/tests/browser/browser_library_infoBox.js
+++ b/browser/components/places/tests/browser/browser_library_infoBox.js
@@ -18,6 +18,7 @@ gTests.push({
desc: "Bug 430148 - Remove or hide the more/less button in details pane...",
run: function() {
var PO = gLibrary.PlacesOrganizer;
+ let ContentTree = gLibrary.ContentTree;
var infoBoxExpanderWrapper = getAndCheckElmtById("infoBoxExpanderWrapper");
// add a visit to browser history
@@ -57,7 +58,7 @@ gTests.push({
checkAddInfoFieldsCollapsed(PO);
// open history item
- var view = PO._content.treeBoxObject.view;
+ var view = ContentTree.view.treeBoxObject.view;
ok(view.rowCount > 0, "History item exists.");
view.selection.select(0);
ok(infoBoxExpanderWrapper.hidden,
@@ -94,7 +95,7 @@ gTests.push({
checkAddInfoFieldsNotCollapsed(PO);
// open first bookmark
- var view = PO._content.treeBoxObject.view;
+ var view = ContentTree.view.treeBoxObject.view;
ok(view.rowCount > 0, "Bookmark item exists.");
view.selection.select(0);
checkInfoBoxSelected(PO);
diff --git a/browser/components/places/tests/browser/browser_library_middleclick.js b/browser/components/places/tests/browser/browser_library_middleclick.js
index 7f1acc099809..13a62da308b1 100644
--- a/browser/components/places/tests/browser/browser_library_middleclick.js
+++ b/browser/components/places/tests/browser/browser_library_middleclick.js
@@ -88,7 +88,7 @@ gTests.push({
isnot(gLibrary.PlacesOrganizer._places.selectedNode, null,
"We correctly have selection in the Library left pane");
// Get our bookmark in the right pane.
- var bookmarkNode = gLibrary.PlacesOrganizer._content.view.nodeForTreeIndex(0);
+ var bookmarkNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
is(bookmarkNode.uri, this.URIs[0], "Found bookmark in the right pane");
},
@@ -130,7 +130,7 @@ gTests.push({
isnot(gLibrary.PlacesOrganizer._places.selectedNode, null,
"We correctly have selection in the Library left pane");
// Get our bookmark in the right pane.
- var folderNode = gLibrary.PlacesOrganizer._content.view.nodeForTreeIndex(0);
+ var folderNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
is(folderNode.title, "Folder", "Found folder in the right pane");
},
@@ -187,7 +187,7 @@ gTests.push({
isnot(gLibrary.PlacesOrganizer._places.selectedNode, null,
"We correctly have selection in the Library left pane");
// Get our bookmark in the right pane.
- var folderNode = gLibrary.PlacesOrganizer._content.view.nodeForTreeIndex(0);
+ var folderNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
is(folderNode.title, "Query", "Found query in the right pane");
},
@@ -243,7 +243,7 @@ function runNextTest() {
// Middle click on first node in the content tree of the Library.
gLibrary.focus();
waitForFocus(function() {
- mouseEventOnCell(gLibrary.PlacesOrganizer._content, 0, 0, { button: 1 });
+ mouseEventOnCell(gLibrary.ContentTree.view, 0, 0, { button: 1 });
}, gLibrary);
}
else {
diff --git a/browser/themes/pinstripe/places/places.css b/browser/themes/pinstripe/places/places.css
index 6ad3a6166ce5..7c8170f2d552 100644
--- a/browser/themes/pinstripe/places/places.css
+++ b/browser/themes/pinstripe/places/places.css
@@ -202,3 +202,71 @@ treechildren::-moz-tree-image(cutting) {
treechildren::-moz-tree-cell-text(cutting) {
opacity: 0.7;
}
+
+
+/** Downloads View **/
+
+richlistitem.download {
+ height: 7em;
+ margin: 0;
+ padding: 8px;
+ -moz-padding-end: 0;
+}
+
+richlistitem.download:first-child {
+ border-top: 1px solid transparent;
+}
+
+richlistitem.download:last-child {
+ border-bottom: 1px solid transparent;
+}
+
+.downloadTypeIcon {
+ -moz-margin-end: 8px;
+ /* Prevent flickering when changing states. */
+ min-height: 32px;
+ min-width: 32px;
+}
+
+.blockedIcon {
+ list-style-image: url("chrome://global/skin/icons/Error.png");
+}
+
+.downloadTarget {
+ margin-bottom: 6px;
+ cursor: inherit;
+}
+
+.downloadDetails {
+ opacity: 0.7;
+ font-size: 95%;
+ cursor: inherit;
+}
+
+.downloadButton {
+ -moz-appearance: none;
+ min-width: 0;
+ min-height: 0;
+ margin: 3px;
+ border: none;
+ padding: 5px;
+ list-style-image: url("chrome://browser/skin/downloads/buttons.png");
+}
+
+.downloadButton > .button-box {
+ padding: 0;
+}
+
+/*** Button icons ***/
+
+.downloadButton.downloadCancel {
+ -moz-image-region: rect(0px, 16px, 16px, 0px);
+}
+
+.downloadButton.downloadShow {
+ -moz-image-region: rect(16px, 16px, 32px, 0px);
+}
+
+.downloadButton.downloadRetry {
+ -moz-image-region: rect(32px, 16px, 48px, 0px);
+}