Bug 1749784 - Overhaul DownloadsIndicatorDataCtor to track each download's attention state individually. r=Gijs

Differential Revision: https://phabricator.services.mozilla.com/D135857
This commit is contained in:
Shane Hughes
2022-01-21 10:49:43 +00:00
parent 10144f9900
commit e85e908b7d
4 changed files with 125 additions and 56 deletions

View File

@@ -158,6 +158,12 @@ var DownloadsCommon = {
ATTENTION_WARNING: "warning", ATTENTION_WARNING: "warning",
ATTENTION_SEVERE: "severe", ATTENTION_SEVERE: "severe",
// Bit flags for the attentionSuppressed property.
SUPPRESS_NONE: 0,
SUPPRESS_PANEL_OPEN: 1,
SUPPRESS_ALL_DOWNLOADS_OPEN: 2,
SUPPRESS_CONTENT_AREA_DOWNLOADS_OPEN: 4,
/** /**
* Returns an object whose keys are the string names from the downloads string * Returns an object whose keys are the string names from the downloads string
* bundle, and whose values are either the translated strings or functions * bundle, and whose values are either the translated strings or functions
@@ -792,7 +798,7 @@ function DownloadsDataCtor({ isPrivate, isHistory, maxHistoryResults } = {}) {
this._isPrivate = !!isPrivate; this._isPrivate = !!isPrivate;
// Contains all the available Download objects and their integer state. // Contains all the available Download objects and their integer state.
this.oldDownloadStates = new Map(); this._oldDownloadStates = new WeakMap();
// For the history downloads list we don't need to register this as a view, // For the history downloads list we don't need to register this as a view,
// but we have to ensure that the DownloadsData object is initialized before // but we have to ensure that the DownloadsData object is initialized before
@@ -844,15 +850,15 @@ DownloadsDataCtor.prototype = {
* Iterator for all the available Download objects. This is empty until the * Iterator for all the available Download objects. This is empty until the
* data has been loaded using the JavaScript API for downloads. * data has been loaded using the JavaScript API for downloads.
*/ */
get downloads() { get _downloads() {
return this.oldDownloadStates.keys(); return ChromeUtils.nondeterministicGetWeakMapKeys(this._oldDownloadStates);
}, },
/** /**
* 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 download of this.downloads) { for (let download of this._downloads) {
// 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;
@@ -869,10 +875,6 @@ DownloadsDataCtor.prototype = {
Downloads.getList(this._isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC) Downloads.getList(this._isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC)
.then(list => list.removeFinished()) .then(list => list.removeFinished())
.catch(Cu.reportError); .catch(Cu.reportError);
let indicatorData = this._isPrivate
? PrivateDownloadsIndicatorData
: DownloadsIndicatorData;
indicatorData.attention = DownloadsCommon.ATTENTION_NONE;
}, },
// Integration with the asynchronous Downloads back-end // Integration with the asynchronous Downloads back-end
@@ -884,7 +886,7 @@ DownloadsDataCtor.prototype = {
// for which the end time is stored differently, as a Places annotation. // for which the end time is stored differently, as a Places annotation.
download.endTime = Date.now(); download.endTime = Date.now();
this.oldDownloadStates.set( this._oldDownloadStates.set(
download, download,
DownloadsCommon.stateOfDownload(download) DownloadsCommon.stateOfDownload(download)
); );
@@ -894,9 +896,9 @@ DownloadsDataCtor.prototype = {
}, },
onDownloadChanged(download) { onDownloadChanged(download) {
let oldState = this.oldDownloadStates.get(download); let oldState = this._oldDownloadStates.get(download);
let newState = DownloadsCommon.stateOfDownload(download); let newState = DownloadsCommon.stateOfDownload(download);
this.oldDownloadStates.set(download, newState); this._oldDownloadStates.set(download, newState);
if (oldState != newState) { if (oldState != newState) {
if ( if (
@@ -927,7 +929,7 @@ DownloadsDataCtor.prototype = {
}, },
onDownloadRemoved(download) { onDownloadRemoved(download) {
this.oldDownloadStates.delete(download); this._oldDownloadStates.delete(download);
}, },
// Registration of views // Registration of views
@@ -999,7 +1001,7 @@ DownloadsDataCtor.prototype = {
Services.prefs.getBoolPref( Services.prefs.getBoolPref(
"browser.download.improvements_to_download_panel" "browser.download.improvements_to_download_panel"
) && ) &&
DownloadsCommon.summarizeDownloads(this.downloads).numDownloading <= 1 && DownloadsCommon.summarizeDownloads(this._downloads).numDownloading <= 1 &&
gAlwaysOpenPanel; gAlwaysOpenPanel;
if ( if (
@@ -1219,7 +1221,7 @@ const DownloadsViewPrototype = {
* @note Subclasses should override this. * @note Subclasses should override this.
*/ */
onDownloadRemoved(download) { onDownloadRemoved(download) {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); this._oldDownloadStates.delete(download);
}, },
/** /**
@@ -1275,6 +1277,27 @@ function DownloadsIndicatorDataCtor(aPrivate) {
DownloadsIndicatorDataCtor.prototype = { DownloadsIndicatorDataCtor.prototype = {
__proto__: DownloadsViewPrototype, __proto__: DownloadsViewPrototype,
/**
* Map of the relative severities of different attention states.
* Used in sorting the map of active downloads' attention states
* to determine the attention state to be displayed.
*/
_attentionPriority: new Map([
[DownloadsCommon.ATTENTION_NONE, 0],
[DownloadsCommon.ATTENTION_SUCCESS, 1],
[DownloadsCommon.ATTENTION_INFO, 2],
[DownloadsCommon.ATTENTION_WARNING, 3],
[DownloadsCommon.ATTENTION_SEVERE, 4],
]),
/**
* Iterator for all the available Download objects. This is empty until the
* data has been loaded using the JavaScript API for downloads.
*/
get _downloads() {
return ChromeUtils.nondeterministicGetWeakMapKeys(this._oldDownloadStates);
},
/** /**
* Removes an object previously added using addView. * Removes an object previously added using addView.
* *
@@ -1296,6 +1319,10 @@ DownloadsIndicatorDataCtor.prototype = {
}, },
onDownloadStateChanged(download) { onDownloadStateChanged(download) {
if (this._attentionSuppressed !== DownloadsCommon.SUPPRESS_NONE) {
return;
}
let attention;
if ( if (
!download.succeeded && !download.succeeded &&
download.error && download.error &&
@@ -1303,46 +1330,30 @@ DownloadsIndicatorDataCtor.prototype = {
) { ) {
switch (download.error.reputationCheckVerdict) { switch (download.error.reputationCheckVerdict) {
case Downloads.Error.BLOCK_VERDICT_UNCOMMON: case Downloads.Error.BLOCK_VERDICT_UNCOMMON:
// Existing higher level attention indication trumps ATTENTION_INFO. attention = DownloadsCommon.ATTENTION_INFO;
if (
this._attention != DownloadsCommon.ATTENTION_SEVERE &&
this._attention != DownloadsCommon.ATTENTION_WARNING
) {
this.attention = DownloadsCommon.ATTENTION_INFO;
}
break; break;
case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: // fall-through case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: // fall-through
case Downloads.Error.BLOCK_VERDICT_INSECURE: case Downloads.Error.BLOCK_VERDICT_INSECURE:
case Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM: case Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM:
// Existing higher level attention indication trumps ATTENTION_WARNING. attention = DownloadsCommon.ATTENTION_WARNING;
if (this._attention != DownloadsCommon.ATTENTION_SEVERE) {
this.attention = DownloadsCommon.ATTENTION_WARNING;
}
break; break;
case Downloads.Error.BLOCK_VERDICT_MALWARE: case Downloads.Error.BLOCK_VERDICT_MALWARE:
this.attention = DownloadsCommon.ATTENTION_SEVERE; attention = DownloadsCommon.ATTENTION_SEVERE;
break; break;
default: default:
this.attention = DownloadsCommon.ATTENTION_SEVERE; attention = DownloadsCommon.ATTENTION_SEVERE;
Cu.reportError( Cu.reportError(
"Unknown reputation verdict: " + "Unknown reputation verdict: " +
download.error.reputationCheckVerdict download.error.reputationCheckVerdict
); );
} }
} else if (download.succeeded) { } else if (download.succeeded) {
// Existing higher level attention indication trumps ATTENTION_SUCCESS. attention = DownloadsCommon.ATTENTION_SUCCESS;
if (
this._attention != DownloadsCommon.ATTENTION_SEVERE &&
this._attention != DownloadsCommon.ATTENTION_WARNING
) {
this.attention = DownloadsCommon.ATTENTION_SUCCESS;
}
} else if (download.error) { } else if (download.error) {
// Existing higher level attention indication trumps ATTENTION_WARNING. attention = DownloadsCommon.ATTENTION_WARNING;
if (this._attention != DownloadsCommon.ATTENTION_SEVERE) {
this.attention = DownloadsCommon.ATTENTION_WARNING;
}
} }
download.attention = attention;
this.updateAttention();
}, },
onDownloadChanged(download) { onDownloadChanged(download) {
@@ -1351,7 +1362,9 @@ DownloadsIndicatorDataCtor.prototype = {
}, },
onDownloadRemoved(download) { onDownloadRemoved(download) {
DownloadsViewPrototype.onDownloadRemoved.call(this, download);
this._itemCount--; this._itemCount--;
this.updateAttention();
this._updateViews(); this._updateViews();
}, },
@@ -1375,12 +1388,37 @@ DownloadsIndicatorDataCtor.prototype = {
* Indicates whether the user is interacting with downloads, thus the * Indicates whether the user is interacting with downloads, thus the
* attention indication should not be shown even if requested. * attention indication should not be shown even if requested.
*/ */
set attentionSuppressed(aValue) { set attentionSuppressed(aFlags) {
this._attentionSuppressed = aValue; this._attentionSuppressed = aFlags;
this._attention = DownloadsCommon.ATTENTION_NONE; if (aFlags !== DownloadsCommon.SUPPRESS_NONE) {
this._updateViews(); for (let download of this._downloads) {
download.attention = DownloadsCommon.ATTENTION_NONE;
}
this.attention = DownloadsCommon.ATTENTION_NONE;
}
},
get attentionSuppressed() {
return this._attentionSuppressed;
},
_attentionSuppressed: DownloadsCommon.SUPPRESS_NONE,
/**
* Set the indicator's attention to the most severe attention state among the
* unseen displayed downloads, or DownloadsCommon.ATTENTION_NONE if empty.
*/
updateAttention() {
let currentAttention = DownloadsCommon.ATTENTION_NONE;
let currentPriority = 0;
for (let download of this._downloads) {
let { attention } = download;
let priority = this._attentionPriority.get(attention);
if (priority > currentPriority) {
currentPriority = priority;
currentAttention = attention;
}
}
this.attention = currentAttention;
}, },
_attentionSuppressed: false,
/** /**
* Updates the specified view with the current aggregate values. * Updates the specified view with the current aggregate values.
@@ -1391,9 +1429,10 @@ DownloadsIndicatorDataCtor.prototype = {
_updateView(aView) { _updateView(aView) {
aView.hasDownloads = this._hasDownloads; aView.hasDownloads = this._hasDownloads;
aView.percentComplete = this._percentComplete; aView.percentComplete = this._percentComplete;
aView.attention = this._attentionSuppressed aView.attention =
? DownloadsCommon.ATTENTION_NONE this.attentionSuppressed !== DownloadsCommon.SUPPRESS_NONE
: this._attention; ? DownloadsCommon.ATTENTION_NONE
: this._attention;
}, },
// Property updating based on current download status // Property updating based on current download status
@@ -1411,8 +1450,8 @@ DownloadsIndicatorDataCtor.prototype = {
*/ */
*_activeDownloads() { *_activeDownloads() {
let downloads = this._isPrivate let downloads = this._isPrivate
? PrivateDownloadsData.downloads ? PrivateDownloadsData._downloads
: DownloadsData.downloads; : DownloadsData._downloads;
for (let download of downloads) { for (let download of downloads) {
if (!download.stopped || (download.canceled && download.hasPartialData)) { if (!download.stopped || (download.canceled && download.hasPartialData)) {
yield download; yield download;
@@ -1535,6 +1574,7 @@ DownloadsSummaryData.prototype = {
}, },
onDownloadRemoved(download) { onDownloadRemoved(download) {
DownloadsViewPrototype.onDownloadRemoved.call(this, download);
let itemIndex = this._downloads.indexOf(download); let itemIndex = this._downloads.indexOf(download);
this._downloads.splice(itemIndex, 1); this._downloads.splice(itemIndex, 1);
this._updateViews(); this._updateViews();

View File

@@ -208,7 +208,11 @@ var DownloadsView = {
* as they exist they "collapses" their history "counterpart" (So we don't show two * as they exist they "collapses" their history "counterpart" (So we don't show two
* items for every download). * items for every download).
*/ */
function DownloadsPlacesView(aRichListBox, aActive = true) { function DownloadsPlacesView(
aRichListBox,
aActive = true,
aSuppressionFlag = DownloadsCommon.SUPPRESS_ALL_DOWNLOADS_OPEN
) {
this._richlistbox = aRichListBox; this._richlistbox = aRichListBox;
this._richlistbox._placesView = this; this._richlistbox._placesView = this;
window.controllers.insertControllerAt(0, this); window.controllers.insertControllerAt(0, this);
@@ -227,16 +231,23 @@ function DownloadsPlacesView(aRichListBox, aActive = true) {
this._waitingForInitialData = true; this._waitingForInitialData = true;
this._downloadsData.addView(this); this._downloadsData.addView(this);
// Get the Download button out of the attention state since we're about to // Pause the download indicator as user is interacting with downloads. This is
// view all downloads. // skipped on about:downloads because it handles this by itself.
DownloadsCommon.getIndicatorData(window).attention = if (aSuppressionFlag === DownloadsCommon.SUPPRESS_ALL_DOWNLOADS_OPEN) {
DownloadsCommon.ATTENTION_NONE; DownloadsCommon.getIndicatorData(
window
).attentionSuppressed |= aSuppressionFlag;
}
// Make sure to unregister the view if the window is closed. // Make sure to unregister the view if the window is closed.
window.addEventListener( window.addEventListener(
"unload", "unload",
() => { () => {
window.controllers.removeController(this); window.controllers.removeController(this);
// Unpause the main window's download indicator.
DownloadsCommon.getIndicatorData(
window
).attentionSuppressed &= ~aSuppressionFlag;
this._downloadsData.removeView(this); this._downloadsData.removeView(this);
this.result = null; this.result = null;
}, },

View File

@@ -11,6 +11,7 @@ const { PrivateBrowsingUtils } = ChromeUtils.import(
var ContentAreaDownloadsView = { var ContentAreaDownloadsView = {
init() { init() {
let box = document.getElementById("downloadsListBox"); let box = document.getElementById("downloadsListBox");
let suppressionFlag = DownloadsCommon.SUPPRESS_CONTENT_AREA_DOWNLOADS_OPEN;
box.addEventListener( box.addEventListener(
"InitialDownloadsLoaded", "InitialDownloadsLoaded",
() => { () => {
@@ -19,10 +20,24 @@ var ContentAreaDownloadsView = {
document document
.getElementById("downloadsListBox") .getElementById("downloadsListBox")
.focus({ preventFocusRing: true }); .focus({ preventFocusRing: true });
// Pause the indicator if the browser is active.
if (document.visibilityState === "visible") {
DownloadsCommon.getIndicatorData(
window
).attentionSuppressed |= suppressionFlag;
}
}, },
{ once: true } { once: true }
); );
let view = new DownloadsPlacesView(box); let view = new DownloadsPlacesView(box, true, suppressionFlag);
document.addEventListener("visibilitychange", aEvent => {
let indicator = DownloadsCommon.getIndicatorData(window);
if (document.visibilityState === "visible") {
indicator.attentionSuppressed |= suppressionFlag;
} else {
indicator.attentionSuppressed &= ~suppressionFlag;
}
});
// Do not display the Places downloads in private windows // Do not display the Places downloads in private windows
if (!PrivateBrowsingUtils.isContentWindowPrivate(window)) { if (!PrivateBrowsingUtils.isContentWindowPrivate(window)) {
view.place = "place:transition=7&sort=4"; view.place = "place:transition=7&sort=4";

View File

@@ -369,7 +369,8 @@ var DownloadsPanel = {
this._state = this.kStateShown; this._state = this.kStateShown;
// Since at most one popup is open at any given time, we can set globally. // Since at most one popup is open at any given time, we can set globally.
DownloadsCommon.getIndicatorData(window).attentionSuppressed = true; DownloadsCommon.getIndicatorData(window).attentionSuppressed |=
DownloadsCommon.SUPPRESS_PANEL_OPEN;
// Ensure that the first item is selected when the panel is focused. // Ensure that the first item is selected when the panel is focused.
if (DownloadsView.richListBox.itemCount > 0) { if (DownloadsView.richListBox.itemCount > 0) {
@@ -399,7 +400,9 @@ var DownloadsPanel = {
this.keyFocusing = false; this.keyFocusing = false;
// Since at most one popup is open at any given time, we can set globally. // Since at most one popup is open at any given time, we can set globally.
DownloadsCommon.getIndicatorData(window).attentionSuppressed = false; DownloadsCommon.getIndicatorData(
window
).attentionSuppressed &= ~DownloadsCommon.SUPPRESS_PANEL_OPEN;
// Allow the anchor to be hidden. // Allow the anchor to be hidden.
DownloadsButton.releaseAnchor(); DownloadsButton.releaseAnchor();