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 };