From 4492b5f8e774bf3b4f21e4e468fc052cbcbb468a Mon Sep 17 00:00:00 2001 From: Thomas Groman Date: Mon, 16 Dec 2019 19:48:42 -0800 Subject: initial commit --- base/content/browser-tabPreviews.js | 1058 +++++++++++++++++++++++++++++++++++ 1 file changed, 1058 insertions(+) create mode 100644 base/content/browser-tabPreviews.js (limited to 'base/content/browser-tabPreviews.js') diff --git a/base/content/browser-tabPreviews.js b/base/content/browser-tabPreviews.js new file mode 100644 index 0000000..e0755b8 --- /dev/null +++ b/base/content/browser-tabPreviews.js @@ -0,0 +1,1058 @@ +/* +#ifdef 0 + * 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/. +#endif + */ + +/** + * Tab previews utility, produces thumbnails + */ +var tabPreviews = { + aspectRatio: 0.5625, // 16:9 + + get width() { + delete this.width; + return this.width = Math.ceil(screen.availWidth / 5.75); + }, + + get height() { + delete this.height; + return this.height = Math.round(this.width * this.aspectRatio); + }, + + init: function tabPreviews_init() { + if (this._selectedTab) + return; + this._selectedTab = gBrowser.selectedTab; + + gBrowser.tabContainer.addEventListener("TabSelect", this, false); + gBrowser.tabContainer.addEventListener("SSTabRestored", this, false); + }, + + 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, 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.linkedBrowser /* bug 795608: the tab may got removed while drawing the thumbnail */) { + 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") && + !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) { + Cc["@mozilla.org/focus-manager;1"] + .getService(Ci.nsIFocusManager) + .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 recentlyUsedLimit () { + delete this.recentlyUsedLimit; + return this.recentlyUsedLimit = gPrefService.getIntPref("browser.ctrlTab.recentlyUsedLimit"); + }, + 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 () this._selectedIndex < 0 ? + document.activeElement : + this.previews.item(this._selectedIndex), + get isOpen () this.panel.state == "open" || this.panel.state == "showing" || this._timer, + get tabCount () this.tabList.length, + get tabPreviewCount () Math.min(this.previews.length - 1, this.tabCount), + get canvasWidth () Math.min(tabPreviews.width, + Math.ceil(screen.availWidth * .85 / this.tabPreviewCount)), + get canvasHeight () Math.round(this.canvasWidth * tabPreviews.aspectRatio), + + get tabList () { + if (this._tabList) + return this._tabList; + + // Using gBrowser.tabs instead of gBrowser.visibleTabs, as the latter + // exlcudes closing tabs, breaking the following loop in case the the + // selected tab is closing. + let list = Array.filter(gBrowser.tabs, function (tab) !tab.hidden); + + // Rotate the list until the selected tab is first + while (!list[0].selected) + list.push(list.shift()); + + list = list.filter(function (tab) !tab.closing); + + if (this.recentlyUsedLimit != 0) { + let recentlyUsedTabs = []; + for (let tab of this._recentlyUsedTabs) { + if (!tab.hidden && !tab.closing) { + recentlyUsedTabs.push(tab); + if (this.recentlyUsedLimit > 0 && recentlyUsedTabs.length >= this.recentlyUsedLimit) + break; + } + } + for (let i = recentlyUsedTabs.length - 1; i >= 0; i--) { + list.splice(list.indexOf(recentlyUsedTabs[i]), 1); + list.unshift(recentlyUsedTabs[i]); + } + } + + return this._tabList = list; + }, + + init: function ctrlTab_init() { + if (!this._recentlyUsedTabs) { + tabPreviews.init(); + + this._recentlyUsedTabs = [gBrowser.selectedTab]; + 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.showAll.label"); + this.showAllButton.label = + PluralForm.get(this.tabCount, showAllLabel).replace("#1", this.tabCount); + }, + + 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 (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; + + allTabs.close(); + + 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); + + Array.forEach(this.previews, function (preview) { + this.updatePreview(preview, null); + }, this); + + this._tabList = 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._tabList = null; + 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 "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; + } + }, + + _init: function ctrlTab__init(enable) { + var toggleEventListener = enable ? "addEventListener" : "removeEventListener"; + + 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 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; + + // Also disable the 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 panel + */ +var allTabs = { + get panel () { + delete this.panel; + return this.panel = document.getElementById("allTabs-panel"); + }, + get filterField () { + delete this.filterField; + return this.filterField = document.getElementById("allTabs-filter"); + }, + get container () { + delete this.container; + return this.container = document.getElementById("allTabs-container"); + }, + get tabCloseButton () { + delete this.tabCloseButton; + return this.tabCloseButton = document.getElementById("allTabs-tab-close-button"); + }, + get toolbarButton() document.getElementById("alltabs-button"), + get previews () this.container.getElementsByClassName("allTabs-preview"), + get isOpen () this.panel.state == "open" || this.panel.state == "showing", + + init: function allTabs_init() { + if (this._initiated) + return; + this._initiated = true; + + tabPreviews.init(); + + Array.forEach(gBrowser.tabs, function (tab) { + this._addPreview(tab); + }, this); + + gBrowser.tabContainer.addEventListener("TabOpen", this, false); + gBrowser.tabContainer.addEventListener("TabAttrModified", this, false); + gBrowser.tabContainer.addEventListener("TabMove", this, false); + gBrowser.tabContainer.addEventListener("TabClose", this, false); + }, + + uninit: function allTabs_uninit() { + if (!this._initiated) + return; + + gBrowser.tabContainer.removeEventListener("TabOpen", this, false); + gBrowser.tabContainer.removeEventListener("TabAttrModified", this, false); + gBrowser.tabContainer.removeEventListener("TabMove", this, false); + gBrowser.tabContainer.removeEventListener("TabClose", this, false); + + while (this.container.hasChildNodes()) + this.container.removeChild(this.container.firstChild); + + this._initiated = false; + }, + + prefName: "browser.allTabs.previews", + readPref: function allTabs_readPref() { + var allTabsButton = this.toolbarButton; + if (!allTabsButton) + return; + + if (gPrefService.getBoolPref(this.prefName)) { + allTabsButton.removeAttribute("type"); + allTabsButton.setAttribute("command", "Browser:ShowAllTabs"); + } else { + allTabsButton.setAttribute("type", "menu"); + allTabsButton.removeAttribute("command"); + allTabsButton.removeAttribute("oncommand"); + } + }, + observe: function (aSubject, aTopic, aPrefName) { + this.readPref(); + }, + + pick: function allTabs_pick(aPreview) { + if (!aPreview) + aPreview = this._firstVisiblePreview; + if (aPreview) + this.tabToSelect = aPreview._tab; + + this.close(); + }, + + closeTab: function allTabs_closeTab(event) { + this.filterField.focus(); + gBrowser.removeTab(event.currentTarget._targetPreview._tab); + }, + + filter: function allTabs_filter() { + if (this._currentFilter == this.filterField.value) + return; + + this._currentFilter = this.filterField.value; + + var filter = this._currentFilter.split(/\s+/g); + this._visible = 0; + Array.forEach(this.previews, function (preview) { + var tab = preview._tab; + var matches = 0; + if (filter.length && !tab.hidden) { + let tabstring = tab.linkedBrowser.currentURI.spec; + try { + tabstring = decodeURI(tabstring); + } catch (e) {} + tabstring = tab.label + " " + tab.label.toLocaleLowerCase() + " " + tabstring; + for (let i = 0; i < filter.length; i++) + matches += tabstring.includes(filter[i]); + } + if (matches < filter.length || tab.hidden) { + preview.hidden = true; + } + else { + this._visible++; + this._updatePreview(preview); + preview.hidden = false; + } + }, this); + + this._reflow(); + }, + + open: function allTabs_open() { + var allTabsButton = this.toolbarButton; + if (allTabsButton && + allTabsButton.getAttribute("type") == "menu") { + // Without setTimeout, the menupopup won't stay open when invoking + // "View > Show All Tabs" and the menu bar auto-hides. + setTimeout(function () { + allTabsButton.open = true; + }, 0); + return; + } + + this.init(); + + if (this.isOpen) + return; + + this._maxPanelHeight = Math.max(gBrowser.clientHeight, screen.availHeight / 2); + this._maxPanelWidth = Math.max(gBrowser.clientWidth, screen.availWidth / 2); + + this.filter(); + + tabPreviewPanelHelper.opening(this); + + this.panel.popupBoxObject.setConsumeRollupEvent(PopupBoxObject.ROLLUP_NO_CONSUME); + this.panel.openPopup(gBrowser, "overlap", 0, 0, false, true); + }, + + close: function allTabs_close() { + this.panel.hidePopup(); + }, + + setupGUI: function allTabs_setupGUI() { + this.filterField.focus(); + this.filterField.placeholder = this.filterField.tooltipText; + + this.panel.addEventListener("keypress", this, false); + this.panel.addEventListener("keypress", this, true); + this._browserCommandSet.addEventListener("command", this, false); + + // When the panel is open, a second click on the all tabs button should + // close the panel but not re-open it. + document.getElementById("Browser:ShowAllTabs").setAttribute("disabled", "true"); + }, + + suspendGUI: function allTabs_suspendGUI() { + this.filterField.placeholder = ""; + this.filterField.value = ""; + this._currentFilter = null; + + this._updateTabCloseButton(); + + this.panel.removeEventListener("keypress", this, false); + this.panel.removeEventListener("keypress", this, true); + this._browserCommandSet.removeEventListener("command", this, false); + + setTimeout(function () { + document.getElementById("Browser:ShowAllTabs").removeAttribute("disabled"); + }, 300); + }, + + handleEvent: function allTabs_handleEvent(event) { + if (event.type.startsWith("Tab")) { + var tab = event.target; + if (event.type != "TabOpen") + var preview = this._getPreview(tab); + } + switch (event.type) { + case "TabAttrModified": + // tab attribute modified (e.g. label, crop, busy, image) + if (!preview.hidden) + this._updatePreview(preview); + break; + case "TabOpen": + if (this.isOpen) + this.close(); + this._addPreview(tab); + break; + case "TabMove": + let siblingPreview = tab.nextSibling && + this._getPreview(tab.nextSibling); + if (siblingPreview) + siblingPreview.parentNode.insertBefore(preview, siblingPreview); + else + this.container.lastChild.appendChild(preview); + if (this.isOpen && !preview.hidden) { + this._reflow(); + preview.focus(); + } + break; + case "TabClose": + this._removePreview(preview); + break; + case "keypress": + this._onKeyPress(event); + break; + case "command": + if (event.target.id != "Browser:ShowAllTabs") { + // Close the panel when there's a browser command executing in the background. + this.close(); + } + break; + } + }, + + _visible: 0, + _currentFilter: null, + get _stack () { + delete this._stack; + return this._stack = document.getElementById("allTabs-stack"); + }, + get _browserCommandSet () { + delete this._browserCommandSet; + return this._browserCommandSet = document.getElementById("mainCommandSet"); + }, + get _previewLabelHeight () { + delete this._previewLabelHeight; + return this._previewLabelHeight = parseInt(getComputedStyle(this.previews[0], "").lineHeight); + }, + + get _visiblePreviews () + Array.filter(this.previews, function (preview) !preview.hidden), + + get _firstVisiblePreview () { + if (this._visible == 0) + return null; + var previews = this.previews; + for (let i = 0; i < previews.length; i++) { + if (!previews[i].hidden) + return previews[i]; + } + return null; + }, + + _reflow: function allTabs_reflow() { + this._updateTabCloseButton(); + + const CONTAINER_MAX_WIDTH = this._maxPanelWidth * .95; + const CONTAINER_MAX_HEIGHT = this._maxPanelHeight - 35; + // the size of the whole preview relative to the thumbnail + const REL_PREVIEW_THUMBNAIL = 1.2; + const REL_PREVIEW_HEIGHT_WIDTH = tabPreviews.height / tabPreviews.width; + const PREVIEW_MAX_WIDTH = tabPreviews.width * REL_PREVIEW_THUMBNAIL; + + var rows, previewHeight, previewWidth, outerHeight; + this._columns = Math.floor(CONTAINER_MAX_WIDTH / PREVIEW_MAX_WIDTH); + do { + rows = Math.ceil(this._visible / this._columns); + previewWidth = Math.min(PREVIEW_MAX_WIDTH, + Math.round(CONTAINER_MAX_WIDTH / this._columns)); + previewHeight = Math.round(previewWidth * REL_PREVIEW_HEIGHT_WIDTH); + outerHeight = previewHeight + this._previewLabelHeight; + } while (rows * outerHeight > CONTAINER_MAX_HEIGHT && ++this._columns); + + var outerWidth = previewWidth; + { + let innerWidth = Math.ceil(previewWidth / REL_PREVIEW_THUMBNAIL); + let innerHeight = Math.ceil(previewHeight / REL_PREVIEW_THUMBNAIL); + var canvasStyle = "max-width:" + innerWidth + "px;" + + "min-width:" + innerWidth + "px;" + + "max-height:" + innerHeight + "px;" + + "min-height:" + innerHeight + "px;"; + } + + var previews = Array.slice(this.previews); + + while (this.container.hasChildNodes()) + this.container.removeChild(this.container.firstChild); + for (let i = rows || 1; i > 0; i--) + this.container.appendChild(document.createElement("hbox")); + + var row = this.container.firstChild; + var colCount = 0; + previews.forEach(function (preview) { + if (!preview.hidden && + ++colCount > this._columns) { + row = row.nextSibling; + colCount = 1; + } + preview.setAttribute("minwidth", outerWidth); + preview.setAttribute("height", outerHeight); + preview.setAttribute("canvasstyle", canvasStyle); + preview.removeAttribute("closebuttonhover"); + row.appendChild(preview); + }, this); + + this._stack.width = this._maxPanelWidth; + this.container.width = Math.ceil(outerWidth * Math.min(this._columns, this._visible)); + this.container.left = Math.round((this._maxPanelWidth - this.container.width) / 2); + this.container.maxWidth = this._maxPanelWidth - this.container.left; + this.container.maxHeight = rows * outerHeight; + }, + + _addPreview: function allTabs_addPreview(aTab) { + var preview = document.createElement("button"); + preview.className = "allTabs-preview"; + preview._tab = aTab; + this.container.lastChild.appendChild(preview); + }, + + _removePreview: function allTabs_removePreview(aPreview) { + var updateUI = (this.isOpen && !aPreview.hidden); + aPreview._tab = null; + aPreview.parentNode.removeChild(aPreview); + if (updateUI) { + this._visible--; + this._reflow(); + this.filterField.focus(); + } + }, + + _getPreview: function allTabs_getPreview(aTab) { + var previews = this.previews; + for (let i = 0; i < previews.length; i++) + if (previews[i]._tab == aTab) + return previews[i]; + return null; + }, + + _updateTabCloseButton: function allTabs_updateTabCloseButton(event) { + if (event && event.target == this.tabCloseButton) + return; + + if (this.tabCloseButton._targetPreview) { + if (event && event.target == this.tabCloseButton._targetPreview) + return; + + this.tabCloseButton._targetPreview.removeAttribute("closebuttonhover"); + } + + if (event && + event.target.parentNode.parentNode == this.container && + (event.target._tab.previousSibling || event.target._tab.nextSibling)) { + let canvas = event.target.firstChild.getBoundingClientRect(); + let container = this.container.getBoundingClientRect(); + let tabCloseButton = this.tabCloseButton.getBoundingClientRect(); + let alignLeft = getComputedStyle(this.panel, "").direction == "rtl"; +#ifdef XP_MACOSX + alignLeft = !alignLeft; +#endif + this.tabCloseButton.left = canvas.left - + container.left + + parseInt(this.container.left) + + (alignLeft ? 0 : + canvas.width - tabCloseButton.width); + this.tabCloseButton.top = canvas.top - container.top; + this.tabCloseButton._targetPreview = event.target; + this.tabCloseButton.style.visibility = "visible"; + event.target.setAttribute("closebuttonhover", "true"); + } else { + this.tabCloseButton.style.visibility = "hidden"; + this.tabCloseButton.left = this.tabCloseButton.top = 0; + this.tabCloseButton._targetPreview = null; + } + }, + + _updatePreview: function allTabs_updatePreview(aPreview) { + aPreview.setAttribute("label", aPreview._tab.label); + aPreview.setAttribute("tooltiptext", aPreview._tab.label); + aPreview.setAttribute("crop", aPreview._tab.crop); + if (aPreview._tab.image) + aPreview.setAttribute("image", aPreview._tab.image); + else + aPreview.removeAttribute("image"); + + aPreview.removeAttribute("soundplaying"); + aPreview.removeAttribute("muted"); + if (aPreview._tab.hasAttribute("muted")) + aPreview.setAttribute("muted", "true"); + else if (aPreview._tab.hasAttribute("soundplaying")) + aPreview.setAttribute("soundplaying", "true"); + + var thumbnail = tabPreviews.get(aPreview._tab); + if (aPreview.firstChild) { + if (aPreview.firstChild == thumbnail) + return; + aPreview.removeChild(aPreview.firstChild); + } + aPreview.appendChild(thumbnail); + }, + + _onKeyPress: function allTabs_onKeyPress(event) { + if (event.eventPhase == event.CAPTURING_PHASE) { + this._onCapturingKeyPress(event); + return; + } + + if (event.keyCode == event.DOM_VK_ESCAPE) { + this.close(); + event.preventDefault(); + event.stopPropagation(); + return; + } + + if (event.target == this.filterField) { + switch (event.keyCode) { + case event.DOM_VK_UP: + if (this._visible) { + let previews = this._visiblePreviews; + let columns = Math.min(previews.length, this._columns); + previews[Math.floor(previews.length / columns) * columns - 1].focus(); + event.preventDefault(); + event.stopPropagation(); + } + break; + case event.DOM_VK_DOWN: + if (this._visible) { + this._firstVisiblePreview.focus(); + event.preventDefault(); + event.stopPropagation(); + } + break; + } + } + }, + + _onCapturingKeyPress: function allTabs_onCapturingKeyPress(event) { + switch (event.keyCode) { + case event.DOM_VK_UP: + case event.DOM_VK_DOWN: + if (event.target != this.filterField) + this._advanceFocusVertically(event); + break; + case event.DOM_VK_RETURN: + if (event.target == this.filterField) { + this.filter(); + this.pick(); + event.preventDefault(); + event.stopPropagation(); + } + break; + } + }, + + _advanceFocusVertically: function allTabs_advanceFocusVertically(event) { + var preview = document.activeElement; + if (!preview || preview.parentNode.parentNode != this.container) + return; + + event.stopPropagation(); + + var up = (event.keyCode == event.DOM_VK_UP); + var previews = this._visiblePreviews; + + if (up && preview == previews[0]) { + this.filterField.focus(); + return; + } + + var i = previews.indexOf(preview); + var columns = Math.min(previews.length, this._columns); + var column = i % columns; + var row = Math.floor(i / columns); + + function newIndex() row * columns + column; + function outOfBounds() newIndex() >= previews.length; + + if (up) { + row--; + if (row < 0) { + let rows = Math.ceil(previews.length / columns); + row = rows - 1; + column--; + if (outOfBounds()) + row--; + } + } else { + row++; + if (outOfBounds()) { + if (column == columns - 1) { + this.filterField.focus(); + return; + } + row = 0; + column++; + } + } + previews[newIndex()].focus(); + } +}; -- cgit v1.2.3