tor-browser

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

ext-tabGroups.js (9188B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 
      6 ChromeUtils.defineESModuleGetters(this, {
      7  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
      8 });
      9 
     10 var { ExtensionError } = ExtensionUtils;
     11 
     12 const spellColour = color => (color === "grey" ? "gray" : color);
     13 
     14 /**
     15 * @param {MozTabbrowserTabGroup} group Group to move.
     16 * @param {DOMWindow} window Browser window to move to.
     17 * @param {integer} index The desired position of the group within the window
     18 * @returns {integer} The tab index that the group should move to, such that
     19 *   after the move operation, the group's position is at the given index.
     20 */
     21 function adjustIndexForMove(group, window, index) {
     22  let tabIndex = index < 0 ? window.gBrowser.tabs.length : index;
     23  if (group.ownerGlobal === window) {
     24    let group_tabs = group.tabs;
     25    if (tabIndex > group_tabs[0]._tPos) {
     26      // When group is moving to a higher index, we need to increase the
     27      // index to account for the fact that the act of moving tab groups
     28      // causes all following tabs to have a decreased index.
     29      tabIndex += group_tabs.length;
     30    }
     31  }
     32  tabIndex = Math.min(tabIndex, window.gBrowser.tabs.length);
     33 
     34  let prevTab = tabIndex > 0 ? window.gBrowser.tabs.at(tabIndex - 1) : null;
     35  let nextTab = window.gBrowser.tabs.at(tabIndex);
     36  if (nextTab?.pinned) {
     37    throw new ExtensionError(
     38      "Cannot move the group to an index that is in the middle of pinned tabs."
     39    );
     40  }
     41  if (prevTab && nextTab?.group && prevTab.group === nextTab.group) {
     42    throw new ExtensionError(
     43      "Cannot move the group to an index that is in the middle of another group."
     44    );
     45  }
     46 
     47  return tabIndex;
     48 }
     49 
     50 this.tabGroups = class extends ExtensionAPIPersistent {
     51  queryGroups({ collapsed, color, title, windowId } = {}) {
     52    color = spellColour(color);
     53    let glob = title != null && new MatchGlob(title);
     54    let window =
     55      windowId != null && windowTracker.getWindow(windowId, null, false);
     56    return windowTracker
     57      .browserWindows()
     58      .filter(
     59        win =>
     60          this.extension.canAccessWindow(win) &&
     61          (windowId == null || win === window)
     62      )
     63      .flatMap(win => win.gBrowser.tabGroups)
     64      .filter(
     65        group =>
     66          (collapsed == null || group.collapsed === collapsed) &&
     67          (color == null || group.color === color) &&
     68          (title == null || glob.matches(group.name))
     69      );
     70  }
     71 
     72  get(groupId) {
     73    let gid = getInternalTabGroupIdForExtTabGroupId(groupId);
     74    if (!gid) {
     75      throw new ExtensionError(`No group with id: ${groupId}`);
     76    }
     77    for (let group of this.queryGroups()) {
     78      if (group.id === gid) {
     79        return group;
     80      }
     81    }
     82    throw new ExtensionError(`No group with id: ${groupId}`);
     83  }
     84 
     85  convert(group) {
     86    return {
     87      collapsed: !!group.collapsed,
     88      /** Internally we use "gray", but Chrome uses "grey" @see spellColour. */
     89      color: group.color === "gray" ? "grey" : group.color,
     90      id: getExtTabGroupIdForInternalTabGroupId(group.id),
     91      title: group.name,
     92      windowId: windowTracker.getId(group.ownerGlobal),
     93    };
     94  }
     95 
     96  PERSISTENT_EVENTS = {
     97    onCreated({ fire }) {
     98      let onCreate = event => {
     99        if (event.detail.isAdoptingGroup) {
    100          // Tab group moved from a different window.
    101          return;
    102        }
    103        if (!this.extension.canAccessWindow(event.originalTarget.ownerGlobal)) {
    104          return;
    105        }
    106        fire.async(this.convert(event.originalTarget));
    107      };
    108      windowTracker.addListener("TabGroupCreate", onCreate);
    109      return {
    110        unregister() {
    111          windowTracker.removeListener("TabGroupCreate", onCreate);
    112        },
    113        convert(_fire) {
    114          fire = _fire;
    115        },
    116      };
    117    },
    118    onMoved({ fire }) {
    119      let onMove = event => {
    120        if (!this.extension.canAccessWindow(event.originalTarget.ownerGlobal)) {
    121          return;
    122        }
    123        fire.async(this.convert(event.originalTarget));
    124      };
    125      let onCreate = event => {
    126        if (!event.detail.isAdoptingGroup) {
    127          // We are only interested in tab groups moved from a different window.
    128          return;
    129        }
    130        if (!this.extension.canAccessWindow(event.originalTarget.ownerGlobal)) {
    131          return;
    132        }
    133        fire.async(this.convert(event.originalTarget));
    134      };
    135      windowTracker.addListener("TabGroupMoved", onMove);
    136      windowTracker.addListener("TabGroupCreate", onCreate);
    137      return {
    138        unregister() {
    139          windowTracker.removeListener("TabGroupMoved", onMove);
    140          windowTracker.removeListener("TabGroupCreate", onCreate);
    141        },
    142        convert(_fire) {
    143          fire = _fire;
    144        },
    145      };
    146    },
    147    onRemoved({ fire }) {
    148      let onRemove = event => {
    149        if (event.originalTarget.removedByAdoption) {
    150          // Tab group moved to a different window.
    151          return;
    152        }
    153        if (!this.extension.canAccessWindow(event.originalTarget.ownerGlobal)) {
    154          return;
    155        }
    156        fire.async(this.convert(event.originalTarget), {
    157          isWindowClosing: false,
    158        });
    159      };
    160      let onClosed = window => {
    161        if (!this.extension.canAccessWindow(window)) {
    162          return;
    163        }
    164        for (const group of window.gBrowser.tabGroups) {
    165          fire.async(this.convert(group), { isWindowClosing: true });
    166        }
    167      };
    168      windowTracker.addListener("TabGroupRemoved", onRemove);
    169      windowTracker.addListener("domwindowclosed", onClosed);
    170      return {
    171        unregister() {
    172          windowTracker.removeListener("TabGroupRemoved", onRemove);
    173          windowTracker.removeListener("domwindowclosed", onClosed);
    174        },
    175        convert(_fire) {
    176          fire = _fire;
    177        },
    178      };
    179    },
    180    onUpdated({ fire }) {
    181      let onUpdate = event => {
    182        if (!this.extension.canAccessWindow(event.originalTarget.ownerGlobal)) {
    183          return;
    184        }
    185        fire.async(this.convert(event.originalTarget));
    186      };
    187      windowTracker.addListener("TabGroupCollapse", onUpdate);
    188      windowTracker.addListener("TabGroupExpand", onUpdate);
    189      windowTracker.addListener("TabGroupUpdate", onUpdate);
    190      return {
    191        unregister() {
    192          windowTracker.removeListener("TabGroupCollapse", onUpdate);
    193          windowTracker.removeListener("TabGroupExpand", onUpdate);
    194          windowTracker.removeListener("TabGroupUpdate", onUpdate);
    195        },
    196        convert(_fire) {
    197          fire = _fire;
    198        },
    199      };
    200    },
    201  };
    202 
    203  getAPI(context) {
    204    const { windowManager } = this.extension;
    205    return {
    206      tabGroups: {
    207        get: groupId => {
    208          return this.convert(this.get(groupId));
    209        },
    210 
    211        move: (groupId, { index, windowId }) => {
    212          let group = this.get(groupId);
    213          let win = group.ownerGlobal;
    214 
    215          if (windowId != null) {
    216            win = windowTracker.getWindow(windowId, context);
    217            if (
    218              PrivateBrowsingUtils.isWindowPrivate(group.ownerGlobal) !==
    219              PrivateBrowsingUtils.isWindowPrivate(win)
    220            ) {
    221              throw new ExtensionError(
    222                "Can't move groups between private and non-private windows"
    223              );
    224            }
    225            if (windowManager.getWrapper(win).type !== "normal") {
    226              throw new ExtensionError(
    227                "Groups can only be moved to normal windows."
    228              );
    229            }
    230          }
    231 
    232          let tabIndex = adjustIndexForMove(group, win, index);
    233          if (win !== group.ownerGlobal) {
    234            group = win.gBrowser.adoptTabGroup(group, { tabIndex });
    235          } else {
    236            win.gBrowser.moveTabTo(group, { tabIndex });
    237          }
    238          return this.convert(group);
    239        },
    240 
    241        query: query => {
    242          return Array.from(this.queryGroups(query), group =>
    243            this.convert(group)
    244          );
    245        },
    246 
    247        update: (groupId, { collapsed, color, title }) => {
    248          let group = this.get(groupId);
    249          if (collapsed != null) {
    250            group.collapsed = collapsed;
    251          }
    252          if (color != null) {
    253            group.color = spellColour(color);
    254          }
    255          if (title != null) {
    256            group.name = title;
    257          }
    258          return this.convert(group);
    259        },
    260 
    261        onCreated: new EventManager({
    262          context,
    263          module: "tabGroups",
    264          event: "onCreated",
    265          extensionApi: this,
    266        }).api(),
    267 
    268        onMoved: new EventManager({
    269          context,
    270          module: "tabGroups",
    271          event: "onMoved",
    272          extensionApi: this,
    273        }).api(),
    274 
    275        onRemoved: new EventManager({
    276          context,
    277          module: "tabGroups",
    278          event: "onRemoved",
    279          extensionApi: this,
    280        }).api(),
    281 
    282        onUpdated: new EventManager({
    283          context,
    284          module: "tabGroups",
    285          event: "onUpdated",
    286          extensionApi: this,
    287        }).api(),
    288      },
    289    };
    290  }
    291 };