1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/base/content/browser-tabPreviews.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,556 @@ 1.4 +/* 1.5 +#ifdef 0 1.6 + * This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. 1.9 +#endif 1.10 + */ 1.11 + 1.12 +/** 1.13 + * Tab previews utility, produces thumbnails 1.14 + */ 1.15 +var tabPreviews = { 1.16 + aspectRatio: 0.5625, // 16:9 1.17 + 1.18 + get width() { 1.19 + delete this.width; 1.20 + return this.width = Math.ceil(screen.availWidth / 5.75); 1.21 + }, 1.22 + 1.23 + get height() { 1.24 + delete this.height; 1.25 + return this.height = Math.round(this.width * this.aspectRatio); 1.26 + }, 1.27 + 1.28 + init: function tabPreviews_init() { 1.29 + if (this._selectedTab) 1.30 + return; 1.31 + this._selectedTab = gBrowser.selectedTab; 1.32 + 1.33 + gBrowser.tabContainer.addEventListener("TabSelect", this, false); 1.34 + gBrowser.tabContainer.addEventListener("SSTabRestored", this, false); 1.35 + }, 1.36 + 1.37 + get: function tabPreviews_get(aTab) { 1.38 + let uri = aTab.linkedBrowser.currentURI.spec; 1.39 + 1.40 + if (aTab.__thumbnail_lastURI && 1.41 + aTab.__thumbnail_lastURI != uri) { 1.42 + aTab.__thumbnail = null; 1.43 + aTab.__thumbnail_lastURI = null; 1.44 + } 1.45 + 1.46 + if (aTab.__thumbnail) 1.47 + return aTab.__thumbnail; 1.48 + 1.49 + if (aTab.getAttribute("pending") == "true") { 1.50 + let img = new Image; 1.51 + img.src = PageThumbs.getThumbnailURL(uri); 1.52 + return img; 1.53 + } 1.54 + 1.55 + return this.capture(aTab, !aTab.hasAttribute("busy")); 1.56 + }, 1.57 + 1.58 + capture: function tabPreviews_capture(aTab, aStore) { 1.59 + var thumbnail = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); 1.60 + thumbnail.mozOpaque = true; 1.61 + thumbnail.height = this.height; 1.62 + thumbnail.width = this.width; 1.63 + 1.64 + var ctx = thumbnail.getContext("2d"); 1.65 + var win = aTab.linkedBrowser.contentWindow; 1.66 + var snippetWidth = win.innerWidth * .6; 1.67 + var scale = this.width / snippetWidth; 1.68 + ctx.scale(scale, scale); 1.69 + ctx.drawWindow(win, win.scrollX, win.scrollY, 1.70 + snippetWidth, snippetWidth * this.aspectRatio, "rgb(255,255,255)"); 1.71 + 1.72 + if (aStore && 1.73 + aTab.linkedBrowser /* bug 795608: the tab may got removed while drawing the thumbnail */) { 1.74 + aTab.__thumbnail = thumbnail; 1.75 + aTab.__thumbnail_lastURI = aTab.linkedBrowser.currentURI.spec; 1.76 + } 1.77 + 1.78 + return thumbnail; 1.79 + }, 1.80 + 1.81 + handleEvent: function tabPreviews_handleEvent(event) { 1.82 + switch (event.type) { 1.83 + case "TabSelect": 1.84 + if (this._selectedTab && 1.85 + this._selectedTab.parentNode && 1.86 + !this._pendingUpdate) { 1.87 + // Generate a thumbnail for the tab that was selected. 1.88 + // The timeout keeps the UI snappy and prevents us from generating thumbnails 1.89 + // for tabs that will be closed. During that timeout, don't generate other 1.90 + // thumbnails in case multiple TabSelect events occur fast in succession. 1.91 + this._pendingUpdate = true; 1.92 + setTimeout(function (self, aTab) { 1.93 + self._pendingUpdate = false; 1.94 + if (aTab.parentNode && 1.95 + !aTab.hasAttribute("busy") && 1.96 + !aTab.hasAttribute("pending")) 1.97 + self.capture(aTab, true); 1.98 + }, 2000, this, this._selectedTab); 1.99 + } 1.100 + this._selectedTab = event.target; 1.101 + break; 1.102 + case "SSTabRestored": 1.103 + this.capture(event.target, true); 1.104 + break; 1.105 + } 1.106 + } 1.107 +}; 1.108 + 1.109 +var tabPreviewPanelHelper = { 1.110 + opening: function (host) { 1.111 + host.panel.hidden = false; 1.112 + 1.113 + var handler = this._generateHandler(host); 1.114 + host.panel.addEventListener("popupshown", handler, false); 1.115 + host.panel.addEventListener("popuphiding", handler, false); 1.116 + 1.117 + host._prevFocus = document.commandDispatcher.focusedElement; 1.118 + }, 1.119 + _generateHandler: function (host) { 1.120 + var self = this; 1.121 + return function (event) { 1.122 + if (event.target == host.panel) { 1.123 + host.panel.removeEventListener(event.type, arguments.callee, false); 1.124 + self["_" + event.type](host); 1.125 + } 1.126 + }; 1.127 + }, 1.128 + _popupshown: function (host) { 1.129 + if ("setupGUI" in host) 1.130 + host.setupGUI(); 1.131 + }, 1.132 + _popuphiding: function (host) { 1.133 + if ("suspendGUI" in host) 1.134 + host.suspendGUI(); 1.135 + 1.136 + if (host._prevFocus) { 1.137 + Cc["@mozilla.org/focus-manager;1"] 1.138 + .getService(Ci.nsIFocusManager) 1.139 + .setFocus(host._prevFocus, Ci.nsIFocusManager.FLAG_NOSCROLL); 1.140 + host._prevFocus = null; 1.141 + } else 1.142 + gBrowser.selectedBrowser.focus(); 1.143 + 1.144 + if (host.tabToSelect) { 1.145 + gBrowser.selectedTab = host.tabToSelect; 1.146 + host.tabToSelect = null; 1.147 + } 1.148 + } 1.149 +}; 1.150 + 1.151 +/** 1.152 + * Ctrl-Tab panel 1.153 + */ 1.154 +var ctrlTab = { 1.155 + get panel () { 1.156 + delete this.panel; 1.157 + return this.panel = document.getElementById("ctrlTab-panel"); 1.158 + }, 1.159 + get showAllButton () { 1.160 + delete this.showAllButton; 1.161 + return this.showAllButton = document.getElementById("ctrlTab-showAll"); 1.162 + }, 1.163 + get previews () { 1.164 + delete this.previews; 1.165 + return this.previews = this.panel.getElementsByClassName("ctrlTab-preview"); 1.166 + }, 1.167 + get keys () { 1.168 + var keys = {}; 1.169 + ["close", "find", "selectAll"].forEach(function (key) { 1.170 + keys[key] = document.getElementById("key_" + key) 1.171 + .getAttribute("key") 1.172 + .toLocaleLowerCase().charCodeAt(0); 1.173 + }); 1.174 + delete this.keys; 1.175 + return this.keys = keys; 1.176 + }, 1.177 + _selectedIndex: 0, 1.178 + get selected () this._selectedIndex < 0 ? 1.179 + document.activeElement : 1.180 + this.previews.item(this._selectedIndex), 1.181 + get isOpen () this.panel.state == "open" || this.panel.state == "showing" || this._timer, 1.182 + get tabCount () this.tabList.length, 1.183 + get tabPreviewCount () Math.min(this.previews.length - 1, this.tabCount), 1.184 + get canvasWidth () Math.min(tabPreviews.width, 1.185 + Math.ceil(screen.availWidth * .85 / this.tabPreviewCount)), 1.186 + get canvasHeight () Math.round(this.canvasWidth * tabPreviews.aspectRatio), 1.187 + 1.188 + get tabList () { 1.189 + return this._recentlyUsedTabs; 1.190 + }, 1.191 + 1.192 + init: function ctrlTab_init() { 1.193 + if (!this._recentlyUsedTabs) { 1.194 + tabPreviews.init(); 1.195 + 1.196 + this._initRecentlyUsedTabs(); 1.197 + this._init(true); 1.198 + } 1.199 + }, 1.200 + 1.201 + uninit: function ctrlTab_uninit() { 1.202 + this._recentlyUsedTabs = null; 1.203 + this._init(false); 1.204 + }, 1.205 + 1.206 + prefName: "browser.ctrlTab.previews", 1.207 + readPref: function ctrlTab_readPref() { 1.208 + var enable = 1.209 + gPrefService.getBoolPref(this.prefName) && 1.210 + (!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") || 1.211 + !gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders")); 1.212 + 1.213 + if (enable) 1.214 + this.init(); 1.215 + else 1.216 + this.uninit(); 1.217 + }, 1.218 + observe: function (aSubject, aTopic, aPrefName) { 1.219 + this.readPref(); 1.220 + }, 1.221 + 1.222 + updatePreviews: function ctrlTab_updatePreviews() { 1.223 + for (let i = 0; i < this.previews.length; i++) 1.224 + this.updatePreview(this.previews[i], this.tabList[i]); 1.225 + 1.226 + var showAllLabel = gNavigatorBundle.getString("ctrlTab.showAll.label"); 1.227 + this.showAllButton.label = 1.228 + PluralForm.get(this.tabCount, showAllLabel).replace("#1", this.tabCount); 1.229 + this.showAllButton.hidden = !allTabs.canOpen; 1.230 + }, 1.231 + 1.232 + updatePreview: function ctrlTab_updatePreview(aPreview, aTab) { 1.233 + if (aPreview == this.showAllButton) 1.234 + return; 1.235 + 1.236 + aPreview._tab = aTab; 1.237 + 1.238 + if (aPreview.firstChild) 1.239 + aPreview.removeChild(aPreview.firstChild); 1.240 + if (aTab) { 1.241 + let canvasWidth = this.canvasWidth; 1.242 + let canvasHeight = this.canvasHeight; 1.243 + aPreview.appendChild(tabPreviews.get(aTab)); 1.244 + aPreview.setAttribute("label", aTab.label); 1.245 + aPreview.setAttribute("tooltiptext", aTab.label); 1.246 + aPreview.setAttribute("crop", aTab.crop); 1.247 + aPreview.setAttribute("canvaswidth", canvasWidth); 1.248 + aPreview.setAttribute("canvasstyle", 1.249 + "max-width:" + canvasWidth + "px;" + 1.250 + "min-width:" + canvasWidth + "px;" + 1.251 + "max-height:" + canvasHeight + "px;" + 1.252 + "min-height:" + canvasHeight + "px;"); 1.253 + if (aTab.image) 1.254 + aPreview.setAttribute("image", aTab.image); 1.255 + else 1.256 + aPreview.removeAttribute("image"); 1.257 + aPreview.hidden = false; 1.258 + } else { 1.259 + aPreview.hidden = true; 1.260 + aPreview.removeAttribute("label"); 1.261 + aPreview.removeAttribute("tooltiptext"); 1.262 + aPreview.removeAttribute("image"); 1.263 + } 1.264 + }, 1.265 + 1.266 + advanceFocus: function ctrlTab_advanceFocus(aForward) { 1.267 + let selectedIndex = Array.indexOf(this.previews, this.selected); 1.268 + do { 1.269 + selectedIndex += aForward ? 1 : -1; 1.270 + if (selectedIndex < 0) 1.271 + selectedIndex = this.previews.length - 1; 1.272 + else if (selectedIndex >= this.previews.length) 1.273 + selectedIndex = 0; 1.274 + } while (this.previews[selectedIndex].hidden); 1.275 + 1.276 + if (this._selectedIndex == -1) { 1.277 + // Focus is already in the panel. 1.278 + this.previews[selectedIndex].focus(); 1.279 + } else { 1.280 + this._selectedIndex = selectedIndex; 1.281 + } 1.282 + 1.283 + if (this._timer) { 1.284 + clearTimeout(this._timer); 1.285 + this._timer = null; 1.286 + this._openPanel(); 1.287 + } 1.288 + }, 1.289 + 1.290 + _mouseOverFocus: function ctrlTab_mouseOverFocus(aPreview) { 1.291 + if (this._trackMouseOver) 1.292 + aPreview.focus(); 1.293 + }, 1.294 + 1.295 + pick: function ctrlTab_pick(aPreview) { 1.296 + if (!this.tabCount) 1.297 + return; 1.298 + 1.299 + var select = (aPreview || this.selected); 1.300 + 1.301 + if (select == this.showAllButton) 1.302 + this.showAllTabs(); 1.303 + else 1.304 + this.close(select._tab); 1.305 + }, 1.306 + 1.307 + showAllTabs: function ctrlTab_showAllTabs(aPreview) { 1.308 + this.close(); 1.309 + document.getElementById("Browser:ShowAllTabs").doCommand(); 1.310 + }, 1.311 + 1.312 + remove: function ctrlTab_remove(aPreview) { 1.313 + if (aPreview._tab) 1.314 + gBrowser.removeTab(aPreview._tab); 1.315 + }, 1.316 + 1.317 + attachTab: function ctrlTab_attachTab(aTab, aPos) { 1.318 + if (aTab.closing) 1.319 + return; 1.320 + 1.321 + if (aPos == 0) 1.322 + this._recentlyUsedTabs.unshift(aTab); 1.323 + else if (aPos) 1.324 + this._recentlyUsedTabs.splice(aPos, 0, aTab); 1.325 + else 1.326 + this._recentlyUsedTabs.push(aTab); 1.327 + }, 1.328 + 1.329 + detachTab: function ctrlTab_detachTab(aTab) { 1.330 + var i = this._recentlyUsedTabs.indexOf(aTab); 1.331 + if (i >= 0) 1.332 + this._recentlyUsedTabs.splice(i, 1); 1.333 + }, 1.334 + 1.335 + open: function ctrlTab_open() { 1.336 + if (this.isOpen) 1.337 + return; 1.338 + 1.339 + document.addEventListener("keyup", this, true); 1.340 + 1.341 + this.updatePreviews(); 1.342 + this._selectedIndex = 1; 1.343 + 1.344 + // Add a slight delay before showing the UI, so that a quick 1.345 + // "ctrl-tab" keypress just flips back to the MRU tab. 1.346 + this._timer = setTimeout(function (self) { 1.347 + self._timer = null; 1.348 + self._openPanel(); 1.349 + }, 200, this); 1.350 + }, 1.351 + 1.352 + _openPanel: function ctrlTab_openPanel() { 1.353 + tabPreviewPanelHelper.opening(this); 1.354 + 1.355 + this.panel.width = Math.min(screen.availWidth * .99, 1.356 + this.canvasWidth * 1.25 * this.tabPreviewCount); 1.357 + var estimateHeight = this.canvasHeight * 1.25 + 75; 1.358 + this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2, 1.359 + screen.availTop + (screen.availHeight - estimateHeight) / 2, 1.360 + false); 1.361 + }, 1.362 + 1.363 + close: function ctrlTab_close(aTabToSelect) { 1.364 + if (!this.isOpen) 1.365 + return; 1.366 + 1.367 + if (this._timer) { 1.368 + clearTimeout(this._timer); 1.369 + this._timer = null; 1.370 + this.suspendGUI(); 1.371 + if (aTabToSelect) 1.372 + gBrowser.selectedTab = aTabToSelect; 1.373 + return; 1.374 + } 1.375 + 1.376 + this.tabToSelect = aTabToSelect; 1.377 + this.panel.hidePopup(); 1.378 + }, 1.379 + 1.380 + setupGUI: function ctrlTab_setupGUI() { 1.381 + this.selected.focus(); 1.382 + this._selectedIndex = -1; 1.383 + 1.384 + // Track mouse movement after a brief delay so that the item that happens 1.385 + // to be under the mouse pointer initially won't be selected unintentionally. 1.386 + this._trackMouseOver = false; 1.387 + setTimeout(function (self) { 1.388 + if (self.isOpen) 1.389 + self._trackMouseOver = true; 1.390 + }, 0, this); 1.391 + }, 1.392 + 1.393 + suspendGUI: function ctrlTab_suspendGUI() { 1.394 + document.removeEventListener("keyup", this, true); 1.395 + 1.396 + Array.forEach(this.previews, function (preview) { 1.397 + this.updatePreview(preview, null); 1.398 + }, this); 1.399 + }, 1.400 + 1.401 + onKeyPress: function ctrlTab_onKeyPress(event) { 1.402 + var isOpen = this.isOpen; 1.403 + 1.404 + if (isOpen) { 1.405 + event.preventDefault(); 1.406 + event.stopPropagation(); 1.407 + } 1.408 + 1.409 + switch (event.keyCode) { 1.410 + case event.DOM_VK_TAB: 1.411 + if (event.ctrlKey && !event.altKey && !event.metaKey) { 1.412 + if (isOpen) { 1.413 + this.advanceFocus(!event.shiftKey); 1.414 + } else if (!event.shiftKey) { 1.415 + event.preventDefault(); 1.416 + event.stopPropagation(); 1.417 + let tabs = gBrowser.visibleTabs; 1.418 + if (tabs.length > 2) { 1.419 + this.open(); 1.420 + } else if (tabs.length == 2) { 1.421 + let index = tabs[0].selected ? 1 : 0; 1.422 + gBrowser.selectedTab = tabs[index]; 1.423 + } 1.424 + } 1.425 + } 1.426 + break; 1.427 + default: 1.428 + if (isOpen && event.ctrlKey) { 1.429 + if (event.keyCode == event.DOM_VK_DELETE) { 1.430 + this.remove(this.selected); 1.431 + break; 1.432 + } 1.433 + switch (event.charCode) { 1.434 + case this.keys.close: 1.435 + this.remove(this.selected); 1.436 + break; 1.437 + case this.keys.find: 1.438 + case this.keys.selectAll: 1.439 + this.showAllTabs(); 1.440 + break; 1.441 + } 1.442 + } 1.443 + } 1.444 + }, 1.445 + 1.446 + removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) { 1.447 + if (this.tabCount == 2) { 1.448 + this.close(); 1.449 + return; 1.450 + } 1.451 + 1.452 + this.updatePreviews(); 1.453 + 1.454 + if (this.selected.hidden) 1.455 + this.advanceFocus(false); 1.456 + if (this.selected == this.showAllButton) 1.457 + this.advanceFocus(false); 1.458 + 1.459 + // If the current tab is removed, another tab can steal our focus. 1.460 + if (aTab.selected && this.panel.state == "open") { 1.461 + setTimeout(function (selected) { 1.462 + selected.focus(); 1.463 + }, 0, this.selected); 1.464 + } 1.465 + }, 1.466 + 1.467 + handleEvent: function ctrlTab_handleEvent(event) { 1.468 + switch (event.type) { 1.469 + case "SSWindowStateReady": 1.470 + this._initRecentlyUsedTabs(); 1.471 + break; 1.472 + case "TabAttrModified": 1.473 + // tab attribute modified (e.g. label, crop, busy, image, selected) 1.474 + for (let i = this.previews.length - 1; i >= 0; i--) { 1.475 + if (this.previews[i]._tab && this.previews[i]._tab == event.target) { 1.476 + this.updatePreview(this.previews[i], event.target); 1.477 + break; 1.478 + } 1.479 + } 1.480 + break; 1.481 + case "TabSelect": 1.482 + this.detachTab(event.target); 1.483 + this.attachTab(event.target, 0); 1.484 + break; 1.485 + case "TabOpen": 1.486 + this.attachTab(event.target, 1); 1.487 + break; 1.488 + case "TabClose": 1.489 + this.detachTab(event.target); 1.490 + if (this.isOpen) 1.491 + this.removeClosingTabFromUI(event.target); 1.492 + break; 1.493 + case "keypress": 1.494 + this.onKeyPress(event); 1.495 + break; 1.496 + case "keyup": 1.497 + if (event.keyCode == event.DOM_VK_CONTROL) 1.498 + this.pick(); 1.499 + break; 1.500 + case "popupshowing": 1.501 + if (event.target.id == "menu_viewPopup") 1.502 + document.getElementById("menu_showAllTabs").hidden = !allTabs.canOpen; 1.503 + break; 1.504 + } 1.505 + }, 1.506 + 1.507 + _initRecentlyUsedTabs: function () { 1.508 + this._recentlyUsedTabs = 1.509 + Array.filter(gBrowser.tabs, tab => !tab.closing) 1.510 + .sort((tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed); 1.511 + }, 1.512 + 1.513 + _init: function ctrlTab__init(enable) { 1.514 + var toggleEventListener = enable ? "addEventListener" : "removeEventListener"; 1.515 + 1.516 + window[toggleEventListener]("SSWindowStateReady", this, false); 1.517 + 1.518 + var tabContainer = gBrowser.tabContainer; 1.519 + tabContainer[toggleEventListener]("TabOpen", this, false); 1.520 + tabContainer[toggleEventListener]("TabAttrModified", this, false); 1.521 + tabContainer[toggleEventListener]("TabSelect", this, false); 1.522 + tabContainer[toggleEventListener]("TabClose", this, false); 1.523 + 1.524 + document[toggleEventListener]("keypress", this, false); 1.525 + gBrowser.mTabBox.handleCtrlTab = !enable; 1.526 + 1.527 + // If we're not running, hide the "Show All Tabs" menu item, 1.528 + // as Shift+Ctrl+Tab will be handled by the tab bar. 1.529 + document.getElementById("menu_showAllTabs").hidden = !enable; 1.530 + document.getElementById("menu_viewPopup")[toggleEventListener]("popupshowing", this); 1.531 + 1.532 + // Also disable the <key> to ensure Shift+Ctrl+Tab never triggers 1.533 + // Show All Tabs. 1.534 + var key_showAllTabs = document.getElementById("key_showAllTabs"); 1.535 + if (enable) 1.536 + key_showAllTabs.removeAttribute("disabled"); 1.537 + else 1.538 + key_showAllTabs.setAttribute("disabled", "true"); 1.539 + } 1.540 +}; 1.541 + 1.542 + 1.543 +/** 1.544 + * All Tabs menu 1.545 + */ 1.546 +var allTabs = { 1.547 + get toolbarButton() document.getElementById("alltabs-button"), 1.548 + get canOpen() isElementVisible(this.toolbarButton), 1.549 + 1.550 + open: function allTabs_open() { 1.551 + if (this.canOpen) { 1.552 + // Without setTimeout, the menupopup won't stay open when invoking 1.553 + // "View > Show All Tabs" and the menu bar auto-hides. 1.554 + setTimeout(function () { 1.555 + allTabs.toolbarButton.open = true; 1.556 + }, 0); 1.557 + } 1.558 + } 1.559 +};