From 72c3bed501ddc949630345281e2222e6c3277541 Mon Sep 17 00:00:00 2001 From: roytam1 Date: Sat, 30 Dec 2017 23:06:32 +0800 Subject: [PATCH] modules: update git folder contents with KM76RC2 --- k-meleon/modules/DownloadsCommon.jsm | 1461 ++++++++++++++++++++++++++ k-meleon/modules/DownloadsLogger.jsm | 75 ++ k-meleon/modules/DownloadsViewUI.jsm | 232 ++++ k-meleon/modules/KMeleon.jsm | 69 -- 4 files changed, 1768 insertions(+), 69 deletions(-) create mode 100644 k-meleon/modules/DownloadsCommon.jsm create mode 100644 k-meleon/modules/DownloadsLogger.jsm create mode 100644 k-meleon/modules/DownloadsViewUI.jsm diff --git a/k-meleon/modules/DownloadsCommon.jsm b/k-meleon/modules/DownloadsCommon.jsm new file mode 100644 index 00000000..f658db6b --- /dev/null +++ b/k-meleon/modules/DownloadsCommon.jsm @@ -0,0 +1,1461 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "DownloadsCommon", +]; + +/** + * Handles the Downloads panel shared methods and data access. + * + * This file includes the following constructors and global objects: + * + * DownloadsCommon + * This object is exposed directly to the consumers of this JavaScript module, + * and provides shared methods for all the instances of the user interface. + * + * DownloadsData + * Retrieves the list of past and completed downloads from the underlying + * Downloads API data, and provides asynchronous notifications allowing + * to build a consistent view of the available data. + * + * DownloadsIndicatorData + * This object registers itself with DownloadsData as a view, and transforms the + * notifications it receives into overall status data, that is then broadcast to + * the registered download status indicators. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper", + "resource://gre/modules/DownloadUIHelper.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", + "resource://gre/modules/DownloadUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm") +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", + "resource:///modules/RecentWindow.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger", + "resource:///modules/DownloadsLogger.jsm"); + +const nsIDM = Ci.nsIDownloadManager; + +const kDownloadsStringBundleUrl = + "chrome://browser/locale/downloads/downloads.properties"; + +const kDownloadsStringsRequiringFormatting = { + sizeWithUnits: true, + shortTimeLeftSeconds: true, + shortTimeLeftMinutes: true, + shortTimeLeftHours: true, + shortTimeLeftDays: true, + statusSeparator: true, + statusSeparatorBeforeNumber: true, + fileExecutableSecurityWarning: true +}; + +const kDownloadsStringsRequiringPluralForm = { + otherDownloads2: true +}; + +const kPartialDownloadSuffix = ".part"; + +const kPrefBranch = Services.prefs.getBranch("browser.download."); + +let PrefObserver = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + getPref(name) { + try { + switch (typeof this.prefs[name]) { + case "boolean": + return kPrefBranch.getBoolPref(name); + } + } catch (ex) { } + return this.prefs[name]; + }, + observe(aSubject, aTopic, aData) { + if (this.prefs.hasOwnProperty(aData)) { + delete this[aData]; + return this[aData] = this.getPref(aData); + } + }, + register(prefs) { + this.prefs = prefs; + kPrefBranch.addObserver("", this, true); + for (let key in prefs) { + let name = key; + XPCOMUtils.defineLazyGetter(this, name, function () { + return PrefObserver.getPref(name); + }); + } + }, +}; + +PrefObserver.register({ + // prefName: defaultValue + debug: false, + animateNotifications: true +}); + + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsCommon + +/** + * This object is exposed directly to the consumers of this JavaScript module, + * and provides shared methods for all the instances of the user interface. + */ +this.DownloadsCommon = { + /** + * Constants with the different types of unblock messages. + */ + BLOCK_VERDICT_MALWARE: "Malware", + BLOCK_VERDICT_POTENTIALLY_UNWANTED: "PotentiallyUnwanted", + BLOCK_VERDICT_UNCOMMON: "Uncommon", + + log(...aMessageArgs) { + if (!PrefObserver.debug) { + return; + } + DownloadsLogger.log(...aMessageArgs); + }, + + error(...aMessageArgs) { + if (!PrefObserver.debug) { + return; + } + DownloadsLogger.reportError(...aMessageArgs); + }, + + /** + * Returns an object whose keys are the string names from the downloads string + * bundle, and whose values are either the translated strings or functions + * returning formatted strings. + */ + get strings() { + let strings = {}; + let sb = Services.strings.createBundle(kDownloadsStringBundleUrl); + let enumerator = sb.getSimpleEnumeration(); + while (enumerator.hasMoreElements()) { + let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); + let stringName = string.key; + if (stringName in kDownloadsStringsRequiringFormatting) { + strings[stringName] = function () { + // Convert "arguments" to a real array before calling into XPCOM. + return sb.formatStringFromName(stringName, + Array.slice(arguments, 0), + arguments.length); + }; + } else if (stringName in kDownloadsStringsRequiringPluralForm) { + strings[stringName] = function (aCount) { + // Convert "arguments" to a real array before calling into XPCOM. + let formattedString = sb.formatStringFromName(stringName, + Array.slice(arguments, 0), + arguments.length); + return PluralForm.get(aCount, formattedString); + }; + } else { + strings[stringName] = string.value; + } + } + delete this.strings; + return this.strings = strings; + }, + + /** + * Generates a very short string representing the given time left. + * + * @param aSeconds + * Value to be formatted. It represents the number of seconds, it must + * be positive but does not need to be an integer. + * + * @return Formatted string, for example "30s" or "2h". The returned value is + * maximum three characters long, at least in English. + */ + formatTimeLeft(aSeconds) { + // Decide what text to show for the time + let seconds = Math.round(aSeconds); + if (!seconds) { + return ""; + } else if (seconds <= 30) { + return DownloadsCommon.strings["shortTimeLeftSeconds"](seconds); + } + let minutes = Math.round(aSeconds / 60); + if (minutes < 60) { + return DownloadsCommon.strings["shortTimeLeftMinutes"](minutes); + } + let hours = Math.round(minutes / 60); + if (hours < 48) { // two days + return DownloadsCommon.strings["shortTimeLeftHours"](hours); + } + let days = Math.round(hours / 24); + return DownloadsCommon.strings["shortTimeLeftDays"](Math.min(days, 99)); + }, + + /** + * Indicates whether we should show visual notification on the indicator + * when a download event is triggered. + */ + get animateNotifications() { + return PrefObserver.animateNotifications; + }, + + /** + * Get access to one of the DownloadsData or PrivateDownloadsData objects, + * depending on the privacy status of the window in question. + * + * @param aWindow + * The browser window which owns the download button. + */ + getData(aWindow) { + if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { + return PrivateDownloadsData; + } else { + return DownloadsData; + } + }, + + /** + * Initializes the Downloads back-end and starts receiving events for both the + * private and non-private downloads data objects. + */ + initializeAllDataLinks() { + DownloadsData.initializeDataLink(); + PrivateDownloadsData.initializeDataLink(); + }, + + /** + * Get access to one of the DownloadsIndicatorData or + * PrivateDownloadsIndicatorData objects, depending on the privacy status of + * the window in question. + */ + getIndicatorData(aWindow) { + if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { + return PrivateDownloadsIndicatorData; + } else { + return DownloadsIndicatorData; + } + }, + + /** + * Returns a reference to the DownloadsSummaryData singleton - creating one + * in the process if one hasn't been instantiated yet. + * + * @param aWindow + * The browser window which owns the download button. + * @param aNumToExclude + * The number of items on the top of the downloads list to exclude + * from the summary. + */ + getSummary(aWindow, aNumToExclude) { + if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { + if (this._privateSummary) { + return this._privateSummary; + } + return this._privateSummary = new DownloadsSummaryData(true, aNumToExclude); + } else { + if (this._summary) { + return this._summary; + } + return this._summary = new DownloadsSummaryData(false, aNumToExclude); + } + }, + _summary: null, + _privateSummary: null, + + /** + * Returns the legacy state integer value for the provided Download object. + */ + stateOfDownload(download) { + // Collapse state using the correct priority. + if (!download.stopped) { + return nsIDM.DOWNLOAD_DOWNLOADING; + } + if (download.succeeded) { + return nsIDM.DOWNLOAD_FINISHED; + } + if (download.error) { + if (download.error.becauseBlockedByParentalControls) { + return nsIDM.DOWNLOAD_BLOCKED_PARENTAL; + } + if (download.error.becauseBlockedByReputationCheck) { + return nsIDM.DOWNLOAD_DIRTY; + } + return nsIDM.DOWNLOAD_FAILED; + } + if (download.canceled) { + if (download.hasPartialData) { + return nsIDM.DOWNLOAD_PAUSED; + } + return nsIDM.DOWNLOAD_CANCELED; + } + return nsIDM.DOWNLOAD_NOTSTARTED; + }, + + /** + * Helper function required because the Downloads Panel and the Downloads View + * don't share the controller yet. + */ + removeAndFinalizeDownload(download) { + Downloads.getList(Downloads.ALL) + .then(list => list.remove(download)) + .then(() => download.finalize(true)) + .catch(Cu.reportError); + }, + + /** + * Given an iterable collection of Download objects, generates and returns + * statistics about that collection. + * + * @param downloads An iterable collection of Download objects. + * + * @return Object whose properties are the generated statistics. Currently, + * we return the following properties: + * + * numActive : The total number of downloads. + * numPaused : The total number of paused downloads. + * numDownloading : The total number of downloads being downloaded. + * totalSize : The total size of all downloads once completed. + * totalTransferred: The total amount of transferred data for these + * downloads. + * slowestSpeed : The slowest download rate. + * rawTimeLeft : The estimated time left for the downloads to + * complete. + * percentComplete : The percentage of bytes successfully downloaded. + */ + summarizeDownloads(downloads) { + let summary = { + numActive: 0, + numPaused: 0, + numDownloading: 0, + totalSize: 0, + totalTransferred: 0, + // slowestSpeed is Infinity so that we can use Math.min to + // 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 + // download. + slowestSpeed: Infinity, + rawTimeLeft: -1, + percentComplete: -1 + } + + for (let download of downloads) { + summary.numActive++; + + if (!download.stopped) { + summary.numDownloading++; + if (download.hasProgress && download.speed > 0) { + let sizeLeft = download.totalBytes - download.currentBytes; + summary.rawTimeLeft = Math.max(summary.rawTimeLeft, + sizeLeft / download.speed); + summary.slowestSpeed = Math.min(summary.slowestSpeed, + download.speed); + } + } else if (download.canceled && download.hasPartialData) { + summary.numPaused++; + } + + // Only add to total values if we actually know the download size. + if (download.succeeded) { + summary.totalSize += download.target.size; + summary.totalTransferred += download.target.size; + } else if (download.hasProgress) { + summary.totalSize += download.totalBytes; + summary.totalTransferred += download.currentBytes; + } + } + + if (summary.totalSize != 0) { + summary.percentComplete = (summary.totalTransferred / + summary.totalSize) * 100; + } + + if (summary.slowestSpeed == Infinity) { + summary.slowestSpeed = 0; + } + + return summary; + }, + + /** + * If necessary, smooths the estimated number of seconds remaining for one + * or more downloads to complete. + * + * @param aSeconds + * Current raw estimate on number of seconds left for one or more + * downloads. This is a floating point value to help get sub-second + * accuracy for current and future estimates. + */ + smoothSeconds(aSeconds, aLastSeconds) { + // We apply an algorithm similar to the DownloadUtils.getTimeLeft function, + // though tailored to a single time estimation for all downloads. We never + // apply something if the new value is less than half the previous value. + let shouldApplySmoothing = aLastSeconds >= 0 && + aSeconds > aLastSeconds / 2; + if (shouldApplySmoothing) { + // Apply hysteresis to favor downward over upward swings. Trust only 30% + // of the new value if lower, and 10% if higher (exponential smoothing). + let diff = aSeconds - aLastSeconds; + aSeconds = aLastSeconds + (diff < 0 ? .3 : .1) * diff; + + // If the new time is similar, reuse something close to the last time + // left, but subtract a little to provide forward progress. + diff = aSeconds - aLastSeconds; + let diffPercent = diff / aLastSeconds * 100; + if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) { + aSeconds = aLastSeconds - (diff < 0 ? .4 : .2); + } + } + + // In the last few seconds of downloading, we are always subtracting and + // 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. + * + * @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(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"); + } + + let promiseShouldLaunch; + if (aFile.isExecutable()) { + // We get a prompter for the provided window here, even though anchoring + // to the most recently active window should work as well. + promiseShouldLaunch = + DownloadUIHelper.getPrompter(aOwnerWindow) + .confirmLaunchExecutable(aFile.path); + } else { + promiseShouldLaunch = Promise.resolve(true); + } + + promiseShouldLaunch.then(shouldLaunch => { + if (!shouldLaunch) { + return; + } + + // 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)); + } + }).then(null, Cu.reportError); + }, + + /** + * Show a downloaded file in the system file manager. + * + * @param aFile + * a downloaded file. + */ + 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)); + } + } + } + }, + + /** + * Displays an alert message box which asks the user if they want to + * unblock the downloaded file or not. + * + * @param aType + * The type of malware the downloaded file contains. + * @param aOwnerWindow + * The window with which this action is associated. + * + * @return True to unblock the file, false to keep the user safe and + * cancel the operation. + */ + confirmUnblockDownload: Task.async(function* (aType, aOwnerWindow) { + let s = DownloadsCommon.strings; + let title = s.unblockHeader; + let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) + + (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) + + Ci.nsIPrompt.BUTTON_POS_1_DEFAULT; + let type = ""; + let message = s.unblockTip; + let okButton = s.unblockButtonContinue; + let cancelButton = s.unblockButtonCancel; + + switch (aType) { + case this.BLOCK_VERDICT_MALWARE: + type = s.unblockTypeMalware; + break; + case this.BLOCK_VERDICT_POTENTIALLY_UNWANTED: + type = s.unblockTypePotentiallyUnwanted; + break; + case this.BLOCK_VERDICT_UNCOMMON: + type = s.unblockTypeUncommon; + break; + } + + if (type) { + message = type + "\n\n" + message; + } + + Services.ww.registerNotification(function onOpen(subj, topic) { + if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) { + // Make sure to listen for "DOMContentLoaded" because it is fired + // before the "load" event. + subj.addEventListener("DOMContentLoaded", function onLoad() { + subj.removeEventListener("DOMContentLoaded", onLoad); + if (subj.document.documentURI == + "chrome://global/content/commonDialog.xul") { + Services.ww.unregisterNotification(onOpen); + let dialog = subj.document.getElementById("commonDialog"); + if (dialog) { + // Change the dialog to use a warning icon. + dialog.classList.add("alert-dialog"); + } + } + }); + } + }); + + // The ordering of the ok/cancel buttons is used this way to allow "cancel" + // to have the same result as hitting the ESC or Close button (see bug 345067). + let rv = Services.prompt.confirmEx(aOwnerWindow, title, message, buttonFlags, + okButton, cancelButton, null, null, {}); + return (rv == 0); + }), +}; + +/** + * Returns true if we are executing on Windows Vista or a later version. + */ +XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function () { + let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; + if (os != "WINNT") { + return false; + } + let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); + return parseFloat(sysInfo.getProperty("version")) >= 6; +}); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsData + +/** + * Retrieves the list of past and completed downloads from the underlying + * Download Manager data, and provides asynchronous notifications allowing to + * build a consistent view of the available data. + * + * This object responds to real-time changes in the underlying Download Manager + * data. For example, the deletion of one or more downloads is notified through + * the nsIObserver interface, while any state or progress change is notified + * through the nsIDownloadProgressListener interface. + * + * Note that using this object does not automatically start the Download Manager + * service. Consumers will see an empty list of downloads until the service is + * actually started. This is useful to display a neutral progress indicator in + * the main browser window until the autostart timeout elapses. + * + * Note that DownloadsData and PrivateDownloadsData are two equivalent singleton + * objects, one accessing non-private downloads, and the other accessing private + * ones. + */ +function DownloadsDataCtor(aPrivate) { + this._isPrivate = aPrivate; + + // Contains all the available Download objects and their integer state. + this.oldDownloadStates = new Map(); + + // Array of view objects that should be notified when the available download + // data changes. + this._views = []; +} + +DownloadsDataCtor.prototype = { + /** + * Starts receiving events for current downloads. + */ + initializeDataLink() { + if (!this._dataLinkInitialized) { + let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE + : Downloads.PUBLIC); + promiseList.then(list => list.addView(this)).then(null, Cu.reportError); + this._dataLinkInitialized = true; + } + }, + _dataLinkInitialized: false, + + /** + * 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. + */ + get canRemoveFinished() { + for (let download of this.downloads) { + // Stopped, paused, and failed downloads with partial data are removed. + if (download.stopped && !(download.canceled && download.hasPartialData)) { + return true; + } + } + return false; + }, + + /** + * Asks the back-end to remove finished downloads from the list. + */ + removeFinished() { + let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE + : Downloads.PUBLIC); + promiseList.then(list => list.removeFinished()) + .then(null, Cu.reportError); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Integration with the asynchronous Downloads back-end + + onDownloadAdded(download) { + // Download objects do not store the end time of downloads, as the Downloads + // API does not need to persist this information for all platforms. Once a + // download terminates on a Desktop browser, it becomes a history download, + // for which the end time is stored differently, as a Places annotation. + download.endTime = Date.now(); + + this.oldDownloadStates.set(download, + DownloadsCommon.stateOfDownload(download)); + + for (let view of this._views) { + view.onDownloadAdded(download, true); + } + }, + + onDownloadChanged(download) { + let oldState = this.oldDownloadStates.get(download); + let newState = DownloadsCommon.stateOfDownload(download); + this.oldDownloadStates.set(download, newState); + + if (oldState != newState) { + if (download.succeeded || + (download.canceled && !download.hasPartialData) || + download.error) { + // Store the end time that may be displayed by the views. + download.endTime = Date.now(); + + // This state transition code should actually be located in a Downloads + // API module (bug 941009). Moreover, the fact that state is stored as + // annotations should be ideally hidden behind methods of + // nsIDownloadHistory (bug 830415). + if (!this._isPrivate) { + try { + let downloadMetaData = { + state: DownloadsCommon.stateOfDownload(download), + endTime: download.endTime, + }; + if (download.succeeded) { + downloadMetaData.fileSize = download.target.size; + } + + PlacesUtils.annotations.setPageAnnotation( + NetUtil.newURI(download.source.url), + "downloads/metaData", + JSON.stringify(downloadMetaData), 0, + PlacesUtils.annotations.EXPIRE_WITH_HISTORY); + } catch (ex) { + Cu.reportError(ex); + } + } + } + + for (let view of this._views) { + try { + view.onDownloadStateChanged(download); + } catch (ex) { + Cu.reportError(ex); + } + } + + if (download.succeeded || + (download.error && download.error.becauseBlocked)) { + this._notifyDownloadEvent("finish"); + } + } + + if (!download.newDownloadNotified) { + download.newDownloadNotified = true; + this._notifyDownloadEvent("start"); + } + + for (let view of this._views) { + view.onDownloadChanged(download); + } + }, + + onDownloadRemoved(download) { + this.oldDownloadStates.delete(download); + + for (let view of this._views) { + view.onDownloadRemoved(download); + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Registration of views + + /** + * Adds an object to be notified when the available download data changes. + * The specified object is initialized with the currently available downloads. + * + * @param aView + * DownloadsView object to be added. This reference must be passed to + * removeView before termination. + */ + addView(aView) { + this._views.push(aView); + this._updateView(aView); + }, + + /** + * Removes an object previously added using addView. + * + * @param aView + * DownloadsView object to be removed. + */ + removeView(aView) { + let index = this._views.indexOf(aView); + if (index != -1) { + this._views.splice(index, 1); + } + }, + + /** + * Ensures that the currently loaded data is added to the specified view. + * + * @param aView + * DownloadsView object to be initialized. + */ + _updateView(aView) { + // Indicate to the view that a batch loading operation is in progress. + aView.onDataLoadStarting(); + + // Sort backwards by start time, ensuring that the most recent + // downloads are added first regardless of their state. + let downloadsArray = [...this.downloads]; + downloadsArray.sort((a, b) => b.startTime - a.startTime); + downloadsArray.forEach(download => aView.onDownloadAdded(download, false)); + + // Notify the view that all data is available. + aView.onDataLoadCompleted(); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Notifications sent to the most recent browser window only + + /** + * Set to true after the first download causes the downloads panel to be + * displayed. + */ + get panelHasShownBefore() { + try { + return Services.prefs.getBoolPref("browser.download.panel.shown"); + } catch (ex) { } + return false; + }, + + set panelHasShownBefore(aValue) { + Services.prefs.setBoolPref("browser.download.panel.shown", aValue); + return aValue; + }, + + /** + * Displays a new or finished download notification in the most recent browser + * window, if one is currently available with the required privacy type. + * + * @param aType + * Set to "start" for new downloads, "finish" for completed downloads. + */ + _notifyDownloadEvent(aType) { + DownloadsCommon.log("Attempting to notify that a new download has started or finished."); + + // Show the panel in the most recent browser window, if present. + let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate }); + if (!browserWin) { + return; + } + + if (this.panelHasShownBefore) { + // For new downloads after the first one, don't show the panel + // automatically, but provide a visible notification in the topmost + // browser window, if the status indicator is already visible. + DownloadsCommon.log("Showing new download notification."); + browserWin.DownloadsIndicatorView.showEventNotification(aType); + return; + } + this.panelHasShownBefore = true; + browserWin.DownloadsPanel.showPanel(); + } +}; + +XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() { + return new DownloadsDataCtor(true); +}); + +XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() { + return new DownloadsDataCtor(false); +}); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsViewPrototype + +/** + * A prototype for an object that registers itself with DownloadsData as soon + * as a view is registered with it. + */ +const DownloadsViewPrototype = { + ////////////////////////////////////////////////////////////////////////////// + //// Registration of views + + /** + * Array of view objects that should be notified when the available status + * data changes. + * + * SUBCLASSES MUST OVERRIDE THIS PROPERTY. + */ + _views: null, + + /** + * Determines whether this view object is over the private or non-private + * downloads. + * + * SUBCLASSES MUST OVERRIDE THIS PROPERTY. + */ + _isPrivate: false, + + /** + * Adds an object to be notified when the available status data changes. + * The specified object is initialized with the currently available status. + * + * @param aView + * View object to be added. This reference must be + * passed to removeView before termination. + */ + addView(aView) { + // Start receiving events when the first of our views is registered. + if (this._views.length == 0) { + if (this._isPrivate) { + PrivateDownloadsData.addView(this); + } else { + DownloadsData.addView(this); + } + } + + this._views.push(aView); + this.refreshView(aView); + }, + + /** + * Updates the properties of an object previously added using addView. + * + * @param aView + * View object to be updated. + */ + refreshView(aView) { + // Update immediately even if we are still loading data asynchronously. + // Subclasses must provide these two functions! + this._refreshProperties(); + this._updateView(aView); + }, + + /** + * Removes an object previously added using addView. + * + * @param aView + * View object to be removed. + */ + removeView(aView) { + let index = this._views.indexOf(aView); + if (index != -1) { + this._views.splice(index, 1); + } + + // Stop receiving events when the last of our views is unregistered. + if (this._views.length == 0) { + if (this._isPrivate) { + PrivateDownloadsData.removeView(this); + } else { + DownloadsData.removeView(this); + } + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Callback functions from DownloadsData + + /** + * Indicates whether we are still loading downloads data asynchronously. + */ + _loading: false, + + /** + * Called before multiple downloads are about to be loaded. + */ + onDataLoadStarting() { + this._loading = true; + }, + + /** + * Called after data loading finished. + */ + onDataLoadCompleted() { + this._loading = false; + }, + + /** + * Called when a new download data item is available, either during the + * asynchronous data load or when a new download is started. + * + * @param download + * Download object that was just added. + * @param newest + * 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. + * + * @note Subclasses should override this. + */ + 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; + }, + + /** + * 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. + * + * @note Subclasses should override this. + */ + onDownloadRemoved(download) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * Private function used to refresh the internal properties being sent to + * each registered view. + * + * @note Subclasses should override this. + */ + _refreshProperties() { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * Private function used to refresh an individual view. + * + * @note Subclasses should override this. + */ + _updateView() { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, +}; + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsIndicatorData + +/** + * This object registers itself with DownloadsData as a view, and transforms the + * notifications it receives into overall status data, that is then broadcast to + * the registered download status indicators. + * + * Note that using this object does not automatically start the Download Manager + * service. Consumers will see an empty list of downloads until the service is + * actually started. This is useful to display a neutral progress indicator in + * the main browser window until the autostart timeout elapses. + */ +function DownloadsIndicatorDataCtor(aPrivate) { + this._isPrivate = aPrivate; + this._views = []; +} +DownloadsIndicatorDataCtor.prototype = { + __proto__: DownloadsViewPrototype, + + /** + * Removes an object previously added using addView. + * + * @param aView + * DownloadsIndicatorView object to be removed. + */ + removeView(aView) { + DownloadsViewPrototype.removeView.call(this, aView); + + if (this._views.length == 0) { + this._itemCount = 0; + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Callback functions from DownloadsData + + onDataLoadCompleted() { + DownloadsViewPrototype.onDataLoadCompleted.call(this); + this._updateViews(); + }, + + onDownloadAdded(download, newest) { + this._itemCount++; + this._updateViews(); + }, + + onDownloadStateChanged(download) { + if (download.succeeded || download.error) { + this.attention = true; + } + + // Since the state of a download changed, reset the estimated time left. + this._lastRawTimeLeft = -1; + this._lastTimeLeft = -1; + }, + + onDownloadChanged(download) { + this._updateViews(); + }, + + onDownloadRemoved(download) { + this._itemCount--; + this._updateViews(); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Propagation of properties to our views + + // The following properties are updated by _refreshProperties and are then + // propagated to the views. See _refreshProperties for details. + _hasDownloads: false, + _counter: "", + _percentComplete: -1, + _paused: false, + + /** + * Indicates whether the download indicators should be highlighted. + */ + set attention(aValue) { + this._attention = aValue; + this._updateViews(); + return aValue; + }, + _attention: false, + + /** + * Indicates whether the user is interacting with downloads, thus the + * attention indication should not be shown even if requested. + */ + set attentionSuppressed(aValue) { + this._attentionSuppressed = aValue; + this._attention = false; + this._updateViews(); + return aValue; + }, + _attentionSuppressed: false, + + /** + * Computes aggregate values and propagates the changes to our views. + */ + _updateViews() { + // Do not update the status indicators during batch loads of download items. + if (this._loading) { + return; + } + + this._refreshProperties(); + this._views.forEach(this._updateView, this); + }, + + /** + * Updates the specified view with the current aggregate values. + * + * @param aView + * DownloadsIndicatorView object to be updated. + */ + _updateView(aView) { + aView.hasDownloads = this._hasDownloads; + aView.counter = this._counter; + aView.percentComplete = this._percentComplete; + aView.paused = this._paused; + aView.attention = this._attention && !this._attentionSuppressed; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Property updating based on current download status + + /** + * Number of download items that are available to be displayed. + */ + _itemCount: 0, + + /** + * 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 + * don't continuously apply smoothing if the actual download state has not + * changed. This is set to -1 if the previous value is unknown. + */ + _lastRawTimeLeft: -1, + + /** + * Last number of seconds estimated until all in-progress downloads with a + * known size and speed will finish. This value is stored to allow smoothing + * in case of small variations. This is set to -1 if the previous value is + * unknown. + */ + _lastTimeLeft: -1, + + /** + * A generator function for the Download objects this summary is currently + * interested in. This generator is passed off to summarizeDownloads in order + * to generate statistics about the downloads we care about - in this case, + * it's all active downloads. + */ + * _activeDownloads() { + let downloads = this._isPrivate ? PrivateDownloadsData.downloads + : DownloadsData.downloads; + for (let download of downloads) { + if (!download.stopped || (download.canceled && download.hasPartialData)) { + yield download; + } + } + }, + + /** + * Computes aggregate values based on the current state of downloads. + */ + _refreshProperties() { + let summary = + DownloadsCommon.summarizeDownloads(this._activeDownloads()); + + // Determine if the indicator should be shown or get attention. + this._hasDownloads = (this._itemCount > 0); + + // If all downloads are paused, show the progress indicator as paused. + this._paused = summary.numActive > 0 && + summary.numActive == summary.numPaused; + + this._percentComplete = summary.percentComplete; + + // Display the estimated time left, if present. + if (summary.rawTimeLeft == -1) { + // There are no downloads with a known time left. + this._lastRawTimeLeft = -1; + this._lastTimeLeft = -1; + this._counter = ""; + } else { + // Compute the new time left only if state actually changed. + if (this._lastRawTimeLeft != summary.rawTimeLeft) { + this._lastRawTimeLeft = summary.rawTimeLeft; + this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, + this._lastTimeLeft); + } + this._counter = DownloadsCommon.formatTimeLeft(this._lastTimeLeft); + } + } +}; + +XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() { + return new DownloadsIndicatorDataCtor(true); +}); + +XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() { + return new DownloadsIndicatorDataCtor(false); +}); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsSummaryData + +/** + * DownloadsSummaryData is a view for DownloadsData that produces a summary + * of all downloads after a certain exclusion point aNumToExclude. For example, + * if there were 5 downloads in progress, and a DownloadsSummaryData was + * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData + * would produce a summary of the last 2 downloads. + * + * @param aIsPrivate + * True if the browser window which owns the download button is a private + * window. + * @param aNumToExclude + * The number of items to exclude from the summary, starting from the + * top of the list. + */ +function DownloadsSummaryData(aIsPrivate, aNumToExclude) { + this._numToExclude = aNumToExclude; + // Since we can have multiple instances of DownloadsSummaryData, we + // override these values from the prototype so that each instance can be + // completely separated from one another. + this._loading = false; + + this._downloads = []; + + // 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 + // don't continuously apply smoothing if the actual download state has not + // changed. This is set to -1 if the previous value is unknown. + this._lastRawTimeLeft = -1; + + // Last number of seconds estimated until all in-progress downloads with a + // known size and speed will finish. This value is stored to allow smoothing + // in case of small variations. This is set to -1 if the previous value is + // unknown. + this._lastTimeLeft = -1; + + // The following properties are updated by _refreshProperties and are then + // propagated to the views. + this._showingProgress = false; + this._details = ""; + this._description = ""; + this._numActive = 0; + this._percentComplete = -1; + + this._isPrivate = aIsPrivate; + this._views = []; +} + +DownloadsSummaryData.prototype = { + __proto__: DownloadsViewPrototype, + + /** + * Removes an object previously added using addView. + * + * @param aView + * DownloadsSummary view to be removed. + */ + removeView(aView) { + DownloadsViewPrototype.removeView.call(this, aView); + + if (this._views.length == 0) { + // Clear out our collection of Download objects. If we ever have + // another view registered with us, this will get re-populated. + this._downloads = []; + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Callback functions from DownloadsData - see the documentation in + //// DownloadsViewPrototype for more information on what these functions + //// are used for. + + onDataLoadCompleted() { + DownloadsViewPrototype.onDataLoadCompleted.call(this); + this._updateViews(); + }, + + onDownloadAdded(download, newest) { + if (newest) { + this._downloads.unshift(download); + } else { + this._downloads.push(download); + } + + this._updateViews(); + }, + + onDownloadStateChanged() { + // Since the state of a download changed, reset the estimated time left. + this._lastRawTimeLeft = -1; + this._lastTimeLeft = -1; + }, + + onDownloadChanged() { + this._updateViews(); + }, + + onDownloadRemoved(download) { + let itemIndex = this._downloads.indexOf(download); + this._downloads.splice(itemIndex, 1); + this._updateViews(); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Propagation of properties to our views + + /** + * Computes aggregate values and propagates the changes to our views. + */ + _updateViews() { + // Do not update the status indicators during batch loads of download items. + if (this._loading) { + return; + } + + this._refreshProperties(); + this._views.forEach(this._updateView, this); + }, + + /** + * Updates the specified view with the current aggregate values. + * + * @param aView + * DownloadsIndicatorView object to be updated. + */ + _updateView(aView) { + aView.showingProgress = this._showingProgress; + aView.percentComplete = this._percentComplete; + aView.description = this._description; + aView.details = this._details; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Property updating based on current download status + + /** + * A generator function for the Download objects this summary is currently + * interested in. This generator is passed off to summarizeDownloads in order + * to generate statistics about the downloads we care about - in this case, + * it's the downloads in this._downloads after the first few to exclude, + * which was set when constructing this DownloadsSummaryData instance. + */ + * _downloadsForSummary() { + if (this._downloads.length > 0) { + for (let i = this._numToExclude; i < this._downloads.length; ++i) { + yield this._downloads[i]; + } + } + }, + + /** + * Computes aggregate values based on the current state of downloads. + */ + _refreshProperties() { + // Pre-load summary with default values. + let summary = + DownloadsCommon.summarizeDownloads(this._downloadsForSummary()); + + this._description = DownloadsCommon.strings + .otherDownloads2(summary.numActive); + this._percentComplete = summary.percentComplete; + + // If all downloads are paused, show the progress indicator as paused. + this._showingProgress = summary.numDownloading > 0 || + summary.numPaused > 0; + + // Display the estimated time left, if present. + if (summary.rawTimeLeft == -1) { + // There are no downloads with a known time left. + this._lastRawTimeLeft = -1; + this._lastTimeLeft = -1; + this._details = ""; + } else { + // Compute the new time left only if state actually changed. + if (this._lastRawTimeLeft != summary.rawTimeLeft) { + this._lastRawTimeLeft = summary.rawTimeLeft; + this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, + this._lastTimeLeft); + } + [this._details] = DownloadUtils.getDownloadStatusNoRate( + summary.totalTransferred, summary.totalSize, summary.slowestSpeed, + this._lastTimeLeft); + } + }, +} diff --git a/k-meleon/modules/DownloadsLogger.jsm b/k-meleon/modules/DownloadsLogger.jsm new file mode 100644 index 00000000..a24c9acc --- /dev/null +++ b/k-meleon/modules/DownloadsLogger.jsm @@ -0,0 +1,75 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 contents of this file were copied almost entirely from + * toolkit/identity/LogUtils.jsm. Until we've got a more generalized logging + * mechanism for toolkit, I think this is going to be how we roll. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["DownloadsLogger"]; +const PREF_DEBUG = "browser.download.debug"; + +const Cu = Components.utils; +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +this.DownloadsLogger = { + _generateLogMessage(args) { + // create a string representation of a list of arbitrary things + let strings = []; + + for (let arg of args) { + if (typeof arg === 'string') { + strings.push(arg); + } else if (arg === undefined) { + strings.push('undefined'); + } else if (arg === null) { + strings.push('null'); + } else { + try { + strings.push(JSON.stringify(arg, null, 2)); + } catch(err) { + strings.push("<>"); + } + } + }; + return 'Downloads: ' + strings.join(' '); + }, + + /** + * log() - utility function to print a list of arbitrary things + * + * Enable with about:config pref browser.download.debug + */ + log(...args) { + let output = this._generateLogMessage(args); + dump(output + "\n"); + + // Additionally, make the output visible in the Error Console + Services.console.logStringMessage(output); + }, + + /** + * reportError() - report an error through component utils as well as + * our log function + */ + reportError(...aArgs) { + // Report the error in the browser + let output = this._generateLogMessage(aArgs); + Cu.reportError(output); + dump("ERROR:" + output + "\n"); + for (let frame = Components.stack.caller; frame; frame = frame.caller) { + dump("\t" + frame + "\n"); + } + }, +}; diff --git a/k-meleon/modules/DownloadsViewUI.jsm b/k-meleon/modules/DownloadsViewUI.jsm new file mode 100644 index 00000000..35b60fa7 --- /dev/null +++ b/k-meleon/modules/DownloadsViewUI.jsm @@ -0,0 +1,232 @@ +/* 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/. */ + +/* + * This module is imported by code that uses the "download.xml" binding, and + * provides prototypes for objects that handle input and display information. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "DownloadsViewUI", +]; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", + "resource://gre/modules/DownloadUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", + "resource:///modules/DownloadsCommon.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); + +this.DownloadsViewUI = {}; + +/** + * A download element shell is responsible for handling the commands and the + * displayed data for a single element that uses the "download.xml" binding. + * + * The information to display is obtained through the associated Download object + * from the JavaScript API for downloads, and commands are executed using a + * combination of Download methods and DownloadsCommon.jsm helper functions. + * + * 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 + * history view may use a HistoryDownload object in place of a Download object. + */ +this.DownloadsViewUI.DownloadElementShell = function () {} + +this.DownloadsViewUI.DownloadElementShell.prototype = { + /** + * The richlistitem for the download, initialized by the derived object. + */ + element: null, + + /** + * URI string for the file type icon displayed in the download element. + */ + get image() { + if (!this.download.target.path) { + // Old history downloads may not have a target path. + return "moz-icon://.unknown?size=32"; + } + + // When a download that was previously in progress finishes successfully, it + // means that the target file now exists and we can extract its specific + // icon, for example from a Windows executable. To ensure that the icon is + // reloaded, however, we must change the URI used by the XUL image element, + // for example by adding a query parameter. This only works if we add one of + // the parameters explicitly supported by the nsIMozIconURI interface. + return "moz-icon://" + this.download.target.path + "?size=32" + + (this.download.succeeded ? "&state=normal" : ""); + }, + + /** + * The user-facing label for the download. This is normally the leaf name of + * the download target file. In case this is a very old history download for + * which the target file is unknown, the download source URI is displayed. + */ + get displayName() { + if (!this.download.target.path) { + return this.download.source.url; + } + return OS.Path.basename(this.download.target.path); + }, + + /** + * The progress element for the download, or undefined in case the XBL binding + * has not been applied yet. + */ + get _progressElement() { + if (!this.__progressElement) { + // If the element is not available now, we will try again the next time. + this.__progressElement = + this.element.ownerDocument.getAnonymousElementByAttribute( + this.element, "anonid", + "progressmeter"); + } + return this.__progressElement; + }, + + /** + * Processes a major state change in the user interface, then proceeds with + * the normal progress update. This function is not called for every progress + * update in order to improve performance. + */ + _updateState() { + this.element.setAttribute("displayName", this.displayName); + this.element.setAttribute("image", this.image); + this.element.setAttribute("state", + DownloadsCommon.stateOfDownload(this.download)); + + // Since state changed, reset the time left estimation. + this.lastEstimatedSecondsLeft = Infinity; + + this._updateProgress(); + }, + + /** + * Updates the elements that change regularly for in-progress downloads, + * namely the progress bar and the status line. + */ + _updateProgress() { + if (this.download.succeeded) { + // We only need to add or remove this attribute for succeeded downloads. + if (this.download.target.exists) { + this.element.setAttribute("exists", "true"); + } else { + this.element.removeAttribute("exists"); + } + } + + // The progress bar is only displayed for in-progress downloads. + if (this.download.hasProgress) { + this.element.setAttribute("progressmode", "normal"); + this.element.setAttribute("progress", this.download.progress); + } else { + this.element.setAttribute("progressmode", "undetermined"); + } + + // Dispatch the ValueChange event for accessibility, if possible. + if (this._progressElement) { + let event = this.element.ownerDocument.createEvent("Events"); + event.initEvent("ValueChange", true, true); + this._progressElement.dispatchEvent(event); + } + + let status = this.statusTextAndTip; + this.element.setAttribute("status", status.text); + this.element.setAttribute("statusTip", status.tip); + }, + + lastEstimatedSecondsLeft: Infinity, + + /** + * Returns the text for the status line and the associated tooltip. These are + * returned by a single property because they are computed together. The + * result may be overridden by derived objects. + */ + get statusTextAndTip() this.rawStatusTextAndTip, + + /** + * Derived objects may call this to get the status text. + */ + get rawStatusTextAndTip() { + const nsIDM = Ci.nsIDownloadManager; + let s = DownloadsCommon.strings; + + let text = ""; + let tip = ""; + + if (!this.download.stopped) { + let totalBytes = this.download.hasProgress ? this.download.totalBytes + : -1; + // By default, extended status information including the individual + // download rate is displayed in the tooltip. The history view overrides + // the getter and displays the datails in the main area instead. + [text] = DownloadUtils.getDownloadStatusNoRate( + this.download.currentBytes, + totalBytes, + this.download.speed, + this.lastEstimatedSecondsLeft); + let newEstimatedSecondsLeft; + [tip, newEstimatedSecondsLeft] = DownloadUtils.getDownloadStatus( + this.download.currentBytes, + totalBytes, + this.download.speed, + this.lastEstimatedSecondsLeft); + this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft; + } else if (this.download.canceled && this.download.hasPartialData) { + let totalBytes = this.download.hasProgress ? this.download.totalBytes + : -1; + let transfer = DownloadUtils.getTransferTotal(this.download.currentBytes, + totalBytes); + + // We use the same XUL label to display both the state and the amount + // transferred, for example "Paused - 1.1 MB". + text = s.statusSeparatorBeforeNumber(s.statePaused, transfer); + } else if (!this.download.succeeded && !this.download.canceled && + !this.download.error) { + text = s.stateStarting; + } else { + let stateLabel; + + if (this.download.succeeded) { + // For completed downloads, show the file size (e.g. "1.5 MB"). + if (this.download.target.size !== undefined) { + let [size, unit] = + DownloadUtils.convertByteUnits(this.download.target.size); + stateLabel = s.sizeWithUnits(size, unit); + } else { + // History downloads may not have a size defined. + stateLabel = s.sizeUnknown; + } + } else if (this.download.canceled) { + stateLabel = s.stateCanceled; + } else if (this.download.error.becauseBlockedByParentalControls) { + stateLabel = s.stateBlockedParentalControls; + } else if (this.download.error.becauseBlockedByReputationCheck) { + stateLabel = s.stateDirty; + } else { + stateLabel = s.stateFailed; + } + + let referrer = this.download.source.referrer || this.download.source.url; + let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer); + + let date = new Date(this.download.endTime); + let [displayDate, fullDate] = DownloadUtils.getReadableDates(date); + + let firstPart = s.statusSeparator(stateLabel, displayHost); + text = s.statusSeparator(firstPart, displayDate); + tip = s.statusSeparator(fullHost, fullDate); + } + + return { text, tip: tip || text }; + }, +}; diff --git a/k-meleon/modules/KMeleon.jsm b/k-meleon/modules/KMeleon.jsm index 73f3e367..4e161b21 100644 --- a/k-meleon/modules/KMeleon.jsm +++ b/k-meleon/modules/KMeleon.jsm @@ -18,59 +18,6 @@ logger.level = Log.Level.Debug; logger.addAppender(new Log.ConsoleAppender(formatter)); logger.addAppender(new Log.DumpAppender(formatter)); -function KMenu(name) { - this.name = name; -} - -KMenu.prototype = { - name: null, - - addItem: function (label, command, position) { - JsBridge.SetMenu(this.name, JsBridge.MENU_COMMAND, label, command, position); - }, - - removeItem: function (command) { - JsBridge.SetMenu(this.name, JsBridge.MENU_COMMAND, '', command); - } -} - -function KToolbar(name) { - this.name = name; -} - -KToolbar.prototype = { - name: null, - - addButton: function(command, menu) { - return JsBridge.AddButton(this.name, command, menu); - }, - - removeButton: function(command) { - return JsBridge.RemoveButton(this.name, command); - }, -} - -function KCommand(name, desc, callback) { - this.name = name; -} - -KCommand.prototype = { - name: null, - - checked: function(bool) { - }, - - disabled: function(bool) { - }, - - setIcon: function(icon) { - }, - - setDesc: function(text) { - } - -} - this.KMeleon = { MENU_COMMAND: JsBridge.MENU_COMMAND, @@ -240,21 +187,5 @@ this.KMeleon = { SetAccel: function(key, command) { return JsBridge.SetAccel(key, command); - }, - - GetMenu: function(name) { - return new KMenu(name); - }, - - GetToolbar: function(name) { - return new KToolbar(name); - }, - - AddListener: function(listener) { - return JsBridge.AddListener(listener); - }, - - RemoveListener: function(name) { - return JsBridge.RemoveListener(listener); } } \ No newline at end of file