tor-browser

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

browser-ctrlTab.js (23633B)


      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 /**
      6 * Tab previews utility, produces thumbnails
      7 */
      8 var tabPreviews = {
      9  get aspectRatio() {
     10    let { PageThumbUtils } = ChromeUtils.importESModule(
     11      "resource://gre/modules/PageThumbUtils.sys.mjs"
     12    );
     13    let [width, height] = PageThumbUtils.getThumbnailSize(window);
     14    delete this.aspectRatio;
     15    return (this.aspectRatio = height / width);
     16  },
     17 
     18  /**
     19   * Get the stored thumbnail URL for a given page URL and wait up to 1s for it
     20   * to load. If the browser is discarded and there is no stored thumbnail, the
     21   * image URL will fail to load and this method will return null after 1s.
     22   * Callers should handle this case by doing nothing or using a fallback image.
     23   *
     24   * @param {string} uri The page URL.
     25   * @returns {Promise<Image|null>}
     26   */
     27  loadImage: async function tabPreviews_loadImage(uri) {
     28    let img = new Image();
     29    img.src = PageThumbs.getThumbnailURL(uri);
     30    if (img.complete && img.naturalWidth) {
     31      return img;
     32    }
     33    return new Promise(resolve => {
     34      const controller = new AbortController();
     35      img.addEventListener(
     36        "load",
     37        () => {
     38          clearTimeout(timeout);
     39          controller.abort();
     40          resolve(img);
     41        },
     42        { signal: controller.signal }
     43      );
     44      const timeout = setTimeout(() => {
     45        controller.abort();
     46        resolve(null);
     47      }, 1000);
     48    });
     49  },
     50 
     51  /**
     52   * For a given tab, retrieve a preview thumbnail (a canvas or an image) from
     53   * storage or capture a new one. If the tab's URL has changed since the
     54   * previous call, the thumbnail will be regenerated.
     55   *
     56   * @param {MozTabbrowserTab} aTab The tab to get a preview for.
     57   * @returns {Promise<HTMLCanvasElement|Image|null>}
     58   *   Resolves to an HTMLCanvasElement if a thumbnail can NOT be captured and
     59   *   stored for the tab, or if the tab is still loading (a snapshot is taken
     60   *   and returned as a canvas). It may be cached as a canvas (separately from
     61   *   thumbnail storage) in aTab.__thumbnail if the tab is finished loading. If
     62   *   the snapshot CAN be stored as a thumbnail, the snapshot is converted to a
     63   *   blob image and drawn in the returned canvas, but the image is added to
     64   *   thumbnail storage and cached in aTab.__thumbnail.
     65   *   Resolves to an Image if a cached blob image from a previous thumbnail
     66   *   capture exists (e.g. <img src="moz-page-thumb://thumbnails/?url=foo.com&revision=bar">).
     67   *   Resolves to null if a thumbnail cannot be captured for any reason (e.g.
     68   *   because the tab is discarded) and there is no cached/stored thumbnail.
     69   */
     70  get: async function tabPreviews_get(aTab) {
     71    let browser = aTab.linkedBrowser;
     72    let uri = browser.currentURI.spec;
     73 
     74    // Invalidate the cached thumbnail since the tab has changed.
     75    if (aTab.__thumbnail_lastURI && aTab.__thumbnail_lastURI != uri) {
     76      aTab.__thumbnail = null;
     77      aTab.__thumbnail_lastURI = null;
     78    }
     79 
     80    // A cached thumbnail (not from thumbnail storage) is available.
     81    if (aTab.__thumbnail) {
     82      return aTab.__thumbnail;
     83    }
     84 
     85    // This means the browser is discarded. Try to load a stored thumbnail, and
     86    // use a fallback style otherwise.
     87    if (!browser.browsingContext) {
     88      return this.loadImage(uri);
     89    }
     90 
     91    // Don't cache or store the thumbnail if the tab is still loading.
     92    return this.capture(aTab, !aTab.hasAttribute("busy"));
     93  },
     94 
     95  /**
     96   * For a given tab, capture a preview thumbnail (a canvas), optionally cache
     97   * it in aTab.__thumbnail, and possibly store it in thumbnail storage.
     98   *
     99   * @param {MozTabbrowserTab} aTab The tab to capture a preview for.
    100   * @param {boolean} aShouldCache Cache/store the captured thumbnail?
    101   * @returns {Promise<HTMLCanvasElement|null>}
    102   *   Resolves to an HTMLCanvasElement snapshot of the tab's content. If the
    103   *   snapshot is safe for storage and aShouldCache is true, the snapshot is
    104   *   converted to a blob image, stored and cached, and drawn in the returned
    105   *   canvas. The thumbnail can then be recovered even if the browser is
    106   *   discarded. Otherwise, the canvas itself is cached in aTab.__thumbnail.
    107   *   Resolves to null if a fatal exception occurred during thumbnail capture.
    108   */
    109  capture: async function tabPreviews_capture(aTab, aShouldCache) {
    110    let browser = aTab.linkedBrowser;
    111    let uri = browser.currentURI.spec;
    112    let canvas = PageThumbs.createCanvas(window);
    113    const doStore = await PageThumbs.shouldStoreThumbnail(browser);
    114 
    115    if (doStore && aShouldCache) {
    116      await PageThumbs.captureAndStore(browser);
    117      let img = await this.loadImage(uri);
    118      if (img) {
    119        // Cache the stored blob image for future use.
    120        aTab.__thumbnail = img;
    121        aTab.__thumbnail_lastURI = uri;
    122        // Draw the stored blob image in the canvas.
    123        canvas.getContext("2d").drawImage(img, 0, 0);
    124      } else {
    125        canvas = null;
    126      }
    127    } else {
    128      try {
    129        await PageThumbs.captureToCanvas(browser, canvas);
    130        if (aShouldCache) {
    131          // Cache the canvas itself for future use.
    132          aTab.__thumbnail = canvas;
    133          aTab.__thumbnail_lastURI = uri;
    134        }
    135      } catch (error) {
    136        console.error(error);
    137        canvas = null;
    138      }
    139    }
    140 
    141    return canvas;
    142  },
    143 };
    144 
    145 var tabPreviewPanelHelper = {
    146  opening(host) {
    147    host.panel.hidden = false;
    148 
    149    var handler = this._generateHandler(host);
    150    host.panel.addEventListener("popupshown", handler);
    151    host.panel.addEventListener("popuphiding", handler);
    152 
    153    host._prevFocus = document.commandDispatcher.focusedElement;
    154  },
    155  _generateHandler(host) {
    156    var self = this;
    157    return function listener(event) {
    158      if (event.target == host.panel) {
    159        host.panel.removeEventListener(event.type, listener);
    160        self["_" + event.type](host);
    161      }
    162    };
    163  },
    164  _popupshown(host) {
    165    if ("setupGUI" in host) {
    166      host.setupGUI();
    167    }
    168  },
    169  _popuphiding(host) {
    170    if ("suspendGUI" in host) {
    171      host.suspendGUI();
    172    }
    173 
    174    if (host._prevFocus) {
    175      Services.focus.setFocus(
    176        host._prevFocus,
    177        Ci.nsIFocusManager.FLAG_NOSCROLL
    178      );
    179      host._prevFocus = null;
    180    } else {
    181      gBrowser.selectedBrowser.focus();
    182    }
    183 
    184    if (host.tabToSelect) {
    185      gBrowser.selectedTab = host.tabToSelect;
    186      host.tabToSelect = null;
    187    }
    188  },
    189 };
    190 
    191 /**
    192 * Ctrl-Tab panel
    193 */
    194 var ctrlTab = {
    195  maxTabPreviews: 7,
    196  get panel() {
    197    delete this.panel;
    198    return (this.panel = document.getElementById("ctrlTab-panel"));
    199  },
    200  get showAllButton() {
    201    delete this.showAllButton;
    202    this.showAllButton = document.createXULElement("button");
    203    this.showAllButton.id = "ctrlTab-showAll";
    204    this.showAllButton.addEventListener("mouseover", this);
    205    this.showAllButton.addEventListener("command", this);
    206    this.showAllButton.addEventListener("click", this);
    207    document
    208      .getElementById("ctrlTab-showAll-container")
    209      .appendChild(this.showAllButton);
    210    return this.showAllButton;
    211  },
    212  get previews() {
    213    delete this.previews;
    214    this.previews = [];
    215    let previewsContainer = document.getElementById("ctrlTab-previews");
    216    for (let i = 0; i < this.maxTabPreviews; i++) {
    217      let preview = this._makePreview();
    218      previewsContainer.appendChild(preview);
    219      this.previews.push(preview);
    220    }
    221    this.previews.push(this.showAllButton);
    222    return this.previews;
    223  },
    224  get keys() {
    225    var keys = {};
    226    ["close", "find", "selectAll"].forEach(function (key) {
    227      keys[key] = document
    228        .getElementById("key_" + key)
    229        .getAttribute("key")
    230        .toLocaleLowerCase()
    231        .charCodeAt(0);
    232    });
    233    delete this.keys;
    234    return (this.keys = keys);
    235  },
    236  _selectedIndex: 0,
    237  get selected() {
    238    return this._selectedIndex < 0
    239      ? document.activeElement
    240      : this.previews[this._selectedIndex];
    241  },
    242  get isOpen() {
    243    return (
    244      this.panel.state == "open" || this.panel.state == "showing" || this._timer
    245    );
    246  },
    247  get tabCount() {
    248    return this.tabList.length;
    249  },
    250  get tabPreviewCount() {
    251    return Math.min(this.maxTabPreviews, this.tabCount);
    252  },
    253 
    254  get tabList() {
    255    return this._recentlyUsedTabs;
    256  },
    257 
    258  init: function ctrlTab_init() {
    259    if (!this._recentlyUsedTabs) {
    260      this._initRecentlyUsedTabs();
    261      this._init(true);
    262    }
    263  },
    264 
    265  uninit: function ctrlTab_uninit() {
    266    if (this._recentlyUsedTabs) {
    267      this._recentlyUsedTabs = null;
    268      this._init(false);
    269    }
    270  },
    271 
    272  prefName: "browser.ctrlTab.sortByRecentlyUsed",
    273  readPref: function ctrlTab_readPref() {
    274    var enable =
    275      Services.prefs.getBoolPref(this.prefName) &&
    276      !Services.prefs.getBoolPref(
    277        "browser.ctrlTab.disallowForScreenReaders",
    278        false
    279      );
    280 
    281    if (enable) {
    282      this.init();
    283    } else {
    284      this.uninit();
    285    }
    286  },
    287  observe() {
    288    this.readPref();
    289  },
    290 
    291  _makePreview() {
    292    let preview = document.createXULElement("button");
    293    preview.className = "ctrlTab-preview";
    294    preview.setAttribute("pack", "center");
    295    preview.setAttribute("flex", "1");
    296    preview.addEventListener("mouseover", this);
    297    preview.addEventListener("command", this);
    298    preview.addEventListener("click", this);
    299 
    300    let previewInner = document.createXULElement("vbox");
    301    previewInner.className = "ctrlTab-preview-inner";
    302    preview.appendChild(previewInner);
    303 
    304    let canvas = (preview._canvas = document.createXULElement("hbox"));
    305    canvas.className = "ctrlTab-canvas";
    306    previewInner.appendChild(canvas);
    307 
    308    let faviconContainer = document.createXULElement("hbox");
    309    faviconContainer.className = "ctrlTab-favicon-container";
    310    previewInner.appendChild(faviconContainer);
    311 
    312    let favicon = (preview._favicon = document.createXULElement("image"));
    313    favicon.className = "ctrlTab-favicon";
    314    faviconContainer.appendChild(favicon);
    315 
    316    let label = (preview._label = document.createXULElement("label"));
    317    label.className = "ctrlTab-label plain";
    318    label.setAttribute("crop", "end");
    319    previewInner.appendChild(label);
    320 
    321    return preview;
    322  },
    323 
    324  updatePreviews: function ctrlTab_updatePreviews() {
    325    for (let i = 0; i < this.previews.length; i++) {
    326      this.updatePreview(this.previews[i], this.tabList[i]);
    327    }
    328 
    329    document.l10n.setAttributes(
    330      this.showAllButton,
    331      "tabbrowser-ctrl-tab-list-all-tabs",
    332      { tabCount: this.tabCount }
    333    );
    334    this.showAllButton.hidden = !gTabsPanel.canOpen;
    335  },
    336 
    337  updatePreview: function ctrlTab_updatePreview(aPreview, aTab) {
    338    if (aPreview == this.showAllButton) {
    339      return;
    340    }
    341 
    342    aPreview._tab = aTab;
    343 
    344    if (aTab) {
    345      let canvas = aPreview._canvas;
    346      let canvasWidth = this.canvasWidth;
    347      let canvasHeight = this.canvasHeight;
    348      let existingPreview = canvas.firstChild;
    349      if (!existingPreview) {
    350        let placeholder = document.createElement("img");
    351        placeholder.className = "ctrlTab-placeholder";
    352        placeholder.setAttribute("width", canvasWidth);
    353        placeholder.setAttribute("height", canvasHeight);
    354        placeholder.setAttribute("alt", "");
    355        canvas.appendChild(placeholder);
    356        existingPreview = placeholder;
    357      }
    358      tabPreviews
    359        .get(aTab)
    360        .then(img => {
    361          switch (aPreview._tab) {
    362            case aTab:
    363              if (img) {
    364                img.style.width = canvasWidth + "px";
    365                img.style.height = canvasHeight + "px";
    366                canvas.replaceChild(img, existingPreview);
    367              }
    368              break;
    369            case null:
    370              // The preview panel is not open, so don't render anything.
    371              this._clearCanvas(canvas);
    372              break;
    373            // If the tab exists but it has changed since updatePreview was
    374            // called, the preview will likely be handled by a later
    375            // updatePreview call, e.g. on TabAttrModified.
    376          }
    377        })
    378        .catch(error => console.error(error));
    379 
    380      aPreview._label.setAttribute("value", aTab.label);
    381      aPreview.setAttribute("tooltiptext", aTab.label);
    382      if (aTab.image) {
    383        aPreview._favicon.setAttribute("src", aTab.image);
    384      } else {
    385        aPreview._favicon.removeAttribute("src");
    386      }
    387      aPreview.hidden = false;
    388    } else {
    389      this._clearCanvas(aPreview._canvas);
    390      aPreview.hidden = true;
    391      aPreview._label.removeAttribute("value");
    392      aPreview.removeAttribute("tooltiptext");
    393      aPreview._favicon.removeAttribute("src");
    394    }
    395  },
    396 
    397  // Remove previous preview images from the canvas box.
    398  _clearCanvas(canvas) {
    399    canvas.replaceChildren();
    400  },
    401 
    402  advanceFocus: function ctrlTab_advanceFocus(aForward) {
    403    let selectedIndex = this.previews.indexOf(this.selected);
    404    do {
    405      selectedIndex += aForward ? 1 : -1;
    406      if (selectedIndex < 0) {
    407        selectedIndex = this.previews.length - 1;
    408      } else if (selectedIndex >= this.previews.length) {
    409        selectedIndex = 0;
    410      }
    411    } while (this.previews[selectedIndex].hidden);
    412 
    413    if (this._selectedIndex == -1) {
    414      // Focus is already in the panel.
    415      this.previews[selectedIndex].focus();
    416    } else {
    417      this._selectedIndex = selectedIndex;
    418    }
    419 
    420    if (this.previews[selectedIndex]._tab) {
    421      gBrowser.warmupTab(this.previews[selectedIndex]._tab);
    422    }
    423 
    424    if (this._timer) {
    425      clearTimeout(this._timer);
    426      this._timer = null;
    427      this._openPanel();
    428    }
    429  },
    430 
    431  pick: function ctrlTab_pick(aPreview) {
    432    if (!this.tabCount) {
    433      return;
    434    }
    435 
    436    var select = aPreview || this.selected;
    437 
    438    if (select == this.showAllButton) {
    439      this.showAllTabs("ctrltab-all-tabs-button");
    440    } else {
    441      this.close(select._tab);
    442    }
    443  },
    444 
    445  showAllTabs: function ctrlTab_showAllTabs(aEntrypoint = "unknown") {
    446    this.close();
    447    gTabsPanel.showAllTabsPanel(null, aEntrypoint);
    448  },
    449 
    450  remove: function ctrlTab_remove(aPreview) {
    451    if (aPreview._tab) {
    452      gBrowser.removeTab(aPreview._tab);
    453    }
    454  },
    455 
    456  attachTab: function ctrlTab_attachTab(aTab, aPos) {
    457    // If the tab is hidden, don't add it to the list unless it's selected
    458    // (Normally hidden tabs would be unhidden when selected, but that doesn't
    459    // happen for Firefox View).
    460    if (aTab.closing || (aTab.hidden && !aTab.selected)) {
    461      return;
    462    }
    463 
    464    // If the tab is already in the list, remove it before re-inserting it.
    465    this.detachTab(aTab);
    466 
    467    if (aPos == 0) {
    468      this._recentlyUsedTabs.unshift(aTab);
    469    } else if (aPos) {
    470      this._recentlyUsedTabs.splice(aPos, 0, aTab);
    471    } else {
    472      this._recentlyUsedTabs.push(aTab);
    473    }
    474  },
    475 
    476  detachTab: function ctrlTab_detachTab(aTab) {
    477    var i = this._recentlyUsedTabs.indexOf(aTab);
    478    if (i >= 0) {
    479      this._recentlyUsedTabs.splice(i, 1);
    480    }
    481  },
    482 
    483  open: function ctrlTab_open() {
    484    if (this.isOpen) {
    485      return;
    486    }
    487 
    488    this.canvasWidth = Math.ceil(
    489      (screen.availWidth * 0.85) / this.maxTabPreviews
    490    );
    491    this.canvasHeight = Math.round(this.canvasWidth * tabPreviews.aspectRatio);
    492    this.updatePreviews();
    493    this._selectedIndex = 1;
    494    gBrowser.warmupTab(this.selected._tab);
    495 
    496    // Add a slight delay before showing the UI, so that a quick
    497    // "ctrl-tab" keypress just flips back to the MRU tab.
    498    this._timer = setTimeout(() => {
    499      this._timer = null;
    500      this._openPanel();
    501    }, 200);
    502  },
    503 
    504  _openPanel: function ctrlTab_openPanel() {
    505    tabPreviewPanelHelper.opening(this);
    506 
    507    let width = Math.min(
    508      screen.availWidth * 0.99,
    509      this.canvasWidth * 1.25 * this.tabPreviewCount
    510    );
    511    this.panel.style.width = width + "px";
    512    var estimateHeight = this.canvasHeight * 1.25 + 75;
    513    this.panel.openPopupAtScreen(
    514      screen.availLeft + (screen.availWidth - width) / 2,
    515      screen.availTop + (screen.availHeight - estimateHeight) / 2,
    516      false
    517    );
    518  },
    519 
    520  close: function ctrlTab_close(aTabToSelect) {
    521    if (!this.isOpen) {
    522      return;
    523    }
    524 
    525    if (this._timer) {
    526      clearTimeout(this._timer);
    527      this._timer = null;
    528      this.suspendGUI();
    529      if (aTabToSelect) {
    530        gBrowser.selectedTab = aTabToSelect;
    531      }
    532      return;
    533    }
    534 
    535    this.tabToSelect = aTabToSelect;
    536    this.panel.hidePopup();
    537  },
    538 
    539  setupGUI: function ctrlTab_setupGUI() {
    540    this.selected.focus();
    541    this._selectedIndex = -1;
    542  },
    543 
    544  suspendGUI: function ctrlTab_suspendGUI() {
    545    for (let preview of this.previews) {
    546      this.updatePreview(preview, null);
    547    }
    548  },
    549 
    550  onKeyDown(event) {
    551    let action = ShortcutUtils.getSystemActionForEvent(event);
    552    if (action != ShortcutUtils.CYCLE_TABS) {
    553      return;
    554    }
    555 
    556    event.preventDefault();
    557    event.stopPropagation();
    558 
    559    if (this.isOpen) {
    560      this.advanceFocus(!event.shiftKey);
    561      return;
    562    }
    563 
    564    if (event.shiftKey) {
    565      this.showAllTabs("shift-tab");
    566      return;
    567    }
    568 
    569    document.addEventListener("keyup", this, { mozSystemGroup: true });
    570 
    571    let tabs = gBrowser.visibleTabs;
    572    if (tabs.length > 2) {
    573      this.open();
    574    } else if (tabs.length == 2) {
    575      let index = tabs[0].selected ? 1 : 0;
    576      gBrowser.selectedTab = tabs[index];
    577    }
    578  },
    579 
    580  onKeyPress(event) {
    581    if (!this.isOpen || !event.ctrlKey) {
    582      return;
    583    }
    584 
    585    event.preventDefault();
    586    event.stopPropagation();
    587 
    588    if (event.keyCode == event.DOM_VK_DELETE) {
    589      this.remove(this.selected);
    590      return;
    591    }
    592 
    593    switch (event.charCode) {
    594      case this.keys.close:
    595        this.remove(this.selected);
    596        break;
    597      case this.keys.find:
    598        this.showAllTabs("ctrltab-key-find");
    599        break;
    600      case this.keys.selectAll:
    601        this.showAllTabs("ctrltab-key-selectAll");
    602        break;
    603    }
    604  },
    605 
    606  removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) {
    607    if (this.tabCount == 2) {
    608      this.close();
    609      return;
    610    }
    611 
    612    this.updatePreviews();
    613 
    614    if (this.selected.hidden) {
    615      this.advanceFocus(false);
    616    }
    617    if (this.selected == this.showAllButton) {
    618      this.advanceFocus(false);
    619    }
    620 
    621    // If the current tab is removed, another tab can steal our focus.
    622    if (aTab.selected && this.panel.state == "open") {
    623      setTimeout(
    624        function (selected) {
    625          selected.focus();
    626        },
    627        0,
    628        this.selected
    629      );
    630    }
    631  },
    632 
    633  handleEvent: function ctrlTab_handleEvent(event) {
    634    switch (event.type) {
    635      case "SSWindowRestored":
    636        this._initRecentlyUsedTabs();
    637        break;
    638      case "TabAttrModified":
    639        // tab attribute modified (i.e. label, busy, image)
    640        // update preview only if tab attribute modified in the list
    641        if (
    642          event.detail.changed.some(elem =>
    643            ["label", "busy", "image"].includes(elem)
    644          )
    645        ) {
    646          for (let i = this.previews.length - 1; i >= 0; i--) {
    647            if (
    648              this.previews[i]._tab &&
    649              this.previews[i]._tab == event.target
    650            ) {
    651              this.updatePreview(this.previews[i], event.target);
    652              break;
    653            }
    654          }
    655        }
    656        break;
    657      case "TabSelect": {
    658        this.attachTab(event.target, 0);
    659        // If the previous tab was hidden (e.g. Firefox View), remove it from
    660        // the list when it's deselected.
    661        let previousTab = event.detail.previousTab;
    662        if (previousTab.hidden) {
    663          this.detachTab(previousTab);
    664        }
    665        break;
    666      }
    667      case "TabOpen":
    668        this.attachTab(event.target, 1);
    669        break;
    670      case "TabClose":
    671        this.detachTab(event.target);
    672        if (this.isOpen) {
    673          this.removeClosingTabFromUI(event.target);
    674        }
    675        break;
    676      case "TabHide":
    677        this.detachTab(event.target);
    678        break;
    679      case "TabShow":
    680        this.attachTab(event.target);
    681        this._sortRecentlyUsedTabs();
    682        break;
    683      case "keydown":
    684        this.onKeyDown(event);
    685        break;
    686      case "keypress":
    687        this.onKeyPress(event);
    688        break;
    689      case "keyup":
    690        // During cycling tabs, we avoid sending keyup event to content document.
    691        event.preventDefault();
    692        event.stopPropagation();
    693 
    694        if (event.keyCode === event.DOM_VK_CONTROL) {
    695          document.removeEventListener("keyup", this, { mozSystemGroup: true });
    696 
    697          if (this.isOpen) {
    698            this.pick();
    699          }
    700        }
    701        break;
    702      case "popupshowing":
    703        if (event.target.id == "menu_viewPopup") {
    704          document.getElementById("menu_showAllTabs").hidden =
    705            !gTabsPanel.canOpen;
    706        }
    707        break;
    708      case "mouseover":
    709        // relatedTarget is the element the mouse came from. It is null when we
    710        // get a synthetic mouse event.
    711        if (event.relatedTarget) {
    712          event.currentTarget.focus();
    713        }
    714        break;
    715      case "command":
    716        this.pick(event.currentTarget);
    717        break;
    718      case "click":
    719        if (event.button == 1) {
    720          this.remove(event.currentTarget);
    721        } else if (AppConstants.platform == "macosx" && event.button == 2) {
    722          // Control+click is a right click on macOS, but in this case we want
    723          // to handle it like a left click.
    724          this.pick(event.currentTarget);
    725        }
    726        break;
    727    }
    728  },
    729 
    730  filterForThumbnailExpiration(aCallback) {
    731    // Save a few more thumbnails than we actually display, so that when tabs
    732    // are closed, the previews we add instead still get thumbnails.
    733    const extraThumbnails = 3;
    734    const thumbnailCount = Math.min(
    735      this.tabPreviewCount + extraThumbnails,
    736      this.tabCount
    737    );
    738 
    739    let urls = [];
    740    for (let i = 0; i < thumbnailCount; i++) {
    741      urls.push(this.tabList[i].linkedBrowser.currentURI.spec);
    742    }
    743 
    744    aCallback(urls);
    745  },
    746  _sortRecentlyUsedTabs() {
    747    this._recentlyUsedTabs.sort(
    748      (tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed
    749    );
    750  },
    751  _initRecentlyUsedTabs() {
    752    this._recentlyUsedTabs = Array.prototype.filter.call(
    753      gBrowser.tabs,
    754      tab => !tab.closing && !tab.hidden
    755    );
    756    this._sortRecentlyUsedTabs();
    757  },
    758 
    759  _init: function ctrlTab__init(enable) {
    760    var toggleEventListener = enable
    761      ? "addEventListener"
    762      : "removeEventListener";
    763 
    764    window[toggleEventListener]("SSWindowRestored", this);
    765 
    766    var tabContainer = gBrowser.tabContainer;
    767    tabContainer[toggleEventListener]("TabOpen", this);
    768    tabContainer[toggleEventListener]("TabAttrModified", this);
    769    tabContainer[toggleEventListener]("TabSelect", this);
    770    tabContainer[toggleEventListener]("TabClose", this);
    771    tabContainer[toggleEventListener]("TabHide", this);
    772    tabContainer[toggleEventListener]("TabShow", this);
    773 
    774    if (enable) {
    775      document.addEventListener("keydown", this, { mozSystemGroup: true });
    776    } else {
    777      document.removeEventListener("keydown", this, { mozSystemGroup: true });
    778    }
    779    document[toggleEventListener]("keypress", this);
    780    gBrowser.tabbox.handleCtrlTab = !enable;
    781 
    782    if (enable) {
    783      PageThumbs.addExpirationFilter(this);
    784    } else {
    785      PageThumbs.removeExpirationFilter(this);
    786    }
    787 
    788    // If we're not running, hide the "Show All Tabs" menu item,
    789    // as Shift+Ctrl+Tab will be handled by the tab bar.
    790    document.getElementById("menu_showAllTabs").hidden = !enable;
    791    document
    792      .getElementById("menu_viewPopup")
    793      [toggleEventListener]("popupshowing", this);
    794  },
    795 };