DownloadsTaskbar.sys.mjs (11574B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 /** 8 * Handles the download progress indicator in the taskbar. 9 */ 10 11 // Globals 12 13 const lazy = {}; 14 const gInterfaces = {}; 15 16 function defineResettableGetter(object, name, callback) { 17 let result = undefined; 18 19 Object.defineProperty(object, name, { 20 get() { 21 if (typeof result == "undefined") { 22 result = callback(); 23 } 24 25 return result; 26 }, 27 set(value) { 28 if (value === null) { 29 result = undefined; 30 } else { 31 throw new Error("don't set this to nonnull"); 32 } 33 }, 34 }); 35 } 36 37 ChromeUtils.defineESModuleGetters(lazy, { 38 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 39 Downloads: "resource://gre/modules/Downloads.sys.mjs", 40 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 41 }); 42 43 defineResettableGetter(gInterfaces, "winTaskbar", function () { 44 if (!("@mozilla.org/windows-taskbar;1" in Cc)) { 45 return null; 46 } 47 let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"].getService( 48 Ci.nsIWinTaskbar 49 ); 50 return winTaskbar.available && winTaskbar; 51 }); 52 53 defineResettableGetter(gInterfaces, "macTaskbarProgress", function () { 54 return ( 55 "@mozilla.org/widget/macdocksupport;1" in Cc && 56 Cc["@mozilla.org/widget/macdocksupport;1"].getService(Ci.nsITaskbarProgress) 57 ); 58 }); 59 60 defineResettableGetter(gInterfaces, "gtkTaskbarProgress", function () { 61 return ( 62 "@mozilla.org/widget/taskbarprogress/gtk;1" in Cc && 63 Cc["@mozilla.org/widget/taskbarprogress/gtk;1"].getService( 64 Ci.nsIGtkTaskbarProgress 65 ) 66 ); 67 }); 68 69 /** 70 * Handles the download progress indicator in the taskbar. 71 */ 72 class DownloadsTaskbarInstance { 73 /** 74 * Underlying DownloadSummary providing the aggregate download information, or 75 * null if the indicator has never been initialized. 76 */ 77 #summary = null; 78 79 /** 80 * nsITaskbarProgress objects to which download information is dispatched. 81 * This can be empty if the indicator has never been initialized or if the 82 * indicator is currently hidden on Windows. 83 * 84 * @type {Set<nsITaskbarProgress>} 85 */ 86 #taskbarProgresses = new Set(); 87 88 /** 89 * The kind of downloads that will be summarized. 90 * 91 * At registration time, this helps create the DownloadsSummary. When the 92 * progress representative unloads, this determines whether the replacement 93 * should be a public or a private window. 94 */ 95 #filter = null; 96 97 /** 98 * Creates a new DownloadsTaskbarInstance. 99 * 100 * A given instance of the browser has two instances of this: one for public 101 * windows (where aFilter is Downloads.PUBLIC) and the other for private windows 102 * (Downloads.PRIVATE). 103 * 104 * This function doesn't actually register the taskbar with a window; you should 105 * call registerIndicator when you add a new window. 106 */ 107 constructor(aFilter) { 108 this.#filter = aFilter; 109 } 110 111 /** 112 * This method is called after a new browser window is opened, and ensures 113 * that the download progress indicator is displayed in the taskbar. 114 * 115 * On Windows, the indicator is attached to the first browser window that 116 * calls this method. When the window is closed, the indicator is moved to 117 * another browser window, if available, in no particular order. When there 118 * are no browser windows visible, the indicator is hidden. 119 * 120 * On Mac OS X, the indicator is initialized globally when this method is 121 * called for the first time. Subsequent calls have no effect. 122 * 123 * @param aBrowserWindow 124 * nsIDOMWindow object of the newly opened browser window to which the 125 * indicator may be attached. 126 */ 127 async registerIndicator(aBrowserWindow, aForcedBackend) { 128 if ( 129 aForcedBackend == "windows" || 130 (!aForcedBackend && gInterfaces.winTaskbar) 131 ) { 132 // On Windows, we show download progress on all browser windows 133 // of the appropriate filter (public or private). See bug 1418568 134 this.#windowsAttachIndicator(aBrowserWindow); 135 } else if (!this.#taskbarProgresses.size) { 136 // On non-Windows platforms, we only show download progress on one 137 // target at a time. 138 if ( 139 aForcedBackend == "mac" || 140 (!aForcedBackend && gInterfaces.macTaskbarProgress) 141 ) { 142 // On Mac OS X, we have to register the global indicator only once. 143 this.#taskbarProgresses.add(gInterfaces.macTaskbarProgress); 144 // Free the XPCOM reference on shutdown, to prevent detecting a leak. 145 Services.obs.addObserver(() => { 146 this.#taskbarProgresses.clear(); 147 gInterfaces.macTaskbarProgress = null; 148 }, "quit-application-granted"); 149 } else if ( 150 aForcedBackend == "linux" || 151 (!aForcedBackend && gInterfaces.gtkTaskbarProgress) 152 ) { 153 this.#taskbarProgresses.add(gInterfaces.gtkTaskbarProgress); 154 155 this.#attachGtkTaskbarProgress(aBrowserWindow); 156 } else { 157 // The taskbar indicator is not available on this platform. 158 return; 159 } 160 } 161 162 // Ensure that the DownloadSummary object will be created asynchronously. 163 if (!this.#summary) { 164 try { 165 let summary = await lazy.Downloads.getSummary(this.#filter); 166 167 if (!this.#summary) { 168 this.#summary = summary; 169 await this.#summary.addView(this); 170 } 171 } catch (e) { 172 console.error(e); 173 } 174 } 175 } 176 177 /** 178 * On Windows, attaches the taskbar indicator to the specified browser window. 179 */ 180 #windowsAttachIndicator(aWindow) { 181 // Activate the indicator on the specified window. 182 let { docShell } = aWindow.browsingContext.topChromeWindow; 183 let taskbarProgress = gInterfaces.winTaskbar.getTaskbarProgress(docShell); 184 this.#taskbarProgresses.add(taskbarProgress); 185 186 // If the DownloadSummary object has already been created, we should update 187 // the state of the new indicator, otherwise it will be updated as soon as 188 // the DownloadSummary view is registered. 189 if (this.#summary) { 190 this.onSummaryChanged(); 191 } 192 193 aWindow.addEventListener("unload", () => { 194 // Remove the taskbar progress indicator from the list of progress indicators 195 // to update. 196 this.#taskbarProgresses.delete(taskbarProgress); 197 }); 198 } 199 200 /** 201 * In gtk3, the window itself implements the progress interface. 202 */ 203 #attachGtkTaskbarProgress(aWindow) { 204 // Set the current window. 205 // For gtk, there's only one entry in #taskbarProgresses 206 let taskbarProgress = this.#taskbarProgresses.values().next().value; 207 taskbarProgress.setPrimaryWindow(aWindow); 208 209 // If the DownloadSummary object has already been created, we should update 210 // the state of the new indicator, otherwise it will be updated as soon as 211 // the DownloadSummary view is registered. 212 if (this.#summary) { 213 this.onSummaryChanged(); 214 } 215 216 aWindow.addEventListener("unload", () => { 217 // Locate another browser window, excluding the one being closed. 218 let browserWindow = this.#determineProgressRepresentative(); 219 if (browserWindow) { 220 // Move the progress indicator to the other browser window. 221 this.#attachGtkTaskbarProgress(browserWindow); 222 } else { 223 // The last browser window has been closed. We remove the reference to 224 // the taskbar progress object so that the indicator will be registered 225 // again on the next browser window that is opened. 226 this.#taskbarProgresses.clear(); 227 } 228 }); 229 } 230 231 /** 232 * Determines the next window to represent the downloads' progress. 233 */ 234 #determineProgressRepresentative() { 235 if (this.#filter == lazy.Downloads.ALL) { 236 return lazy.BrowserWindowTracker.getTopWindow(); 237 } 238 239 return lazy.BrowserWindowTracker.getTopWindow({ 240 private: this.#filter == lazy.Downloads.PRIVATE, 241 }); 242 } 243 244 reset() { 245 if (this.#summary) { 246 this.#summary.removeView(this); 247 } 248 249 this.#taskbarProgresses.clear(); 250 } 251 252 /** 253 * Updates progress for all nsITaskbarProgress objects. 254 * 255 * @param {number} aProgressState An nsTaskbarProgressState constant from nsITaskbarProgress 256 * @param {number} aCurrentValue Current progress value. 257 * @param {number} aMaxValue Maximum progress value 258 */ 259 updateProgress(aProgressState, aCurrentValue, aMaxValue) { 260 for (let progress of this.#taskbarProgresses) { 261 progress.setProgressState(aProgressState, aCurrentValue, aMaxValue); 262 } 263 } 264 265 // DownloadSummary view 266 267 onSummaryChanged() { 268 // If the last browser window has been closed, we have no indicator any more. 269 if (!this.#taskbarProgresses.size) { 270 return; 271 } 272 273 if (this.#summary.allHaveStopped || this.#summary.progressTotalBytes == 0) { 274 this.updateProgress(Ci.nsITaskbarProgress.STATE_NO_PROGRESS, 0, 0); 275 } else if (this.#summary.allUnknownSize) { 276 this.updateProgress(Ci.nsITaskbarProgress.STATE_INDETERMINATE, 0, 0); 277 } else { 278 // For a brief moment before completion, some download components may 279 // report more transferred bytes than the total number of bytes. Thus, 280 // ensure that we never break the expectations of the progress indicator. 281 let progressCurrentBytes = Math.min( 282 this.#summary.progressTotalBytes, 283 this.#summary.progressCurrentBytes 284 ); 285 this.updateProgress( 286 Ci.nsITaskbarProgress.STATE_NORMAL, 287 progressCurrentBytes, 288 this.#summary.progressTotalBytes 289 ); 290 } 291 } 292 } 293 294 const gDownloadsTaskbarInstances = {}; 295 296 export var DownloadsTaskbar = { 297 async registerIndicator(aWindow, aForcedBackend) { 298 let filter = this._selectFilterForWindow(aWindow, aForcedBackend); 299 if (!(filter in gDownloadsTaskbarInstances)) { 300 gDownloadsTaskbarInstances[filter] = new DownloadsTaskbarInstance(filter); 301 } 302 303 await gDownloadsTaskbarInstances[filter].registerIndicator( 304 aWindow, 305 aForcedBackend 306 ); 307 }, 308 309 _selectFilterForWindow(aWindow, aForcedBackend) { 310 if ( 311 aForcedBackend == "windows" || 312 (!aForcedBackend && gInterfaces.winTaskbar) 313 ) { 314 // On Windows, the private and public windows are separated. Plus, the native code 315 // supports multiple taskbar progresses at a time. Therefore, have a separate 316 // instance for each. 317 return lazy.PrivateBrowsingUtils.isBrowserPrivate(aWindow) 318 ? lazy.Downloads.PRIVATE 319 : lazy.Downloads.PUBLIC; 320 } 321 322 // macOS has a single application icon for all Firefox windows, both private and 323 // public. As a result, the Downloads.ALL filter should always be used. 324 // 325 // On GTK, taskbar progress is indicated by the _NET_WM_XAPP_PROGRESS property for 326 // X11, with no Wayland equivalent. Since X11 panels are likely to not group 327 // applications, it'd be better to have separate progress bars; however, the native 328 // code only supports a single progress bar right now. As such, don't try to have 329 // multiple. 330 return lazy.Downloads.ALL; 331 }, 332 333 resetBetweenTests() { 334 for (const key of Object.keys(gDownloadsTaskbarInstances)) { 335 gDownloadsTaskbarInstances[key].reset(); 336 delete gDownloadsTaskbarInstances[key]; 337 } 338 339 gInterfaces.macTaskbarProgress = null; 340 gInterfaces.winTaskbar = null; 341 gInterfaces.gtkTaskbarProgress = null; 342 }, 343 };