[PALEMOON] Bug 1117141 - Part 2 of 2 - Refactor notifications and remove the DownloadsDataItem object

This commit is contained in:
janekptacijarabaci 2018-07-29 11:41:22 +02:00 • committed by Roy Tam
commit 63e8a36496
4 changed files with 234 additions and 593 deletions

View file

@ -8,7 +8,6 @@
this.EXPORTED_SYMBOLS = [ this.EXPORTED_SYMBOLS = [
"DownloadsCommon", "DownloadsCommon",
"DownloadsDataItem",
]; ];
/** /**
@ -22,14 +21,9 @@ this.EXPORTED_SYMBOLS = [
* *
* DownloadsData * DownloadsData
* Retrieves the list of past and completed downloads from the underlying * Retrieves the list of past and completed downloads from the underlying
* Download Manager data, and provides asynchronous notifications allowing * Downloads API data, and provides asynchronous notifications allowing
* to build a consistent view of the available data. * to build a consistent view of the available data.
* *
* DownloadsDataItem
* Represents a single item in the list of downloads. This object wraps the
* Download object from the JavaScript API for downloads. A specialized version
* of this object is implemented in the Places front-end view.
*
* DownloadsIndicatorData * DownloadsIndicatorData
* This object registers itself with DownloadsData as a view, and transforms the * This object registers itself with DownloadsData as a view, and transforms the
* notifications it receives into overall status data, that is then broadcast to * notifications it receives into overall status data, that is then broadcast to
@ -399,10 +393,10 @@ this.DownloadsCommon = {
}, },
/** /**
* Given an iterable collection of DownloadDataItems, generates and returns * Given an iterable collection of Download objects, generates and returns
* statistics about that collection. * statistics about that collection.
* *
* @param aDataItems An iterable collection of DownloadDataItems. * @param downloads An iterable collection of Download objects.
* *
* @return Object whose properties are the generated statistics. Currently, * @return Object whose properties are the generated statistics. Currently,
* we return the following properties: * we return the following properties:
@ -419,7 +413,7 @@ this.DownloadsCommon = {
* complete. * complete.
* percentComplete : The percentage of bytes successfully downloaded. * percentComplete : The percentage of bytes successfully downloaded.
*/ */
summarizeDownloads: function DC_summarizeDownloads(aDataItems) summarizeDownloads(downloads) {
{ {
let summary = { let summary = {
numActive: 0, numActive: 0,
@ -431,14 +425,13 @@ this.DownloadsCommon = {
// slowestSpeed is Infinity so that we can use Math.min to // slowestSpeed is Infinity so that we can use Math.min to
// find the slowest speed. We'll set this to 0 afterwards if // find the slowest speed. We'll set this to 0 afterwards if
// it's still at Infinity by the time we're done iterating all // it's still at Infinity by the time we're done iterating all
// dataItems. // download.
slowestSpeed: Infinity, slowestSpeed: Infinity,
rawTimeLeft: -1, rawTimeLeft: -1,
percentComplete: -1 percentComplete: -1
} }
for (let dataItem of aDataItems) { for (let download of downloads) {
let download = dataItem.download;
let state = DownloadsCommon.stateOfDownload(download); let state = DownloadsCommon.stateOfDownload(download);
let maxBytes = DownloadsCommon.maxBytesOfDownload(download); let maxBytes = DownloadsCommon.maxBytesOfDownload(download);
@ -670,16 +663,12 @@ XPCOMUtils.defineLazyGetter(DownloadsCommon, "useJSTransfer", function () {
function DownloadsDataCtor(aPrivate) { function DownloadsDataCtor(aPrivate) {
this._isPrivate = aPrivate; this._isPrivate = aPrivate;
// Contains all the available DownloadsDataItem objects. // Contains all the available Download objects and their integer state.
this.dataItems = new Set();
this.oldDownloadStates = new Map(); this.oldDownloadStates = new Map();
// Array of view objects that should be notified when the available download // Array of view objects that should be notified when the available download
// data changes. // data changes.
this._views = []; this._views = [];
// Maps Download objects to DownloadDataItem objects.
this._downloadToDataItemMap = new Map();
} }
DownloadsDataCtor.prototype = { DownloadsDataCtor.prototype = {
@ -705,13 +694,18 @@ DownloadsDataCtor.prototype = {
return; return;
}, },
/**
* Iterator for all the available Download objects. This is empty until the
* data has been loaded using the JavaScript API for downloads.
*/
get downloads() this.oldDownloadStates.keys(),
/** /**
* True if there are finished downloads that can be removed from the list. * True if there are finished downloads that can be removed from the list.
*/ */
get canRemoveFinished() get canRemoveFinished()
{ {
for (let dataItem of this.dataItems) { for (let download of this.oldDownloadStates.keys()) {
let download = dataItem.download;
// Stopped, paused, and failed downloads with partial data are removed. // Stopped, paused, and failed downloads with partial data are removed.
if (download.stopped && !(download.canceled && download.hasPartialData)) { if (download.stopped && !(download.canceled && download.hasPartialData)) {
return true; return true;
@ -734,37 +728,32 @@ DownloadsDataCtor.prototype = {
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
//// Integration with the asynchronous Downloads back-end //// Integration with the asynchronous Downloads back-end
onDownloadAdded: function (aDownload) onDownloadAdded(download) {
{ // Download objects do not store the end time of downloads, as the Downloads
let dataItem = new DownloadsDataItem(aDownload); // API does not need to persist this information for all platforms. Once a
this._downloadToDataItemMap.set(aDownload, dataItem); // download terminates on a Desktop browser, it becomes a history download,
this.dataItems.add(dataItem); // for which the end time is stored differently, as a Places annotation.
this.oldDownloadStates.set(aDownload, download.endTime = Date.now();
DownloadsCommon.stateOfDownload(aDownload));
this.oldDownloadStates.set(download,
DownloadsCommon.stateOfDownload(download));
for (let view of this._views) { for (let view of this._views) {
view.onDataItemAdded(dataItem, true); view.onDownloadAdded(download, true);
} }
}, },
onDownloadChanged: function (aDownload) onDownloadChanged(download) {
{ let oldState = this.oldDownloadStates.get(download);
let aDataItem = this._downloadToDataItemMap.get(aDownload); let newState = DownloadsCommon.stateOfDownload(download);
if (!aDataItem) { this.oldDownloadStates.set(download, newState);
Cu.reportError("Download doesn't exist.");
return;
}
let oldState = this.oldDownloadStates.get(aDownload);
let newState = DownloadsCommon.stateOfDownload(aDownload);
this.oldDownloadStates.set(aDownload, newState);
if (oldState != newState) { if (oldState != newState) {
if (aDownload.succeeded || if (download.succeeded ||
(aDownload.canceled && !aDownload.hasPartialData) || (download.canceled && !download.hasPartialData) ||
aDownload.error) { download.error) {
// Store the end time that may be displayed by the views. // Store the end time that may be displayed by the views.
aDownload.endTime = Date.now(); download.endTime = Date.now();
// This state transition code should actually be located in a Downloads // This state transition code should actually be located in a Downloads
// API module (bug 941009). Moreover, the fact that state is stored as // API module (bug 941009). Moreover, the fact that state is stored as
@ -773,17 +762,17 @@ DownloadsDataCtor.prototype = {
if (!this._isPrivate) { if (!this._isPrivate) {
try { try {
let downloadMetaData = { let downloadMetaData = {
state: DownloadsCommon.stateOfDownload(aDownload), state: DownloadsCommon.stateOfDownload(download),
endTime: aDownload.endTime, endTime: download.endTime,
}; };
if (aDownload.succeeded || if (download.succeeded ||
(aDownload.error && aDownload.error.becauseBlocked)) { (download.error && download.error.becauseBlocked)) {
downloadMetaData.fileSize = downloadMetaData.fileSize =
DownloadsCommon.maxBytesOfDownload(aDataItem.download); DownloadsCommon.maxBytesOfDownload(download);
} }
PlacesUtils.annotations.setPageAnnotation( PlacesUtils.annotations.setPageAnnotation(
NetUtil.newURI(aDownload.source.url), NetUtil.newURI(download.source.url),
"downloads/metaData", "downloads/metaData",
JSON.stringify(downloadMetaData), 0, JSON.stringify(downloadMetaData), 0,
PlacesUtils.annotations.EXPIRE_WITH_HISTORY); PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
@ -795,41 +784,33 @@ DownloadsDataCtor.prototype = {
for (let view of this._views) { for (let view of this._views) {
try { try {
view.onDataItemStateChanged(aDataItem); view.onDownloadStateChanged(download);
} catch (ex) { } catch (ex) {
Cu.reportError(ex); Cu.reportError(ex);
} }
} }
if (aDownload.succeeded || if (download.succeeded ||
(aDownload.error && aDownload.error.becauseBlocked)) { (download.error && download.error.becauseBlocked)) {
this._notifyDownloadEvent("finish"); this._notifyDownloadEvent("finish");
} }
} }
if (!aDownload.newDownloadNotified) { if (!download.newDownloadNotified) {
aDownload.newDownloadNotified = true; download.newDownloadNotified = true;
this._notifyDownloadEvent("start"); this._notifyDownloadEvent("start");
} }
for (let view of this._views) { for (let view of this._views) {
view.onDataItemChanged(aDataItem); view.onDownloadChanged(download);
} }
}, },
onDownloadRemoved: function (aDownload) onDownloadRemoved(download) {
{ this.oldDownloadStates.delete(download);
let dataItem = this._downloadToDataItemMap.get(aDownload);
if (!dataItem) {
Cu.reportError("Download doesn't exist.");
return;
}
this._downloadToDataItemMap.delete(aDownload);
this.dataItems.delete(dataItem);
this.oldDownloadStates.delete(aDownload);
for (let view of this._views) { for (let view of this._views) {
view.onDataItemRemoved(dataItem); view.onDownloadRemoved(download);
} }
}, },
@ -881,12 +862,9 @@ DownloadsDataCtor.prototype = {
//let loadedItemsArray = [dataItem //let loadedItemsArray = [dataItem
// for each (dataItem in this.dataItems) // for each (dataItem in this.dataItems)
// if (dataItem)]; // if (dataItem)];
let loadedItemsArray = [...this.dataItems]; let downloadsArray = [...this.oldDownloadStates.keys()];
downloadsArray.sort((a, b) => b.startTime - a.startTime);
loadedItemsArray.sort(function(a, b) b.download.startTime - a.download.startTime); downloadsArray.forEach(download => aView.onDownloadAdded(download, false));
loadedItemsArray.forEach(
function (dataItem) aView.onDataItemAdded(dataItem, false)
);
// Notify the view that all data is available unless loading is in progress. // Notify the view that all data is available unless loading is in progress.
if (!this._pendingStatement) { if (!this._pendingStatement) {
@ -1337,254 +1315,6 @@ XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
return new DownloadsDataCtor(false); return new DownloadsDataCtor(false);
}); });
////////////////////////////////////////////////////////////////////////////////
//// DownloadsDataItem
/**
* Represents a single item in the list of downloads. This object either wraps
* an existing nsIDownload from the Download Manager, or provides the same
* information read directly from the downloads database, with the possibility
* of querying the nsIDownload lazily, for performance reasons.
*
* @param aSource
* Object containing the data with which the item should be initialized.
* This should implement either nsIDownload or mozIStorageRow. If the
* JavaScript API for downloads is enabled, this is a Download object.
*/
function DownloadsDataItem(aSource)
{
this._initFromJSDownload(aSource);
}
DownloadsDataItem.prototype = {
get state() DownloadsCommon.stateOfDownload(this.download),
/**
* Initializes this object from the JavaScript API for downloads.
*
* The endTime property is initialized to the current date and time.
*
* @param aDownload
* The Download object with the current state.
*/
_initFromJSDownload: function (aDownload)
{
this.download = aDownload;
this.download.endTime = Date.now();
},
/**
* Initializes this object from a download object of the Download Manager.
*
* The endTime property is initialized to the current date and time.
*
* @param aDownload
* The nsIDownload with the current state.
*/
_initFromDownload: function DDI_initFromDownload(aDownload)
{
this._download = aDownload;
// Fetch all the download properties eagerly.
this.downloadGuid = aDownload.guid;
this.file = aDownload.target.spec;
this.target = aDownload.displayName;
this.uri = aDownload.source.spec;
this.referrer = aDownload.referrer && aDownload.referrer.spec;
this.state = aDownload.state;
this.startTime = Math.round(aDownload.startTime / 1000);
this.endTime = Date.now();
this.currBytes = aDownload.amountTransferred;
this.maxBytes = aDownload.size;
this.resumable = aDownload.resumable;
this.speed = aDownload.speed;
this.percentComplete = aDownload.percentComplete;
},
/**
* Initializes this object from a data row in the downloads database, without
* querying the associated nsIDownload object, to improve performance when
* loading the list of downloads asynchronously.
*
* When this object is initialized in this way, accessing the "download"
* property loads the underlying nsIDownload object synchronously, and should
* be avoided unless the object is really required.
*
* @param aStorageRow
* The mozIStorageRow from the downloads database.
*/
_initFromDataRow: function DDI_initFromDataRow(aStorageRow)
{
// Get the download properties from the data row.
this._download = null;
this.downloadGuid = aStorageRow.getResultByName("guid");
this.file = aStorageRow.getResultByName("target");
this.target = aStorageRow.getResultByName("name");
this.uri = aStorageRow.getResultByName("source");
this.referrer = aStorageRow.getResultByName("referrer");
this.state = aStorageRow.getResultByName("state");
this.startTime = Math.round(aStorageRow.getResultByName("startTime") / 1000);
this.endTime = Math.round(aStorageRow.getResultByName("endTime") / 1000);
this.currBytes = aStorageRow.getResultByName("currBytes");
this.maxBytes = aStorageRow.getResultByName("maxBytes");
// Now we have to determine if the download is resumable, but don't want to
// access the underlying download object unnecessarily. The only case where
// the property is relevant is when we are currently downloading data, and
// in this case the download object is already loaded in memory or will be
// loaded very soon in any case. In all the other cases, including a paused
// download, we assume that the download is resumable. The property will be
// updated as soon as the underlying download state changes.
// We'll start by assuming we're resumable, and then if we're downloading,
// update resumable property in case we were wrong.
this.resumable = true;
if (this.state == nsIDM.DOWNLOAD_DOWNLOADING) {
this.getDownload(function(aDownload) {
this.resumable = aDownload.resumable;
}.bind(this));
}
// Compute the other properties without accessing the download object.
this.speed = 0;
this.percentComplete = this.maxBytes <= 0
? -1
: Math.round(this.currBytes / this.maxBytes * 100);
},
/**
* Asynchronous getter for the download object corresponding to this data item.
*
* @param aCallback
* A callback function which will be called when the download object is
* available. It should accept one argument which will be the download
* object.
*/
getDownload: function DDI_getDownload(aCallback) {
if (this._download) {
// Return the download object asynchronously to the caller
let download = this._download;
Services.tm.mainThread.dispatch(function () aCallback(download),
Ci.nsIThread.DISPATCH_NORMAL);
} else {
Services.downloads.getDownloadByGUID(this.downloadGuid,
function(aStatus, aResult) {
if (!Components.isSuccessCode(aStatus)) {
Cu.reportError(
new Components.Exception("Cannot retrieve download for GUID: " +
this.downloadGuid));
} else {
this._download = aResult;
aCallback(aResult);
}
}.bind(this));
}
},
/**
* Indicates whether the download is proceeding normally, and not finished
* yet. This includes paused downloads. When this property is true, the
* "progress" property represents the current progress of the download.
*/
get inProgress()
{
return [
nsIDM.DOWNLOAD_NOTSTARTED,
nsIDM.DOWNLOAD_QUEUED,
nsIDM.DOWNLOAD_DOWNLOADING,
nsIDM.DOWNLOAD_PAUSED,
nsIDM.DOWNLOAD_SCANNING,
].indexOf(this.state) != -1;
},
/**
* This is true during the initial phases of a download, before the actual
* download of data bytes starts.
*/
get starting()
{
return this.state == nsIDM.DOWNLOAD_NOTSTARTED ||
this.state == nsIDM.DOWNLOAD_QUEUED;
},
/**
* Indicates whether the download is paused.
*/
get paused()
{
return this.state == nsIDM.DOWNLOAD_PAUSED;
},
/**
* Indicates whether the download is in a final state, either because it
* completed successfully or because it was blocked.
*/
get done()
{
return [
nsIDM.DOWNLOAD_FINISHED,
nsIDM.DOWNLOAD_BLOCKED_PARENTAL,
nsIDM.DOWNLOAD_BLOCKED_POLICY,
nsIDM.DOWNLOAD_DIRTY,
].indexOf(this.state) != -1;
},
/**
* Indicates whether the download stopped because of an error, and can be
* resumed manually.
*/
get canRetry()
{
return this.state == nsIDM.DOWNLOAD_CANCELED ||
this.state == nsIDM.DOWNLOAD_FAILED;
},
/**
* Returns the nsILocalFile for the download target.
*
* @throws if the native path is not valid. This can happen if the same
* profile is used on different platforms, for example if a native
* Windows path is stored and then the item is accessed on a Mac.
*
* @deprecated Callers should use OS.File and "download.target.path".
*/
get localFile()
{
// We should remove should use this.download.target.partFilePath and check asyncrhonously.
return new FileUtils.File(this.download.target.path);
},
/**
* Returns the nsILocalFile for the partially downloaded target.
*
* @throws if the native path is not valid. This can happen if the same
* profile is used on different platforms, for example if a native
* Windows path is stored and then the item is accessed on a Mac.
*
* @deprecated Callers should use OS.File and "download.target.partFilePath".
*/
get partFile()
{
return new FileUtils.File(this.download.target.path + kPartialDownloadSuffix);
},
/**
* 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) { }
}
};
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
//// DownloadsViewPrototype //// DownloadsViewPrototype
@ -1712,9 +1442,9 @@ const DownloadsViewPrototype = {
* Called when a new download data item is available, either during the * Called when a new download data item is available, either during the
* asynchronous data load or when a new download is started. * asynchronous data load or when a new download is started.
* *
* @param aDataItem * @param download
* DownloadsDataItem object that was just added. * Download object that was just added.
* @param aNewest * @param newest
* When true, indicates that this item is the most recent and should be * When true, indicates that this item is the most recent and should be
* added in the topmost position. This happens when a new download is * added in the topmost position. This happens when a new download is
* started. When false, indicates that the item is the least recent * started. When false, indicates that the item is the least recent
@ -1723,7 +1453,33 @@ const DownloadsViewPrototype = {
* *
* @note Subclasses should override this. * @note Subclasses should override this.
*/ */
onDataItemAdded: function DVP_onDataItemAdded(aDataItem, aNewest) onDownloadAdded(download, newest) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
/**
* Called when the overall state of a Download has changed. In particular,
* this is called only once when the download succeeds or is blocked
* permanently, and is never called if only the current progress changed.
*
* The onDownloadChanged notification will always be sent afterwards.
*
* @note Subclasses should override this.
*/
onDownloadStateChanged(download) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
/**
* Called every time any state property of a Download may have changed,
* including progress properties.
*
* Note that progress notification changes are throttled at the Downloads.jsm
* API level, and there is no throttling mechanism in the front-end.
*
* @note Subclasses should override this.
*/
onDownloadChanged(download) {
{ {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED; throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}, },
@ -1732,37 +1488,12 @@ const DownloadsViewPrototype = {
* Called when a data item is removed, ensures that the widget associated with * Called when a data item is removed, ensures that the widget associated with
* the view item is removed from the user interface. * the view item is removed from the user interface.
* *
* @param aDataItem * @param download
* DownloadsDataItem object that is being removed. * Download object that is being removed.
* *
* @note Subclasses should override this. * @note Subclasses should override this.
*/ */
onDataItemRemoved: function DVP_onDataItemRemoved(aDataItem) onDownloadRemoved(download) {
{
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
/**
* Called when the "state" property of a DownloadsDataItem has changed.
*
* The onDataItemChanged notification will be sent afterwards.
*
* @note Subclasses should override this.
*/
onDataItemStateChanged(aDataItem) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
/**
* Called every time any state property of a DownloadsDataItem may have
* changed, including progress properties and the "state" property.
*
* Note that progress notification changes are throttled at the Downloads.jsm
* API level, and there is no throttling mechanism in the front-end.
*
* @note Subclasses should override this.
*/
onDataItemChanged(aDataItem) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED; throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}, },
@ -1826,9 +1557,6 @@ DownloadsIndicatorDataCtor.prototype = {
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
//// Callback functions from DownloadsData //// Callback functions from DownloadsData
/**
* Called after data loading finished.
*/
onDataLoadCompleted: function DID_onDataLoadCompleted() onDataLoadCompleted: function DID_onDataLoadCompleted()
{ {
DownloadsViewPrototype.onDataLoadCompleted.call(this); DownloadsViewPrototype.onDataLoadCompleted.call(this);
@ -1845,42 +1573,13 @@ DownloadsIndicatorDataCtor.prototype = {
this._itemCount = 0; this._itemCount = 0;
}, },
/** onDownloadAdded(download, newest) {
* Called when a new download data item is available, either during the
* asynchronous data load or when a new download is started.
*
* @param aDataItem
* DownloadsDataItem object that was just added.
* @param aNewest
* When true, indicates that this item is the most recent and should be
* added in the topmost position. This happens when a new download is
* started. When false, indicates that the item is the least recent
* with regard to the items that have been already added. The latter
* generally happens during the asynchronous data load.
*/
onDataItemAdded: function DID_onDataItemAdded(aDataItem, aNewest)
{ {
this._itemCount++; this._itemCount++;
this._updateViews(); this._updateViews();
}, },
/** onDownloadStateChanged(download) {
* Called when a data item is removed, ensures that the widget associated with
* the view item is removed from the user interface.
*
* @param aDataItem
* DownloadsDataItem object that is being removed.
*/
onDataItemRemoved: function DID_onDataItemRemoved(aDataItem)
{
this._itemCount--;
this._updateViews();
},
// DownloadsView
onDataItemStateChanged(aDataItem) {
let download = aDataItem.download;
if (download.succeeded || download.error) { if (download.succeeded || download.error) {
this.attention = true; this.attention = true;
} }
@ -1890,8 +1589,12 @@ DownloadsIndicatorDataCtor.prototype = {
this._lastTimeLeft = -1; this._lastTimeLeft = -1;
}, },
// DownloadsView onDownloadChanged(download) {
onDataItemChanged() { this._updateViews();
},
onDownloadRemoved(download) {
this._itemCount--;
this._updateViews(); this._updateViews();
}, },
@ -1983,22 +1686,17 @@ DownloadsIndicatorDataCtor.prototype = {
_lastTimeLeft: -1, _lastTimeLeft: -1,
/** /**
* A generator function for the dataItems that this summary is currently * A generator function for the Download objects this summary is currently
* interested in. This generator is passed off to summarizeDownloads in order * interested in. This generator is passed off to summarizeDownloads in order
* to generate statistics about the dataItems we care about - in this case, * to generate statistics about the downloads we care about - in this case,
* it's all dataItems for active downloads. * it's all active downloads.
*/ */
_activeDataItems: function DID_activeDataItems() _activeDownloads() {
{ let downloads = this._isPrivate ? PrivateDownloadsData.downloads
let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems : DownloadsData.downloads;
: DownloadsData.dataItems; for (let download of downloads) {
for (let dataItem of dataItems) {
if (!dataItem) {
continue;
}
let download = dataItem.download;
if (!download.stopped || (download.canceled && download.hasPartialData)) { if (!download.stopped || (download.canceled && download.hasPartialData)) {
yield dataItem; yield download;
} }
} }
}, },
@ -2009,7 +1707,7 @@ DownloadsIndicatorDataCtor.prototype = {
_refreshProperties: function DID_refreshProperties() _refreshProperties: function DID_refreshProperties()
{ {
let summary = let summary =
DownloadsCommon.summarizeDownloads(this._activeDataItems()); DownloadsCommon.summarizeDownloads(this._activeDownloads());
// Determine if the indicator should be shown or get attention. // Determine if the indicator should be shown or get attention.
this._hasDownloads = (this._itemCount > 0); this._hasDownloads = (this._itemCount > 0);
@ -2070,7 +1768,7 @@ function DownloadsSummaryData(aIsPrivate, aNumToExclude) {
// completely separated from one another. // completely separated from one another.
this._loading = false; this._loading = false;
this._dataItems = []; this._downloads = [];
// Floating point value indicating the last number of seconds estimated until // Floating point value indicating the last number of seconds estimated until
// the longest download will finish. We need to store this value so that we // the longest download will finish. We need to store this value so that we
@ -2110,9 +1808,9 @@ DownloadsSummaryData.prototype = {
DownloadsViewPrototype.removeView.call(this, aView); DownloadsViewPrototype.removeView.call(this, aView);
if (this._views.length == 0) { if (this._views.length == 0) {
// Clear out our collection of DownloadDataItems. If we ever have // Clear out our collection of Download objects. If we ever have
// another view registered with us, this will get re-populated. // another view registered with us, this will get re-populated.
this._dataItems = []; this._downloads = [];
} }
}, },
@ -2132,33 +1830,29 @@ DownloadsSummaryData.prototype = {
this._dataItems = []; this._dataItems = [];
}, },
onDataItemAdded: function DSD_onDataItemAdded(aDataItem, aNewest) onDownloadAdded(download, newest) {
{ if (newest) {
if (aNewest) { this._downloads.unshift(download);
this._dataItems.unshift(aDataItem);
} else { } else {
this._dataItems.push(aDataItem); this._downloads.push(download);
} }
this._updateViews(); this._updateViews();
}, },
onDataItemRemoved: function DSD_onDataItemRemoved(aDataItem) onDownloadStateChanged() {
{
let itemIndex = this._dataItems.indexOf(aDataItem);
this._dataItems.splice(itemIndex, 1);
this._updateViews();
},
// DownloadsView
onDataItemStateChanged() {
// Since the state of a download changed, reset the estimated time left. // Since the state of a download changed, reset the estimated time left.
this._lastRawTimeLeft = -1; this._lastRawTimeLeft = -1;
this._lastTimeLeft = -1; this._lastTimeLeft = -1;
}, },
// DownloadsView onDownloadChanged() {
onDataItemChanged() { this._updateViews();
},
onDownloadRemoved(download) {
let itemIndex = this._downloads.indexOf(download);
this._downloads.splice(itemIndex, 1);
this._updateViews(); this._updateViews();
}, },
@ -2197,17 +1891,16 @@ DownloadsSummaryData.prototype = {
//// Property updating based on current download status //// Property updating based on current download status
/** /**
* A generator function for the dataItems that this summary is currently * A generator function for the Download objects this summary is currently
* interested in. This generator is passed off to summarizeDownloads in order * interested in. This generator is passed off to summarizeDownloads in order
* to generate statistics about the dataItems we care about - in this case, * to generate statistics about the downloads we care about - in this case,
* it's the dataItems in this._dataItems after the first few to exclude, * it's the downloads in this._downloads after the first few to exclude,
* which was set when constructing this DownloadsSummaryData instance. * which was set when constructing this DownloadsSummaryData instance.
*/ */
_dataItemsForSummary: function DSD_dataItemsForSummary() _downloadsForSummary() {
{ if (this._downloads.length > 0) {
if (this._dataItems.length > 0) { for (let i = this._numToExclude; i < this._downloads.length; ++i) {
for (let i = this._numToExclude; i < this._dataItems.length; ++i) { yield this._downloads[i];
yield this._dataItems[i];
} }
} }
}, },
@ -2219,7 +1912,7 @@ DownloadsSummaryData.prototype = {
{ {
// Pre-load summary with default values. // Pre-load summary with default values.
let summary = let summary =
DownloadsCommon.summarizeDownloads(this._dataItemsForSummary()); DownloadsCommon.summarizeDownloads(this._downloadsForSummary());
this._description = DownloadsCommon.strings this._description = DownloadsCommon.strings
.otherDownloads2(summary.numActive); .otherDownloads2(summary.numActive);

View file

@ -2,8 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file, * 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/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsDataItem",
"resource:///modules/DownloadsCommon.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm"); "resource:///modules/RecentWindow.jsm");
@ -127,21 +125,6 @@ HistoryDownload.prototype = {
}, },
}; };
/**
* Represents a download from the browser history. It uses the same interface as
* the DownloadsDataItem object.
*
* @param aPlacesNode
* The Places node for the history download.
*/
function DownloadsHistoryDataItem(aPlacesNode) {
this.download = new HistoryDownload(aPlacesNode);
}
DownloadsHistoryDataItem.prototype = {
__proto__: DownloadsDataItem.prototype,
};
/** /**
* A download element shell is responsible for handling the commands and the * A download element shell is responsible for handling the commands and the
* displayed data for a single download view element. * displayed data for a single download view element.
@ -157,23 +140,23 @@ DownloadsHistoryDataItem.prototype = {
* The caller is also responsible for forwarding status notifications for * The caller is also responsible for forwarding status notifications for
* session downloads, calling the onStateChanged and onChanged methods. * session downloads, calling the onStateChanged and onChanged methods.
* *
* @param [optional] aSessionDataItem * @param [optional] aSessionDownload
* The session download, required if aHistoryDataItem is not set. * The session download, required if aHistoryDownload is not set.
* @param [optional] aHistoryDataItem * @param [optional] aHistoryDownload
* The history download, required if aSessionDataItem is not set. * The history download, required if aSessionDownload is not set.
*/ */
function HistoryDownloadElementShell(aSessionDataItem, aHistoryDataItem) { function HistoryDownloadElementShell(aSessionDownload, aHistoryDownload) {
this.element = document.createElement("richlistitem"); this.element = document.createElement("richlistitem");
this.element._shell = this; this.element._shell = this;
this.element.classList.add("download"); this.element.classList.add("download");
this.element.classList.add("download-state"); this.element.classList.add("download-state");
if (aSessionDataItem) { if (aSessionDownload) {
this.sessionDataItem = aSessionDataItem; this.sessionDownload = aSessionDownload;
} }
if (aHistoryDataItem) { if (aHistoryDownload) {
this.historyDataItem = aHistoryDataItem; this.historyDownload = aHistoryDownload;
} }
} }
@ -196,20 +179,20 @@ HistoryDownloadElementShell.prototype = {
get active() !!this._active, get active() !!this._active,
/** /**
* DownloadsDataItem or DownloadsHistoryDataItem object to use for displaying * Overrides the base getter to return the Download or HistoryDownload object
* information and for executing commands in the user interface. * for displaying information and executing commands in the user interface.
*/ */
get dataItem() this._sessionDataItem || this._historyDataItem, get download() this._sessionDownload || this._historyDownload,
_sessionDataItem: null, _sessionDownload: null,
get sessionDataItem() this._sessionDataItem, get sessionDownload() this._sessionDownload,
set sessionDataItem(aValue) { set sessionDownload(aValue) {
if (this._sessionDataItem != aValue) { if (this._sessionDownload != aValue) {
if (!aValue && !this._historyDataItem) { if (!aValue && !this._historyDownload) {
throw new Error("Should always have either a dataItem or a historyDataItem"); throw new Error("Should always have either a Download or a HistoryDownload");
} }
this._sessionDataItem = aValue; this._sessionDownload = aValue;
this.ensureActive(); this.ensureActive();
this._updateUI(); this._updateUI();
@ -217,19 +200,19 @@ HistoryDownloadElementShell.prototype = {
return aValue; return aValue;
}, },
_historyDataItem: null, _historyDownload: null,
get historyDataItem() this._historyDataItem, get historyDownload() this._historyDownload,
set historyDataItem(aValue) { set historyDownload(aValue) {
if (this._historyDataItem != aValue) { if (this._historyDownload != aValue) {
if (!aValue && !this._sessionDataItem) { if (!aValue && !this._sessionDownload) {
throw new Error("Should always have either a dataItem or a historyDataItem"); throw new Error("Should always have either a Download or a HistoryDownload");
} }
this._historyDataItem = aValue; this._historyDownload = aValue;
// We don't need to update the UI if we had a session data item, because // We don't need to update the UI if we had a session data item, because
// the places information isn't used in this case. // the places information isn't used in this case.
if (!this._sessionDataItem) { if (!this._sessionDownload) {
this._updateUI(); this._updateUI();
} }
} }
@ -286,7 +269,7 @@ HistoryDownloadElementShell.prototype = {
// We cannot open a session download file unless it's succeeded. // We cannot open a session download file unless it's succeeded.
// If it's succeeded, we need to make sure the file was not removed, // If it's succeeded, we need to make sure the file was not removed,
// as we do for past downloads. // as we do for past downloads.
if (this._sessionDataItem && !this.download.succeeded) { if (this._sessionDownload && !this.download.succeeded) {
return false; return false;
} }
@ -299,7 +282,7 @@ HistoryDownloadElementShell.prototype = {
return this.download.succeeded; return this.download.succeeded;
case "downloadsCmd_show": case "downloadsCmd_show":
// TODO: Bug 827010 - Handle part-file asynchronously. // TODO: Bug 827010 - Handle part-file asynchronously.
if (this._sessionDataItem && this.download.target.partFilePath) { if (this._sessionDownload && this.download.target.partFilePath) {
let partFile = new FileUtils.File(this.download.target.partFilePath); let partFile = new FileUtils.File(this.download.target.partFilePath);
if (partFile.exists()) { if (partFile.exists()) {
return true; return true;
@ -323,7 +306,7 @@ HistoryDownloadElementShell.prototype = {
// We don't want in-progress downloads to be removed accidentally. // We don't want in-progress downloads to be removed accidentally.
return this.download.stopped; return this.download.stopped;
case "downloadsCmd_cancel": case "downloadsCmd_cancel":
return !!this._sessionDataItem; return !!this._sessionDownload;
} }
return false; return false;
}, },
@ -351,13 +334,13 @@ HistoryDownloadElementShell.prototype = {
break; break;
} }
case "cmd_delete": { case "cmd_delete": {
if (this._sessionDataItem) { if (this._sessionDownload) {
Downloads.getList(Downloads.ALL) Downloads.getList(Downloads.ALL)
.then(list => list.remove(this.download)) .then(list => list.remove(this.download))
.then(() => this.download.finalize(true)) .then(() => this.download.finalize(true))
.catch(Cu.reportError); .catch(Cu.reportError);
} }
if (this._historyDataItem) { if (this._historyDownload) {
let uri = NetUtil.newURI(this.download.source.url); let uri = NetUtil.newURI(this.download.source.url);
PlacesUtils.bhistory.removePage(uri); PlacesUtils.bhistory.removePage(uri);
} }
@ -480,7 +463,7 @@ function DownloadsPlacesView(aRichListBox, aActive = true) {
this._downloadElementsShellsForURI = new Map(); this._downloadElementsShellsForURI = new Map();
// Map download data items to their element shells. // Map download data items to their element shells.
this._viewItemsForDataItems = new WeakMap(); this._viewItemsForDownloads = new WeakMap();
// Points to the last session download element. We keep track of this // Points to the last session download element. We keep track of this
// in order to keep all session downloads above past downloads. // in order to keep all session downloads above past downloads.
@ -626,14 +609,12 @@ DownloadsPlacesView.prototype = {
* alongside the other session downloads. If we don't, then we go ahead * alongside the other session downloads. If we don't, then we go ahead
* and create a new element for the download. * and create a new element for the download.
* *
* @param aDataItem * @param [optional] sessionDownload
* The data item of a session download. Set to null for history * A Download object, or null for history downloads.
* downloads data.
* @param [optional] aPlacesNode * @param [optional] aPlacesNode
* The places node for a history download. Required if there's no data * The Places node for a history download, or null for session downloads.
* item.
* @param [optional] aNewest * @param [optional] aNewest
* @see onDataItemAdded. Ignored for history downloads. * @see onDownloadAdded. Ignored for history downloads.
* @param [optional] aDocumentFragment * @param [optional] aDocumentFragment
* To speed up the appending of multiple elements to the end of the * To speed up the appending of multiple elements to the end of the
* list which are coming in a single batch (i.e. invalidateContainer), * list which are coming in a single batch (i.e. invalidateContainer),
@ -641,10 +622,8 @@ DownloadsPlacesView.prototype = {
* be appended. It's the caller's job to ensure the fragment is merged * be appended. It's the caller's job to ensure the fragment is merged
* to the richlistbox at the end. * to the richlistbox at the end.
*/ */
_addDownloadData: _addDownloadData(sessionDownload, aPlacesNode, aNewest = false,
function DPV_addDownloadData(aDataItem, aPlacesNode, aNewest = false,
aDocumentFragment = null) { aDocumentFragment = null) {
let sessionDownload = aDataItem && aDataItem.download;
let downloadURI = aPlacesNode ? aPlacesNode.uri let downloadURI = aPlacesNode ? aPlacesNode.uri
: sessionDownload.source.url; : sessionDownload.source.url;
let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI); let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI);
@ -670,21 +649,21 @@ DownloadsPlacesView.prototype = {
// item). // item).
// //
// Note: If a cancelled session download is already in the list, and the // Note: If a cancelled session download is already in the list, and the
// download is retired, onDataItemAdded is called again for the same // download is retried, onDownloadAdded is called again for the same
// data item. Thus, we also check that we make sure we don't have a view item // data item. Thus, we also check that we make sure we don't have a view item
// already. // already.
if (!shouldCreateShell && if (!shouldCreateShell &&
aDataItem && !this._viewItemsForDataItems.has(aDataItem)) { sessionDownload && !this._viewItemsForDownloads.has(sessionDownload)) {
// If there's a past-download-only shell for this download-uri with no // 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 // associated data item, use it for the new data item. Otherwise, go ahead
// and create another shell. // and create another shell.
shouldCreateShell = true; shouldCreateShell = true;
for (let shell of shellsForURI) { for (let shell of shellsForURI) {
if (!shell.sessionDataItem) { if (!shell.sessionDownload) {
shouldCreateShell = false; shouldCreateShell = false;
shell.sessionDataItem = aDataItem; shell.sessionDownload = sessionDownload;
newOrUpdatedShell = shell; newOrUpdatedShell = shell;
this._viewItemsForDataItems.set(aDataItem, shell); this._viewItemsForDownloads.set(sessionDownload, shell);
break; break;
} }
} }
@ -694,18 +673,20 @@ DownloadsPlacesView.prototype = {
// If we are adding a new history download here, it means there is no // If we are adding a new history download here, it means there is no
// associated session download, thus we must read the Places metadata, // associated session download, thus we must read the Places metadata,
// because it will not be obscured by the session download. // because it will not be obscured by the session download.
let historyDataItem = null; let historyDownload = null;
if (aPlacesNode) { if (aPlacesNode) {
let metaData = this._getCachedPlacesMetaDataFor(aPlacesNode.uri); let metaData = this._getCachedPlacesMetaDataFor(aPlacesNode.uri);
historyDataItem = new DownloadsHistoryDataItem(aPlacesNode); historyDownload = new HistoryDownload(aPlacesNode);
historyDataItem.download.updateFromMetaData(metaData); historyDownload.updateFromMetaData(metaData);
} }
let shell = new HistoryDownloadElementShell(aDataItem, historyDataItem); let shell = new HistoryDownloadElementShell(sessionDownload,
historyDownload);
shell.element._placesNode = aPlacesNode; shell.element._placesNode = aPlacesNode;
newOrUpdatedShell = shell; newOrUpdatedShell = shell;
shellsForURI.add(shell); shellsForURI.add(shell);
if (aDataItem) if (sessionDownload) {
this._viewItemsForDataItems.set(aDataItem, shell); this._viewItemsForDownloads.set(sessionDownload, shell);
}
} }
else if (aPlacesNode) { else if (aPlacesNode) {
// We are updating information for a history download for which we have // We are updating information for a history download for which we have
@ -720,9 +701,9 @@ DownloadsPlacesView.prototype = {
// changed, just the reference to the Places node object is different. // changed, just the reference to the Places node object is different.
// So, we update all the node references and keep the metadata intact. // So, we update all the node references and keep the metadata intact.
for (let shell of shellsForURI) { for (let shell of shellsForURI) {
if (!shell.historyDataItem) { if (!shell.historyDownload) {
// Create the element to host the metadata when needed. // Create the element to host the metadata when needed.
shell.historyDataItem = new DownloadsHistoryDataItem(aPlacesNode); shell.historyDownload = new HistoryDownload(aPlacesNode);
} }
shell.element._placesNode = aPlacesNode; shell.element._placesNode = aPlacesNode;
} }
@ -740,7 +721,7 @@ DownloadsPlacesView.prototype = {
// More generally, if a new download is added, should be made visible. // More generally, if a new download is added, should be made visible.
this._richlistbox.ensureElementIsVisible(newOrUpdatedShell.element); this._richlistbox.ensureElementIsVisible(newOrUpdatedShell.element);
} }
else if (aDataItem) { } else if (sessionDownload) {
let before = this._lastSessionDownloadElement ? let before = this._lastSessionDownloadElement ?
this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild; this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild;
this._richlistbox.insertBefore(newOrUpdatedShell.element, before); this._richlistbox.insertBefore(newOrUpdatedShell.element, before);
@ -791,8 +772,8 @@ DownloadsPlacesView.prototype = {
let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI); let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI);
if (shellsForURI) { if (shellsForURI) {
for (let shell of shellsForURI) { for (let shell of shellsForURI) {
if (shell.sessionDataItem) { if (shell.sessionDownload) {
shell.historyDataItem = null; shell.historyDownload = null;
} }
else { else {
this._removeElement(shell.element); this._removeElement(shell.element);
@ -804,15 +785,13 @@ DownloadsPlacesView.prototype = {
} }
}, },
_removeSessionDownloadFromView: _removeSessionDownloadFromView(download) {
function DPV__removeSessionDownloadFromView(aDataItem) {
let download = aDataItem.download;
let shells = this._downloadElementsShellsForURI let shells = this._downloadElementsShellsForURI
.get(download.source.url); .get(download.source.url);
if (shells.size == 0) if (shells.size == 0)
throw new Error("Should have had at leaat one shell for this uri"); throw new Error("Should have had at leaat one shell for this uri");
let shell = this._viewItemsForDataItems.get(aDataItem); let shell = this._viewItemsForDownloads.get(download);
if (!shells.has(shell)) if (!shells.has(shell))
throw new Error("Missing download element shell in shells list for url"); throw new Error("Missing download element shell in shells list for url");
@ -820,7 +799,7 @@ DownloadsPlacesView.prototype = {
// view item for this this particular data item go away. // view item for this this particular data item go away.
// If there's only one item for this download uri, we should only // If there's only one item for this download uri, we should only
// keep it if it is associated with a history download. // keep it if it is associated with a history download.
if (shells.size > 1 || !shell.historyDataItem) { if (shells.size > 1 || !shell.historyDownload) {
this._removeElement(shell.element); this._removeElement(shell.element);
shells.delete(shell); shells.delete(shell);
if (shells.size == 0) if (shells.size == 0)
@ -832,10 +811,10 @@ DownloadsPlacesView.prototype = {
// Previously, we did not use the Places metadata because it was obscured // Previously, we did not use the Places metadata because it was obscured
// by the session download. Since this is no longer the case, we have to // by the session download. Since this is no longer the case, we have to
// read the latest metadata before removing the session download. // read the latest metadata before removing the session download.
let url = shell.historyDataItem.download.source.url; let url = shell.historyDownload.source.url;
let metaData = this._getPlacesMetaDataFor(url); let metaData = this._getPlacesMetaDataFor(url);
shell.historyDataItem.download.updateFromMetaData(metaData); shell.historyDownload.updateFromMetaData(metaData);
shell.sessionDataItem = null; shell.sessionDownload = null;
// Move it below the session-download items; // Move it below the session-download items;
if (this._lastSessionDownloadElement == shell.element) { if (this._lastSessionDownloadElement == shell.element) {
this._lastSessionDownloadElement = shell.element.previousSibling; this._lastSessionDownloadElement = shell.element.previousSibling;
@ -1108,22 +1087,20 @@ DownloadsPlacesView.prototype = {
this._ensureInitialSelection(); this._ensureInitialSelection();
}, },
onDataItemAdded: function DPV_onDataItemAdded(aDataItem, aNewest) { onDownloadAdded(download, newest) {
this._addDownloadData(aDataItem, null, aNewest); this._addDownloadData(download, null, newest);
}, },
onDataItemRemoved: function DPV_onDataItemRemoved(aDataItem) { onDownloadStateChanged(download) {
this._removeSessionDownloadFromView(aDataItem); this._viewItemsForDownloads.get(download).onStateChanged();
}, },
// DownloadsView onDownloadChanged(download) {
onDataItemStateChanged(aDataItem) { this._viewItemsForDownloads.get(download).onChanged();
this._viewItemsForDataItems.get(aDataItem).onStateChanged();
}, },
// DownloadsView onDownloadRemoved(download) {
onDataItemChanged(aDataItem) { this._removeSessionDownloadFromView(download);
this._viewItemsForDataItems.get(aDataItem).onChanged();
}, },
supportsCommand: function DPV_supportsCommand(aCommand) { supportsCommand: function DPV_supportsCommand(aCommand) {

View file

@ -687,15 +687,15 @@ const DownloadsView = {
loading: false, loading: false,
/** /**
* Ordered array of all DownloadsDataItem objects. We need to keep this array * Ordered array of all Download objects. We need to keep this array because
* because only a limited number of items are shown at once, and if an item * only a limited number of items are shown at once, and if an item that is
* that is currently visible is removed from the list, we might need to take * currently visible is removed from the list, we might need to take another
* another item from the array and make it appear at the bottom. * item from the array and make it appear at the bottom.
*/ */
_dataItems: [], _downloads: [],
/** /**
* Associates the visible DownloadsDataItem objects with their corresponding * Associates the visible Download objects with their corresponding
* DownloadsViewItem object. There is a limited number of view items in the * DownloadsViewItem object. There is a limited number of view items in the
* panel at any given time. * panel at any given time.
*/ */
@ -707,8 +707,8 @@ const DownloadsView = {
_itemCountChanged: function DV_itemCountChanged() _itemCountChanged: function DV_itemCountChanged()
{ {
DownloadsCommon.log("The downloads item count has changed - we are tracking", DownloadsCommon.log("The downloads item count has changed - we are tracking",
this._dataItems.length, "downloads in total."); this._downloads.length, "downloads in total.");
let count = this._dataItems.length; let count = this._downloads.length;
let hiddenCount = count - this.kItemCountLimit; let hiddenCount = count - this.kItemCountLimit;
if (count > 0) { if (count > 0) {
@ -797,8 +797,8 @@ const DownloadsView = {
* Called when a new download data item is available, either during the * Called when a new download data item is available, either during the
* asynchronous data load or when a new download is started. * asynchronous data load or when a new download is started.
* *
* @param aDataItem * @param aDownload
* DownloadsDataItem object that was just added. * Download object that was just added.
* @param aNewest * @param aNewest
* When true, indicates that this item is the most recent and should be * When true, indicates that this item is the most recent and should be
* added in the topmost position. This happens when a new download is * added in the topmost position. This happens when a new download is
@ -806,28 +806,28 @@ const DownloadsView = {
* and should be appended. The latter generally happens during the * and should be appended. The latter generally happens during the
* asynchronous data load. * asynchronous data load.
*/ */
onDataItemAdded: function DV_onDataItemAdded(aDataItem, aNewest) onDownloadAdded(download, aNewest) {
{ {
DownloadsCommon.log("A new download data item was added - aNewest =", DownloadsCommon.log("A new download data item was added - aNewest =",
aNewest); aNewest);
if (aNewest) { if (aNewest) {
this._dataItems.unshift(aDataItem); this._downloads.unshift(download);
} else { } else {
this._dataItems.push(aDataItem); this._downloads.push(download);
} }
let itemsNowOverflow = this._dataItems.length > this.kItemCountLimit; let itemsNowOverflow = this._downloads.length > this.kItemCountLimit;
if (aNewest || !itemsNowOverflow) { if (aNewest || !itemsNowOverflow) {
// The newly added item is visible in the panel and we must add the // The newly added item is visible in the panel and we must add the
// corresponding element. This is either because it is the first item, or // corresponding element. This is either because it is the first item, or
// because it was added at the bottom but the list still doesn't overflow. // because it was added at the bottom but the list still doesn't overflow.
this._addViewItem(aDataItem, aNewest); this._addViewItem(download, aNewest);
} }
if (aNewest && itemsNowOverflow) { if (aNewest && itemsNowOverflow) {
// If the list overflows, remove the last item from the panel to make room // If the list overflows, remove the last item from the panel to make room
// for the new one that we just added at the top. // for the new one that we just added at the top.
this._removeViewItem(this._dataItems[this.kItemCountLimit]); this._removeViewItem(this._downloads[this.kItemCountLimit]);
} }
// For better performance during batch loads, don't update the count for // For better performance during batch loads, don't update the count for
@ -837,48 +837,45 @@ const DownloadsView = {
} }
}, },
/** onDownloadStateChanged(download) {
* Called when a data item is removed. Ensures that the widget associated let viewItem = this._visibleViewItems.get(download);
* with the view item is removed from the user interface.
*
* @param aDataItem
* DownloadsDataItem object that is being removed.
*/
onDataItemRemoved: function DV_onDataItemRemoved(aDataItem)
{
DownloadsCommon.log("A download data item was removed.");
let itemIndex = this._dataItems.indexOf(aDataItem);
this._dataItems.splice(itemIndex, 1);
if (itemIndex < this.kItemCountLimit) {
// The item to remove is visible in the panel.
this._removeViewItem(aDataItem);
if (this._dataItems.length >= this.kItemCountLimit) {
// Reinsert the next item into the panel.
this._addViewItem(this._dataItems[this.kItemCountLimit - 1], false);
}
}
this._itemCountChanged();
},
// DownloadsView
onDataItemStateChanged(aDataItem) {
let viewItem = this._visibleViewItems.get(aDataItem);
if (viewItem) { if (viewItem) {
viewItem.onStateChanged(); viewItem.onStateChanged();
} }
}, },
// DownloadsView onDownloadChanged(download) {
onDataItemChanged(aDataItem) { let viewItem = this._visibleViewItems.get(download);
let viewItem = this._visibleViewItems.get(aDataItem);
if (viewItem) { if (viewItem) {
viewItem.onChanged(); viewItem.onChanged();
} }
}, },
/**
* Called when a data item is removed. Ensures that the widget associated
* with the view item is removed from the user interface.
*
* @param download
* Download object that is being removed.
*/
onDownloadRemoved(download) {
DownloadsCommon.log("A download data item was removed.");
let itemIndex = this._downloads.indexOf(download);
this._downloads.splice(itemIndex, 1);
if (itemIndex < this.kItemCountLimit) {
// The item to remove is visible in the panel.
this._removeViewItem(download);
if (this._downloads.length >= this.kItemCountLimit) {
// Reinsert the next item into the panel.
this._addViewItem(this._downloads[this.kItemCountLimit - 1], false);
}
}
this._itemCountChanged();
},
/** /**
* Associates each richlistitem for a download with its corresponding * Associates each richlistitem for a download with its corresponding
* DownloadsViewItemController object. * DownloadsViewItemController object.
@ -893,15 +890,15 @@ const DownloadsView = {
* Creates a new view item associated with the specified data item, and adds * Creates a new view item associated with the specified data item, and adds
* it to the top or the bottom of the list. * it to the top or the bottom of the list.
*/ */
_addViewItem: function DV_addViewItem(aDataItem, aNewest) _addViewItem(download, aNewest)
{ {
DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.", DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.",
"aNewest =", aNewest); "aNewest =", aNewest);
let element = document.createElement("richlistitem"); let element = document.createElement("richlistitem");
let viewItem = new DownloadsViewItem(aDataItem, element); let viewItem = new DownloadsViewItem(download, element);
this._visibleViewItems.set(aDataItem, viewItem); this._visibleViewItems.set(download, viewItem);
let viewItemController = new DownloadsViewItemController(aDataItem); let viewItemController = new DownloadsViewItemController(download);
this._controllersForElements.set(element, viewItemController); this._controllersForElements.set(element, viewItemController);
if (aNewest) { if (aNewest) {
this.richListBox.insertBefore(element, this.richListBox.firstChild); this.richListBox.insertBefore(element, this.richListBox.firstChild);
@ -913,17 +910,17 @@ const DownloadsView = {
/** /**
* Removes the view item associated with the specified data item. * Removes the view item associated with the specified data item.
*/ */
_removeViewItem: function DV_removeViewItem(aDataItem) _removeViewItem(download) {
{ {
DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list."); DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list.");
let element = this._visibleViewItems.get(aDataItem).element; let element = this._visibleViewItems.get(download).element;
let previousSelectedIndex = this.richListBox.selectedIndex; let previousSelectedIndex = this.richListBox.selectedIndex;
this.richListBox.removeChild(element); this.richListBox.removeChild(element);
if (previousSelectedIndex != -1) { if (previousSelectedIndex != -1) {
this.richListBox.selectedIndex = Math.min(previousSelectedIndex, this.richListBox.selectedIndex = Math.min(previousSelectedIndex,
this.richListBox.itemCount - 1); this.richListBox.itemCount - 1);
} }
this._visibleViewItems.delete(aDataItem); this._visibleViewItems.delete(download);
this._controllersForElements.delete(element); this._controllersForElements.delete(element);
}, },
@ -1053,14 +1050,13 @@ XPCOMUtils.defineConstant(this, "DownloadsView", DownloadsView);
* Builds and updates a single item in the downloads list widget, responding to * Builds and updates a single item in the downloads list widget, responding to
* changes in the download state and real-time data. * changes in the download state and real-time data.
* *
* @param aDataItem * @param download
* DownloadsDataItem to be associated with the view item. * Download object to be associated with the view item.
* @param aElement * @param aElement
* XUL element corresponding to the single download item in the view. * XUL element corresponding to the single download item in the view.
*/ */
function DownloadsViewItem(aDataItem, aElement) function DownloadsViewItem(download, aElement) {
{ this.download = download;
this.dataItem = aDataItem;
this.element = aElement; this.element = aElement;
this.element._shell = this; this.element._shell = this;
@ -1075,11 +1071,6 @@ function DownloadsViewItem(aDataItem, aElement)
DownloadsViewItem.prototype = { DownloadsViewItem.prototype = {
__proto__: DownloadElementShell.prototype, __proto__: DownloadElementShell.prototype,
/**
* The DownloadDataItem associated with this view item.
*/
dataItem: null,
/** /**
* The XUL element corresponding to the associated richlistbox item. * The XUL element corresponding to the associated richlistbox item.
*/ */
@ -1233,21 +1224,11 @@ XPCOMUtils.defineConstant(this, "DownloadsViewController", DownloadsViewControll
* Handles all the user interaction events, in particular the "commands", * Handles all the user interaction events, in particular the "commands",
* related to a single item in the downloads list widgets. * related to a single item in the downloads list widgets.
*/ */
function DownloadsViewItemController(aDataItem) { function DownloadsViewItemController(download) {
this.dataItem = aDataItem; this.download = download;
} }
DownloadsViewItemController.prototype = { DownloadsViewItemController.prototype = {
//////////////////////////////////////////////////////////////////////////////
//// Command dispatching
/**
* The DownloadDataItem controlled by this object.
*/
dataItem: null,
get download() this.dataItem.download,
isCommandEnabled: function DVIC_isCommandEnabled(aCommand) isCommandEnabled: function DVIC_isCommandEnabled(aCommand)
{ {
switch (aCommand) { switch (aCommand) {

View file

@ -45,7 +45,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
* from the JavaScript API for downloads, and commands are executed using a * from the JavaScript API for downloads, and commands are executed using a
* combination of Download methods and DownloadsCommon.jsm helper functions. * combination of Download methods and DownloadsCommon.jsm helper functions.
* *
* Specialized versions of this shell must be defined, currently they are the * Specialized versions of this shell must be defined, and they are required to
* implement the "download" property or getter. Currently these objects are the
* HistoryDownloadElementShell and the DownloadsViewItem for the panel. The * HistoryDownloadElementShell and the DownloadsViewItem for the panel. The
* history view may use a HistoryDownload object in place of a Download object. * history view may use a HistoryDownload object in place of a Download object.
*/ */
@ -57,17 +58,6 @@ DownloadElementShell.prototype = {
*/ */
element: null, element: null,
/**
* The DownloadsDataItem for the download, overridden by the derived object.
*/
dataItem: null,
/**
* Download or HistoryDownload object to use for displaying information and
* for executing commands in the user interface.
*/
get download() this.dataItem.download,
/** /**
* URI string for the file type icon displayed in the download element. * URI string for the file type icon displayed in the download element.
*/ */