3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
10 * Tab previews utility, produces thumbnails
13 aspectRatio: 0.5625, // 16:9
17 return this.width = Math.ceil(screen.availWidth / 5.75);
22 return this.height = Math.round(this.width * this.aspectRatio);
25 init: function tabPreviews_init() {
26 if (this._selectedTab)
28 this._selectedTab = gBrowser.selectedTab;
30 gBrowser.tabContainer.addEventListener("TabSelect", this, false);
31 gBrowser.tabContainer.addEventListener("SSTabRestored", this, false);
34 get: function tabPreviews_get(aTab) {
35 let uri = aTab.linkedBrowser.currentURI.spec;
37 if (aTab.__thumbnail_lastURI &&
38 aTab.__thumbnail_lastURI != uri) {
39 aTab.__thumbnail = null;
40 aTab.__thumbnail_lastURI = null;
44 return aTab.__thumbnail;
46 if (aTab.getAttribute("pending") == "true") {
48 img.src = PageThumbs.getThumbnailURL(uri);
52 return this.capture(aTab, !aTab.hasAttribute("busy"));
55 capture: function tabPreviews_capture(aTab, aStore) {
56 var thumbnail = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
57 thumbnail.mozOpaque = true;
58 thumbnail.height = this.height;
59 thumbnail.width = this.width;
61 // drawWindow doesn't yet work with e10s (bug 698371)
62 if (gMultiProcessBrowser)
65 var ctx = thumbnail.getContext("2d");
66 var win = aTab.linkedBrowser.contentWindow;
67 var snippetWidth = win.innerWidth * .6;
68 var scale = this.width / snippetWidth;
69 ctx.scale(scale, scale);
70 ctx.drawWindow(win, win.scrollX, win.scrollY,
71 snippetWidth, snippetWidth * this.aspectRatio, "rgb(255,255,255)");
74 aTab.linkedBrowser /* bug 795608: the tab may got removed while drawing the thumbnail */) {
75 aTab.__thumbnail = thumbnail;
76 aTab.__thumbnail_lastURI = aTab.linkedBrowser.currentURI.spec;
82 handleEvent: function tabPreviews_handleEvent(event) {
85 if (this._selectedTab &&
86 this._selectedTab.parentNode &&
87 !this._pendingUpdate) {
88 // Generate a thumbnail for the tab that was selected.
89 // The timeout keeps the UI snappy and prevents us from generating thumbnails
90 // for tabs that will be closed. During that timeout, don't generate other
91 // thumbnails in case multiple TabSelect events occur fast in succession.
92 this._pendingUpdate = true;
93 setTimeout(function (self, aTab) {
94 self._pendingUpdate = false;
95 if (aTab.parentNode &&
96 !aTab.hasAttribute("busy") &&
97 !aTab.hasAttribute("pending"))
98 self.capture(aTab, true);
99 }, 2000, this, this._selectedTab);
101 this._selectedTab = event.target;
103 case "SSTabRestored":
104 this.capture(event.target, true);
110 var tabPreviewPanelHelper = {
111 opening: function (host) {
112 host.panel.hidden = false;
114 var handler = this._generateHandler(host);
115 host.panel.addEventListener("popupshown", handler, false);
116 host.panel.addEventListener("popuphiding", handler, false);
118 host._prevFocus = document.commandDispatcher.focusedElement;
120 _generateHandler: function (host) {
122 return function (event) {
123 if (event.target == host.panel) {
124 host.panel.removeEventListener(event.type, arguments.callee, false);
125 self["_" + event.type](host);
129 _popupshown: function (host) {
130 if ("setupGUI" in host)
133 _popuphiding: function (host) {
134 if ("suspendGUI" in host)
137 if (host._prevFocus) {
138 Cc["@mozilla.org/focus-manager;1"]
139 .getService(Ci.nsIFocusManager)
140 .setFocus(host._prevFocus, Ci.nsIFocusManager.FLAG_NOSCROLL);
141 host._prevFocus = null;
143 gBrowser.selectedBrowser.focus();
145 if (host.tabToSelect) {
146 gBrowser.selectedTab = host.tabToSelect;
147 host.tabToSelect = null;
158 return this.panel = document.getElementById("ctrlTab-panel");
160 get showAllButton () {
161 delete this.showAllButton;
162 return this.showAllButton = document.getElementById("ctrlTab-showAll");
165 delete this.previews;
166 return this.previews = this.panel.getElementsByClassName("ctrlTab-preview");
170 ["close", "find", "selectAll"].forEach(function (key) {
171 keys[key] = document.getElementById("key_" + key)
173 .toLocaleLowerCase().charCodeAt(0);
176 return this.keys = keys;
179 get selected () this._selectedIndex < 0 ?
180 document.activeElement :
181 this.previews.item(this._selectedIndex),
182 get isOpen () this.panel.state == "open" || this.panel.state == "showing" || this._timer,
183 get tabCount () this.tabList.length,
184 get tabPreviewCount () Math.min(this.previews.length - 1, this.tabCount),
185 get canvasWidth () Math.min(tabPreviews.width,
186 Math.ceil(screen.availWidth * .85 / this.tabPreviewCount)),
187 get canvasHeight () Math.round(this.canvasWidth * tabPreviews.aspectRatio),
190 return this._recentlyUsedTabs;
193 init: function ctrlTab_init() {
194 if (!this._recentlyUsedTabs) {
197 this._initRecentlyUsedTabs();
202 uninit: function ctrlTab_uninit() {
203 this._recentlyUsedTabs = null;
207 prefName: "browser.ctrlTab.previews",
208 readPref: function ctrlTab_readPref() {
210 gPrefService.getBoolPref(this.prefName) &&
211 (!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") ||
212 !gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders"));
219 observe: function (aSubject, aTopic, aPrefName) {
223 updatePreviews: function ctrlTab_updatePreviews() {
224 for (let i = 0; i < this.previews.length; i++)
225 this.updatePreview(this.previews[i], this.tabList[i]);
227 var showAllLabel = gNavigatorBundle.getString("ctrlTab.showAll.label");
228 this.showAllButton.label =
229 PluralForm.get(this.tabCount, showAllLabel).replace("#1", this.tabCount);
230 this.showAllButton.hidden = !allTabs.canOpen;
233 updatePreview: function ctrlTab_updatePreview(aPreview, aTab) {
234 if (aPreview == this.showAllButton)
237 aPreview._tab = aTab;
239 if (aPreview.firstChild)
240 aPreview.removeChild(aPreview.firstChild);
242 let canvasWidth = this.canvasWidth;
243 let canvasHeight = this.canvasHeight;
244 aPreview.appendChild(tabPreviews.get(aTab));
245 aPreview.setAttribute("label", aTab.label);
246 aPreview.setAttribute("tooltiptext", aTab.label);
247 aPreview.setAttribute("crop", aTab.crop);
248 aPreview.setAttribute("canvaswidth", canvasWidth);
249 aPreview.setAttribute("canvasstyle",
250 "max-width:" + canvasWidth + "px;" +
251 "min-width:" + canvasWidth + "px;" +
252 "max-height:" + canvasHeight + "px;" +
253 "min-height:" + canvasHeight + "px;");
255 aPreview.setAttribute("image", aTab.image);
257 aPreview.removeAttribute("image");
258 aPreview.hidden = false;
260 aPreview.hidden = true;
261 aPreview.removeAttribute("label");
262 aPreview.removeAttribute("tooltiptext");
263 aPreview.removeAttribute("image");
267 advanceFocus: function ctrlTab_advanceFocus(aForward) {
268 let selectedIndex = Array.indexOf(this.previews, this.selected);
270 selectedIndex += aForward ? 1 : -1;
271 if (selectedIndex < 0)
272 selectedIndex = this.previews.length - 1;
273 else if (selectedIndex >= this.previews.length)
275 } while (this.previews[selectedIndex].hidden);
277 if (this._selectedIndex == -1) {
278 // Focus is already in the panel.
279 this.previews[selectedIndex].focus();
281 this._selectedIndex = selectedIndex;
285 clearTimeout(this._timer);
291 _mouseOverFocus: function ctrlTab_mouseOverFocus(aPreview) {
292 if (this._trackMouseOver)
296 pick: function ctrlTab_pick(aPreview) {
300 var select = (aPreview || this.selected);
302 if (select == this.showAllButton)
305 this.close(select._tab);
308 showAllTabs: function ctrlTab_showAllTabs(aPreview) {
310 document.getElementById("Browser:ShowAllTabs").doCommand();
313 remove: function ctrlTab_remove(aPreview) {
315 gBrowser.removeTab(aPreview._tab);
318 attachTab: function ctrlTab_attachTab(aTab, aPos) {
323 this._recentlyUsedTabs.unshift(aTab);
325 this._recentlyUsedTabs.splice(aPos, 0, aTab);
327 this._recentlyUsedTabs.push(aTab);
330 detachTab: function ctrlTab_detachTab(aTab) {
331 var i = this._recentlyUsedTabs.indexOf(aTab);
333 this._recentlyUsedTabs.splice(i, 1);
336 open: function ctrlTab_open() {
340 document.addEventListener("keyup", this, true);
342 this.updatePreviews();
343 this._selectedIndex = 1;
345 // Add a slight delay before showing the UI, so that a quick
346 // "ctrl-tab" keypress just flips back to the MRU tab.
347 this._timer = setTimeout(function (self) {
353 _openPanel: function ctrlTab_openPanel() {
354 tabPreviewPanelHelper.opening(this);
356 this.panel.width = Math.min(screen.availWidth * .99,
357 this.canvasWidth * 1.25 * this.tabPreviewCount);
358 var estimateHeight = this.canvasHeight * 1.25 + 75;
359 this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2,
360 screen.availTop + (screen.availHeight - estimateHeight) / 2,
364 close: function ctrlTab_close(aTabToSelect) {
369 clearTimeout(this._timer);
373 gBrowser.selectedTab = aTabToSelect;
377 this.tabToSelect = aTabToSelect;
378 this.panel.hidePopup();
381 setupGUI: function ctrlTab_setupGUI() {
382 this.selected.focus();
383 this._selectedIndex = -1;
385 // Track mouse movement after a brief delay so that the item that happens
386 // to be under the mouse pointer initially won't be selected unintentionally.
387 this._trackMouseOver = false;
388 setTimeout(function (self) {
390 self._trackMouseOver = true;
394 suspendGUI: function ctrlTab_suspendGUI() {
395 document.removeEventListener("keyup", this, true);
397 Array.forEach(this.previews, function (preview) {
398 this.updatePreview(preview, null);
402 onKeyPress: function ctrlTab_onKeyPress(event) {
403 var isOpen = this.isOpen;
406 event.preventDefault();
407 event.stopPropagation();
410 switch (event.keyCode) {
411 case event.DOM_VK_TAB:
412 if (event.ctrlKey && !event.altKey && !event.metaKey) {
414 this.advanceFocus(!event.shiftKey);
415 } else if (!event.shiftKey) {
416 event.preventDefault();
417 event.stopPropagation();
418 let tabs = gBrowser.visibleTabs;
419 if (tabs.length > 2) {
421 } else if (tabs.length == 2) {
422 let index = tabs[0].selected ? 1 : 0;
423 gBrowser.selectedTab = tabs[index];
429 if (isOpen && event.ctrlKey) {
430 if (event.keyCode == event.DOM_VK_DELETE) {
431 this.remove(this.selected);
434 switch (event.charCode) {
435 case this.keys.close:
436 this.remove(this.selected);
439 case this.keys.selectAll:
447 removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) {
448 if (this.tabCount == 2) {
453 this.updatePreviews();
455 if (this.selected.hidden)
456 this.advanceFocus(false);
457 if (this.selected == this.showAllButton)
458 this.advanceFocus(false);
460 // If the current tab is removed, another tab can steal our focus.
461 if (aTab.selected && this.panel.state == "open") {
462 setTimeout(function (selected) {
464 }, 0, this.selected);
468 handleEvent: function ctrlTab_handleEvent(event) {
469 switch (event.type) {
470 case "SSWindowStateReady":
471 this._initRecentlyUsedTabs();
473 case "TabAttrModified":
474 // tab attribute modified (e.g. label, crop, busy, image, selected)
475 for (let i = this.previews.length - 1; i >= 0; i--) {
476 if (this.previews[i]._tab && this.previews[i]._tab == event.target) {
477 this.updatePreview(this.previews[i], event.target);
483 this.detachTab(event.target);
484 this.attachTab(event.target, 0);
487 this.attachTab(event.target, 1);
490 this.detachTab(event.target);
492 this.removeClosingTabFromUI(event.target);
495 this.onKeyPress(event);
498 if (event.keyCode == event.DOM_VK_CONTROL)
502 if (event.target.id == "menu_viewPopup")
503 document.getElementById("menu_showAllTabs").hidden = !allTabs.canOpen;
508 _initRecentlyUsedTabs: function () {
509 this._recentlyUsedTabs =
510 Array.filter(gBrowser.tabs, tab => !tab.closing)
511 .sort((tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed);
514 _init: function ctrlTab__init(enable) {
515 var toggleEventListener = enable ? "addEventListener" : "removeEventListener";
517 window[toggleEventListener]("SSWindowStateReady", this, false);
519 var tabContainer = gBrowser.tabContainer;
520 tabContainer[toggleEventListener]("TabOpen", this, false);
521 tabContainer[toggleEventListener]("TabAttrModified", this, false);
522 tabContainer[toggleEventListener]("TabSelect", this, false);
523 tabContainer[toggleEventListener]("TabClose", this, false);
525 document[toggleEventListener]("keypress", this, false);
526 gBrowser.mTabBox.handleCtrlTab = !enable;
528 // If we're not running, hide the "Show All Tabs" menu item,
529 // as Shift+Ctrl+Tab will be handled by the tab bar.
530 document.getElementById("menu_showAllTabs").hidden = !enable;
531 document.getElementById("menu_viewPopup")[toggleEventListener]("popupshowing", this);
533 // Also disable the <key> to ensure Shift+Ctrl+Tab never triggers
535 var key_showAllTabs = document.getElementById("key_showAllTabs");
537 key_showAllTabs.removeAttribute("disabled");
539 key_showAllTabs.setAttribute("disabled", "true");
548 get toolbarButton() document.getElementById("alltabs-button"),
549 get canOpen() isElementVisible(this.toolbarButton),
551 open: function allTabs_open() {
553 // Without setTimeout, the menupopup won't stay open when invoking
554 // "View > Show All Tabs" and the menu bar auto-hides.
555 setTimeout(function () {
556 allTabs.toolbarButton.open = true;