|
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 }; |