summaryrefslogtreecommitdiffstats
path: root/browser/base/content/browser-ctrlTab.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/base/content/browser-ctrlTab.js810
1 files changed, 810 insertions, 0 deletions
diff --git a/browser/base/content/browser-ctrlTab.js b/browser/base/content/browser-ctrlTab.js
new file mode 100644
index 0000000000..31617f13a3
--- /dev/null
+++ b/browser/base/content/browser-ctrlTab.js
@@ -0,0 +1,810 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+/**
+ * Tab previews utility, produces thumbnails
+ */
+var tabPreviews = {
+ get aspectRatio() {
+ let { PageThumbUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PageThumbUtils.sys.mjs"
+ );
+ let [width, height] = PageThumbUtils.getThumbnailSize(window);
+ delete this.aspectRatio;
+ return (this.aspectRatio = height / width);
+ },
+
+ /**
+ * Get the stored thumbnail URL for a given page URL and wait up to 1s for it
+ * to load. If the browser is discarded and there is no stored thumbnail, the
+ * image URL will fail to load and this method will return null after 1s.
+ * Callers should handle this case by doing nothing or using a fallback image.
+ * @param {String} uri The page URL.
+ * @returns {Promise<Image|null>}
+ */
+ loadImage: async function tabPreviews_loadImage(uri) {
+ let img = new Image();
+ img.src = PageThumbs.getThumbnailURL(uri);
+ if (img.complete && img.naturalWidth) {
+ return img;
+ }
+ return new Promise(resolve => {
+ const controller = new AbortController();
+ img.addEventListener(
+ "load",
+ () => {
+ clearTimeout(timeout);
+ controller.abort();
+ resolve(img);
+ },
+ { signal: controller.signal }
+ );
+ const timeout = setTimeout(() => {
+ controller.abort();
+ resolve(null);
+ }, 1000);
+ });
+ },
+
+ /**
+ * For a given tab, retrieve a preview thumbnail (a canvas or an image) from
+ * storage or capture a new one. If the tab's URL has changed since the
+ * previous call, the thumbnail will be regenerated.
+ * @param {MozTabbrowserTab} aTab The tab to get a preview for.
+ * @returns {Promise<HTMLCanvasElement|Image|null>} Resolves to...
+ * @resolves {HTMLCanvasElement} If a thumbnail can NOT be captured and stored
+ * for the tab, or if the tab is still loading, a snapshot is taken and
+ * returned as a canvas. It may be cached as a canvas (separately from
+ * thumbnail storage) in aTab.__thumbnail if the tab is finished loading. If
+ * the snapshot CAN be stored as a thumbnail, the snapshot is converted to a
+ * blob image and drawn in the returned canvas, but the image is added to
+ * thumbnail storage and cached in aTab.__thumbnail.
+ * @resolves {Image} A cached blob image from a previous thumbnail capture.
+ * e.g. <img src="moz-page-thumb://thumbnails/?url=foo.com&revision=bar">
+ * @resolves {null} If a thumbnail cannot be captured for any reason (e.g.
+ * because the tab is discarded) and there is no cached/stored thumbnail.
+ */
+ get: async function tabPreviews_get(aTab) {
+ let browser = aTab.linkedBrowser;
+ let uri = browser.currentURI.spec;
+
+ // Invalidate the cached thumbnail since the tab has changed.
+ if (aTab.__thumbnail_lastURI && aTab.__thumbnail_lastURI != uri) {
+ aTab.__thumbnail = null;
+ aTab.__thumbnail_lastURI = null;
+ }
+
+ // A cached thumbnail (not from thumbnail storage) is available.
+ if (aTab.__thumbnail) {
+ return aTab.__thumbnail;
+ }
+
+ // This means the browser is discarded. Try to load a stored thumbnail, and
+ // use a fallback style otherwise.
+ if (!browser.browsingContext) {
+ return this.loadImage(uri);
+ }
+
+ // Don't cache or store the thumbnail if the tab is still loading.
+ return this.capture(aTab, !aTab.hasAttribute("busy"));
+ },
+
+ /**
+ * For a given tab, capture a preview thumbnail (a canvas), optionally cache
+ * it in aTab.__thumbnail, and possibly store it in thumbnail storage.
+ * @param {MozTabbrowserTab} aTab The tab to capture a preview for.
+ * @param {Boolean} aShouldCache Cache/store the captured thumbnail?
+ * @returns {Promise<HTMLCanvasElement|null>} Resolves to...
+ * @resolves {HTMLCanvasElement} A snapshot of the tab's content. If the
+ * snapshot is safe for storage and aShouldCache is true, the snapshot is
+ * converted to a blob image, stored and cached, and drawn in the returned
+ * canvas. The thumbnail can then be recovered even if the browser is
+ * discarded. Otherwise, the canvas itself is cached in aTab.__thumbnail.
+ * @resolves {null} If a fatal exception occurred during thumbnail capture.
+ */
+ capture: async function tabPreviews_capture(aTab, aShouldCache) {
+ let browser = aTab.linkedBrowser;
+ let uri = browser.currentURI.spec;
+ let canvas = PageThumbs.createCanvas(window);
+ const doStore = await PageThumbs.shouldStoreThumbnail(browser);
+
+ if (doStore && aShouldCache) {
+ await PageThumbs.captureAndStore(browser);
+ let img = await this.loadImage(uri);
+ if (img) {
+ // Cache the stored blob image for future use.
+ aTab.__thumbnail = img;
+ aTab.__thumbnail_lastURI = uri;
+ // Draw the stored blob image in the canvas.
+ canvas.getContext("2d").drawImage(img, 0, 0);
+ } else {
+ canvas = null;
+ }
+ } else {
+ try {
+ await PageThumbs.captureToCanvas(browser, canvas);
+ if (aShouldCache) {
+ // Cache the canvas itself for future use.
+ aTab.__thumbnail = canvas;
+ aTab.__thumbnail_lastURI = uri;
+ }
+ } catch (error) {
+ console.error(error);
+ canvas = null;
+ }
+ }
+
+ return canvas;
+ },
+};
+
+var tabPreviewPanelHelper = {
+ opening(host) {
+ host.panel.hidden = false;
+
+ var handler = this._generateHandler(host);
+ host.panel.addEventListener("popupshown", handler);
+ host.panel.addEventListener("popuphiding", handler);
+
+ host._prevFocus = document.commandDispatcher.focusedElement;
+ },
+ _generateHandler(host) {
+ var self = this;
+ return function listener(event) {
+ if (event.target == host.panel) {
+ host.panel.removeEventListener(event.type, listener);
+ self["_" + event.type](host);
+ }
+ };
+ },
+ _popupshown(host) {
+ if ("setupGUI" in host) {
+ host.setupGUI();
+ }
+ },
+ _popuphiding(host) {
+ if ("suspendGUI" in host) {
+ host.suspendGUI();
+ }
+
+ if (host._prevFocus) {
+ Services.focus.setFocus(
+ host._prevFocus,
+ Ci.nsIFocusManager.FLAG_NOSCROLL
+ );
+ host._prevFocus = null;
+ } else {
+ gBrowser.selectedBrowser.focus();
+ }
+
+ if (host.tabToSelect) {
+ gBrowser.selectedTab = host.tabToSelect;
+ host.tabToSelect = null;
+ }
+ },
+};
+
+/**
+ * Ctrl-Tab panel
+ */
+var ctrlTab = {
+ maxTabPreviews: 7,
+ get panel() {
+ delete this.panel;
+ return (this.panel = document.getElementById("ctrlTab-panel"));
+ },
+ get showAllButton() {
+ delete this.showAllButton;
+ this.showAllButton = document.createXULElement("button");
+ this.showAllButton.id = "ctrlTab-showAll";
+ this.showAllButton.addEventListener("mouseover", this);
+ this.showAllButton.addEventListener("command", this);
+ this.showAllButton.addEventListener("click", this);
+ document
+ .getElementById("ctrlTab-showAll-container")
+ .appendChild(this.showAllButton);
+ return this.showAllButton;
+ },
+ get previews() {
+ delete this.previews;
+ this.previews = [];
+ let previewsContainer = document.getElementById("ctrlTab-previews");
+ for (let i = 0; i < this.maxTabPreviews; i++) {
+ let preview = this._makePreview();
+ previewsContainer.appendChild(preview);
+ this.previews.push(preview);
+ }
+ this.previews.push(this.showAllButton);
+ return this.previews;
+ },
+ get keys() {
+ var keys = {};
+ ["close", "find", "selectAll"].forEach(function (key) {
+ keys[key] = document
+ .getElementById("key_" + key)
+ .getAttribute("key")
+ .toLocaleLowerCase()
+ .charCodeAt(0);
+ });
+ delete this.keys;
+ return (this.keys = keys);
+ },
+ _selectedIndex: 0,
+ get selected() {
+ return this._selectedIndex < 0
+ ? document.activeElement
+ : this.previews[this._selectedIndex];
+ },
+ get isOpen() {
+ return (
+ this.panel.state == "open" || this.panel.state == "showing" || this._timer
+ );
+ },
+ get tabCount() {
+ return this.tabList.length;
+ },
+ get tabPreviewCount() {
+ return Math.min(this.maxTabPreviews, this.tabCount);
+ },
+
+ get tabList() {
+ return this._recentlyUsedTabs;
+ },
+
+ init: function ctrlTab_init() {
+ if (!this._recentlyUsedTabs) {
+ this._initRecentlyUsedTabs();
+ this._init(true);
+ }
+ },
+
+ uninit: function ctrlTab_uninit() {
+ if (this._recentlyUsedTabs) {
+ this._recentlyUsedTabs = null;
+ this._init(false);
+ }
+ },
+
+ prefName: "browser.ctrlTab.sortByRecentlyUsed",
+ readPref: function ctrlTab_readPref() {
+ var enable =
+ Services.prefs.getBoolPref(this.prefName) &&
+ !Services.prefs.getBoolPref(
+ "browser.ctrlTab.disallowForScreenReaders",
+ false
+ );
+
+ if (enable) {
+ this.init();
+ } else {
+ this.uninit();
+ }
+ },
+ observe(aSubject, aTopic, aPrefName) {
+ this.readPref();
+ },
+
+ _makePreview() {
+ let preview = document.createXULElement("button");
+ preview.className = "ctrlTab-preview";
+ preview.setAttribute("pack", "center");
+ preview.setAttribute("flex", "1");
+ preview.addEventListener("mouseover", this);
+ preview.addEventListener("command", this);
+ preview.addEventListener("click", this);
+
+ let previewInner = document.createXULElement("vbox");
+ previewInner.className = "ctrlTab-preview-inner";
+ preview.appendChild(previewInner);
+
+ let canvas = (preview._canvas = document.createXULElement("hbox"));
+ canvas.className = "ctrlTab-canvas";
+ previewInner.appendChild(canvas);
+
+ let faviconContainer = document.createXULElement("hbox");
+ faviconContainer.className = "ctrlTab-favicon-container";
+ previewInner.appendChild(faviconContainer);
+
+ let favicon = (preview._favicon = document.createXULElement("image"));
+ favicon.className = "ctrlTab-favicon";
+ faviconContainer.appendChild(favicon);
+
+ let label = (preview._label = document.createXULElement("label"));
+ label.className = "ctrlTab-label plain";
+ label.setAttribute("crop", "end");
+ previewInner.appendChild(label);
+
+ return preview;
+ },
+
+ updatePreviews: function ctrlTab_updatePreviews() {
+ for (let i = 0; i < this.previews.length; i++) {
+ this.updatePreview(this.previews[i], this.tabList[i]);
+ }
+
+ document.l10n.setAttributes(
+ this.showAllButton,
+ "tabbrowser-ctrl-tab-list-all-tabs",
+ { tabCount: this.tabCount }
+ );
+ this.showAllButton.hidden = !gTabsPanel.canOpen;
+ },
+
+ updatePreview: function ctrlTab_updatePreview(aPreview, aTab) {
+ if (aPreview == this.showAllButton) {
+ return;
+ }
+
+ aPreview._tab = aTab;
+
+ if (aTab) {
+ let canvas = aPreview._canvas;
+ let canvasWidth = this.canvasWidth;
+ let canvasHeight = this.canvasHeight;
+ canvas.setAttribute("width", canvasWidth);
+ canvas.style.minWidth = canvasWidth + "px";
+ canvas.style.maxWidth = canvasWidth + "px";
+ canvas.style.minHeight = canvasHeight + "px";
+ canvas.style.maxHeight = canvasHeight + "px";
+ tabPreviews
+ .get(aTab)
+ .then(img => {
+ switch (aPreview._tab) {
+ case aTab:
+ this._clearCanvas(canvas);
+ if (img) {
+ canvas.appendChild(img);
+ }
+ break;
+ case null:
+ // The preview panel is not open, so don't render anything.
+ this._clearCanvas(canvas);
+ break;
+ // If the tab exists but it has changed since updatePreview was
+ // called, the preview will likely be handled by a later
+ // updatePreview call, e.g. on TabAttrModified.
+ }
+ })
+ .catch(error => console.error(error));
+
+ aPreview._label.setAttribute("value", aTab.label);
+ aPreview.setAttribute("tooltiptext", aTab.label);
+ if (aTab.image) {
+ aPreview._favicon.setAttribute("src", aTab.image);
+ } else {
+ aPreview._favicon.removeAttribute("src");
+ }
+ aPreview.hidden = false;
+ } else {
+ this._clearCanvas(aPreview._canvas);
+ aPreview.hidden = true;
+ aPreview._label.removeAttribute("value");
+ aPreview.removeAttribute("tooltiptext");
+ aPreview._favicon.removeAttribute("src");
+ }
+ },
+
+ // Remove previous preview images from the canvas box.
+ _clearCanvas(canvas) {
+ while (canvas.firstElementChild) {
+ canvas.firstElementChild.remove();
+ }
+ },
+
+ advanceFocus: function ctrlTab_advanceFocus(aForward) {
+ let selectedIndex = this.previews.indexOf(this.selected);
+ do {
+ selectedIndex += aForward ? 1 : -1;
+ if (selectedIndex < 0) {
+ selectedIndex = this.previews.length - 1;
+ } else if (selectedIndex >= this.previews.length) {
+ selectedIndex = 0;
+ }
+ } while (this.previews[selectedIndex].hidden);
+
+ if (this._selectedIndex == -1) {
+ // Focus is already in the panel.
+ this.previews[selectedIndex].focus();
+ } else {
+ this._selectedIndex = selectedIndex;
+ }
+
+ if (this.previews[selectedIndex]._tab) {
+ gBrowser.warmupTab(this.previews[selectedIndex]._tab);
+ }
+
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = null;
+ this._openPanel();
+ }
+ },
+
+ _mouseOverFocus: function ctrlTab_mouseOverFocus(aPreview) {
+ if (this._trackMouseOver) {
+ aPreview.focus();
+ }
+ },
+
+ pick: function ctrlTab_pick(aPreview) {
+ if (!this.tabCount) {
+ return;
+ }
+
+ var select = aPreview || this.selected;
+
+ if (select == this.showAllButton) {
+ this.showAllTabs("ctrltab-all-tabs-button");
+ } else {
+ this.close(select._tab);
+ }
+ },
+
+ showAllTabs: function ctrlTab_showAllTabs(aEntrypoint = "unknown") {
+ this.close();
+ gTabsPanel.showAllTabsPanel(null, aEntrypoint);
+ },
+
+ remove: function ctrlTab_remove(aPreview) {
+ if (aPreview._tab) {
+ gBrowser.removeTab(aPreview._tab);
+ }
+ },
+
+ attachTab: function ctrlTab_attachTab(aTab, aPos) {
+ // If the tab is hidden, don't add it to the list unless it's selected
+ // (Normally hidden tabs would be unhidden when selected, but that doesn't
+ // happen for Firefox View).
+ if (aTab.closing || (aTab.hidden && !aTab.selected)) {
+ return;
+ }
+
+ // If the tab is already in the list, remove it before re-inserting it.
+ this.detachTab(aTab);
+
+ if (aPos == 0) {
+ this._recentlyUsedTabs.unshift(aTab);
+ } else if (aPos) {
+ this._recentlyUsedTabs.splice(aPos, 0, aTab);
+ } else {
+ this._recentlyUsedTabs.push(aTab);
+ }
+ },
+
+ detachTab: function ctrlTab_detachTab(aTab) {
+ var i = this._recentlyUsedTabs.indexOf(aTab);
+ if (i >= 0) {
+ this._recentlyUsedTabs.splice(i, 1);
+ }
+ },
+
+ open: function ctrlTab_open() {
+ if (this.isOpen) {
+ return;
+ }
+
+ this.canvasWidth = Math.ceil(
+ (screen.availWidth * 0.85) / this.maxTabPreviews
+ );
+ this.canvasHeight = Math.round(this.canvasWidth * tabPreviews.aspectRatio);
+ this.updatePreviews();
+ this._selectedIndex = 1;
+ gBrowser.warmupTab(this.selected._tab);
+
+ // Add a slight delay before showing the UI, so that a quick
+ // "ctrl-tab" keypress just flips back to the MRU tab.
+ this._timer = setTimeout(() => {
+ this._timer = null;
+ this._openPanel();
+ }, 200);
+ },
+
+ _openPanel: function ctrlTab_openPanel() {
+ tabPreviewPanelHelper.opening(this);
+
+ let width = Math.min(
+ screen.availWidth * 0.99,
+ this.canvasWidth * 1.25 * this.tabPreviewCount
+ );
+ this.panel.style.width = width + "px";
+ var estimateHeight = this.canvasHeight * 1.25 + 75;
+ this.panel.openPopupAtScreen(
+ screen.availLeft + (screen.availWidth - width) / 2,
+ screen.availTop + (screen.availHeight - estimateHeight) / 2,
+ false
+ );
+ },
+
+ close: function ctrlTab_close(aTabToSelect) {
+ if (!this.isOpen) {
+ return;
+ }
+
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = null;
+ this.suspendGUI();
+ if (aTabToSelect) {
+ gBrowser.selectedTab = aTabToSelect;
+ }
+ return;
+ }
+
+ this.tabToSelect = aTabToSelect;
+ this.panel.hidePopup();
+ },
+
+ setupGUI: function ctrlTab_setupGUI() {
+ this.selected.focus();
+ this._selectedIndex = -1;
+
+ // Track mouse movement after a brief delay so that the item that happens
+ // to be under the mouse pointer initially won't be selected unintentionally.
+ this._trackMouseOver = false;
+ setTimeout(
+ function (self) {
+ if (self.isOpen) {
+ self._trackMouseOver = true;
+ }
+ },
+ 0,
+ this
+ );
+ },
+
+ suspendGUI: function ctrlTab_suspendGUI() {
+ for (let preview of this.previews) {
+ this.updatePreview(preview, null);
+ }
+ },
+
+ onKeyDown(event) {
+ let action = ShortcutUtils.getSystemActionForEvent(event);
+ if (action != ShortcutUtils.CYCLE_TABS) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (this.isOpen) {
+ this.advanceFocus(!event.shiftKey);
+ return;
+ }
+
+ if (event.shiftKey) {
+ this.showAllTabs("shift-tab");
+ return;
+ }
+
+ Services.els.addSystemEventListener(document, "keyup", this, false);
+
+ let tabs = gBrowser.visibleTabs;
+ if (tabs.length > 2) {
+ this.open();
+ } else if (tabs.length == 2) {
+ let index = tabs[0].selected ? 1 : 0;
+ gBrowser.selectedTab = tabs[index];
+ }
+ },
+
+ onKeyPress(event) {
+ if (!this.isOpen || !event.ctrlKey) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (event.keyCode == event.DOM_VK_DELETE) {
+ this.remove(this.selected);
+ return;
+ }
+
+ switch (event.charCode) {
+ case this.keys.close:
+ this.remove(this.selected);
+ break;
+ case this.keys.find:
+ this.showAllTabs("ctrltab-key-find");
+ break;
+ case this.keys.selectAll:
+ this.showAllTabs("ctrltab-key-selectAll");
+ break;
+ }
+ },
+
+ removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) {
+ if (this.tabCount == 2) {
+ this.close();
+ return;
+ }
+
+ this.updatePreviews();
+
+ if (this.selected.hidden) {
+ this.advanceFocus(false);
+ }
+ if (this.selected == this.showAllButton) {
+ this.advanceFocus(false);
+ }
+
+ // If the current tab is removed, another tab can steal our focus.
+ if (aTab.selected && this.panel.state == "open") {
+ setTimeout(
+ function (selected) {
+ selected.focus();
+ },
+ 0,
+ this.selected
+ );
+ }
+ },
+
+ handleEvent: function ctrlTab_handleEvent(event) {
+ switch (event.type) {
+ case "SSWindowRestored":
+ this._initRecentlyUsedTabs();
+ break;
+ case "TabAttrModified":
+ // tab attribute modified (i.e. label, busy, image)
+ // update preview only if tab attribute modified in the list
+ if (
+ event.detail.changed.some((elem, ind, arr) =>
+ ["label", "busy", "image"].includes(elem)
+ )
+ ) {
+ for (let i = this.previews.length - 1; i >= 0; i--) {
+ if (
+ this.previews[i]._tab &&
+ this.previews[i]._tab == event.target
+ ) {
+ this.updatePreview(this.previews[i], event.target);
+ break;
+ }
+ }
+ }
+ break;
+ case "TabSelect":
+ this.attachTab(event.target, 0);
+ // If the previous tab was hidden (e.g. Firefox View), remove it from
+ // the list when it's deselected.
+ let previousTab = event.detail.previousTab;
+ if (previousTab.hidden) {
+ this.detachTab(previousTab);
+ }
+ break;
+ case "TabOpen":
+ this.attachTab(event.target, 1);
+ break;
+ case "TabClose":
+ this.detachTab(event.target);
+ if (this.isOpen) {
+ this.removeClosingTabFromUI(event.target);
+ }
+ break;
+ case "TabHide":
+ this.detachTab(event.target);
+ break;
+ case "TabShow":
+ this.attachTab(event.target);
+ this._sortRecentlyUsedTabs();
+ break;
+ case "keydown":
+ this.onKeyDown(event);
+ break;
+ case "keypress":
+ this.onKeyPress(event);
+ break;
+ case "keyup":
+ // During cycling tabs, we avoid sending keyup event to content document.
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (event.keyCode === event.DOM_VK_CONTROL) {
+ Services.els.removeSystemEventListener(
+ document,
+ "keyup",
+ this,
+ false
+ );
+
+ if (this.isOpen) {
+ this.pick();
+ }
+ }
+ break;
+ case "popupshowing":
+ if (event.target.id == "menu_viewPopup") {
+ document.getElementById("menu_showAllTabs").hidden =
+ !gTabsPanel.canOpen;
+ }
+ break;
+ case "mouseover":
+ this._mouseOverFocus(event.currentTarget);
+ break;
+ case "command":
+ this.pick(event.currentTarget);
+ break;
+ case "click":
+ if (event.button == 1) {
+ this.remove(event.currentTarget);
+ } else if (AppConstants.platform == "macosx" && event.button == 2) {
+ // Control+click is a right click on macOS, but in this case we want
+ // to handle it like a left click.
+ this.pick(event.currentTarget);
+ }
+ break;
+ }
+ },
+
+ filterForThumbnailExpiration(aCallback) {
+ // Save a few more thumbnails than we actually display, so that when tabs
+ // are closed, the previews we add instead still get thumbnails.
+ const extraThumbnails = 3;
+ const thumbnailCount = Math.min(
+ this.tabPreviewCount + extraThumbnails,
+ this.tabCount
+ );
+
+ let urls = [];
+ for (let i = 0; i < thumbnailCount; i++) {
+ urls.push(this.tabList[i].linkedBrowser.currentURI.spec);
+ }
+
+ aCallback(urls);
+ },
+ _sortRecentlyUsedTabs() {
+ this._recentlyUsedTabs.sort(
+ (tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed
+ );
+ },
+ _initRecentlyUsedTabs() {
+ this._recentlyUsedTabs = Array.prototype.filter.call(
+ gBrowser.tabs,
+ tab => !tab.closing && !tab.hidden
+ );
+ this._sortRecentlyUsedTabs();
+ },
+
+ _init: function ctrlTab__init(enable) {
+ var toggleEventListener = enable
+ ? "addEventListener"
+ : "removeEventListener";
+
+ window[toggleEventListener]("SSWindowRestored", this);
+
+ var tabContainer = gBrowser.tabContainer;
+ tabContainer[toggleEventListener]("TabOpen", this);
+ tabContainer[toggleEventListener]("TabAttrModified", this);
+ tabContainer[toggleEventListener]("TabSelect", this);
+ tabContainer[toggleEventListener]("TabClose", this);
+ tabContainer[toggleEventListener]("TabHide", this);
+ tabContainer[toggleEventListener]("TabShow", this);
+
+ if (enable) {
+ Services.els.addSystemEventListener(document, "keydown", this, false);
+ } else {
+ Services.els.removeSystemEventListener(document, "keydown", this, false);
+ }
+ document[toggleEventListener]("keypress", this);
+ gBrowser.tabbox.handleCtrlTab = !enable;
+
+ if (enable) {
+ PageThumbs.addExpirationFilter(this);
+ } else {
+ PageThumbs.removeExpirationFilter(this);
+ }
+
+ // If we're not running, hide the "Show All Tabs" menu item,
+ // as Shift+Ctrl+Tab will be handled by the tab bar.
+ document.getElementById("menu_showAllTabs").hidden = !enable;
+ document
+ .getElementById("menu_viewPopup")
+ [toggleEventListener]("popupshowing", this);
+ },
+};