Merge mozilla-central to autoland. a=merge CLOSED TREE
[gecko.git] / browser / base / content / browser-ctrlTab.js
blobe5d16e605b7edf11fc9f52b93e415087b76398f1
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/. */
5 // This file is loaded into the browser window scope.
6 /* eslint-env mozilla/browser-window */
8 /**
9  * Tab previews utility, produces thumbnails
10  */
11 var tabPreviews = {
12   get aspectRatio() {
13     let { PageThumbUtils } = ChromeUtils.importESModule(
14       "resource://gre/modules/PageThumbUtils.sys.mjs"
15     );
16     let [width, height] = PageThumbUtils.getThumbnailSize(window);
17     delete this.aspectRatio;
18     return (this.aspectRatio = height / width);
19   },
21   /**
22    * Get the stored thumbnail URL for a given page URL and wait up to 1s for it
23    * to load. If the browser is discarded and there is no stored thumbnail, the
24    * image URL will fail to load and this method will return null after 1s.
25    * Callers should handle this case by doing nothing or using a fallback image.
26    * @param {String} uri The page URL.
27    * @returns {Promise<Image|null>}
28    */
29   loadImage: async function tabPreviews_loadImage(uri) {
30     let img = new Image();
31     img.src = PageThumbs.getThumbnailURL(uri);
32     if (img.complete && img.naturalWidth) {
33       return img;
34     }
35     return new Promise(resolve => {
36       const controller = new AbortController();
37       img.addEventListener(
38         "load",
39         () => {
40           clearTimeout(timeout);
41           controller.abort();
42           resolve(img);
43         },
44         { signal: controller.signal }
45       );
46       const timeout = setTimeout(() => {
47         controller.abort();
48         resolve(null);
49       }, 1000);
50     });
51   },
53   /**
54    * For a given tab, retrieve a preview thumbnail (a canvas or an image) from
55    * storage or capture a new one. If the tab's URL has changed since the
56    * previous call, the thumbnail will be regenerated.
57    * @param {MozTabbrowserTab} aTab The tab to get a preview for.
58    * @returns {Promise<HTMLCanvasElement|Image|null>} Resolves to...
59    * @resolves {HTMLCanvasElement} If a thumbnail can NOT be captured and stored
60    *   for the tab, or if the tab is still loading, a snapshot is taken and
61    *   returned as a canvas. It may be cached as a canvas (separately from
62    *   thumbnail storage) in aTab.__thumbnail if the tab is finished loading. If
63    *   the snapshot CAN be stored as a thumbnail, the snapshot is converted to a
64    *   blob image and drawn in the returned canvas, but the image is added to
65    *   thumbnail storage and cached in aTab.__thumbnail.
66    * @resolves {Image} A cached blob image from a previous thumbnail capture.
67    *   e.g. <img src="moz-page-thumb://thumbnails/?url=foo.com&revision=bar">
68    * @resolves {null} If a thumbnail cannot be captured for any reason (e.g.
69    *   because the tab is discarded) and there is no cached/stored thumbnail.
70    */
71   get: async function tabPreviews_get(aTab) {
72     let browser = aTab.linkedBrowser;
73     let uri = browser.currentURI.spec;
75     // Invalidate the cached thumbnail since the tab has changed.
76     if (aTab.__thumbnail_lastURI && aTab.__thumbnail_lastURI != uri) {
77       aTab.__thumbnail = null;
78       aTab.__thumbnail_lastURI = null;
79     }
81     // A cached thumbnail (not from thumbnail storage) is available.
82     if (aTab.__thumbnail) {
83       return aTab.__thumbnail;
84     }
86     // This means the browser is discarded. Try to load a stored thumbnail, and
87     // use a fallback style otherwise.
88     if (!browser.browsingContext) {
89       return this.loadImage(uri);
90     }
92     // Don't cache or store the thumbnail if the tab is still loading.
93     return this.capture(aTab, !aTab.hasAttribute("busy"));
94   },
96   /**
97    * For a given tab, capture a preview thumbnail (a canvas), optionally cache
98    * it in aTab.__thumbnail, and possibly store it in thumbnail storage.
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>} Resolves to...
102    * @resolves {HTMLCanvasElement} A 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 {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);
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     }
141     return canvas;
142   },
145 var tabPreviewPanelHelper = {
146   opening(host) {
147     host.panel.hidden = false;
149     var handler = this._generateHandler(host);
150     host.panel.addEventListener("popupshown", handler);
151     host.panel.addEventListener("popuphiding", handler);
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     }
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     }
184     if (host.tabToSelect) {
185       gBrowser.selectedTab = host.tabToSelect;
186       host.tabToSelect = null;
187     }
188   },
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   },
254   get tabList() {
255     return this._recentlyUsedTabs;
256   },
258   init: function ctrlTab_init() {
259     if (!this._recentlyUsedTabs) {
260       this._initRecentlyUsedTabs();
261       this._init(true);
262     }
263   },
265   uninit: function ctrlTab_uninit() {
266     if (this._recentlyUsedTabs) {
267       this._recentlyUsedTabs = null;
268       this._init(false);
269     }
270   },
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       );
281     if (enable) {
282       this.init();
283     } else {
284       this.uninit();
285     }
286   },
287   observe() {
288     this.readPref();
289   },
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);
300     let previewInner = document.createXULElement("vbox");
301     previewInner.className = "ctrlTab-preview-inner";
302     preview.appendChild(previewInner);
304     let canvas = (preview._canvas = document.createXULElement("hbox"));
305     canvas.className = "ctrlTab-canvas";
306     previewInner.appendChild(canvas);
308     let faviconContainer = document.createXULElement("hbox");
309     faviconContainer.className = "ctrlTab-favicon-container";
310     previewInner.appendChild(faviconContainer);
312     let favicon = (preview._favicon = document.createXULElement("image"));
313     favicon.className = "ctrlTab-favicon";
314     faviconContainer.appendChild(favicon);
316     let label = (preview._label = document.createXULElement("label"));
317     label.className = "ctrlTab-label plain";
318     label.setAttribute("crop", "end");
319     previewInner.appendChild(label);
321     return preview;
322   },
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     }
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   },
337   updatePreview: function ctrlTab_updatePreview(aPreview, aTab) {
338     if (aPreview == this.showAllButton) {
339       return;
340     }
342     aPreview._tab = aTab;
344     if (aTab) {
345       let canvas = aPreview._canvas;
346       let canvasWidth = this.canvasWidth;
347       let canvasHeight = this.canvasHeight;
348       canvas.setAttribute("width", canvasWidth);
349       canvas.style.minWidth = canvasWidth + "px";
350       canvas.style.maxWidth = canvasWidth + "px";
351       canvas.style.minHeight = canvasHeight + "px";
352       canvas.style.maxHeight = canvasHeight + "px";
353       tabPreviews
354         .get(aTab)
355         .then(img => {
356           switch (aPreview._tab) {
357             case aTab:
358               this._clearCanvas(canvas);
359               if (img) {
360                 canvas.appendChild(img);
361               }
362               break;
363             case null:
364               // The preview panel is not open, so don't render anything.
365               this._clearCanvas(canvas);
366               break;
367             // If the tab exists but it has changed since updatePreview was
368             // called, the preview will likely be handled by a later
369             // updatePreview call, e.g. on TabAttrModified.
370           }
371         })
372         .catch(error => console.error(error));
374       aPreview._label.setAttribute("value", aTab.label);
375       aPreview.setAttribute("tooltiptext", aTab.label);
376       if (aTab.image) {
377         aPreview._favicon.setAttribute("src", aTab.image);
378       } else {
379         aPreview._favicon.removeAttribute("src");
380       }
381       aPreview.hidden = false;
382     } else {
383       this._clearCanvas(aPreview._canvas);
384       aPreview.hidden = true;
385       aPreview._label.removeAttribute("value");
386       aPreview.removeAttribute("tooltiptext");
387       aPreview._favicon.removeAttribute("src");
388     }
389   },
391   // Remove previous preview images from the canvas box.
392   _clearCanvas(canvas) {
393     while (canvas.firstElementChild) {
394       canvas.firstElementChild.remove();
395     }
396   },
398   advanceFocus: function ctrlTab_advanceFocus(aForward) {
399     let selectedIndex = this.previews.indexOf(this.selected);
400     do {
401       selectedIndex += aForward ? 1 : -1;
402       if (selectedIndex < 0) {
403         selectedIndex = this.previews.length - 1;
404       } else if (selectedIndex >= this.previews.length) {
405         selectedIndex = 0;
406       }
407     } while (this.previews[selectedIndex].hidden);
409     if (this._selectedIndex == -1) {
410       // Focus is already in the panel.
411       this.previews[selectedIndex].focus();
412     } else {
413       this._selectedIndex = selectedIndex;
414     }
416     if (this.previews[selectedIndex]._tab) {
417       gBrowser.warmupTab(this.previews[selectedIndex]._tab);
418     }
420     if (this._timer) {
421       clearTimeout(this._timer);
422       this._timer = null;
423       this._openPanel();
424     }
425   },
427   _mouseOverFocus: function ctrlTab_mouseOverFocus(aPreview) {
428     if (this._trackMouseOver) {
429       aPreview.focus();
430     }
431   },
433   pick: function ctrlTab_pick(aPreview) {
434     if (!this.tabCount) {
435       return;
436     }
438     var select = aPreview || this.selected;
440     if (select == this.showAllButton) {
441       this.showAllTabs("ctrltab-all-tabs-button");
442     } else {
443       this.close(select._tab);
444     }
445   },
447   showAllTabs: function ctrlTab_showAllTabs(aEntrypoint = "unknown") {
448     this.close();
449     gTabsPanel.showAllTabsPanel(null, aEntrypoint);
450   },
452   remove: function ctrlTab_remove(aPreview) {
453     if (aPreview._tab) {
454       gBrowser.removeTab(aPreview._tab);
455     }
456   },
458   attachTab: function ctrlTab_attachTab(aTab, aPos) {
459     // If the tab is hidden, don't add it to the list unless it's selected
460     // (Normally hidden tabs would be unhidden when selected, but that doesn't
461     // happen for Firefox View).
462     if (aTab.closing || (aTab.hidden && !aTab.selected)) {
463       return;
464     }
466     // If the tab is already in the list, remove it before re-inserting it.
467     this.detachTab(aTab);
469     if (aPos == 0) {
470       this._recentlyUsedTabs.unshift(aTab);
471     } else if (aPos) {
472       this._recentlyUsedTabs.splice(aPos, 0, aTab);
473     } else {
474       this._recentlyUsedTabs.push(aTab);
475     }
476   },
478   detachTab: function ctrlTab_detachTab(aTab) {
479     var i = this._recentlyUsedTabs.indexOf(aTab);
480     if (i >= 0) {
481       this._recentlyUsedTabs.splice(i, 1);
482     }
483   },
485   open: function ctrlTab_open() {
486     if (this.isOpen) {
487       return;
488     }
490     this.canvasWidth = Math.ceil(
491       (screen.availWidth * 0.85) / this.maxTabPreviews
492     );
493     this.canvasHeight = Math.round(this.canvasWidth * tabPreviews.aspectRatio);
494     this.updatePreviews();
495     this._selectedIndex = 1;
496     gBrowser.warmupTab(this.selected._tab);
498     // Add a slight delay before showing the UI, so that a quick
499     // "ctrl-tab" keypress just flips back to the MRU tab.
500     this._timer = setTimeout(() => {
501       this._timer = null;
502       this._openPanel();
503     }, 200);
504   },
506   _openPanel: function ctrlTab_openPanel() {
507     tabPreviewPanelHelper.opening(this);
509     let width = Math.min(
510       screen.availWidth * 0.99,
511       this.canvasWidth * 1.25 * this.tabPreviewCount
512     );
513     this.panel.style.width = width + "px";
514     var estimateHeight = this.canvasHeight * 1.25 + 75;
515     this.panel.openPopupAtScreen(
516       screen.availLeft + (screen.availWidth - width) / 2,
517       screen.availTop + (screen.availHeight - estimateHeight) / 2,
518       false
519     );
520   },
522   close: function ctrlTab_close(aTabToSelect) {
523     if (!this.isOpen) {
524       return;
525     }
527     if (this._timer) {
528       clearTimeout(this._timer);
529       this._timer = null;
530       this.suspendGUI();
531       if (aTabToSelect) {
532         gBrowser.selectedTab = aTabToSelect;
533       }
534       return;
535     }
537     this.tabToSelect = aTabToSelect;
538     this.panel.hidePopup();
539   },
541   setupGUI: function ctrlTab_setupGUI() {
542     this.selected.focus();
543     this._selectedIndex = -1;
545     // Track mouse movement after a brief delay so that the item that happens
546     // to be under the mouse pointer initially won't be selected unintentionally.
547     this._trackMouseOver = false;
548     setTimeout(
549       function (self) {
550         if (self.isOpen) {
551           self._trackMouseOver = true;
552         }
553       },
554       0,
555       this
556     );
557   },
559   suspendGUI: function ctrlTab_suspendGUI() {
560     for (let preview of this.previews) {
561       this.updatePreview(preview, null);
562     }
563   },
565   onKeyDown(event) {
566     let action = ShortcutUtils.getSystemActionForEvent(event);
567     if (action != ShortcutUtils.CYCLE_TABS) {
568       return;
569     }
571     event.preventDefault();
572     event.stopPropagation();
574     if (this.isOpen) {
575       this.advanceFocus(!event.shiftKey);
576       return;
577     }
579     if (event.shiftKey) {
580       this.showAllTabs("shift-tab");
581       return;
582     }
584     document.addEventListener("keyup", this, { mozSystemGroup: true });
586     let tabs = gBrowser.visibleTabs;
587     if (tabs.length > 2) {
588       this.open();
589     } else if (tabs.length == 2) {
590       let index = tabs[0].selected ? 1 : 0;
591       gBrowser.selectedTab = tabs[index];
592     }
593   },
595   onKeyPress(event) {
596     if (!this.isOpen || !event.ctrlKey) {
597       return;
598     }
600     event.preventDefault();
601     event.stopPropagation();
603     if (event.keyCode == event.DOM_VK_DELETE) {
604       this.remove(this.selected);
605       return;
606     }
608     switch (event.charCode) {
609       case this.keys.close:
610         this.remove(this.selected);
611         break;
612       case this.keys.find:
613         this.showAllTabs("ctrltab-key-find");
614         break;
615       case this.keys.selectAll:
616         this.showAllTabs("ctrltab-key-selectAll");
617         break;
618     }
619   },
621   removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) {
622     if (this.tabCount == 2) {
623       this.close();
624       return;
625     }
627     this.updatePreviews();
629     if (this.selected.hidden) {
630       this.advanceFocus(false);
631     }
632     if (this.selected == this.showAllButton) {
633       this.advanceFocus(false);
634     }
636     // If the current tab is removed, another tab can steal our focus.
637     if (aTab.selected && this.panel.state == "open") {
638       setTimeout(
639         function (selected) {
640           selected.focus();
641         },
642         0,
643         this.selected
644       );
645     }
646   },
648   handleEvent: function ctrlTab_handleEvent(event) {
649     switch (event.type) {
650       case "SSWindowRestored":
651         this._initRecentlyUsedTabs();
652         break;
653       case "TabAttrModified":
654         // tab attribute modified (i.e. label, busy, image)
655         // update preview only if tab attribute modified in the list
656         if (
657           event.detail.changed.some(elem =>
658             ["label", "busy", "image"].includes(elem)
659           )
660         ) {
661           for (let i = this.previews.length - 1; i >= 0; i--) {
662             if (
663               this.previews[i]._tab &&
664               this.previews[i]._tab == event.target
665             ) {
666               this.updatePreview(this.previews[i], event.target);
667               break;
668             }
669           }
670         }
671         break;
672       case "TabSelect":
673         this.attachTab(event.target, 0);
674         // If the previous tab was hidden (e.g. Firefox View), remove it from
675         // the list when it's deselected.
676         let previousTab = event.detail.previousTab;
677         if (previousTab.hidden) {
678           this.detachTab(previousTab);
679         }
680         break;
681       case "TabOpen":
682         this.attachTab(event.target, 1);
683         break;
684       case "TabClose":
685         this.detachTab(event.target);
686         if (this.isOpen) {
687           this.removeClosingTabFromUI(event.target);
688         }
689         break;
690       case "TabHide":
691         this.detachTab(event.target);
692         break;
693       case "TabShow":
694         this.attachTab(event.target);
695         this._sortRecentlyUsedTabs();
696         break;
697       case "keydown":
698         this.onKeyDown(event);
699         break;
700       case "keypress":
701         this.onKeyPress(event);
702         break;
703       case "keyup":
704         // During cycling tabs, we avoid sending keyup event to content document.
705         event.preventDefault();
706         event.stopPropagation();
708         if (event.keyCode === event.DOM_VK_CONTROL) {
709           document.removeEventListener("keyup", this, { mozSystemGroup: true });
711           if (this.isOpen) {
712             this.pick();
713           }
714         }
715         break;
716       case "popupshowing":
717         if (event.target.id == "menu_viewPopup") {
718           document.getElementById("menu_showAllTabs").hidden =
719             !gTabsPanel.canOpen;
720         }
721         break;
722       case "mouseover":
723         this._mouseOverFocus(event.currentTarget);
724         break;
725       case "command":
726         this.pick(event.currentTarget);
727         break;
728       case "click":
729         if (event.button == 1) {
730           this.remove(event.currentTarget);
731         } else if (AppConstants.platform == "macosx" && event.button == 2) {
732           // Control+click is a right click on macOS, but in this case we want
733           // to handle it like a left click.
734           this.pick(event.currentTarget);
735         }
736         break;
737     }
738   },
740   filterForThumbnailExpiration(aCallback) {
741     // Save a few more thumbnails than we actually display, so that when tabs
742     // are closed, the previews we add instead still get thumbnails.
743     const extraThumbnails = 3;
744     const thumbnailCount = Math.min(
745       this.tabPreviewCount + extraThumbnails,
746       this.tabCount
747     );
749     let urls = [];
750     for (let i = 0; i < thumbnailCount; i++) {
751       urls.push(this.tabList[i].linkedBrowser.currentURI.spec);
752     }
754     aCallback(urls);
755   },
756   _sortRecentlyUsedTabs() {
757     this._recentlyUsedTabs.sort(
758       (tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed
759     );
760   },
761   _initRecentlyUsedTabs() {
762     this._recentlyUsedTabs = Array.prototype.filter.call(
763       gBrowser.tabs,
764       tab => !tab.closing && !tab.hidden
765     );
766     this._sortRecentlyUsedTabs();
767   },
769   _init: function ctrlTab__init(enable) {
770     var toggleEventListener = enable
771       ? "addEventListener"
772       : "removeEventListener";
774     window[toggleEventListener]("SSWindowRestored", this);
776     var tabContainer = gBrowser.tabContainer;
777     tabContainer[toggleEventListener]("TabOpen", this);
778     tabContainer[toggleEventListener]("TabAttrModified", this);
779     tabContainer[toggleEventListener]("TabSelect", this);
780     tabContainer[toggleEventListener]("TabClose", this);
781     tabContainer[toggleEventListener]("TabHide", this);
782     tabContainer[toggleEventListener]("TabShow", this);
784     if (enable) {
785       document.addEventListener("keydown", this, { mozSystemGroup: true });
786     } else {
787       document.removeEventListener("keydown", this, { mozSystemGroup: true });
788     }
789     document[toggleEventListener]("keypress", this);
790     gBrowser.tabbox.handleCtrlTab = !enable;
792     if (enable) {
793       PageThumbs.addExpirationFilter(this);
794     } else {
795       PageThumbs.removeExpirationFilter(this);
796     }
798     // If we're not running, hide the "Show All Tabs" menu item,
799     // as Shift+Ctrl+Tab will be handled by the tab bar.
800     document.getElementById("menu_showAllTabs").hidden = !enable;
801     document
802       .getElementById("menu_viewPopup")
803       [toggleEventListener]("popupshowing", this);
804   },