diff options
Diffstat (limited to 'browser/base/content/browser-ctrlTab.js')
-rw-r--r-- | browser/base/content/browser-ctrlTab.js | 587 |
1 files changed, 587 insertions, 0 deletions
diff --git a/browser/base/content/browser-ctrlTab.js b/browser/base/content/browser-ctrlTab.js new file mode 100644 index 000000000..c761ea095 --- /dev/null +++ b/browser/base/content/browser-ctrlTab.js @@ -0,0 +1,587 @@ +/* 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/. */ + +/** + * Tab previews utility, produces thumbnails + */ +var tabPreviews = { + init: function tabPreviews_init() { + if (this._selectedTab) + return; + this._selectedTab = gBrowser.selectedTab; + + gBrowser.tabContainer.addEventListener("TabSelect", this, false); + gBrowser.tabContainer.addEventListener("SSTabRestored", this, false); + + let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"] + .getService(Ci.nsIScreenManager); + let left = {}, top = {}, width = {}, height = {}; + screenManager.primaryScreen.GetRectDisplayPix(left, top, width, height); + this.aspectRatio = height.value / width.value; + }, + + get: function tabPreviews_get(aTab) { + let uri = aTab.linkedBrowser.currentURI.spec; + + if (aTab.__thumbnail_lastURI && + aTab.__thumbnail_lastURI != uri) { + aTab.__thumbnail = null; + aTab.__thumbnail_lastURI = null; + } + + if (aTab.__thumbnail) + return aTab.__thumbnail; + + if (aTab.getAttribute("pending") == "true") { + let img = new Image; + img.src = PageThumbs.getThumbnailURL(uri); + return img; + } + + return this.capture(aTab, !aTab.hasAttribute("busy")); + }, + + capture: function tabPreviews_capture(aTab, aShouldCache) { + let browser = aTab.linkedBrowser; + let uri = browser.currentURI.spec; + let canvas = PageThumbs.createCanvas(window); + PageThumbs.shouldStoreThumbnail(browser, (aDoStore) => { + if (aDoStore && aShouldCache) { + PageThumbs.captureAndStore(browser, function () { + let img = new Image; + img.src = PageThumbs.getThumbnailURL(uri); + aTab.__thumbnail = img; + aTab.__thumbnail_lastURI = uri; + canvas.getContext("2d").drawImage(img, 0, 0); + }); + } else { + PageThumbs.captureToCanvas(browser, canvas, () => { + if (aShouldCache) { + aTab.__thumbnail = canvas; + aTab.__thumbnail_lastURI = uri; + } + }); + } + }); + return canvas; + }, + + handleEvent: function tabPreviews_handleEvent(event) { + switch (event.type) { + case "TabSelect": + if (this._selectedTab && + this._selectedTab.parentNode && + !this._pendingUpdate) { + // Generate a thumbnail for the tab that was selected. + // The timeout keeps the UI snappy and prevents us from generating thumbnails + // for tabs that will be closed. During that timeout, don't generate other + // thumbnails in case multiple TabSelect events occur fast in succession. + this._pendingUpdate = true; + setTimeout(function (self, aTab) { + self._pendingUpdate = false; + if (aTab.parentNode && + !aTab.hasAttribute("busy") && + !aTab.hasAttribute("pending")) + self.capture(aTab, true); + }, 2000, this, this._selectedTab); + } + this._selectedTab = event.target; + break; + case "SSTabRestored": + this.capture(event.target, true); + break; + } + } +}; + +var tabPreviewPanelHelper = { + opening: function (host) { + host.panel.hidden = false; + + var handler = this._generateHandler(host); + host.panel.addEventListener("popupshown", handler, false); + host.panel.addEventListener("popuphiding", handler, false); + + host._prevFocus = document.commandDispatcher.focusedElement; + }, + _generateHandler: function (host) { + var self = this; + return function (event) { + if (event.target == host.panel) { + host.panel.removeEventListener(event.type, arguments.callee, false); + self["_" + event.type](host); + } + }; + }, + _popupshown: function (host) { + if ("setupGUI" in host) + host.setupGUI(); + }, + _popuphiding: function (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 = { + get panel () { + delete this.panel; + return this.panel = document.getElementById("ctrlTab-panel"); + }, + get showAllButton () { + delete this.showAllButton; + return this.showAllButton = document.getElementById("ctrlTab-showAll"); + }, + get previews () { + delete this.previews; + return this.previews = this.panel.getElementsByClassName("ctrlTab-preview"); + }, + get maxTabPreviews () { + delete this.maxTabPreviews; + return this.maxTabPreviews = this.previews.length - 1; + }, + get canvasWidth () { + delete this.canvasWidth; + return this.canvasWidth = Math.ceil(screen.availWidth * .85 / this.maxTabPreviews); + }, + get canvasHeight () { + delete this.canvasHeight; + return this.canvasHeight = Math.round(this.canvasWidth * tabPreviews.aspectRatio); + }, + 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.item(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) { + tabPreviews.init(); + + this._initRecentlyUsedTabs(); + this._init(true); + } + }, + + uninit: function ctrlTab_uninit() { + this._recentlyUsedTabs = null; + this._init(false); + }, + + prefName: "browser.ctrlTab.previews", + readPref: function ctrlTab_readPref() { + var enable = + gPrefService.getBoolPref(this.prefName) && + (!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") || + !gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders")); + + if (enable) + this.init(); + else + this.uninit(); + }, + observe: function (aSubject, aTopic, aPrefName) { + this.readPref(); + }, + + updatePreviews: function ctrlTab_updatePreviews() { + for (let i = 0; i < this.previews.length; i++) + this.updatePreview(this.previews[i], this.tabList[i]); + + var showAllLabel = gNavigatorBundle.getString("ctrlTab.listAllTabs.label"); + this.showAllButton.label = + PluralForm.get(this.tabCount, showAllLabel).replace("#1", this.tabCount); + this.showAllButton.hidden = !allTabs.canOpen; + }, + + updatePreview: function ctrlTab_updatePreview(aPreview, aTab) { + if (aPreview == this.showAllButton) + return; + + aPreview._tab = aTab; + + if (aPreview.firstChild) + aPreview.removeChild(aPreview.firstChild); + if (aTab) { + let canvasWidth = this.canvasWidth; + let canvasHeight = this.canvasHeight; + aPreview.appendChild(tabPreviews.get(aTab)); + aPreview.setAttribute("label", aTab.label); + aPreview.setAttribute("tooltiptext", aTab.label); + aPreview.setAttribute("crop", aTab.crop); + aPreview.setAttribute("canvaswidth", canvasWidth); + aPreview.setAttribute("canvasstyle", + "max-width:" + canvasWidth + "px;" + + "min-width:" + canvasWidth + "px;" + + "max-height:" + canvasHeight + "px;" + + "min-height:" + canvasHeight + "px;"); + if (aTab.image) + aPreview.setAttribute("image", aTab.image); + else + aPreview.removeAttribute("image"); + aPreview.hidden = false; + } else { + aPreview.hidden = true; + aPreview.removeAttribute("label"); + aPreview.removeAttribute("tooltiptext"); + aPreview.removeAttribute("image"); + } + }, + + advanceFocus: function ctrlTab_advanceFocus(aForward) { + let selectedIndex = Array.indexOf(this.previews, 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._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(); + else + this.close(select._tab); + }, + + showAllTabs: function ctrlTab_showAllTabs(aPreview) { + this.close(); + document.getElementById("Browser:ShowAllTabs").doCommand(); + }, + + remove: function ctrlTab_remove(aPreview) { + if (aPreview._tab) + gBrowser.removeTab(aPreview._tab); + }, + + attachTab: function ctrlTab_attachTab(aTab, aPos) { + if (aTab.closing) + return; + + 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; + + document.addEventListener("keyup", this, true); + + this.updatePreviews(); + this._selectedIndex = 1; + + // 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(function (self) { + self._timer = null; + self._openPanel(); + }, 200, this); + }, + + _openPanel: function ctrlTab_openPanel() { + tabPreviewPanelHelper.opening(this); + + this.panel.width = Math.min(screen.availWidth * .99, + this.canvasWidth * 1.25 * this.tabPreviewCount); + var estimateHeight = this.canvasHeight * 1.25 + 75; + this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.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() { + document.removeEventListener("keyup", this, true); + + for (let preview of this.previews) { + this.updatePreview(preview, null); + } + }, + + onKeyPress: function ctrlTab_onKeyPress(event) { + var isOpen = this.isOpen; + + if (isOpen) { + event.preventDefault(); + event.stopPropagation(); + } + + switch (event.keyCode) { + case event.DOM_VK_TAB: + if (event.ctrlKey && !event.altKey && !event.metaKey) { + if (isOpen) { + this.advanceFocus(!event.shiftKey); + } else if (!event.shiftKey) { + event.preventDefault(); + event.stopPropagation(); + 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]; + } + } + } + break; + default: + if (isOpen && event.ctrlKey) { + if (event.keyCode == event.DOM_VK_DELETE) { + this.remove(this.selected); + break; + } + switch (event.charCode) { + case this.keys.close: + this.remove(this.selected); + break; + case this.keys.find: + case this.keys.selectAll: + this.showAllTabs(); + 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 (e.g. label, crop, busy, image, selected) + 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.detachTab(event.target); + this.attachTab(event.target, 0); + break; + case "TabOpen": + this.attachTab(event.target, 1); + break; + case "TabClose": + this.detachTab(event.target); + if (this.isOpen) + this.removeClosingTabFromUI(event.target); + break; + case "keypress": + this.onKeyPress(event); + break; + case "keyup": + if (event.keyCode == event.DOM_VK_CONTROL) + this.pick(); + break; + case "popupshowing": + if (event.target.id == "menu_viewPopup") + document.getElementById("menu_showAllTabs").hidden = !allTabs.canOpen; + break; + } + }, + + filterForThumbnailExpiration: function (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); + }, + + _initRecentlyUsedTabs: function () { + this._recentlyUsedTabs = + Array.filter(gBrowser.tabs, tab => !tab.closing) + .sort((tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed); + }, + + _init: function ctrlTab__init(enable) { + var toggleEventListener = enable ? "addEventListener" : "removeEventListener"; + + window[toggleEventListener]("SSWindowRestored", this, false); + + var tabContainer = gBrowser.tabContainer; + tabContainer[toggleEventListener]("TabOpen", this, false); + tabContainer[toggleEventListener]("TabAttrModified", this, false); + tabContainer[toggleEventListener]("TabSelect", this, false); + tabContainer[toggleEventListener]("TabClose", this, false); + + document[toggleEventListener]("keypress", this, false); + gBrowser.mTabBox.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); + + // Also disable the <key> to ensure Shift+Ctrl+Tab never triggers + // Show All Tabs. + var key_showAllTabs = document.getElementById("key_showAllTabs"); + if (enable) + key_showAllTabs.removeAttribute("disabled"); + else + key_showAllTabs.setAttribute("disabled", "true"); + } +}; + + +/** + * All Tabs menu + */ +var allTabs = { + get toolbarButton() { + return document.getElementById("alltabs-button"); + }, + + get canOpen() { + return isElementVisible(this.toolbarButton); + }, + + open: function allTabs_open() { + if (this.canOpen) { + // Without setTimeout, the menupopup won't stay open when invoking + // "View > Show All Tabs" and the menu bar auto-hides. + setTimeout(() => { + this.toolbarButton.open = true; + }, 0); + } + } +}; |