Bumping manifests a=b2g-bump
[gecko.git] / browser / base / content / browser-tabPreviews.js
blob7fbfc6aea0ead0cc7381aa9a1a0c547987ef852a
1 /*
2 #ifdef 0
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/.
6 #endif
7  */
9 /**
10  * Tab previews utility, produces thumbnails
11  */
12 var tabPreviews = {
13   aspectRatio: 0.5625, // 16:9
15   get width() {
16     delete this.width;
17     return this.width = Math.ceil(screen.availWidth / 5.75);
18   },
20   get height() {
21     delete this.height;
22     return this.height = Math.round(this.width * this.aspectRatio);
23   },
25   init: function tabPreviews_init() {
26     if (this._selectedTab)
27       return;
28     this._selectedTab = gBrowser.selectedTab;
30     gBrowser.tabContainer.addEventListener("TabSelect", this, false);
31     gBrowser.tabContainer.addEventListener("SSTabRestored", this, false);
32   },
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;
41     }
43     if (aTab.__thumbnail)
44       return aTab.__thumbnail;
46     if (aTab.getAttribute("pending") == "true") {
47       let img = new Image;
48       img.src = PageThumbs.getThumbnailURL(uri);
49       return img;
50     }
52     return this.capture(aTab, !aTab.hasAttribute("busy"));
53   },
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)
63       return thumbnail;
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)");
73     if (aStore &&
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;
77     }
79     return thumbnail;
80   },
82   handleEvent: function tabPreviews_handleEvent(event) {
83     switch (event.type) {
84       case "TabSelect":
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);
100         }
101         this._selectedTab = event.target;
102         break;
103       case "SSTabRestored":
104         this.capture(event.target, true);
105         break;
106     }
107   }
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;
119   },
120   _generateHandler: function (host) {
121     var self = this;
122     return function (event) {
123       if (event.target == host.panel) {
124         host.panel.removeEventListener(event.type, arguments.callee, false);
125         self["_" + event.type](host);
126       }
127     };
128   },
129   _popupshown: function (host) {
130     if ("setupGUI" in host)
131       host.setupGUI();
132   },
133   _popuphiding: function (host) {
134     if ("suspendGUI" in host)
135       host.suspendGUI();
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;
142     } else
143       gBrowser.selectedBrowser.focus();
145     if (host.tabToSelect) {
146       gBrowser.selectedTab = host.tabToSelect;
147       host.tabToSelect = null;
148     }
149   }
153  * Ctrl-Tab panel
154  */
155 var ctrlTab = {
156   get panel () {
157     delete this.panel;
158     return this.panel = document.getElementById("ctrlTab-panel");
159   },
160   get showAllButton () {
161     delete this.showAllButton;
162     return this.showAllButton = document.getElementById("ctrlTab-showAll");
163   },
164   get previews () {
165     delete this.previews;
166     return this.previews = this.panel.getElementsByClassName("ctrlTab-preview");
167   },
168   get keys () {
169     var keys = {};
170     ["close", "find", "selectAll"].forEach(function (key) {
171       keys[key] = document.getElementById("key_" + key)
172                           .getAttribute("key")
173                           .toLocaleLowerCase().charCodeAt(0);
174     });
175     delete this.keys;
176     return this.keys = keys;
177   },
178   _selectedIndex: 0,
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),
189   get tabList () {
190     return this._recentlyUsedTabs;
191   },
193   init: function ctrlTab_init() {
194     if (!this._recentlyUsedTabs) {
195       tabPreviews.init();
197       this._initRecentlyUsedTabs();
198       this._init(true);
199     }
200   },
202   uninit: function ctrlTab_uninit() {
203     this._recentlyUsedTabs = null;
204     this._init(false);
205   },
207   prefName: "browser.ctrlTab.previews",
208   readPref: function ctrlTab_readPref() {
209     var enable =
210       gPrefService.getBoolPref(this.prefName) &&
211       (!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") ||
212        !gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders"));
214     if (enable)
215       this.init();
216     else
217       this.uninit();
218   },
219   observe: function (aSubject, aTopic, aPrefName) {
220     this.readPref();
221   },
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;
231   },
233   updatePreview: function ctrlTab_updatePreview(aPreview, aTab) {
234     if (aPreview == this.showAllButton)
235       return;
237     aPreview._tab = aTab;
239     if (aPreview.firstChild)
240       aPreview.removeChild(aPreview.firstChild);
241     if (aTab) {
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;");
254       if (aTab.image)
255         aPreview.setAttribute("image", aTab.image);
256       else
257         aPreview.removeAttribute("image");
258       aPreview.hidden = false;
259     } else {
260       aPreview.hidden = true;
261       aPreview.removeAttribute("label");
262       aPreview.removeAttribute("tooltiptext");
263       aPreview.removeAttribute("image");
264     }
265   },
267   advanceFocus: function ctrlTab_advanceFocus(aForward) {
268     let selectedIndex = Array.indexOf(this.previews, this.selected);
269     do {
270       selectedIndex += aForward ? 1 : -1;
271       if (selectedIndex < 0)
272         selectedIndex = this.previews.length - 1;
273       else if (selectedIndex >= this.previews.length)
274         selectedIndex = 0;
275     } while (this.previews[selectedIndex].hidden);
277     if (this._selectedIndex == -1) {
278       // Focus is already in the panel.
279       this.previews[selectedIndex].focus();
280     } else {
281       this._selectedIndex = selectedIndex;
282     }
284     if (this._timer) {
285       clearTimeout(this._timer);
286       this._timer = null;
287       this._openPanel();
288     }
289   },
291   _mouseOverFocus: function ctrlTab_mouseOverFocus(aPreview) {
292     if (this._trackMouseOver)
293       aPreview.focus();
294   },
296   pick: function ctrlTab_pick(aPreview) {
297     if (!this.tabCount)
298       return;
300     var select = (aPreview || this.selected);
302     if (select == this.showAllButton)
303       this.showAllTabs();
304     else
305       this.close(select._tab);
306   },
308   showAllTabs: function ctrlTab_showAllTabs(aPreview) {
309     this.close();
310     document.getElementById("Browser:ShowAllTabs").doCommand();
311   },
313   remove: function ctrlTab_remove(aPreview) {
314     if (aPreview._tab)
315       gBrowser.removeTab(aPreview._tab);
316   },
318   attachTab: function ctrlTab_attachTab(aTab, aPos) {
319     if (aTab.closing)
320       return;
322     if (aPos == 0)
323       this._recentlyUsedTabs.unshift(aTab);
324     else if (aPos)
325       this._recentlyUsedTabs.splice(aPos, 0, aTab);
326     else
327       this._recentlyUsedTabs.push(aTab);
328   },
330   detachTab: function ctrlTab_detachTab(aTab) {
331     var i = this._recentlyUsedTabs.indexOf(aTab);
332     if (i >= 0)
333       this._recentlyUsedTabs.splice(i, 1);
334   },
336   open: function ctrlTab_open() {
337     if (this.isOpen)
338       return;
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) {
348       self._timer = null;
349       self._openPanel();
350     }, 200, this);
351   },
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,
361                                  false);
362   },
364   close: function ctrlTab_close(aTabToSelect) {
365     if (!this.isOpen)
366       return;
368     if (this._timer) {
369       clearTimeout(this._timer);
370       this._timer = null;
371       this.suspendGUI();
372       if (aTabToSelect)
373         gBrowser.selectedTab = aTabToSelect;
374       return;
375     }
377     this.tabToSelect = aTabToSelect;
378     this.panel.hidePopup();
379   },
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) {
389       if (self.isOpen)
390         self._trackMouseOver = true;
391     }, 0, this);
392   },
394   suspendGUI: function ctrlTab_suspendGUI() {
395     document.removeEventListener("keyup", this, true);
397     Array.forEach(this.previews, function (preview) {
398       this.updatePreview(preview, null);
399     }, this);
400   },
402   onKeyPress: function ctrlTab_onKeyPress(event) {
403     var isOpen = this.isOpen;
405     if (isOpen) {
406       event.preventDefault();
407       event.stopPropagation();
408     }
410     switch (event.keyCode) {
411       case event.DOM_VK_TAB:
412         if (event.ctrlKey && !event.altKey && !event.metaKey) {
413           if (isOpen) {
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) {
420               this.open();
421             } else if (tabs.length == 2) {
422               let index = tabs[0].selected ? 1 : 0;
423               gBrowser.selectedTab = tabs[index];
424             }
425           }
426         }
427         break;
428       default:
429         if (isOpen && event.ctrlKey) {
430           if (event.keyCode == event.DOM_VK_DELETE) {
431             this.remove(this.selected);
432             break;
433           }
434           switch (event.charCode) {
435             case this.keys.close:
436               this.remove(this.selected);
437               break;
438             case this.keys.find:
439             case this.keys.selectAll:
440               this.showAllTabs();
441               break;
442           }
443         }
444     }
445   },
447   removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) {
448     if (this.tabCount == 2) {
449       this.close();
450       return;
451     }
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) {
463         selected.focus();
464       }, 0, this.selected);
465     }
466   },
468   handleEvent: function ctrlTab_handleEvent(event) {
469     switch (event.type) {
470       case "SSWindowStateReady":
471         this._initRecentlyUsedTabs();
472         break;
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);
478             break;
479           }
480         }
481         break;
482       case "TabSelect":
483         this.detachTab(event.target);
484         this.attachTab(event.target, 0);
485         break;
486       case "TabOpen":
487         this.attachTab(event.target, 1);
488         break;
489       case "TabClose":
490         this.detachTab(event.target);
491         if (this.isOpen)
492           this.removeClosingTabFromUI(event.target);
493         break;
494       case "keypress":
495         this.onKeyPress(event);
496         break;
497       case "keyup":
498         if (event.keyCode == event.DOM_VK_CONTROL)
499           this.pick();
500         break;
501       case "popupshowing":
502         if (event.target.id == "menu_viewPopup")
503           document.getElementById("menu_showAllTabs").hidden = !allTabs.canOpen;
504         break;
505     }
506   },
508   _initRecentlyUsedTabs: function () {
509     this._recentlyUsedTabs =
510       Array.filter(gBrowser.tabs, tab => !tab.closing)
511            .sort((tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed);
512   },
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
534     // Show All Tabs.
535     var key_showAllTabs = document.getElementById("key_showAllTabs");
536     if (enable)
537       key_showAllTabs.removeAttribute("disabled");
538     else
539       key_showAllTabs.setAttribute("disabled", "true");
540   }
545  * All Tabs menu
546  */
547 var allTabs = {
548   get toolbarButton() document.getElementById("alltabs-button"),
549   get canOpen() isElementVisible(this.toolbarButton),
551   open: function allTabs_open() {
552     if (this.canOpen) {
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;
557       }, 0);
558     }
559   }