michael@0: /* michael@0: #ifdef 0 michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: #endif michael@0: */ michael@0: michael@0: /** michael@0: * Tab previews utility, produces thumbnails michael@0: */ michael@0: var tabPreviews = { michael@0: aspectRatio: 0.5625, // 16:9 michael@0: michael@0: get width() { michael@0: delete this.width; michael@0: return this.width = Math.ceil(screen.availWidth / 5.75); michael@0: }, michael@0: michael@0: get height() { michael@0: delete this.height; michael@0: return this.height = Math.round(this.width * this.aspectRatio); michael@0: }, michael@0: michael@0: init: function tabPreviews_init() { michael@0: if (this._selectedTab) michael@0: return; michael@0: this._selectedTab = gBrowser.selectedTab; michael@0: michael@0: gBrowser.tabContainer.addEventListener("TabSelect", this, false); michael@0: gBrowser.tabContainer.addEventListener("SSTabRestored", this, false); michael@0: }, michael@0: michael@0: get: function tabPreviews_get(aTab) { michael@0: let uri = aTab.linkedBrowser.currentURI.spec; michael@0: michael@0: if (aTab.__thumbnail_lastURI && michael@0: aTab.__thumbnail_lastURI != uri) { michael@0: aTab.__thumbnail = null; michael@0: aTab.__thumbnail_lastURI = null; michael@0: } michael@0: michael@0: if (aTab.__thumbnail) michael@0: return aTab.__thumbnail; michael@0: michael@0: if (aTab.getAttribute("pending") == "true") { michael@0: let img = new Image; michael@0: img.src = PageThumbs.getThumbnailURL(uri); michael@0: return img; michael@0: } michael@0: michael@0: return this.capture(aTab, !aTab.hasAttribute("busy")); michael@0: }, michael@0: michael@0: capture: function tabPreviews_capture(aTab, aStore) { michael@0: var thumbnail = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); michael@0: thumbnail.mozOpaque = true; michael@0: thumbnail.height = this.height; michael@0: thumbnail.width = this.width; michael@0: michael@0: var ctx = thumbnail.getContext("2d"); michael@0: var win = aTab.linkedBrowser.contentWindow; michael@0: var snippetWidth = win.innerWidth * .6; michael@0: var scale = this.width / snippetWidth; michael@0: ctx.scale(scale, scale); michael@0: ctx.drawWindow(win, win.scrollX, win.scrollY, michael@0: snippetWidth, snippetWidth * this.aspectRatio, "rgb(255,255,255)"); michael@0: michael@0: if (aStore && michael@0: aTab.linkedBrowser /* bug 795608: the tab may got removed while drawing the thumbnail */) { michael@0: aTab.__thumbnail = thumbnail; michael@0: aTab.__thumbnail_lastURI = aTab.linkedBrowser.currentURI.spec; michael@0: } michael@0: michael@0: return thumbnail; michael@0: }, michael@0: michael@0: handleEvent: function tabPreviews_handleEvent(event) { michael@0: switch (event.type) { michael@0: case "TabSelect": michael@0: if (this._selectedTab && michael@0: this._selectedTab.parentNode && michael@0: !this._pendingUpdate) { michael@0: // Generate a thumbnail for the tab that was selected. michael@0: // The timeout keeps the UI snappy and prevents us from generating thumbnails michael@0: // for tabs that will be closed. During that timeout, don't generate other michael@0: // thumbnails in case multiple TabSelect events occur fast in succession. michael@0: this._pendingUpdate = true; michael@0: setTimeout(function (self, aTab) { michael@0: self._pendingUpdate = false; michael@0: if (aTab.parentNode && michael@0: !aTab.hasAttribute("busy") && michael@0: !aTab.hasAttribute("pending")) michael@0: self.capture(aTab, true); michael@0: }, 2000, this, this._selectedTab); michael@0: } michael@0: this._selectedTab = event.target; michael@0: break; michael@0: case "SSTabRestored": michael@0: this.capture(event.target, true); michael@0: break; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: var tabPreviewPanelHelper = { michael@0: opening: function (host) { michael@0: host.panel.hidden = false; michael@0: michael@0: var handler = this._generateHandler(host); michael@0: host.panel.addEventListener("popupshown", handler, false); michael@0: host.panel.addEventListener("popuphiding", handler, false); michael@0: michael@0: host._prevFocus = document.commandDispatcher.focusedElement; michael@0: }, michael@0: _generateHandler: function (host) { michael@0: var self = this; michael@0: return function (event) { michael@0: if (event.target == host.panel) { michael@0: host.panel.removeEventListener(event.type, arguments.callee, false); michael@0: self["_" + event.type](host); michael@0: } michael@0: }; michael@0: }, michael@0: _popupshown: function (host) { michael@0: if ("setupGUI" in host) michael@0: host.setupGUI(); michael@0: }, michael@0: _popuphiding: function (host) { michael@0: if ("suspendGUI" in host) michael@0: host.suspendGUI(); michael@0: michael@0: if (host._prevFocus) { michael@0: Cc["@mozilla.org/focus-manager;1"] michael@0: .getService(Ci.nsIFocusManager) michael@0: .setFocus(host._prevFocus, Ci.nsIFocusManager.FLAG_NOSCROLL); michael@0: host._prevFocus = null; michael@0: } else michael@0: gBrowser.selectedBrowser.focus(); michael@0: michael@0: if (host.tabToSelect) { michael@0: gBrowser.selectedTab = host.tabToSelect; michael@0: host.tabToSelect = null; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Ctrl-Tab panel michael@0: */ michael@0: var ctrlTab = { michael@0: get panel () { michael@0: delete this.panel; michael@0: return this.panel = document.getElementById("ctrlTab-panel"); michael@0: }, michael@0: get showAllButton () { michael@0: delete this.showAllButton; michael@0: return this.showAllButton = document.getElementById("ctrlTab-showAll"); michael@0: }, michael@0: get previews () { michael@0: delete this.previews; michael@0: return this.previews = this.panel.getElementsByClassName("ctrlTab-preview"); michael@0: }, michael@0: get keys () { michael@0: var keys = {}; michael@0: ["close", "find", "selectAll"].forEach(function (key) { michael@0: keys[key] = document.getElementById("key_" + key) michael@0: .getAttribute("key") michael@0: .toLocaleLowerCase().charCodeAt(0); michael@0: }); michael@0: delete this.keys; michael@0: return this.keys = keys; michael@0: }, michael@0: _selectedIndex: 0, michael@0: get selected () this._selectedIndex < 0 ? michael@0: document.activeElement : michael@0: this.previews.item(this._selectedIndex), michael@0: get isOpen () this.panel.state == "open" || this.panel.state == "showing" || this._timer, michael@0: get tabCount () this.tabList.length, michael@0: get tabPreviewCount () Math.min(this.previews.length - 1, this.tabCount), michael@0: get canvasWidth () Math.min(tabPreviews.width, michael@0: Math.ceil(screen.availWidth * .85 / this.tabPreviewCount)), michael@0: get canvasHeight () Math.round(this.canvasWidth * tabPreviews.aspectRatio), michael@0: michael@0: get tabList () { michael@0: return this._recentlyUsedTabs; michael@0: }, michael@0: michael@0: init: function ctrlTab_init() { michael@0: if (!this._recentlyUsedTabs) { michael@0: tabPreviews.init(); michael@0: michael@0: this._initRecentlyUsedTabs(); michael@0: this._init(true); michael@0: } michael@0: }, michael@0: michael@0: uninit: function ctrlTab_uninit() { michael@0: this._recentlyUsedTabs = null; michael@0: this._init(false); michael@0: }, michael@0: michael@0: prefName: "browser.ctrlTab.previews", michael@0: readPref: function ctrlTab_readPref() { michael@0: var enable = michael@0: gPrefService.getBoolPref(this.prefName) && michael@0: (!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") || michael@0: !gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders")); michael@0: michael@0: if (enable) michael@0: this.init(); michael@0: else michael@0: this.uninit(); michael@0: }, michael@0: observe: function (aSubject, aTopic, aPrefName) { michael@0: this.readPref(); michael@0: }, michael@0: michael@0: updatePreviews: function ctrlTab_updatePreviews() { michael@0: for (let i = 0; i < this.previews.length; i++) michael@0: this.updatePreview(this.previews[i], this.tabList[i]); michael@0: michael@0: var showAllLabel = gNavigatorBundle.getString("ctrlTab.showAll.label"); michael@0: this.showAllButton.label = michael@0: PluralForm.get(this.tabCount, showAllLabel).replace("#1", this.tabCount); michael@0: this.showAllButton.hidden = !allTabs.canOpen; michael@0: }, michael@0: michael@0: updatePreview: function ctrlTab_updatePreview(aPreview, aTab) { michael@0: if (aPreview == this.showAllButton) michael@0: return; michael@0: michael@0: aPreview._tab = aTab; michael@0: michael@0: if (aPreview.firstChild) michael@0: aPreview.removeChild(aPreview.firstChild); michael@0: if (aTab) { michael@0: let canvasWidth = this.canvasWidth; michael@0: let canvasHeight = this.canvasHeight; michael@0: aPreview.appendChild(tabPreviews.get(aTab)); michael@0: aPreview.setAttribute("label", aTab.label); michael@0: aPreview.setAttribute("tooltiptext", aTab.label); michael@0: aPreview.setAttribute("crop", aTab.crop); michael@0: aPreview.setAttribute("canvaswidth", canvasWidth); michael@0: aPreview.setAttribute("canvasstyle", michael@0: "max-width:" + canvasWidth + "px;" + michael@0: "min-width:" + canvasWidth + "px;" + michael@0: "max-height:" + canvasHeight + "px;" + michael@0: "min-height:" + canvasHeight + "px;"); michael@0: if (aTab.image) michael@0: aPreview.setAttribute("image", aTab.image); michael@0: else michael@0: aPreview.removeAttribute("image"); michael@0: aPreview.hidden = false; michael@0: } else { michael@0: aPreview.hidden = true; michael@0: aPreview.removeAttribute("label"); michael@0: aPreview.removeAttribute("tooltiptext"); michael@0: aPreview.removeAttribute("image"); michael@0: } michael@0: }, michael@0: michael@0: advanceFocus: function ctrlTab_advanceFocus(aForward) { michael@0: let selectedIndex = Array.indexOf(this.previews, this.selected); michael@0: do { michael@0: selectedIndex += aForward ? 1 : -1; michael@0: if (selectedIndex < 0) michael@0: selectedIndex = this.previews.length - 1; michael@0: else if (selectedIndex >= this.previews.length) michael@0: selectedIndex = 0; michael@0: } while (this.previews[selectedIndex].hidden); michael@0: michael@0: if (this._selectedIndex == -1) { michael@0: // Focus is already in the panel. michael@0: this.previews[selectedIndex].focus(); michael@0: } else { michael@0: this._selectedIndex = selectedIndex; michael@0: } michael@0: michael@0: if (this._timer) { michael@0: clearTimeout(this._timer); michael@0: this._timer = null; michael@0: this._openPanel(); michael@0: } michael@0: }, michael@0: michael@0: _mouseOverFocus: function ctrlTab_mouseOverFocus(aPreview) { michael@0: if (this._trackMouseOver) michael@0: aPreview.focus(); michael@0: }, michael@0: michael@0: pick: function ctrlTab_pick(aPreview) { michael@0: if (!this.tabCount) michael@0: return; michael@0: michael@0: var select = (aPreview || this.selected); michael@0: michael@0: if (select == this.showAllButton) michael@0: this.showAllTabs(); michael@0: else michael@0: this.close(select._tab); michael@0: }, michael@0: michael@0: showAllTabs: function ctrlTab_showAllTabs(aPreview) { michael@0: this.close(); michael@0: document.getElementById("Browser:ShowAllTabs").doCommand(); michael@0: }, michael@0: michael@0: remove: function ctrlTab_remove(aPreview) { michael@0: if (aPreview._tab) michael@0: gBrowser.removeTab(aPreview._tab); michael@0: }, michael@0: michael@0: attachTab: function ctrlTab_attachTab(aTab, aPos) { michael@0: if (aTab.closing) michael@0: return; michael@0: michael@0: if (aPos == 0) michael@0: this._recentlyUsedTabs.unshift(aTab); michael@0: else if (aPos) michael@0: this._recentlyUsedTabs.splice(aPos, 0, aTab); michael@0: else michael@0: this._recentlyUsedTabs.push(aTab); michael@0: }, michael@0: michael@0: detachTab: function ctrlTab_detachTab(aTab) { michael@0: var i = this._recentlyUsedTabs.indexOf(aTab); michael@0: if (i >= 0) michael@0: this._recentlyUsedTabs.splice(i, 1); michael@0: }, michael@0: michael@0: open: function ctrlTab_open() { michael@0: if (this.isOpen) michael@0: return; michael@0: michael@0: document.addEventListener("keyup", this, true); michael@0: michael@0: this.updatePreviews(); michael@0: this._selectedIndex = 1; michael@0: michael@0: // Add a slight delay before showing the UI, so that a quick michael@0: // "ctrl-tab" keypress just flips back to the MRU tab. michael@0: this._timer = setTimeout(function (self) { michael@0: self._timer = null; michael@0: self._openPanel(); michael@0: }, 200, this); michael@0: }, michael@0: michael@0: _openPanel: function ctrlTab_openPanel() { michael@0: tabPreviewPanelHelper.opening(this); michael@0: michael@0: this.panel.width = Math.min(screen.availWidth * .99, michael@0: this.canvasWidth * 1.25 * this.tabPreviewCount); michael@0: var estimateHeight = this.canvasHeight * 1.25 + 75; michael@0: this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2, michael@0: screen.availTop + (screen.availHeight - estimateHeight) / 2, michael@0: false); michael@0: }, michael@0: michael@0: close: function ctrlTab_close(aTabToSelect) { michael@0: if (!this.isOpen) michael@0: return; michael@0: michael@0: if (this._timer) { michael@0: clearTimeout(this._timer); michael@0: this._timer = null; michael@0: this.suspendGUI(); michael@0: if (aTabToSelect) michael@0: gBrowser.selectedTab = aTabToSelect; michael@0: return; michael@0: } michael@0: michael@0: this.tabToSelect = aTabToSelect; michael@0: this.panel.hidePopup(); michael@0: }, michael@0: michael@0: setupGUI: function ctrlTab_setupGUI() { michael@0: this.selected.focus(); michael@0: this._selectedIndex = -1; michael@0: michael@0: // Track mouse movement after a brief delay so that the item that happens michael@0: // to be under the mouse pointer initially won't be selected unintentionally. michael@0: this._trackMouseOver = false; michael@0: setTimeout(function (self) { michael@0: if (self.isOpen) michael@0: self._trackMouseOver = true; michael@0: }, 0, this); michael@0: }, michael@0: michael@0: suspendGUI: function ctrlTab_suspendGUI() { michael@0: document.removeEventListener("keyup", this, true); michael@0: michael@0: Array.forEach(this.previews, function (preview) { michael@0: this.updatePreview(preview, null); michael@0: }, this); michael@0: }, michael@0: michael@0: onKeyPress: function ctrlTab_onKeyPress(event) { michael@0: var isOpen = this.isOpen; michael@0: michael@0: if (isOpen) { michael@0: event.preventDefault(); michael@0: event.stopPropagation(); michael@0: } michael@0: michael@0: switch (event.keyCode) { michael@0: case event.DOM_VK_TAB: michael@0: if (event.ctrlKey && !event.altKey && !event.metaKey) { michael@0: if (isOpen) { michael@0: this.advanceFocus(!event.shiftKey); michael@0: } else if (!event.shiftKey) { michael@0: event.preventDefault(); michael@0: event.stopPropagation(); michael@0: let tabs = gBrowser.visibleTabs; michael@0: if (tabs.length > 2) { michael@0: this.open(); michael@0: } else if (tabs.length == 2) { michael@0: let index = tabs[0].selected ? 1 : 0; michael@0: gBrowser.selectedTab = tabs[index]; michael@0: } michael@0: } michael@0: } michael@0: break; michael@0: default: michael@0: if (isOpen && event.ctrlKey) { michael@0: if (event.keyCode == event.DOM_VK_DELETE) { michael@0: this.remove(this.selected); michael@0: break; michael@0: } michael@0: switch (event.charCode) { michael@0: case this.keys.close: michael@0: this.remove(this.selected); michael@0: break; michael@0: case this.keys.find: michael@0: case this.keys.selectAll: michael@0: this.showAllTabs(); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) { michael@0: if (this.tabCount == 2) { michael@0: this.close(); michael@0: return; michael@0: } michael@0: michael@0: this.updatePreviews(); michael@0: michael@0: if (this.selected.hidden) michael@0: this.advanceFocus(false); michael@0: if (this.selected == this.showAllButton) michael@0: this.advanceFocus(false); michael@0: michael@0: // If the current tab is removed, another tab can steal our focus. michael@0: if (aTab.selected && this.panel.state == "open") { michael@0: setTimeout(function (selected) { michael@0: selected.focus(); michael@0: }, 0, this.selected); michael@0: } michael@0: }, michael@0: michael@0: handleEvent: function ctrlTab_handleEvent(event) { michael@0: switch (event.type) { michael@0: case "SSWindowStateReady": michael@0: this._initRecentlyUsedTabs(); michael@0: break; michael@0: case "TabAttrModified": michael@0: // tab attribute modified (e.g. label, crop, busy, image, selected) michael@0: for (let i = this.previews.length - 1; i >= 0; i--) { michael@0: if (this.previews[i]._tab && this.previews[i]._tab == event.target) { michael@0: this.updatePreview(this.previews[i], event.target); michael@0: break; michael@0: } michael@0: } michael@0: break; michael@0: case "TabSelect": michael@0: this.detachTab(event.target); michael@0: this.attachTab(event.target, 0); michael@0: break; michael@0: case "TabOpen": michael@0: this.attachTab(event.target, 1); michael@0: break; michael@0: case "TabClose": michael@0: this.detachTab(event.target); michael@0: if (this.isOpen) michael@0: this.removeClosingTabFromUI(event.target); michael@0: break; michael@0: case "keypress": michael@0: this.onKeyPress(event); michael@0: break; michael@0: case "keyup": michael@0: if (event.keyCode == event.DOM_VK_CONTROL) michael@0: this.pick(); michael@0: break; michael@0: case "popupshowing": michael@0: if (event.target.id == "menu_viewPopup") michael@0: document.getElementById("menu_showAllTabs").hidden = !allTabs.canOpen; michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _initRecentlyUsedTabs: function () { michael@0: this._recentlyUsedTabs = michael@0: Array.filter(gBrowser.tabs, tab => !tab.closing) michael@0: .sort((tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed); michael@0: }, michael@0: michael@0: _init: function ctrlTab__init(enable) { michael@0: var toggleEventListener = enable ? "addEventListener" : "removeEventListener"; michael@0: michael@0: window[toggleEventListener]("SSWindowStateReady", this, false); michael@0: michael@0: var tabContainer = gBrowser.tabContainer; michael@0: tabContainer[toggleEventListener]("TabOpen", this, false); michael@0: tabContainer[toggleEventListener]("TabAttrModified", this, false); michael@0: tabContainer[toggleEventListener]("TabSelect", this, false); michael@0: tabContainer[toggleEventListener]("TabClose", this, false); michael@0: michael@0: document[toggleEventListener]("keypress", this, false); michael@0: gBrowser.mTabBox.handleCtrlTab = !enable; michael@0: michael@0: // If we're not running, hide the "Show All Tabs" menu item, michael@0: // as Shift+Ctrl+Tab will be handled by the tab bar. michael@0: document.getElementById("menu_showAllTabs").hidden = !enable; michael@0: document.getElementById("menu_viewPopup")[toggleEventListener]("popupshowing", this); michael@0: michael@0: // Also disable the to ensure Shift+Ctrl+Tab never triggers michael@0: // Show All Tabs. michael@0: var key_showAllTabs = document.getElementById("key_showAllTabs"); michael@0: if (enable) michael@0: key_showAllTabs.removeAttribute("disabled"); michael@0: else michael@0: key_showAllTabs.setAttribute("disabled", "true"); michael@0: } michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * All Tabs menu michael@0: */ michael@0: var allTabs = { michael@0: get toolbarButton() document.getElementById("alltabs-button"), michael@0: get canOpen() isElementVisible(this.toolbarButton), michael@0: michael@0: open: function allTabs_open() { michael@0: if (this.canOpen) { michael@0: // Without setTimeout, the menupopup won't stay open when invoking michael@0: // "View > Show All Tabs" and the menu bar auto-hides. michael@0: setTimeout(function () { michael@0: allTabs.toolbarButton.open = true; michael@0: }, 0); michael@0: } michael@0: } michael@0: };