browser/base/content/browser-tabPreviews.js

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

mercurial