 |
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