tor-browser

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

TaskbarTabsWindowManager.sys.mjs (10541B)


      1 /* vim: se cin sw=2 ts=2 et filetype=javascript :
      2 * This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 
      8 const kTaskbarTabsWindowFeatures =
      9  "titlebar,close,toolbar,location,personalbar=no,status,menubar=no,resizable,minimizable,scrollbars";
     10 
     11 let lazy = {};
     12 
     13 ChromeUtils.defineESModuleGetters(lazy, {
     14  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     15  TaskbarTabsUtils: "resource:///modules/taskbartabs/TaskbarTabsUtils.sys.mjs",
     16 });
     17 
     18 XPCOMUtils.defineLazyServiceGetters(lazy, {
     19  WindowsUIUtils: ["@mozilla.org/windows-ui-utils;1", Ci.nsIWindowsUIUtils],
     20  WinTaskbar: ["@mozilla.org/windows-taskbar;1", Ci.nsIWinTaskbar],
     21 });
     22 
     23 ChromeUtils.defineLazyGetter(lazy, "logConsole", () => {
     24  return console.createInstance({
     25    prefix: "TaskbarTabs",
     26    maxLogLevel: "Warn",
     27  });
     28 });
     29 
     30 /**
     31 * Manager for the lifetimes of Taskbar Tab windows.
     32 */
     33 export class TaskbarTabsWindowManager {
     34  // Map from the taskbar tab ID to a Set of window IDs. Use #trackWindow
     35  // and #untrackWindow.
     36  #openWindows = new Map();
     37  // Map from the tab browser permanent key to originating window ID.
     38  #tabOriginMap = new WeakMap();
     39 
     40  /**
     41   * Moves an existing browser tab into a Taskbar Tab.
     42   *
     43   * @param {TaskbarTab} aTaskbarTab - The Taskbar Tab to replace the window with.
     44   * @param {MozTabbrowserTab} aTab - The tab to adopt as a Taskbar Tab.
     45   * @returns {Promise<DOMWindow>} The newly created Taskbar Tab window.
     46   */
     47  async replaceTabWithWindow(aTaskbarTab, aTab) {
     48    let originWindow = aTab.ownerGlobal;
     49 
     50    Glean.webApp.moveToTaskbar.record({});
     51 
     52    // Save the parent window of this tab, so we can revert back if needed.
     53    let tabId = getTabId(aTab);
     54    let windowId = getWindowId(originWindow);
     55 
     56    let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
     57      Ci.nsIWritablePropertyBag2
     58    );
     59    extraOptions.setPropertyAsAString("taskbartab", aTaskbarTab.id);
     60 
     61    let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
     62    args.appendElement(aTab);
     63    args.appendElement(extraOptions);
     64 
     65    this.#tabOriginMap.set(tabId, windowId);
     66    return await this.#openWindow(aTaskbarTab, args);
     67  }
     68 
     69  /**
     70   * Opens a new Taskbar Tab Window.
     71   *
     72   * @param {TaskbarTab} aTaskbarTab - The Taskbar Tab to open.
     73   * @returns {Promise<DOMWindow>} The newly-created Taskbar Tab window.
     74   */
     75  async openWindow(aTaskbarTab) {
     76    let url = Cc["@mozilla.org/supports-string;1"].createInstance(
     77      Ci.nsISupportsString
     78    );
     79    url.data = aTaskbarTab.startUrl;
     80 
     81    let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
     82      Ci.nsIWritablePropertyBag2
     83    );
     84    extraOptions.setPropertyAsAString("taskbartab", aTaskbarTab.id);
     85 
     86    let userContextId = Cc["@mozilla.org/supports-PRUint32;1"].createInstance(
     87      Ci.nsISupportsPRUint32
     88    );
     89    userContextId.data = aTaskbarTab.userContextId;
     90 
     91    let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
     92    args.appendElement(url);
     93    args.appendElement(extraOptions);
     94    args.appendElement(null);
     95    args.appendElement(null);
     96    args.appendElement(undefined);
     97    args.appendElement(userContextId);
     98    args.appendElement(null);
     99    args.appendElement(null);
    100    args.appendElement(Services.scriptSecurityManager.getSystemPrincipal());
    101 
    102    return await this.#openWindow(aTaskbarTab, args);
    103  }
    104 
    105  /**
    106   * Handles common window opening behavior for Taskbar Tabs.
    107   *
    108   * @param {TaskbarTab} aTaskbarTab - The Taskbar Tab associated to the window.
    109   * @param {nsIMutableArray} aArgs - `args` to pass to the opening window.
    110   * @returns {Promise<DOMWindow>} Resolves once window has opened and tab count
    111   * has been incremented.
    112   */
    113  async #openWindow(aTaskbarTab, aArgs) {
    114    let url = Services.io.newURI(aTaskbarTab.startUrl);
    115    let imgPromise = lazy.TaskbarTabsUtils.getFavicon(url);
    116 
    117    let win = await lazy.BrowserWindowTracker.promiseOpenWindow({
    118      args: aArgs,
    119      features: kTaskbarTabsWindowFeatures,
    120      all: false,
    121    });
    122 
    123    imgPromise.then(imgContainer =>
    124      lazy.WindowsUIUtils.setWindowIcon(win, imgContainer, imgContainer)
    125    );
    126 
    127    this.#trackWindow(aTaskbarTab.id, win);
    128 
    129    lazy.WinTaskbar.setGroupIdForWindow(win, aTaskbarTab.id);
    130 
    131    this.#attachWindowFocusTelemetry(win);
    132    Glean.webApp.activate.record({});
    133 
    134    win.focus();
    135 
    136    win.gBrowser.tabs.forEach(tab => {
    137      const browser = win.gBrowser.getBrowserForTab(tab);
    138      browser.browsingContext.displayMode = "minimal-ui";
    139    });
    140 
    141    return win;
    142  }
    143 
    144  /**
    145   * Adds the window to the set of windows open within the taskbar tab.
    146   * The window will automatically be removed when the window closes if
    147   * it hasn't been untracked already.
    148   *
    149   * @param {string} aId Taskbar Tab ID that the window should be assigned to.
    150   * @param {DOMWindow} aWindow Window to track.
    151   */
    152  #trackWindow(aId, aWindow) {
    153    let openWindows = this.#openWindows.get(aId);
    154    if (typeof openWindows === "undefined") {
    155      openWindows = new Set();
    156      this.#openWindows.set(aId, openWindows);
    157    }
    158 
    159    openWindows.add(getWindowId(aWindow));
    160    aWindow.addEventListener("unload", _e => this.#untrackWindow(aId, aWindow));
    161  }
    162 
    163  /**
    164   * Remove the window from the set of windows open within the taskbar tab.
    165   * This function is idempotent.
    166   *
    167   * @param {string} aId Taskbar Tab ID that the window should be assigned to.
    168   * @param {DOMWindow} aWindow Window to track.
    169   */
    170  #untrackWindow(aId, aWindow) {
    171    let openWindows = this.#openWindows.get(aId);
    172    if (typeof openWindows === "undefined") {
    173      // If it is undefined, the window wasn't being tracked anyways.
    174      return;
    175    }
    176 
    177    openWindows.delete(getWindowId(aWindow));
    178    if (openWindows.size === 0) {
    179      // Avoid leaking entries in the map.
    180      this.#openWindows.delete(aId);
    181    }
    182  }
    183 
    184  #attachWindowFocusTelemetry(aWindow) {
    185    let timerId = null;
    186 
    187    function focused() {
    188      if (timerId == null) {
    189        timerId = Glean.webApp.usageTime.start();
    190      }
    191    }
    192 
    193    function blur() {
    194      if (timerId != null) {
    195        Glean.webApp.usageTime.stopAndAccumulate(timerId);
    196      }
    197 
    198      timerId = null;
    199    }
    200 
    201    aWindow.addEventListener("focus", focused);
    202    aWindow.addEventListener("blur", blur);
    203    aWindow.addEventListener("unload", blur);
    204 
    205    // The window might already have been focused, and at any rate it'll get
    206    // focused. Forcefully trigger focused now to account for that.
    207    focused();
    208  }
    209 
    210  /**
    211   * Reverts a web app to a tab in a regular Firefox window. We will try to use
    212   * the window the taskbar tab originated from, if that's not avaliable, we
    213   * will use the most recently active window. If no window is avalaible, a new
    214   * one will be opened.
    215   *
    216   * @param {DOMWindow} aWindow - A Taskbar Tab window.
    217   */
    218  async ejectWindow(aWindow) {
    219    lazy.logConsole.info("Ejecting window from Taskbar Tabs.");
    220 
    221    let taskbarTabId = lazy.TaskbarTabsUtils.getTaskbarTabIdFromWindow(aWindow);
    222    if (!taskbarTabId) {
    223      throw new Error("No Taskbar Tab ID found on window.");
    224    } else {
    225      lazy.logConsole.debug(`Taskbar Tab ID is ${taskbarTabId}`);
    226    }
    227 
    228    let windowList = lazy.BrowserWindowTracker.getOrderedWindows({
    229      private: false,
    230    });
    231 
    232    Glean.webApp.eject.record({});
    233 
    234    // A Taskbar Tab should only contain one tab, but iterate over the browser's
    235    // tabs just in case one snuck in.
    236    for (const tab of aWindow.gBrowser.tabs) {
    237      let tabId = getTabId(tab);
    238      let originWindowId = this.#tabOriginMap.get(tabId);
    239 
    240      let win =
    241        // Find the originating window for the Taskbar Tab if it still exists.
    242        windowList.find(window => {
    243          let windowId = getWindowId(window);
    244          let matching = windowId === originWindowId;
    245          if (matching) {
    246            lazy.logConsole.debug(
    247              `Ejecting into originating window: ${windowId}`
    248            );
    249          }
    250          return matching;
    251        });
    252 
    253      if (!win) {
    254        // Otherwise the most recent non-Taskbar Tabs window interacted with.
    255        win = lazy.BrowserWindowTracker.getTopWindow({
    256          private: false,
    257        });
    258 
    259        if (win) {
    260          lazy.logConsole.debug(`Ejecting into top window.`);
    261        }
    262      }
    263 
    264      let newTab;
    265      if (win) {
    266        // Set this tab to the last tab position of the window.
    267        newTab = win.gBrowser.adoptTab(tab, {
    268          tabIndex: win.gBrowser.openTabs.length,
    269          selectTab: true,
    270        });
    271      } else {
    272        lazy.logConsole.debug(
    273          "No originating or existing browser window found, ejecting into newly created window."
    274        );
    275        win = await lazy.BrowserWindowTracker.promiseOpenWindow({ args: tab });
    276        newTab = win.gBrowser.tabs[0];
    277      }
    278 
    279      win.focus();
    280 
    281      let browser = win.gBrowser.getBrowserForTab(newTab);
    282      browser.browsingContext.displayMode = "browser";
    283 
    284      this.#tabOriginMap.delete(tabId);
    285    }
    286 
    287    this.#untrackWindow(taskbarTabId, aWindow);
    288  }
    289 
    290  /**
    291   * Returns a count of the current windows associated to a Taskbar Tab.
    292   *
    293   * @param {string} aId - The Taskbar Tab ID.
    294   * @returns {integer} Count of windows associated to the Taskbar Tab ID.
    295   */
    296  getCountForId(aId) {
    297    return this.#openWindows.get(aId)?.size ?? 0;
    298  }
    299 
    300  /**
    301   * Utility function to mock `nsIWindowsUIUtils`.
    302   *
    303   * @param {nsIWindowsUIUtils} mock - A mock of nsIWindowsUIUtils.
    304   */
    305  testOnlyMockUIUtils(mock) {
    306    if (!Cu.isInAutomation) {
    307      throw new Error("Can only mock utils in automation.");
    308    }
    309    // eslint-disable-next-line mozilla/valid-lazy
    310    Object.defineProperty(lazy, "WindowsUIUtils", {
    311      get() {
    312        if (mock) {
    313          return mock;
    314        }
    315        return Cc["@mozilla.org/windows-ui-utils;1"].getService(
    316          Ci.nsIWindowsUIUtils
    317        );
    318      },
    319    });
    320  }
    321 }
    322 
    323 /**
    324 * Retrieves the browser tab's ID.
    325 *
    326 * @param {MozTabbrowserTab} aTab - Tab to retrieve the ID from.
    327 * @returns {object} The permanent key identifying the tab.
    328 */
    329 function getTabId(aTab) {
    330  return aTab.permanentKey;
    331 }
    332 
    333 /**
    334 * Retrieves the window ID.
    335 *
    336 * @param {DOMWindow} aWindow
    337 * @returns {string} A unique string identifying the window.
    338 */
    339 function getWindowId(aWindow) {
    340  return aWindow.docShell.outerWindowID;
    341 }