tor-browser

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

RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs (19621B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  PlacesUIUtils: "moz-src:///browser/components/places/PlacesUIUtils.sys.mjs",
     11  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     12  SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
     13  SessionWindowUI: "resource:///modules/sessionstore/SessionWindowUI.sys.mjs",
     14 });
     15 
     16 ChromeUtils.defineLazyGetter(lazy, "l10n", () => {
     17  return new Localization(["browser/recentlyClosed.ftl"], true);
     18 });
     19 
     20 XPCOMUtils.defineLazyPreferenceGetter(
     21  lazy,
     22  "closedTabsFromAllWindowsEnabled",
     23  "browser.sessionstore.closedTabsFromAllWindows"
     24 );
     25 
     26 XPCOMUtils.defineLazyPreferenceGetter(
     27  lazy,
     28  "closedTabsFromClosedWindowsEnabled",
     29  "browser.sessionstore.closedTabsFromClosedWindows"
     30 );
     31 
     32 /**
     33 * @returns {Map<string, TabGroupStateData>}
     34 *   Map of closed tab groups keyed by tab group ID
     35 */
     36 function getClosedTabGroupsById() {
     37  const closedTabGroups = lazy.SessionStore.getClosedTabGroups();
     38  const closedTabGroupsById = new Map();
     39  closedTabGroups.forEach(tabGroup =>
     40    closedTabGroupsById.set(tabGroup.id, tabGroup)
     41  );
     42  return closedTabGroupsById;
     43 }
     44 
     45 export var RecentlyClosedTabsAndWindowsMenuUtils = {
     46  /**
     47   * Builds up a document fragment of UI items for the recently closed tabs.
     48   *
     49   * @param   {Window} aWindow
     50   *          The window that the tabs were closed in.
     51   * @param   {"menuitem"|"toolbarbutton"} aTagName
     52   *          The tag name that will be used when creating the UI items.
     53   * @returns {DocumentFragment} A document fragment with UI items for each recently closed tab.
     54   */
     55  getTabsFragment(aWindow, aTagName) {
     56    let doc = aWindow.document;
     57    const isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(aWindow);
     58    const fragment = doc.createDocumentFragment();
     59    let isEmpty = true;
     60 
     61    if (
     62      lazy.SessionStore.getClosedTabCount({
     63        sourceWindow: aWindow,
     64      })
     65    ) {
     66      isEmpty = false;
     67 
     68      const browserWindows = lazy.closedTabsFromAllWindowsEnabled
     69        ? lazy.SessionStore.getWindows(aWindow)
     70        : [aWindow];
     71      const closedTabSets = [];
     72      for (const win of browserWindows) {
     73        closedTabSets.push(lazy.SessionStore.getClosedTabDataForWindow(win));
     74      }
     75 
     76      if (
     77        !isPrivate &&
     78        lazy.closedTabsFromClosedWindowsEnabled &&
     79        lazy.SessionStore.getClosedTabCountFromClosedWindows()
     80      ) {
     81        closedTabSets.push(
     82          lazy.SessionStore.getClosedTabDataFromClosedWindows()
     83        );
     84      }
     85 
     86      const closedTabGroupsById = getClosedTabGroupsById();
     87 
     88      let currentGroupId = null;
     89 
     90      closedTabSets.forEach(tabSet => {
     91        tabSet.forEach((tab, index) => {
     92          let groupId = tab.closedInTabGroupId;
     93          if (groupId && closedTabGroupsById.has(groupId)) {
     94            if (groupId != currentGroupId) {
     95              // This is the first tab in a new group. Push all the tabs into the menu.
     96              // Note that the calls to the createTabGroup methods below use the
     97              // tab itself as a closed data source, since it will always contain
     98              // one of either sourceClosedId or sourceWindowId.
     99              if (aTagName == "menuitem") {
    100                createTabGroupSubmenu(
    101                  closedTabGroupsById.get(groupId),
    102                  index,
    103                  tab,
    104                  doc,
    105                  fragment
    106                );
    107              } else {
    108                createTabGroupSubpanel(
    109                  closedTabGroupsById.get(groupId),
    110                  index,
    111                  tab,
    112                  doc,
    113                  fragment
    114                );
    115              }
    116 
    117              currentGroupId = groupId;
    118            } else {
    119              // We have already seen this group. Ignore.
    120            }
    121          } else {
    122            createEntry(aTagName, false, index, tab, doc, tab.title, fragment);
    123            currentGroupId = null;
    124          }
    125        });
    126      });
    127    }
    128 
    129    if (!isEmpty) {
    130      createRestoreAllEntry(
    131        doc,
    132        fragment,
    133        false,
    134        aTagName == "menuitem"
    135          ? "recently-closed-menu-reopen-all-tabs"
    136          : "recently-closed-panel-reopen-all-tabs",
    137        aTagName
    138      );
    139    }
    140    return fragment;
    141  },
    142 
    143  /**
    144   * Builds up a document fragment of UI items for the recently closed windows.
    145   *
    146   * @param   {Window} aWindow
    147   *          A window that can be used to create the elements and document fragment.
    148   * @param   {"menuitem"|"toolbarbutton"} aTagName
    149   *          The tag name that will be used when creating the UI items.
    150   * @returns {DocumentFragment} A document fragment with UI items for each recently closed window.
    151   */
    152  getWindowsFragment(aWindow, aTagName) {
    153    let closedWindowData = lazy.SessionStore.getClosedWindowData();
    154    let doc = aWindow.document;
    155    let fragment = doc.createDocumentFragment();
    156    if (closedWindowData.length) {
    157      for (let i = 0; i < closedWindowData.length; i++) {
    158        const { selected, tabs, title } = closedWindowData[i];
    159        const selectedTab = tabs[selected - 1];
    160        if (selectedTab) {
    161          const menuLabel = lazy.l10n.formatValueSync(
    162            "recently-closed-undo-close-window-label",
    163            { tabCount: tabs.length - 1, winTitle: title }
    164          );
    165          createEntry(aTagName, true, i, selectedTab, doc, menuLabel, fragment);
    166        }
    167      }
    168 
    169      createRestoreAllEntry(
    170        doc,
    171        fragment,
    172        true,
    173        aTagName == "menuitem"
    174          ? "recently-closed-menu-reopen-all-windows"
    175          : "recently-closed-panel-reopen-all-windows",
    176        aTagName
    177      );
    178    }
    179    return fragment;
    180  },
    181 
    182  /**
    183   * Handle a command event to re-open all closed tabs
    184   *
    185   * @param aEvent
    186   *        The command event when the user clicks the restore all menu item
    187   */
    188  onRestoreAllTabsCommand(aEvent) {
    189    const currentWindow = aEvent.target.ownerGlobal;
    190    const browserWindows = lazy.closedTabsFromAllWindowsEnabled
    191      ? lazy.SessionStore.getWindows(currentWindow)
    192      : [currentWindow];
    193    const closedTabGroupsById = getClosedTabGroupsById();
    194 
    195    const undoAllInTabData = function (tabData, tabMethod, tabGroupMethod) {
    196      while (tabData.length) {
    197        let currentTabGroupId = tabData[0].state.groupId;
    198 
    199        if (currentTabGroupId && closedTabGroupsById.has(currentTabGroupId)) {
    200          let currentTabGroup = closedTabGroupsById.get(currentTabGroupId);
    201          let splicedTabs = tabData.splice(0, currentTabGroup.tabs.length);
    202          tabGroupMethod(splicedTabs);
    203        } else {
    204          let splicedTabs = tabData.splice(0, 1);
    205          tabMethod(splicedTabs[0]);
    206        }
    207      }
    208    };
    209 
    210    for (const sourceWindow of browserWindows) {
    211      let tabData = lazy.SessionStore.getClosedTabDataForWindow(sourceWindow);
    212 
    213      undoAllInTabData(
    214        tabData,
    215        _tabs => {
    216          lazy.SessionStore.undoCloseTab(sourceWindow, 0, currentWindow);
    217        },
    218        tabs => {
    219          lazy.SessionStore.undoCloseTabGroup(
    220            sourceWindow,
    221            tabs[0].state.groupId,
    222            currentWindow
    223          );
    224        }
    225      );
    226    }
    227    if (lazy.closedTabsFromClosedWindowsEnabled) {
    228      let tabData = lazy.SessionStore.getClosedTabDataFromClosedWindows();
    229 
    230      undoAllInTabData(
    231        tabData,
    232        tab => {
    233          lazy.SessionStore.undoCloseTabFromClosedWindow(
    234            { sourceClosedId: tab.sourceClosedId },
    235            tab.closedId,
    236            currentWindow
    237          );
    238        },
    239        tabs => {
    240          lazy.SessionStore.undoCloseTabGroup(
    241            { sourceClosedId: tabs[0].sourceClosedId },
    242            tabs[0].state.groupId,
    243            currentWindow
    244          );
    245        }
    246      );
    247    }
    248  },
    249 
    250  /**
    251   * Handle a command event to re-open all closed windows
    252   *
    253   * @param aEvent
    254   *        The command event when the user clicks the restore all menu item
    255   */
    256  onRestoreAllWindowsCommand() {
    257    const closedData = lazy.SessionStore.getClosedWindowData();
    258    for (const { closedId } of closedData) {
    259      lazy.SessionStore.undoCloseById(closedId);
    260    }
    261  },
    262 
    263  /**
    264   * Re-open a closed tab and put it to the end of the tab strip.
    265   * Used for a middle click.
    266   *
    267   * @param aEvent
    268   *        The event when the user clicks the menu item
    269   */
    270  _undoCloseMiddleClick(aEvent) {
    271    if (aEvent.button != 1) {
    272      return;
    273    }
    274    if (aEvent.originalTarget.hasAttribute("source-closed-id")) {
    275      lazy.SessionStore.undoClosedTabFromClosedWindow(
    276        {
    277          sourceClosedId:
    278            aEvent.originalTarget.getAttribute("source-closed-id"),
    279        },
    280        aEvent.originalTarget.getAttribute("value")
    281      );
    282    } else {
    283      lazy.SessionWindowUI.undoCloseTab(
    284        aEvent.view,
    285        aEvent.originalTarget.getAttribute("value"),
    286        aEvent.originalTarget.getAttribute("source-window-id")
    287      );
    288    }
    289    aEvent.view.gBrowser.moveTabToEnd();
    290    let ancestorPanel = aEvent.target.closest("panel");
    291    if (ancestorPanel) {
    292      ancestorPanel.hidePopup();
    293    }
    294  },
    295 };
    296 
    297 /**
    298 * @param {Element} element
    299 * @param {TabGroupStateData} tabGroup
    300 */
    301 function setTabGroupColorProperties(element, tabGroup) {
    302  element.style.setProperty(
    303    "--tab-group-color",
    304    `var(--tab-group-color-${tabGroup.color})`
    305  );
    306  element.style.setProperty(
    307    "--tab-group-color-invert",
    308    `var(--tab-group-color-${tabGroup.color}-invert)`
    309  );
    310  element.style.setProperty(
    311    "--tab-group-color-pale",
    312    `var(--tab-group-color-${tabGroup.color}-pale)`
    313  );
    314 }
    315 
    316 /**
    317 * Creates a `menuitem` for the tab group that will expand to a newly
    318 * created submenu of the tab group's tab contents when selected.
    319 *
    320 * @param {TabGroupStateData} aTabGroup
    321 *        Session store state for the closed tab group.
    322 * @param {number} aIndex
    323 *        The index of the first tab in the tab group, relative to the tab strip.
    324 * @param {{sourceClosedId: number}|{sourceWindowId: string}} aSource
    325 *        An object that can be resolved to a closed data source.
    326 * @param {Document} aDocument
    327 *        A document object that can be used to create the entry.
    328 * @param {DocumentFragment} aFragment
    329 *        The DOM fragment that the created entry will be in.
    330 */
    331 function createTabGroupSubmenu(
    332  aTabGroup,
    333  aIndex,
    334  aSource,
    335  aDocument,
    336  aFragment
    337 ) {
    338  let element = aDocument.createXULElement("menu");
    339  if (aTabGroup.name) {
    340    element.setAttribute("label", aTabGroup.name);
    341  } else {
    342    aDocument.l10n.setAttributes(element, "tab-context-unnamed-group");
    343  }
    344 
    345  element.classList.add("menu-iconic", "tab-group-icon");
    346  setTabGroupColorProperties(element, aTabGroup);
    347 
    348  let menuPopup = aDocument.createXULElement("menupopup");
    349 
    350  aTabGroup.tabs.forEach(tab => {
    351    createEntry(
    352      "menuitem",
    353      false,
    354      aIndex,
    355      tab,
    356      aDocument,
    357      tab.title,
    358      menuPopup
    359    );
    360    aIndex++;
    361  });
    362 
    363  menuPopup.appendChild(aDocument.createXULElement("menuseparator"));
    364 
    365  let reopenTabGroupItem = aDocument.createXULElement("menuitem");
    366  aDocument.l10n.setAttributes(
    367    reopenTabGroupItem,
    368    "tab-context-reopen-tab-group"
    369  );
    370  reopenTabGroupItem.addEventListener("command", () => {
    371    lazy.SessionStore.undoCloseTabGroup(aSource, aTabGroup.id);
    372  });
    373  menuPopup.appendChild(reopenTabGroupItem);
    374 
    375  element.appendChild(menuPopup);
    376  aFragment.appendChild(element);
    377 }
    378 
    379 /**
    380 * Creates a `toolbarbutton` for the tab group that will navigate to a newly
    381 * created subpanel of the tab group's tab contents when selected.
    382 *
    383 * @param {TabGroupStateData} aTabGroup
    384 *        Session store state for the closed tab group.
    385 * @param {number} aIndex
    386 *        The index of the first tab in the tab group, relative to the tab strip.
    387 * @param {{sourceClosedId: number}|{sourceWindowId: string}} aSource
    388 *        An object that can be resolved to a closed data source.
    389 * @param {Document} aDocument
    390 *        A document object that can be used to create the entry.
    391 * @param {DocumentFragment} aFragment
    392 *        The DOM fragment that the created entry will be in.
    393 */
    394 function createTabGroupSubpanel(
    395  aTabGroup,
    396  aIndex,
    397  aSource,
    398  aDocument,
    399  aFragment
    400 ) {
    401  let element = aDocument.createXULElement("toolbarbutton");
    402  if (aTabGroup.name) {
    403    element.setAttribute("label", aTabGroup.name);
    404  } else {
    405    aDocument.l10n.setAttributes(element, "tab-context-unnamed-group");
    406  }
    407 
    408  element.classList.add(
    409    "subviewbutton",
    410    "subviewbutton-iconic",
    411    "subviewbutton-nav",
    412    "tab-group-icon"
    413  );
    414  element.setAttribute("closemenu", "none");
    415  setTabGroupColorProperties(element, aTabGroup);
    416 
    417  const panelviewId = `closed-tabs-tab-group-${aTabGroup.id}`;
    418  let panelview = aDocument.getElementById(panelviewId);
    419 
    420  if (panelview) {
    421    // panelviews get moved around the DOM by PanelMultiView, so if it still
    422    // exists, remove it so we can rebuild a new panelview
    423    panelview.remove();
    424  }
    425 
    426  panelview = aDocument.createXULElement("panelview");
    427  panelview.id = panelviewId;
    428  let panelBody = aDocument.createXULElement("vbox");
    429  panelBody.className = "panel-subview-body";
    430 
    431  aTabGroup.tabs.forEach(tab => {
    432    createEntry(
    433      "toolbarbutton",
    434      false,
    435      aIndex,
    436      tab,
    437      aDocument,
    438      tab.title,
    439      panelBody
    440    );
    441    aIndex++;
    442  });
    443 
    444  panelview.appendChild(panelBody);
    445  panelview.appendChild(aDocument.createXULElement("toolbarseparator"));
    446 
    447  let reopenTabGroupItem = aDocument.createXULElement("toolbarbutton");
    448  aDocument.l10n.setAttributes(
    449    reopenTabGroupItem,
    450    "tab-context-reopen-tab-group"
    451  );
    452  reopenTabGroupItem.classList.add(
    453    "reopentabgroupitem",
    454    "subviewbutton",
    455    "panel-subview-footer-button"
    456  );
    457  reopenTabGroupItem.addEventListener("command", () => {
    458    lazy.SessionStore.undoCloseTabGroup(aSource, aTabGroup.id);
    459  });
    460 
    461  panelview.appendChild(reopenTabGroupItem);
    462 
    463  element.addEventListener("command", () => {
    464    aDocument.ownerGlobal.PanelUI.showSubView(panelview.id, element);
    465  });
    466 
    467  aFragment.appendChild(panelview);
    468  aFragment.appendChild(element);
    469 }
    470 
    471 /**
    472 * Create a UI entry for a recently closed tab, tab group, or window.
    473 *
    474 * @param {"menuitem"|"toolbarbutton"} aTagName
    475 *        the tag name that will be used when creating the UI entry
    476 * @param {boolean} aIsWindowsFragment
    477 *        whether or not this entry will represent a closed window
    478 * @param {number} aIndex
    479 *        the index of the closed tab
    480 * @param {TabStateData} aClosedTab
    481 *        the closed tab
    482 * @param {Document} aDocument
    483 *        a document that can be used to create the entry
    484 * @param {string} aMenuLabel
    485 *        the label the created entry will have
    486 * @param {DocumentFragment} aFragment
    487 *        the fragment the created entry will be in
    488 */
    489 function createEntry(
    490  aTagName,
    491  aIsWindowsFragment,
    492  aIndex,
    493  aClosedTab,
    494  aDocument,
    495  aMenuLabel,
    496  aFragment
    497 ) {
    498  let element = aDocument.createXULElement(aTagName);
    499 
    500  element.setAttribute("label", aMenuLabel);
    501  if (aClosedTab.image) {
    502    const iconURL = lazy.PlacesUIUtils.getImageURL(aClosedTab.image);
    503    element.setAttribute("image", ChromeUtils.encodeURIForSrcset(iconURL));
    504  }
    505 
    506  if (aIsWindowsFragment) {
    507    element.addEventListener("command", () =>
    508      lazy.SessionWindowUI.undoCloseWindow(aIndex)
    509    );
    510  } else if (typeof aClosedTab.sourceClosedId == "number") {
    511    // sourceClosedId is used to look up the closed window to remove it when the tab is restored
    512    let sourceClosedId = aClosedTab.sourceClosedId;
    513    element.setAttribute("source-closed-id", sourceClosedId);
    514    element.setAttribute("value", aClosedTab.closedId);
    515    element.addEventListener(
    516      "command",
    517      () => {
    518        lazy.SessionStore.undoClosedTabFromClosedWindow(
    519          { sourceClosedId },
    520          aClosedTab.closedId
    521        );
    522      },
    523      { once: true }
    524    );
    525  } else {
    526    // sourceWindowId is used to look up the closed tab entry to remove it when it is restored
    527    let sourceWindowId = aClosedTab.sourceWindowId;
    528    element.setAttribute("value", aIndex);
    529    element.setAttribute("source-window-id", sourceWindowId);
    530    element.addEventListener("command", event =>
    531      lazy.SessionWindowUI.undoCloseTab(
    532        event.target.ownerGlobal,
    533        aIndex,
    534        sourceWindowId
    535      )
    536    );
    537  }
    538 
    539  if (aTagName == "menuitem") {
    540    element.setAttribute(
    541      "class",
    542      "menuitem-iconic bookmark-item menuitem-with-favicon"
    543    );
    544  } else if (aTagName == "toolbarbutton") {
    545    element.setAttribute(
    546      "class",
    547      "subviewbutton subviewbutton-iconic bookmark-item"
    548    );
    549  }
    550 
    551  // Set the targetURI attribute so it will be shown in tooltip.
    552  // SessionStore uses one-based indexes, so we need to normalize them.
    553  let tabData;
    554  tabData = aIsWindowsFragment ? aClosedTab : aClosedTab.state;
    555  let activeIndex = (tabData.index || tabData.entries.length) - 1;
    556  if (activeIndex >= 0 && tabData.entries[activeIndex]) {
    557    element.setAttribute("targetURI", tabData.entries[activeIndex].url);
    558  }
    559 
    560  // Windows don't open in new tabs and menuitems dispatch command events on
    561  // middle click, so we only need to manually handle middle clicks for
    562  // toolbarbuttons.
    563  if (!aIsWindowsFragment && aTagName != "menuitem") {
    564    element.addEventListener(
    565      "click",
    566      RecentlyClosedTabsAndWindowsMenuUtils._undoCloseMiddleClick
    567    );
    568  }
    569 
    570  if (aIndex == 0) {
    571    element.setAttribute(
    572      "key",
    573      aIsWindowsFragment
    574        ? "key_undoCloseWindow"
    575        : "key_restoreLastClosedTabOrWindowOrSession"
    576    );
    577  }
    578 
    579  aFragment.appendChild(element);
    580 }
    581 
    582 /**
    583 * Create an entry to restore all closed windows or tabs.
    584 * For menus, adds a menu separator and a menu item.
    585 * For toolbar panels, adds a toolbar button only and expects
    586 * CustomizableWidgets.sys.mjs to add its own separator elsewhere in the DOM
    587 *
    588 * @param {Document} aDocument
    589 *        a document that can be used to create the entry
    590 * @param {DocumentFragment} aFragment
    591 *        the fragment the created entry will be in
    592 * @param {boolean} aIsWindowsFragment
    593 *        whether or not this entry will represent a closed window
    594 * @param {string} aRestoreAllLabel
    595 *        which localizable string to use for the entry
    596 * @param {"menuitem"|"toolbarbutton"} aTagName
    597 *        the tag name that will be used when creating the UI entry
    598 */
    599 function createRestoreAllEntry(
    600  aDocument,
    601  aFragment,
    602  aIsWindowsFragment,
    603  aRestoreAllLabel,
    604  aTagName
    605 ) {
    606  let restoreAllElements = aDocument.createXULElement(aTagName);
    607  restoreAllElements.classList.add("restoreallitem");
    608 
    609  if (aTagName == "toolbarbutton") {
    610    restoreAllElements.classList.add(
    611      "subviewbutton",
    612      "panel-subview-footer-button"
    613    );
    614  }
    615 
    616  // We cannot use aDocument.l10n.setAttributes because the menubar label is not
    617  // updated in time and displays a blank string (see Bug 1691553).
    618  restoreAllElements.setAttribute(
    619    "label",
    620    lazy.l10n.formatValueSync(aRestoreAllLabel)
    621  );
    622 
    623  restoreAllElements.addEventListener(
    624    "command",
    625    aIsWindowsFragment
    626      ? RecentlyClosedTabsAndWindowsMenuUtils.onRestoreAllWindowsCommand
    627      : RecentlyClosedTabsAndWindowsMenuUtils.onRestoreAllTabsCommand
    628  );
    629 
    630  if (aTagName == "menuitem") {
    631    aFragment.appendChild(aDocument.createXULElement("menuseparator"));
    632  }
    633 
    634  aFragment.appendChild(restoreAllElements);
    635 }