/* #ifdef 0 * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Tab Previews. * * The Initial Developer of the Original Code is Mozilla. * Portions created by the Initial Developer are Copyright (C) 2008 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Dão Gottwald * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** #endif */ /** * Tab previews utility, produces thumbnails */ var tabPreviews = { aspectRatio: 0.5625, // 16:9 init: function tabPreviews__init() { this.width = Math.ceil(screen.availWidth / 5); this.height = Math.round(this.width * this.aspectRatio); gBrowser.tabContainer.addEventListener("TabSelect", this, false); gBrowser.tabContainer.addEventListener("SSTabRestored", this, false); }, uninit: function tabPreviews__uninit() { gBrowser.tabContainer.removeEventListener("TabSelect", this, false); gBrowser.tabContainer.removeEventListener("SSTabRestored", this, false); this._selectedTab = null; }, get: function tabPreviews__get(aTab) { if (aTab.__thumbnail_lastURI && aTab.__thumbnail_lastURI != aTab.linkedBrowser.currentURI.spec) { aTab.__thumbnail = null; aTab.__thumbnail_lastURI = null; } return aTab.__thumbnail || this.capture(aTab, !aTab.hasAttribute("busy")); }, capture: function tabPreviews__capture(aTab, aStore) { var thumbnail = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); thumbnail.mozOpaque = true; thumbnail.height = this.height; thumbnail.width = this.width; var ctx = thumbnail.getContext("2d"); var win = aTab.linkedBrowser.contentWindow; var snippetWidth = win.innerWidth * .6; var scale = this.width / snippetWidth; ctx.scale(scale, scale); ctx.drawWindow(win, win.scrollX, win.scrollY, snippetWidth, snippetWidth * this.aspectRatio, "rgb(255,255,255)"); if (aStore) { aTab.__thumbnail = thumbnail; aTab.__thumbnail_lastURI = aTab.linkedBrowser.currentURI.spec; } return thumbnail; }, 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")) self.capture(aTab, true); }, 2000, this, this._selectedTab); } this._selectedTab = event.target; break; case "SSTabRestored": this.capture(event.target, true); break; } } }; /** * Ctrl-Tab panel */ var ctrlTab = { get panel () { delete this.panel; return this.panel = document.getElementById("ctrlTab-panel"); }, get searchField () { delete this.searchField; return this.searchField = document.getElementById("ctrlTab-search"); }, get pagesBar () { delete this.pagesBar; return this.pagesBar = document.getElementById("ctrlTab-pages"); }, get thumbnails () { delete this.thumbnails; return this.thumbnails = this.panel.getElementsByClassName("ctrlTab-thumbnail"); }, get columns () { delete this.columns; return this.columns = this.thumbnails.length / this.panel.getElementsByClassName("ctrlTab-row").length; }, get closeCharCode () { delete this.closeCharCode; return this.closeCharCode = document.getElementById("key_close") .getAttribute("key") .toLocaleLowerCase().charCodeAt(0); }, get findCharCode () { delete this.findCharCode; return this.findCharCode = document.getElementById("key_find") .getAttribute("key") .toLocaleLowerCase().charCodeAt(0); }, get recentlyUsedLimit () { delete this.recentlyUsedLimit; return this.recentlyUsedLimit = gPrefService.getIntPref("browser.ctrlTab.recentlyUsedLimit"); }, selectedIndex: 0, get selected () this.thumbnails.item(this.selectedIndex), get isOpen () this.panel.state == "open" || this.panel.state == "showing", get tabCount () this.tabList.length, get sticky () this.panel.hasAttribute("sticky"), set sticky (val) { if (val) this.panel.setAttribute("sticky", "true"); else this.panel.removeAttribute("sticky"); return val; }, get pages () Math.ceil(this.tabCount / this.thumbnails.length), get page () this._page || 0, set page (page) { if (page < 0) page = this.pages - 1; else if (page >= this.pages) page = 0; if (this.pagesBar.childNodes.length) { this.pagesBar.childNodes[this.page].removeAttribute("selected"); this.pagesBar.childNodes[page].setAttribute("selected", "true"); } this._page = page; this.updatePreviews(); return page; }, get tabList () { if (this._tabList) return this._tabList; var list = Array.slice(gBrowser.mTabs); if (this._closing) this.detachTab(this._closing, list); for (let i = 0; i < gBrowser.tabContainer.selectedIndex; i++) list.push(list.shift()); if (!this._useTabBarOrder && this.recentlyUsedLimit != 0) { let recentlyUsedTabs = this._recentlyUsedTabs; if (this.recentlyUsedLimit > 0) recentlyUsedTabs = this._recentlyUsedTabs.slice(0, this.recentlyUsedLimit); for (let i = recentlyUsedTabs.length - 1; i >= 0; i--) { list.splice(list.indexOf(recentlyUsedTabs[i]), 1); list.unshift(recentlyUsedTabs[i]); } } if (this.searchField.value) { list = list.filter(function (tab) { let lowerCaseLabel, uri; for (let i = 0; i < this.length; i++) { if (tab.label.indexOf(this[i]) != -1) continue; if (!lowerCaseLabel) lowerCaseLabel = tab.label.toLocaleLowerCase(); if (lowerCaseLabel.indexOf(this[i]) != -1) continue; if (!uri) { uri = tab.linkedBrowser.currentURI.spec; try { uri = decodeURI(uri); } catch (e) {} } if (uri.indexOf(this[i]) != -1) continue; return false; } return true; }, this.searchField.value.split(/\s+/g)); } return this._tabList = list; }, init: function ctrlTab__init() { if (this._recentlyUsedTabs) return; this._recentlyUsedTabs = [gBrowser.selectedTab]; var tabContainer = gBrowser.tabContainer; tabContainer.addEventListener("TabOpen", this, false); tabContainer.addEventListener("TabSelect", this, false); tabContainer.addEventListener("TabClose", this, false); this._handleCtrlTab = gPrefService.getBoolPref("browser.ctrlTab.previews") && (!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") || !gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders")); if (this._handleCtrlTab) gBrowser.mTabBox.handleCtrlTab = false; document.addEventListener("keypress", this, false); }, uninit: function ctrlTab__uninit() { this._recentlyUsedTabs = null; var tabContainer = gBrowser.tabContainer; tabContainer.removeEventListener("TabOpen", this, false); tabContainer.removeEventListener("TabSelect", this, false); tabContainer.removeEventListener("TabClose", this, false); this.panel.removeEventListener("popuphiding", this, false); this.panel.removeEventListener("popupshown", this, false); this.panel.removeEventListener("popuphidden", this, false); document.removeEventListener("keypress", this, false); if (this._handleCtrlTab) gBrowser.mTabBox.handleCtrlTab = true; }, search: function ctrlTab__search() { if (this.isOpen) { this._tabList = null; this.buildPagesBar(); this.goToPage(0, 0); this.updatePreviews(); } }, buildPagesBar: function ctrlTab__buildPagesBar() { var pages = this.pages; if (pages == 1) pages = 0; while (this.pagesBar.childNodes.length > pages) this.pagesBar.removeChild(this.pagesBar.lastChild); while (this.pagesBar.childNodes.length < pages) { let pointer = document.createElement("spacer"); pointer.setAttribute("onclick", "ctrlTab.goToPage(" + this.pagesBar.childNodes.length + ");"); pointer.setAttribute("class", "ctrlTab-pagePointer"); this.pagesBar.appendChild(pointer); } }, goToPage: function ctrlTab__goToPage(aPage, aIndex) { this.page = aPage; this.selected.removeAttribute("selected"); if (aIndex) { this.selectedIndex = aIndex; while (!this.selected || !this.selected.hasAttribute("valid")) this.selectedIndex--; } else { this.selectedIndex = 0; } this.updateSelected(); }, updatePreviews: function ctrlTab__updatePreviews() { var tabs = this.tabList; var offset = this.page * this.thumbnails.length; for (let i = 0; i < this.thumbnails.length; i++) this.updatePreview(this.thumbnails[i], tabs[i + offset]); }, updatePreview: function ctrlTab__updatePreview(aThumbnail, aTab) { do { if (aThumbnail._tab) { if (aThumbnail._tab == aTab) break; aThumbnail._tab.removeEventListener("DOMAttrModified", this, false); } aThumbnail._tab = aTab; if (aTab) aTab.addEventListener("DOMAttrModified", this, false); } while (false); if (aThumbnail.firstChild) aThumbnail.removeChild(aThumbnail.firstChild); if (aTab) { aThumbnail.appendChild(tabPreviews.get(aTab)); aThumbnail.setAttribute("valid", "true"); aThumbnail.setAttribute("label", aTab.label); aThumbnail.setAttribute("crop", aTab.crop); } else { let placeholder = document.createElement("hbox"); placeholder.height = tabPreviews.height; aThumbnail.appendChild(placeholder); aThumbnail.removeAttribute("valid"); aThumbnail.setAttribute("label", "placeholder"); } aThumbnail.width = tabPreviews.width; }, tabAttrModified: function ctrlTab__tabAttrModified(aTab, aAttrName) { switch (aAttrName) { case "label": case "crop": case "busy": for (let i = this.thumbnails.length - 1; i >= 0; i--) { if (this.thumbnails[i]._tab == aTab) { this.updatePreview(this.thumbnails[i], aTab); break; } } break; } }, advanceSelected: function ctrlTab__advanceSelected() { this.selected.removeAttribute("selected"); this.selectedIndex += this.invertDirection ? -1 : 1; if (this.selectedIndex < 0) { this.page--; this.selectedIndex = this.thumbnails.length - 1; while (!this.selected.hasAttribute("valid")) this.selectedIndex--; } else if (this.selectedIndex >= this.thumbnails.length || !this.selected.hasAttribute("valid")) { this.page++; this.selectedIndex = 0; } this.updateSelected(); }, updateSelected: function ctrlTab__updateSelected() { if (this.tabCount) this.selected.setAttribute("selected", "true"); }, selectThumbnail: function ctrlTab__selectThumbnail(aThumbnail) { if (this.tabCount) { this._tabToSelect = (aThumbnail || this.selected)._tab; this.panel.hidePopup(); } }, attachTab: function ctrlTab__attachTab(aTab, aPos) { 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, aTabs) { var tabs = aTabs || this._recentlyUsedTabs; var i = tabs.indexOf(aTab); if (i >= 0) tabs.splice(i, 1); }, open: function ctrlTab__open(aSticky) { if (this.isOpen && this.sticky) { this.panel.hidePopup(); return; } this.sticky = !!aSticky; this._deferOnTabSelect = []; if (this.invertDirection) this._useTabBarOrder = true; this._tabBarHandlesCtrlPageUpDown = gBrowser.mTabBox.handleCtrlPageUpDown; gBrowser.mTabBox.handleCtrlPageUpDown = false; document.addEventListener("keyup", this, true); document.addEventListener("keydown", this, true); this.panel.addEventListener("popupshown", this, false); this.panel.addEventListener("popuphiding", this, false); this.panel.addEventListener("popuphidden", this, false); this._prevFocus = document.commandDispatcher.focusedElement || document.commandDispatcher.focusedWindow; this.panel.hidden = false; this.panel.width = screen.availWidth * .85; this.panel.popupBoxObject.setConsumeRollupEvent(Ci.nsIPopupBoxObject.ROLLUP_CONSUME); this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2, screen.availTop + screen.availHeight * .12, false); this.buildPagesBar(); this.selectedIndex = 0; this.page = 0; this.advanceSelected(); }, onKeyPress: function ctrlTab__onKeyPress(event) { var isOpen = this.isOpen; if (isOpen && event.target == this.searchField && event.keyCode != event.DOM_VK_ESCAPE) return; if (isOpen) { event.preventDefault(); event.stopPropagation(); } switch (event.keyCode) { case event.DOM_VK_TAB: if ((event.ctrlKey || this.sticky) && !event.altKey && !event.metaKey) { this.invertDirection = event.shiftKey; if (isOpen) { this.advanceSelected(); } else if (this._handleCtrlTab) { event.preventDefault(); event.stopPropagation(); if (gBrowser.mTabs.length > 2) { this.open(); } else if (gBrowser.mTabs.length == 2) { gBrowser.selectedTab = gBrowser.selectedTab.nextSibling || gBrowser.selectedTab.previousSibling; } } } break; case event.DOM_VK_UP: if (isOpen) { let index = this.selectedIndex - this.columns; if (index < 0) { this.goToPage(this.page - 1, this.thumbnails.length + index); } else { this.selected.removeAttribute("selected"); this.selectedIndex = index; this.updateSelected(); } } break; case event.DOM_VK_DOWN: if (isOpen) { let index = this.selectedIndex + this.columns; if (index >= this.thumbnails.length || !this.thumbnails[index].hasAttribute("valid")) { this.goToPage(this.page + 1, this.selectedIndex % this.columns); } else { this.selected.removeAttribute("selected"); this.selectedIndex = index; while (!this.selected.hasAttribute("valid")) this.selectedIndex--; this.updateSelected(); } } break; case event.DOM_VK_LEFT: if (isOpen) { this.invertDirection = true; this.advanceSelected(); } break; case event.DOM_VK_RIGHT: if (isOpen) { this.invertDirection = false; this.advanceSelected(); } break; case event.DOM_VK_HOME: if (isOpen) this.goToPage(0); break; case event.DOM_VK_END: if (isOpen) this.goToPage(this.pages - 1, this.thumbnails.length - 1); break; case event.DOM_VK_PAGE_UP: if (isOpen) this.goToPage(this.page - 1); break; case event.DOM_VK_PAGE_DOWN: if (isOpen) this.goToPage(this.page + 1); break; case event.DOM_VK_RETURN: if (isOpen && this.sticky) this.selectThumbnail(); break; case event.DOM_VK_ESCAPE: if (isOpen) this.panel.hidePopup(); break; default: if (isOpen && event.ctrlKey) { switch (event.charCode) { case this.closeCharCode: gBrowser.removeTab(this.selected._tab); break; case this.findCharCode: this.searchField.focus(); break; } } } }, onPopupHiding: function ctrlTab__onPopupHiding() { gBrowser.mTabBox.handleCtrlPageUpDown = this._tabBarHandlesCtrlPageUpDown; document.removeEventListener("keyup", this, true); document.removeEventListener("keydown", this, true); this.selected.removeAttribute("selected"); if (this.pagesBar.childNodes.length) this.pagesBar.childNodes[this.page].removeAttribute("selected"); Array.forEach(this.thumbnails, function (thumbnail) { this.updatePreview(thumbnail, null); }, this); this.searchField.value = ""; this.invertDirection = false; this.sticky = false; this._useTabBarOrder = false; this._page = null; this._tabList = null; this._deferOnTabSelect.forEach(this.onTabSelect, this); this._deferOnTabSelect = null; this._prevFocus.focus(); this._prevFocus = null; if (this._tabToSelect) { gBrowser.selectedTab = this._tabToSelect; this._tabToSelect = null; } }, onTabSelect: function ctrlTab__onTabSelect(aTab) { if (aTab.parentNode) { this.detachTab(aTab); this.attachTab(aTab, 0); } }, removeClosingTabFromUI: function ctrlTab__removeClosingTabFromUI(aTab) { this._closing = aTab; this._tabList = null; if (this.tabCount == 1) { this.panel.hidePopup(); } else { this.buildPagesBar(); this.updatePreviews(); if (!this.selected.hasAttribute("valid")) this.advanceSelected(); else this.updateSelected(); } this._closing = null; }, handleEvent: function ctrlTab__handleEvent(event) { switch (event.type) { case "DOMAttrModified": this.tabAttrModified(event.target, event.attrName); break; case "TabSelect": if (this.isOpen) // don't change the tab order while the panel is open this._deferOnTabSelect.push(event.target); else this.onTabSelect(event.target); 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 "keydown": case "keyup": if (event.target == this.searchField) { if (event.keyCode == event.DOM_VK_RETURN) { // If there's a pending search, kick it off now. if (this.searchField._timer) this.search(); this.selectThumbnail(); } } else { // Manually consume the events, as the panel is open but doesn't // necessarily have focus. event.stopPropagation(); event.preventDefault(); } if (!this.sticky && event.type == "keyup" && event.keyCode == event.DOM_VK_CONTROL) this.selectThumbnail(); break; case "popupshown": if (this.sticky && event.target == this.panel) this.searchField.focus(); break; case "popuphiding": if (event.target == this.panel) this.onPopupHiding(); break; case "popuphidden": if (event.target == this.panel) { // Destroy the widget in order to prevent outdated content // when re-opening the panel. this.panel.hidden = true; } break; } } };