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