/* #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.6875, // 16:11 init: function () { this.width = Math.ceil(screen.availWidth / 7.5); this.height = Math.round(this.width * this.aspectRatio); gBrowser.tabContainer.addEventListener("TabSelect", this, false); gBrowser.tabContainer.addEventListener("SSTabRestored", this, false); }, uninit: function () { gBrowser.tabContainer.removeEventListener("TabSelect", this, false); gBrowser.tabContainer.removeEventListener("SSTabRestored", this, false); this._selectedTab = null; }, get: function (aTab) { return aTab.__thumbnail || this.capture(aTab, !aTab.hasAttribute("busy")); }, capture: function (aTab, aStore) { var win = aTab.linkedBrowser.contentWindow; 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 widthScale = this.width / win.innerWidth; ctx.scale(widthScale, widthScale); ctx.drawWindow(win, win.scrollX, win.scrollY, win.innerWidth, win.innerWidth * this.aspectRatio, "rgb(255,255,255)"); var data = thumbnail.toDataURL("image/jpeg", "quality=60"); if (aStore) aTab.__thumbnail = data; return data; }, handleEvent: function (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 = { tabs: [], _uniqid: 0, get panel () { delete this.panel; return this.panel = document.getElementById("ctrlTab-panel"); }, get label () { delete this.label; return this.label = document.getElementById("ctrlTab-label"); }, get svgRoot () { delete this.svgRoot; let (groundFade = document.getElementById("ctrlTab-groundFade")) { groundFade.setAttribute("height", Math.ceil(tabPreviews.height * .25) + 1); groundFade.setAttribute("y", tabPreviews.height + 1); } this.svgRoot = document.getElementById("ctrlTab-svgRoot"); this.svgRoot.setAttribute("height", tabPreviews.height * 1.25 + 2); return this.svgRoot; }, get container () { delete this.container; return this.container = document.getElementById("ctrlTab-container"); }, get rtl () { delete this.rtl; return this.rtl = getComputedStyle(this.panel, "").direction == "rtl"; }, get iconSize () { delete this.iconSize; return this.iconSize = Math.round(tabPreviews.height / 4); }, get smoothScroll () { delete this.smoothScroll; return this.smoothScroll = gPrefService.getBoolPref("browser.ctrlTab.smoothScroll"); }, get previewsCount () { delete this.previewsCount; return this.previewsCount = Math.max(gPrefService.getIntPref("browser.ctrlTab.previewsCount"), 3); }, get visibleCount () { return Math.min(this.previewsCount, this.tabs.length); }, get offscreenStart () { return Array.indexOf(this.container.childNodes, this.selected) - 1; }, get offscreenEnd () { return this.container.childNodes.length - this.visibleCount - this.offscreenStart; }, get offsetX () { return - tabPreviews.width * (this.rtl ? this.offscreenEnd : this.offscreenStart); }, init: function () { var tabContainer = gBrowser.tabContainer; Array.forEach(tabContainer.childNodes, function (tab) { this.attachTab(tab, tab == gBrowser.selectedTab); }, this); tabContainer.addEventListener("TabOpen", this, false); tabContainer.addEventListener("TabSelect", this, false); tabContainer.addEventListener("TabClose", this, false); gBrowser.mTabBox.handleCtrlTab = false; window.addEventListener("keydown", this, true); }, uninit: function () { 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); window.removeEventListener("keydown", this, true); }, addBox: function (aAtStart) { const SVGNS = "http://www.w3.org/2000/svg"; var thumbnail = document.createElementNS(SVGNS, "image"); thumbnail.setAttribute("class", "ctrlTab-thumbnail"); thumbnail.setAttribute("height", tabPreviews.height); thumbnail.setAttribute("width", tabPreviews.width); var thumbnail_border = document.createElementNS(SVGNS, "rect"); thumbnail_border.setAttribute("class", "ctrlTab-thumbnailborder"); thumbnail_border.setAttribute("height", tabPreviews.height); thumbnail_border.setAttribute("width", tabPreviews.width); var icon = document.createElementNS(SVGNS, "image"); icon.setAttribute("class", "ctrlTab-icon"); icon.setAttribute("height", this.iconSize); icon.setAttribute("width", this.iconSize); icon.setAttribute("transform", "skewY(10)"); icon.setAttribute("x", - this.iconSize / 3); icon.setAttribute("y", tabPreviews.height * .9 - this.iconSize); var thumbnail_and_icon = document.createElementNS(SVGNS, "g"); thumbnail_and_icon.appendChild(thumbnail); thumbnail_and_icon.appendChild(thumbnail_border); thumbnail_and_icon.appendChild(icon); var reflection = document.createElementNS(SVGNS, "use"); reflection.setAttribute("class", "ctrlTab-reflection"); var ref_scale = .5; reflection.setAttribute("transform", "scale(1,-" + ref_scale + ")"); reflection.setAttribute("y", - ((1 / ref_scale + 1) * tabPreviews.height + (1 / ref_scale) * 2)); var box = document.createElementNS(SVGNS, "g"); box.setAttribute("class", "ctrlTab-box"); box.appendChild(thumbnail_and_icon); box.appendChild(reflection); if (aAtStart) this.container.insertBefore(box, this.container.firstChild); else this.container.appendChild(box); return box; }, removeBox: function (aBox) { this.container.removeChild(aBox); if (!Array.some(this.container.childNodes, function (box) box._tab == aBox._tab)) aBox._tab.removeEventListener("DOMAttrModified", this, false); aBox._tab = null; }, addPreview: function (aBox, aTab) { const XLinkNS = "http://www.w3.org/1999/xlink"; aBox._tab = aTab; let (thumbnail = aBox.firstChild.firstChild) thumbnail.setAttributeNS(XLinkNS, "href", tabPreviews.get(aTab)); this.updateIcon(aBox); aTab.addEventListener("DOMAttrModified", this, false); if (!aBox.firstChild.hasAttribute("id")) { // set up reflection this._uniqid++; aBox.firstChild.setAttribute("id", "ctrlTab-preview-" + this._uniqid); aBox.lastChild.setAttributeNS(XLinkNS, "href", "#ctrlTab-preview-" + this._uniqid); } }, updateIcon: function (aBox) { const XLinkNS = "http://www.w3.org/1999/xlink"; var url = aBox._tab.hasAttribute("busy") ? "chrome://global/skin/icons/loading_16.png" : aBox._tab.getAttribute("image"); var icon = aBox.firstChild.lastChild; if (url) icon.setAttributeNS(XLinkNS, "href", url); else icon.removeAttributeNS(XLinkNS, "href"); }, tabAttrModified: function (aTab, aAttrName) { switch (aAttrName) { case "busy": case "image": Array.forEach(this.container.childNodes, function (box) { if (box._tab == aTab) { if (aAttrName == "busy") this.addPreview(box, aTab); else this.updateIcon(box); } }, this); break; case "label": case "crop": if (!this._scrollTimer) { let boxes = this.container.childNodes; for (let i = boxes.length - 1; i >= 0; i--) { if (boxes[i]._tab == aTab && boxes[i] == this.selected) { this.label[aAttrName == "label" ? "value" : aAttrName] = aTab.getAttribute(aAttrName); break; } } } break; } }, scroll: function () { if (!this.smoothScroll) { this._move = true; this.stopScroll(); return; } this.stopScroll(); this._move = true; let (next = this.invertDirection ? this.selected.previousSibling : this.selected.nextSibling) { this.setStatusbarValue(next); this.label.value = next._tab.label; this.label.crop = next._tab.crop; } const FRAME_LENGTH = 40; var x = this.offsetX; var scrollAmounts = let (tenth = tabPreviews.width / (this.invertDirection == this.rtl ? -10 : 10)) [3 * tenth, 4 * tenth, 2 * tenth, tenth]; function processFrame(self, lateness) { lateness += FRAME_LENGTH / 2; do { x += scrollAmounts.shift(); lateness -= FRAME_LENGTH; } while (lateness > 0 && scrollAmounts.length); self.container.setAttribute("transform", "translate("+ x +",0)"); self.svgRoot.forceRedraw(); if (!scrollAmounts.length) self.stopScroll(); } this._scrollTimer = setInterval(processFrame, FRAME_LENGTH, this); processFrame(this, 0); }, stopScroll: function () { if (this._scrollTimer) { clearInterval(this._scrollTimer); this._scrollTimer = 0; } if (this._move) this.updateSelected(); }, updateSelected: function (aClosing) { var index = 1; if (this._move) { this._move = false; index += this.invertDirection ? -1 : 1; } if (this.selected) { index += this.offscreenStart + this.tabs.length; index %= this.tabs.length; if (index < 2) index += this.tabs.length; if (index > this.container.childNodes.length - this.visibleCount + 1) index -= this.tabs.length; } this.selected = this.container.childNodes[index]; if (aClosing) return; this.addOffscreenBox(this.invertDirection); this.addOffscreenBox(!this.invertDirection); // having lots of off-screen boxes reduce the scrolling speed, remove some for (let i = this.offscreenStart; i > 1; i--) this.removeBox(this.container.firstChild); for (let i = this.offscreenEnd; i > 1; i--) this.removeBox(this.container.lastChild); this.container.setAttribute("transform", "translate("+ this.offsetX +", 0)"); for (let i = 0, l = this.container.childNodes.length; i < l; i++) this.arrange(i); }, addOffscreenBox: function (aAtStart) { if (this.container.childNodes.length < this.tabs.length + this.visibleCount + 1 && !(aAtStart ? this.offscreenStart : this.offscreenEnd)) { let i = aAtStart ? this.tabs.indexOf(this.container.firstChild._tab) - 1: this.tabs.indexOf(this.container.lastChild._tab) + 1; i = (i + this.tabs.length) % this.tabs.length; this.addPreview(this.addBox(aAtStart), this.tabs[i]); } }, arrange: function (aIndex) { var box = this.container.childNodes[aIndex]; var selected = box == this.selected; if (selected) { box.setAttribute("selected", "true"); this.setStatusbarValue(box); this.label.value = box._tab.label; this.label.crop = box._tab.crop; } else { box.removeAttribute("selected"); } var scale = selected ? 1 : .75; var pos = this.rtl ? this.container.childNodes.length - 1 - aIndex : aIndex; var trans_x = tabPreviews.width * (pos + (1 - scale) / 2) / scale; var trans_y = (tabPreviews.height + 1) * (1 / scale - 1); box.setAttribute("transform", "scale(" + scale + "," + scale + ") " + "translate("+ trans_x + "," + trans_y + ")"); }, setStatusbarValue: function (aBox) { var value = ""; if (aBox) { value = aBox._tab.linkedBrowser.currentURI.spec; if (value == "about:blank") { // XXXhack: Passing a space here (and not "") // to make sure the browser implementation would // still consider it a hovered link. value = " "; } else { try { value = decodeURI(value); } catch (e) {} } } XULBrowserWindow.setOverLink(value, null); }, attachTab: function (aTab, aSelected) { if (aSelected) this.tabs.unshift(aTab); else this.tabs.push(aTab); }, detachTab: function (aTab) { var i = this.tabs.indexOf(aTab); if (i >= 0) this.tabs.splice(i, 1); }, open: function () { window.addEventListener("keyup", this, true); window.addEventListener("keypress", this, true); this.panel.addEventListener("popuphiding", this, false); this.panel.hidden = false; this.panel.width = tabPreviews.width * this.visibleCount; this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2, screen.availTop + (screen.availHeight - this.svgRoot.getAttribute("height")) / 2, false); for (let index = this.invertDirection ? this.tabs.length - 2 : 0, i = this.visibleCount; i > 0; i--) this.addPreview(this.addBox(), this.tabs[index++ % this.tabs.length]); this.updateSelected(); }, onKeyDown: function (event) { var isOpen = this.panel.state == "open" || this.panel.state == "showing"; var propagate = !isOpen; switch (event.keyCode) { case event.DOM_VK_TAB: if (event.ctrlKey && !event.altKey && !event.metaKey && this.tabs.length > 1) { propagate = false; this.invertDirection = event.shiftKey; if (isOpen) this.scroll(); else if (this.tabs.length == 2) gBrowser.selectedTab = this.tabs[1]; else this.open(); } break; case event.DOM_VK_ESCAPE: if (isOpen) this.panel.hidePopup(); break; } if (!propagate) { event.stopPropagation(); event.preventDefault(); } }, onKeyUp: function (event) { if (event.keyCode == event.DOM_VK_CONTROL) { if (this._move) this.updateSelected(true); let selectedTab = this.selected._tab; this.panel.hidePopup(); gBrowser.selectedTab = selectedTab; } }, onPopupHiding: function () { this.stopScroll(); window.removeEventListener("keyup", this, true); window.removeEventListener("keypress", this, true); while (this.container.childNodes.length) this.removeBox(this.container.lastChild); this.selected = null; this.invertDirection = false; this._move = false; this._uniqid = 0; this.label.value = ""; this.setStatusbarValue(); this.container.removeAttribute("transform"); this.svgRoot.forceRedraw(); }, handleEvent: function (event) { switch (event.type) { case "DOMAttrModified": this.tabAttrModified(event.target, event.attrName); break; case "TabSelect": this.detachTab(event.target); this.attachTab(event.target, true); break; case "TabOpen": this.attachTab(event.target); break; case "TabClose": this.detachTab(event.target); break; case "keydown": this.onKeyDown(event); break; case "keyup": case "keypress": // the panel is open; don't propagate any key events event.stopPropagation(); event.preventDefault(); case "keyup": this.onKeyUp(event); break; case "popuphiding": this.onPopupHiding(); break; } } };