tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 };