Mozilla Cross-Reference mozilla-central
mozilla/ browser/ base/ content/ browser-ctrlTab.js
Hg Log
Hg Blame
Diff file
Raw file
view using tree:
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  */
8 
9 /**
10  * Tab previews utility, produces thumbnails
11  */
12 var tabPreviews = {
13   init: function tabPreviews_init() {
14     if (this._selectedTab)
15       return;
16     this._selectedTab = gBrowser.selectedTab;
17 
18     gBrowser.tabContainer.addEventListener("TabSelect", this, false);
19     gBrowser.tabContainer.addEventListener("SSTabRestored", this, false);
20 
21     let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
22                           .getService(Ci.nsIScreenManager);
23     let left = {}, top = {}, width = {}, height = {};
24     screenManager.primaryScreen.GetRectDisplayPix(left, top, width, height);
25     this.aspectRatio = height.value / width.value;
26   },
27 
28   get: function tabPreviews_get(aTab) {
29     let uri = aTab.linkedBrowser.currentURI.spec;
30 
31     if (aTab.__thumbnail_lastURI &&
32         aTab.__thumbnail_lastURI != uri) {
33       aTab.__thumbnail = null;
34       aTab.__thumbnail_lastURI = null;
35     }
36 
37     if (aTab.__thumbnail)
38       return aTab.__thumbnail;
39 
40     if (aTab.getAttribute("pending") == "true") {
41       let img = new Image;
42       img.src = PageThumbs.getThumbnailURL(uri);
43       return img;
44     }
45 
46     return this.capture(aTab, !aTab.hasAttribute("busy"));
47   },
48 
49   capture: function tabPreviews_capture(aTab, aShouldCache) {
50     let browser = aTab.linkedBrowser;
51     let uri = browser.currentURI.spec;
52     let canvas = PageThumbs.createCanvas(window);
53     PageThumbs.shouldStoreThumbnail(browser, (aDoStore) => {
54       if (aDoStore && aShouldCache) {
55         PageThumbs.captureAndStore(browser, function () {
56           let img = new Image;
57           img.src = PageThumbs.getThumbnailURL(uri);
58           aTab.__thumbnail = img;
59           aTab.__thumbnail_lastURI = uri;
60           canvas.getContext("2d").drawImage(img, 0, 0);
61         });
62       } else {
63         PageThumbs.captureToCanvas(browser, canvas, () => {
64           if (aShouldCache) {
65             aTab.__thumbnail = canvas;
66             aTab.__thumbnail_lastURI = uri;
67           }
68         });
69       }
70     });
71     return canvas;
72   },
73 
74   handleEvent: function tabPreviews_handleEvent(event) {
75     switch (event.type) {
76       case "TabSelect":
77         if (this._selectedTab &&
78             this._selectedTab.parentNode &&
79             !this._pendingUpdate) {
80           // Generate a thumbnail for the tab that was selected.
81           // The timeout keeps the UI snappy and prevents us from generating thumbnails
82           // for tabs that will be closed. During that timeout, don't generate other
83           // thumbnails in case multiple TabSelect events occur fast in succession.
84           this._pendingUpdate = true;
85           setTimeout(function (self, aTab) {
86             self._pendingUpdate = false;
87             if (aTab.parentNode &&
88                 !aTab.hasAttribute("busy") &&
89                 !aTab.hasAttribute("pending"))
90               self.capture(aTab, true);
91           }, 2000, this, this._selectedTab);
92         }
93         this._selectedTab = event.target;
94         break;
95       case "SSTabRestored":
96         this.capture(event.target, true);
97         break;
98     }
99   }
100 };
101 
102 var tabPreviewPanelHelper = {
103   opening: function (host) {
104     host.panel.hidden = false;
105 
106     var handler = this._generateHandler(host);
107     host.panel.addEventListener("popupshown", handler, false);
108     host.panel.addEventListener("popuphiding", handler, false);
109 
110     host._prevFocus = document.commandDispatcher.focusedElement;
111   },
112   _generateHandler: function (host) {
113     var self = this;
114     return function (event) {
115       if (event.target == host.panel) {
116         host.panel.removeEventListener(event.type, arguments.callee, false);
117         self["_" + event.type](host);
118       }
119     };
120   },
121   _popupshown: function (host) {
122     if ("setupGUI" in host)
123       host.setupGUI();
124   },
125   _popuphiding: function (host) {
126     if ("suspendGUI" in host)
127       host.suspendGUI();
128 
129     if (host._prevFocus) {
130       Services.focus.setFocus(host._prevFocus, Ci.nsIFocusManager.FLAG_NOSCROLL);
131       host._prevFocus = null;
132     } else
133       gBrowser.selectedBrowser.focus();
134 
135     if (host.tabToSelect) {
136       gBrowser.selectedTab = host.tabToSelect;
137       host.tabToSelect = null;
138     }
139   }
140 };
141 
142 /**
143  * Ctrl-Tab panel
144  */
145 var ctrlTab = {
146   get panel () {
147     delete this.panel;
148     return this.panel = document.getElementById("ctrlTab-panel");
149   },
150   get showAllButton () {
151     delete this.showAllButton;
152     return this.showAllButton = document.getElementById("ctrlTab-showAll");
153   },
154   get previews () {
155     delete this.previews;
156     return this.previews = this.panel.getElementsByClassName("ctrlTab-preview");
157   },
158   get maxTabPreviews () {
159     delete this.maxTabPreviews;
160     return this.maxTabPreviews = this.previews.length - 1;
161   },
162   get canvasWidth () {
163     delete this.canvasWidth;
164     return this.canvasWidth = Math.ceil(screen.availWidth * .85 / this.maxTabPreviews);
165   },
166   get canvasHeight () {
167     delete this.canvasHeight;
168     return this.canvasHeight = Math.round(this.canvasWidth * tabPreviews.aspectRatio);
169   },
170   get keys () {
171     var keys = {};
172     ["close", "find", "selectAll"].forEach(function (key) {
173       keys[key] = document.getElementById("key_" + key)
174                           .getAttribute("key")
175                           .toLocaleLowerCase().charCodeAt(0);
176     });
177     delete this.keys;
178     return this.keys = keys;
179   },
180   _selectedIndex: 0,
181   get selected () {
182     return this._selectedIndex < 0 ?
183              document.activeElement :
184              this.previews.item(this._selectedIndex);
185   },
186   get isOpen () {
187     return this.panel.state == "open" || this.panel.state == "showing" || this._timer;
188   },
189   get tabCount () {
190     return this.tabList.length;
191   },
192   get tabPreviewCount () {
193     return Math.min(this.maxTabPreviews, this.tabCount);
194   },
195 
196   get tabList () {
197     return this._recentlyUsedTabs;
198   },
199 
200   init: function ctrlTab_init() {
201     if (!this._recentlyUsedTabs) {
202       tabPreviews.init();
203 
204       this._initRecentlyUsedTabs();
205       this._init(true);
206     }
207   },
208 
209   uninit: function ctrlTab_uninit() {
210     this._recentlyUsedTabs = null;
211     this._init(false);
212   },
213 
214   prefName: "browser.ctrlTab.previews",
215   readPref: function ctrlTab_readPref() {
216     var enable =
217       gPrefService.getBoolPref(this.prefName) &&
218       (!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") ||
219        !gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders"));
220 
221     if (enable)
222       this.init();
223     else
224       this.uninit();
225   },
226   observe: function (aSubject, aTopic, aPrefName) {
227     this.readPref();
228   },
229 
230   updatePreviews: function ctrlTab_updatePreviews() {
231     for (let i = 0; i < this.previews.length; i++)
232       this.updatePreview(this.previews[i], this.tabList[i]);
233 
234     var showAllLabel = gNavigatorBundle.getString("ctrlTab.listAllTabs.label");
235     this.showAllButton.label =
236       PluralForm.get(this.tabCount, showAllLabel).replace("#1", this.tabCount);
237     this.showAllButton.hidden = !allTabs.canOpen;
238   },
239 
240   updatePreview: function ctrlTab_updatePreview(aPreview, aTab) {
241     if (aPreview == this.showAllButton)
242       return;
243 
244     aPreview._tab = aTab;
245 
246     if (aPreview.firstChild)
247       aPreview.removeChild(aPreview.firstChild);
248     if (aTab) {
249       let canvasWidth = this.canvasWidth;
250       let canvasHeight = this.canvasHeight;
251       aPreview.appendChild(tabPreviews.get(aTab));
252       aPreview.setAttribute("label", aTab.label);
253       aPreview.setAttribute("tooltiptext", aTab.label);
254       aPreview.setAttribute("crop", aTab.crop);
255       aPreview.setAttribute("canvaswidth", canvasWidth);
256       aPreview.setAttribute("canvasstyle",
257                             "max-width:" + canvasWidth + "px;" +
258                             "min-width:" + canvasWidth + "px;" +
259                             "max-height:" + canvasHeight + "px;" +
260                             "min-height:" + canvasHeight + "px;");
261       if (aTab.image)
262         aPreview.setAttribute("image", aTab.image);
263       else
264         aPreview.removeAttribute("image");
265       aPreview.hidden = false;
266     } else {
267       aPreview.hidden = true;
268       aPreview.removeAttribute("label");
269       aPreview.removeAttribute("tooltiptext");
270       aPreview.removeAttribute("image");
271     }
272   },
273 
274   advanceFocus: function ctrlTab_advanceFocus(aForward) {
275     let selectedIndex = Array.indexOf(this.previews, this.selected);
276     do {
277       selectedIndex += aForward ? 1 : -1;
278       if (selectedIndex < 0)
279         selectedIndex = this.previews.length - 1;
280       else if (selectedIndex >= this.previews.length)
281         selectedIndex = 0;
282     } while (this.previews[selectedIndex].hidden);
283 
284     if (this._selectedIndex == -1) {
285       // Focus is already in the panel.
286       this.previews[selectedIndex].focus();
287     } else {
288       this._selectedIndex = selectedIndex;
289     }
290 
291     if (this._timer) {
292       clearTimeout(this._timer);
293       this._timer = null;
294       this._openPanel();
295     }
296   },
297 
298   _mouseOverFocus: function ctrlTab_mouseOverFocus(aPreview) {
299     if (this._trackMouseOver)
300       aPreview.focus();
301   },
302 
303   pick: function ctrlTab_pick(aPreview) {
304     if (!this.tabCount)
305       return;
306 
307     var select = (aPreview || this.selected);
308 
309     if (select == this.showAllButton)
310       this.showAllTabs();
311     else
312       this.close(select._tab);
313   },
314 
315   showAllTabs: function ctrlTab_showAllTabs(aPreview) {
316     this.close();
317     document.getElementById("Browser:ShowAllTabs").doCommand();
318   },
319 
320   remove: function ctrlTab_remove(aPreview) {
321     if (aPreview._tab)
322       gBrowser.removeTab(aPreview._tab);
323   },
324 
325   attachTab: function ctrlTab_attachTab(aTab, aPos) {
326     if (aTab.closing)
327       return;
328 
329     if (aPos == 0)
330       this._recentlyUsedTabs.unshift(aTab);
331     else if (aPos)
332       this._recentlyUsedTabs.splice(aPos, 0, aTab);
333     else
334       this._recentlyUsedTabs.push(aTab);
335   },
336 
337   detachTab: function ctrlTab_detachTab(aTab) {
338     var i = this._recentlyUsedTabs.indexOf(aTab);
339     if (i >= 0)
340       this._recentlyUsedTabs.splice(i, 1);
341   },
342 
343   open: function ctrlTab_open() {
344     if (this.isOpen)
345       return;
346 
347     document.addEventListener("keyup", this, true);
348 
349     this.updatePreviews();
350     this._selectedIndex = 1;
351 
352     // Add a slight delay before showing the UI, so that a quick
353     // "ctrl-tab" keypress just flips back to the MRU tab.
354     this._timer = setTimeout(function (self) {
355       self._timer = null;
356       self._openPanel();
357     }, 200, this);
358   },
359 
360   _openPanel: function ctrlTab_openPanel() {
361     tabPreviewPanelHelper.opening(this);
362 
363     this.panel.width = Math.min(screen.availWidth * .99,
364                                 this.canvasWidth * 1.25 * this.tabPreviewCount);
365     var estimateHeight = this.canvasHeight * 1.25 + 75;
366     this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2,
367                                  screen.availTop + (screen.availHeight - estimateHeight) / 2,
368                                  false);
369   },
370 
371   close: function ctrlTab_close(aTabToSelect) {
372     if (!this.isOpen)
373       return;
374 
375     if (this._timer) {
376       clearTimeout(this._timer);
377       this._timer = null;
378       this.suspendGUI();
379       if (aTabToSelect)
380         gBrowser.selectedTab = aTabToSelect;
381       return;
382     }
383 
384     this.tabToSelect = aTabToSelect;
385     this.panel.hidePopup();
386   },
387 
388   setupGUI: function ctrlTab_setupGUI() {
389     this.selected.focus();
390     this._selectedIndex = -1;
391 
392     // Track mouse movement after a brief delay so that the item that happens
393     // to be under the mouse pointer initially won't be selected unintentionally.
394     this._trackMouseOver = false;
395     setTimeout(function (self) {
396       if (self.isOpen)
397         self._trackMouseOver = true;
398     }, 0, this);
399   },
400 
401   suspendGUI: function ctrlTab_suspendGUI() {
402     document.removeEventListener("keyup", this, true);
403 
404     for (let preview of this.previews) {
405       this.updatePreview(preview, null);
406     }
407   },
408 
409   onKeyPress: function ctrlTab_onKeyPress(event) {
410     var isOpen = this.isOpen;
411 
412     if (isOpen) {
413       event.preventDefault();
414       event.stopPropagation();
415     }
416 
417     switch (event.keyCode) {
418       case event.DOM_VK_TAB:
419         if (event.ctrlKey && !event.altKey && !event.metaKey) {
420           if (isOpen) {
421             this.advanceFocus(!event.shiftKey);
422           } else if (!event.shiftKey) {
423             event.preventDefault();
424             event.stopPropagation();
425             let tabs = gBrowser.visibleTabs;
426             if (tabs.length > 2) {
427               this.open();
428             } else if (tabs.length == 2) {
429               let index = tabs[0].selected ? 1 : 0;
430               gBrowser.selectedTab = tabs[index];
431             }
432           }
433         }
434         break;
435       default:
436         if (isOpen && event.ctrlKey) {
437           if (event.keyCode == event.DOM_VK_DELETE) {
438             this.remove(this.selected);
439             break;
440           }
441           switch (event.charCode) {
442             case this.keys.close:
443               this.remove(this.selected);
444               break;
445             case this.keys.find:
446             case this.keys.selectAll:
447               this.showAllTabs();
448               break;
449           }
450         }
451     }
452   },
453 
454   removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) {
455     if (this.tabCount == 2) {
456       this.close();
457       return;
458     }
459 
460     this.updatePreviews();
461 
462     if (this.selected.hidden)
463       this.advanceFocus(false);
464     if (this.selected == this.showAllButton)
465       this.advanceFocus(false);
466 
467     // If the current tab is removed, another tab can steal our focus.
468     if (aTab.selected && this.panel.state == "open") {
469       setTimeout(function (selected) {
470         selected.focus();
471       }, 0, this.selected);
472     }
473   },
474 
475   handleEvent: function ctrlTab_handleEvent(event) {
476     switch (event.type) {
477       case "SSWindowStateReady":
478         this._initRecentlyUsedTabs();
479         break;
480       case "TabAttrModified":
481         // tab attribute modified (e.g. label, crop, busy, image, selected)
482         for (let i = this.previews.length - 1; i >= 0; i--) {
483           if (this.previews[i]._tab && this.previews[i]._tab == event.target) {
484             this.updatePreview(this.previews[i], event.target);
485             break;
486           }
487         }
488         break;
489       case "TabSelect":
490         this.detachTab(event.target);
491         this.attachTab(event.target, 0);
492         break;
493       case "TabOpen":
494         this.attachTab(event.target, 1);
495         break;
496       case "TabClose":
497         this.detachTab(event.target);
498         if (this.isOpen)
499           this.removeClosingTabFromUI(event.target);
500         break;
501       case "keypress":
502         this.onKeyPress(event);
503         break;
504       case "keyup":
505         if (event.keyCode == event.DOM_VK_CONTROL)
506           this.pick();
507         break;
508       case "popupshowing":
509         if (event.target.id == "menu_viewPopup")
510           document.getElementById("menu_showAllTabs").hidden = !allTabs.canOpen;
511         break;
512     }
513   },
514 
515   filterForThumbnailExpiration: function (aCallback) {
516     // Save a few more thumbnails than we actually display, so that when tabs
517     // are closed, the previews we add instead still get thumbnails.
518     const extraThumbnails = 3;
519     const thumbnailCount = Math.min(this.tabPreviewCount + extraThumbnails,
520                                     this.tabCount);
521 
522     let urls = [];
523     for (let i = 0; i < thumbnailCount; i++)
524       urls.push(this.tabList[i].linkedBrowser.currentURI.spec);
525 
526     aCallback(urls);
527   },
528 
529   _initRecentlyUsedTabs: function () {
530     this._recentlyUsedTabs =
531       Array.filter(gBrowser.tabs, tab => !tab.closing)
532            .sort((tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed);
533   },
534 
535   _init: function ctrlTab__init(enable) {
536     var toggleEventListener = enable ? "addEventListener" : "removeEventListener";
537 
538     window[toggleEventListener]("SSWindowStateReady", this, false);
539 
540     var tabContainer = gBrowser.tabContainer;
541     tabContainer[toggleEventListener]("TabOpen", this, false);
542     tabContainer[toggleEventListener]("TabAttrModified", this, false);
543     tabContainer[toggleEventListener]("TabSelect", this, false);
544     tabContainer[toggleEventListener]("TabClose", this, false);
545 
546     document[toggleEventListener]("keypress", this, false);
547     gBrowser.mTabBox.handleCtrlTab = !enable;
548 
549     if (enable)
550       PageThumbs.addExpirationFilter(this);
551     else
552       PageThumbs.removeExpirationFilter(this);
553 
554     // If we're not running, hide the "Show All Tabs" menu item,
555     // as Shift+Ctrl+Tab will be handled by the tab bar.
556     document.getElementById("menu_showAllTabs").hidden = !enable;
557     document.getElementById("menu_viewPopup")[toggleEventListener]("popupshowing", this);
558 
559     // Also disable the <key> to ensure Shift+Ctrl+Tab never triggers
560     // Show All Tabs.
561     var key_showAllTabs = document.getElementById("key_showAllTabs");
562     if (enable)
563       key_showAllTabs.removeAttribute("disabled");
564     else
565       key_showAllTabs.setAttribute("disabled", "true");
566   }
567 };
568 
569 
570 /**
571  * All Tabs menu
572  */
573 var allTabs = {
574   get toolbarButton() {
575     return document.getElementById("alltabs-button");
576   },
577 
578   get canOpen() {
579     return isElementVisible(this.toolbarButton);
580   },
581 
582   open: function allTabs_open() {
583     if (this.canOpen) {
584       // Without setTimeout, the menupopup won't stay open when invoking
585       // "View > Show All Tabs" and the menu bar auto-hides.
586       setTimeout(() => {
587         this.toolbarButton.open = true;
588       }, 0);
589     }
590   }
591 };
592 
view http://hg.mozilla.org/mozilla-central/rev/ /browser/base/content/browser-ctrlTab.js