1 /* vim: se cin sw=2 ts=2 et filetype=javascript :
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * This module implements the front end behavior for AeroPeek. Starting in
7 * Windows Vista, the taskbar began showing live thumbnail previews of windows
8 * when the user hovered over the window icon in the taskbar. Starting with
9 * Windows 7, the taskbar allows an application to expose its tabbed interface
10 * in the taskbar by showing thumbnail previews rather than the default window
11 * preview. Additionally, when a user hovers over a thumbnail (tab or window),
12 * they are shown a live preview of the window (or tab + its containing window).
14 * In Windows 7, a title, icon, close button and optional toolbar are shown for
15 * each preview. This feature does not make use of the toolbar. For window
16 * previews, the title is the window title and the icon the window icon. For
17 * tab previews, the title is the page title and the page's favicon. In both
18 * cases, the close button "does the right thing."
20 * The primary objects behind this feature are nsITaskbarTabPreview and
21 * nsITaskbarPreviewController. Each preview has a controller. The controller
22 * responds to the user's interactions on the taskbar and provides the required
23 * data to the preview for determining the size of the tab and thumbnail. The
24 * PreviewController class implements this interface. The preview will request
25 * the controller to provide a thumbnail or preview when the user interacts with
26 * the taskbar. To reduce the overhead of drawing the tab area, the controller
27 * implementation caches the tab's contents in a <canvas> element. If no
28 * previews or thumbnails have been requested for some time, the controller will
29 * discard its cached tab contents.
31 * Screen real estate is limited so when there are too many thumbnails to fit
32 * on the screen, the taskbar stops displaying thumbnails and instead displays
33 * just the title, icon and close button in a similar fashion to previous
34 * versions of the taskbar. If there are still too many previews to fit on the
35 * screen, the taskbar resorts to a scroll up and scroll down button pair to let
36 * the user scroll through the list of tabs. Since this is undoubtedly
37 * inconvenient for users with many tabs, the AeroPeek objects turns off all of
38 * the tab previews. This tells the taskbar to revert to one preview per window.
39 * If the number of tabs falls below this magic threshold, the preview-per-tab
40 * behavior returns. There is no reliable way to determine when the scroll
41 * buttons appear on the taskbar, so a magic pref-controlled number determines
42 * when this threshold has been crossed.
44 import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs";
45 import { PlacesUtils } from "resource://gre/modules/PlacesUtils.sys.mjs";
46 import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs";
47 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
49 // Pref to enable/disable preview-per-tab
50 const TOGGLE_PREF_NAME = "browser.taskbar.previews.enable";
51 // Pref to determine the magic auto-disable threshold
52 const DISABLE_THRESHOLD_PREF_NAME = "browser.taskbar.previews.max";
53 // Pref to control the time in seconds that tab contents live in the cache
54 const CACHE_EXPIRATION_TIME_PREF_NAME = "browser.taskbar.previews.cachetime";
56 const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
60 // Various utility properties
61 XPCOMUtils.defineLazyServiceGetter(
64 "@mozilla.org/image/tools;1",
67 ChromeUtils.defineESModuleGetters(lazy, {
68 PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
71 // nsIURI -> imgIContainer
72 function _imageFromURI(uri, privateMode, callback) {
73 let channel = NetUtil.newChannel({
75 loadUsingSystemPrincipal: true,
76 contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE,
80 channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
81 channel.setPrivate(privateMode);
83 // Ignore channels which do not support nsIPrivateBrowsingChannel
85 NetUtil.asyncFetch(channel, function (inputStream, resultCode) {
86 if (!Components.isSuccessCode(resultCode)) {
90 const decodeCallback = {
93 // We failed, so use the default favicon (only if this wasn't the
95 let defaultURI = PlacesUtils.favicons.defaultFavicon;
96 if (!defaultURI.equals(uri)) {
97 _imageFromURI(defaultURI, privateMode, callback);
107 let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
108 lazy.imgTools.decodeImageAsync(
112 threadManager.currentThread
115 // We failed, so use the default favicon (only if this wasn't the default
117 let defaultURI = PlacesUtils.favicons.defaultFavicon;
118 if (!defaultURI.equals(uri)) {
119 _imageFromURI(defaultURI, privateMode, callback);
125 // string? -> imgIContainer
126 function getFaviconAsImage(iconurl, privateMode, callback) {
128 _imageFromURI(NetUtil.newURI(iconurl), privateMode, callback);
130 _imageFromURI(PlacesUtils.favicons.defaultFavicon, privateMode, callback);
137 * This class manages the behavior of thumbnails and previews. It has the following
139 * 1) responding to requests from Windows taskbar for a thumbnail or window
141 * 2) listens for dom events that result in a thumbnail or window preview needing
142 * to be refresh, and communicates this to the taskbar.
143 * 3) Handles querying and returning to the taskbar new thumbnail or window
144 * preview images through PageThumbs.
147 * The TabWindow (see below) that owns the preview that this controls
149 * The <tab> that this preview is associated with
151 function PreviewController(win, tab) {
154 this.linkedBrowser = tab.linkedBrowser;
155 this.preview = this.win.createTabPreview(this);
157 this.tab.addEventListener("TabAttrModified", this);
159 ChromeUtils.defineLazyGetter(this, "canvasPreview", function () {
160 let canvas = lazy.PageThumbs.createCanvas(this.win.win);
161 canvas.mozOpaque = true;
166 PreviewController.prototype = {
167 QueryInterface: ChromeUtils.generateQI(["nsITaskbarPreviewController"]),
170 this.tab.removeEventListener("TabAttrModified", this);
172 // Break cycles, otherwise we end up leaking the window with everything
178 get wrappedJSObject() {
182 // Resizes the canvasPreview to 0x0, essentially freeing its memory.
183 resetCanvasPreview() {
184 this.canvasPreview.width = 0;
185 this.canvasPreview.height = 0;
189 * Set the canvas dimensions.
191 resizeCanvasPreview(aRequestedWidth, aRequestedHeight) {
192 this.canvasPreview.width = aRequestedWidth;
193 this.canvasPreview.height = aRequestedHeight;
197 return this.tab.linkedBrowser.getBoundingClientRect();
201 let dims = this.browserDims;
202 this._cachedWidth = dims.width;
203 this._cachedHeight = dims.height;
206 testCacheBrowserDims() {
207 let dims = this.browserDims;
208 return this._cachedWidth == dims.width && this._cachedHeight == dims.height;
212 * Capture a new thumbnail image for this preview. Called by the controller
213 * in response to a request for a new thumbnail image.
215 updateCanvasPreview(aFullScale) {
216 // Update our cached browser dims so that delayed resize
217 // events don't trigger another invalidation if this tab becomes active.
218 this.cacheBrowserDims();
219 AeroPeek.resetCacheTimer();
220 return lazy.PageThumbs.captureToCanvas(
224 fullScale: aFullScale,
226 ).catch(console.error);
227 // If we're updating the canvas, then we're in the middle of a peek so
228 // don't discard the cache of previews.
231 updateTitleAndTooltip() {
232 let title = this.win.tabbrowser.getWindowTitleForBrowser(
235 this.preview.title = title;
236 this.preview.tooltip = title;
239 // nsITaskbarPreviewController
241 // window width and height, not browser
243 return this.win.width;
246 // window width and height, not browser
248 return this.win.height;
251 get thumbnailAspectRatio() {
252 let browserDims = this.browserDims;
254 let tabWidth = browserDims.width || 1;
256 let tabHeight = browserDims.height || 1;
257 return tabWidth / tabHeight;
261 * Responds to taskbar requests for window previews. Returns the results asynchronously
262 * through updateCanvasPreview.
264 * @param aTaskbarCallback nsITaskbarPreviewCallback results callback
266 requestPreview(aTaskbarCallback) {
267 // Grab a high res content preview
268 this.resetCanvasPreview();
269 this.updateCanvasPreview(true).then(aPreviewCanvas => {
270 let winWidth = this.win.width;
271 let winHeight = this.win.height;
273 let composite = lazy.PageThumbs.createCanvas(this.win.win);
275 // Use transparency, Aero glass is drawn black without it.
276 composite.mozOpaque = false;
278 let ctx = composite.getContext("2d");
279 let scale = this.win.win.devicePixelRatio;
281 composite.width = winWidth * scale;
282 composite.height = winHeight * scale;
285 ctx.scale(scale, scale);
287 // Draw chrome. Note we currently do not get scrollbars for remote frames
288 // in the image above.
289 ctx.drawWindow(this.win.win, 0, 0, winWidth, winHeight, "rgba(0,0,0,0)");
291 // Draw the content are into the composite canvas at the right location.
296 aPreviewCanvas.width,
297 aPreviewCanvas.height
301 // Deliver the resulting composite canvas to Windows
302 this.win.tabbrowser.previewTab(this.tab, function () {
303 aTaskbarCallback.done(composite, false);
309 * Responds to taskbar requests for tab thumbnails. Returns the results asynchronously
310 * through updateCanvasPreview.
312 * Note Windows requests a specific width and height here, if the resulting thumbnail
313 * does not match these dimensions thumbnail display will fail.
315 * @param aTaskbarCallback nsITaskbarPreviewCallback results callback
316 * @param aRequestedWidth width of the requested thumbnail
317 * @param aRequestedHeight height of the requested thumbnail
319 requestThumbnail(aTaskbarCallback, aRequestedWidth, aRequestedHeight) {
320 this.resizeCanvasPreview(aRequestedWidth, aRequestedHeight);
321 this.updateCanvasPreview(false).then(aThumbnailCanvas => {
322 aTaskbarCallback.done(aThumbnailCanvas, false);
329 this.win.tabbrowser.removeTab(this.tab);
333 this.win.tabbrowser.selectedTab = this.tab;
335 // Accept activation - this will restore the browser window
343 case "TabAttrModified":
344 this.updateTitleAndTooltip();
353 * This class monitors a browser window for changes to its tabs
356 * The nsIDOMWindow browser window
358 function TabWindow(win) {
360 this.tabbrowser = win.gBrowser;
362 this.previews = new Map();
364 for (let i = 0; i < this.tabEvents.length; i++) {
365 this.tabbrowser.tabContainer.addEventListener(this.tabEvents[i], this);
368 for (let i = 0; i < this.winEvents.length; i++) {
369 this.win.addEventListener(this.winEvents[i], this);
372 this.tabbrowser.addTabsProgressListener(this);
374 AeroPeek.windows.push(this);
375 let tabs = this.tabbrowser.tabs;
376 for (let i = 0; i < tabs.length; i++) {
377 this.newTab(tabs[i]);
380 this.updateTabOrdering();
381 AeroPeek.checkPreviewCount();
384 TabWindow.prototype = {
388 tabEvents: ["TabOpen", "TabClose", "TabSelect", "TabMove"],
389 winEvents: ["resize"],
392 this._destroying = true;
394 let tabs = this.tabbrowser.tabs;
396 this.tabbrowser.removeTabsProgressListener(this);
398 for (let i = 0; i < this.winEvents.length; i++) {
399 this.win.removeEventListener(this.winEvents[i], this);
402 for (let i = 0; i < this.tabEvents.length; i++) {
403 this.tabbrowser.tabContainer.removeEventListener(this.tabEvents[i], this);
406 for (let i = 0; i < tabs.length; i++) {
407 this.removeTab(tabs[i]);
410 let idx = AeroPeek.windows.indexOf(this.win.gTaskbarTabGroup);
411 AeroPeek.windows.splice(idx, 1);
412 AeroPeek.checkPreviewCount();
416 return this.win.innerWidth;
419 return this.win.innerHeight;
423 this._cachedWidth = this.width;
424 this._cachedHeight = this.height;
428 return this._cachedWidth == this.width && this._cachedHeight == this.height;
431 // Invoked when the given tab is added to this window
433 let controller = new PreviewController(this, tab);
434 // It's OK to add the preview now while the favicon still loads.
435 this.previews.set(tab, controller.preview);
436 AeroPeek.addPreview(controller.preview);
437 // updateTitleAndTooltip relies on having controller.preview which is lazily resolved.
438 // Now that we've updated this.previews, it will resolve successfully.
439 controller.updateTitleAndTooltip();
442 createTabPreview(controller) {
443 let docShell = this.win.docShell;
444 let preview = AeroPeek.taskbar.createTaskbarTabPreview(
448 preview.visible = AeroPeek.enabled;
449 let { tab } = controller;
450 preview.active = this.tabbrowser.selectedTab == tab;
451 this.updateFavicon(tab, tab.getAttribute("image"));
455 // Invoked when the given tab is closed
457 let preview = this.previewFromTab(tab);
458 preview.active = false;
459 preview.visible = false;
461 preview.controller.wrappedJSObject.destroy();
463 this.previews.delete(tab);
464 AeroPeek.removePreview(preview);
468 return this._enabled;
471 set enabled(enable) {
472 this._enabled = enable;
473 // Because making a tab visible requires that the tab it is next to be
474 // visible, it is far simpler to unset the 'next' tab and recreate them all
476 for (let [, preview] of this.previews) {
478 preview.visible = enable;
480 this.updateTabOrdering();
483 previewFromTab(tab) {
484 return this.previews.get(tab);
487 updateTabOrdering() {
488 let previews = this.previews;
489 let tabs = this.tabbrowser.tabs;
491 // Previews are internally stored using a map, so we need to iterate the
492 // tabbrowser's array of tabs to retrieve previews in the same order.
494 for (let t of tabs) {
495 if (previews.has(t)) {
496 inorder.push(previews.get(t));
500 // Since the internal taskbar array has not yet been updated we must force
501 // on it the sorting order of our local array. To do so we must walk
502 // the local array backwards, otherwise we would send move requests in the
503 // wrong order. See bug 522610 for details.
504 for (let i = inorder.length - 1; i >= 0; i--) {
505 inorder[i].move(inorder[i + 1] || null);
511 let tab = evt.originalTarget;
515 this.updateTabOrdering();
519 this.updateTabOrdering();
522 this.previewFromTab(tab).active = true;
525 this.updateTabOrdering();
528 if (!AeroPeek._prefenabled) {
536 // Set or reset a timer that will invalidate visible thumbnails soon.
537 setInvalidationTimer() {
538 if (!this.invalidateTimer) {
539 this.invalidateTimer = Cc["@mozilla.org/timer;1"].createInstance(
543 this.invalidateTimer.cancel();
545 // delay 1 second before invalidating
546 this.invalidateTimer.initWithCallback(
548 // invalidate every preview. note the internal implementation of
549 // invalidate ignores thumbnails that aren't visible.
550 this.previews.forEach(function (aPreview) {
551 let controller = aPreview.controller.wrappedJSObject;
552 if (!controller.testCacheBrowserDims()) {
553 controller.cacheBrowserDims();
554 aPreview.invalidate();
559 Ci.nsITimer.TYPE_ONE_SHOT
564 // Specific to a window.
566 // Call invalidate on each tab thumbnail so that Windows will request an
567 // updated image. However don't do this repeatedly across multiple resize
568 // events triggered during window border drags.
570 if (this.testCacheDims()) {
574 // update the window dims on our TabWindow object.
578 this.setInvalidationTimer();
581 invalidateTabPreview(aBrowser) {
582 for (let [tab, preview] of this.previews) {
583 if (aBrowser == tab.linkedBrowser) {
584 preview.invalidate();
590 // Browser progress listener
593 // I'm not sure we need this, onStateChange does a really good job
594 // of picking up page changes.
595 // this.invalidateTabPreview(aBrowser);
598 onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags) {
600 aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
601 aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
603 this.invalidateTabPreview(aBrowser);
607 onLinkIconAvailable(aBrowser, aIconURL) {
608 let tab = this.win.gBrowser.getTabForBrowser(aBrowser);
609 this.updateFavicon(tab, aIconURL);
611 updateFavicon(aTab, aIconURL) {
612 let requestURL = null;
615 requestURL = PlacesUtils.favicons.getFaviconLinkForIcon(
616 Services.io.newURI(aIconURL)
619 requestURL = aIconURL;
622 let isDefaultFavicon = !requestURL;
625 PrivateBrowsingUtils.isWindowPrivate(this.win),
627 // The tab could have closed, and there's no guarantee the icons
628 // will have finished fetching 'in order'.
629 if (this.win.closed || aTab.closing || !aTab.linkedBrowser) {
632 // Note that bizarrely, we can get to updateFavicon via a sync codepath
633 // where the new preview controller hasn't yet been added to the
634 // window's map of previews. So `preview` would be null here - except
635 // getFaviconAsImage is async so that should never happen, as we add
636 // the controller to the preview collection straight after creating it.
637 // However, if any of this code ever tries to access this
638 // synchronously, that won't work.
639 let preview = this.previews.get(aTab);
641 aTab.getAttribute("image") == aIconURL ||
642 (!preview.icon && isDefaultFavicon)
654 * This object acts as global storage and external interface for this feature.
655 * It maintains the values of the prefs.
657 export var AeroPeek = {
659 // Does the pref say we're enabled?
660 __prefenabled: false,
666 // nsITaskbarTabPreview array
672 // nsIWinTaskbar service
675 // Maximum number of previews
678 // Length of time in seconds that previews are cached
682 if (!(WINTASKBAR_CONTRACTID in Cc)) {
685 this.taskbar = Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar);
686 this.available = this.taskbar.available;
687 if (!this.available) {
691 Services.prefs.addObserver(TOGGLE_PREF_NAME, this, true);
692 this.enabled = this._prefenabled =
693 Services.prefs.getBoolPref(TOGGLE_PREF_NAME);
694 this.initialized = true;
697 destroy: function destroy() {
698 this._enabled = false;
700 if (this.cacheTimer) {
701 this.cacheTimer.cancel();
706 return this._enabled;
709 set enabled(enable) {
710 if (this._enabled == enable) {
714 this._enabled = enable;
716 this.windows.forEach(function (win) {
717 win.enabled = enable;
722 return this.__prefenabled;
725 set _prefenabled(enable) {
726 if (enable == this.__prefenabled) {
729 this.__prefenabled = enable;
738 _observersAdded: false,
741 if (!this._observersAdded) {
742 Services.prefs.addObserver(DISABLE_THRESHOLD_PREF_NAME, this, true);
743 Services.prefs.addObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this, true);
744 this._placesListener = this.handlePlacesEvents.bind(this);
745 PlacesUtils.observers.addListener(
749 this._observersAdded = true;
752 this.cacheLifespan = Services.prefs.getIntPref(
753 CACHE_EXPIRATION_TIME_PREF_NAME
756 this.maxpreviews = Services.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
758 // If the user toggled us on/off while the browser was already up
759 // (rather than this code running on startup because the pref was
760 // already set to true), we must initialize previews for open windows:
761 if (this.initialized) {
762 for (let win of Services.wm.getEnumerator("navigator:browser")) {
764 this.onOpenWindow(win);
771 while (this.windows.length) {
772 // We can't call onCloseWindow here because it'll bail if we're not
774 let tabWinObject = this.windows[0];
775 tabWinObject.destroy(); // This will remove us from the array.
776 delete tabWinObject.win.gTaskbarTabGroup; // Tidy up the window.
778 PlacesUtils.observers.removeListener(
784 addPreview(preview) {
785 this.previews.push(preview);
786 this.checkPreviewCount();
789 removePreview(preview) {
790 let idx = this.previews.indexOf(preview);
791 this.previews.splice(idx, 1);
792 this.checkPreviewCount();
795 checkPreviewCount() {
796 if (!this._prefenabled) {
799 this.enabled = this.previews.length <= this.maxpreviews;
803 // This occurs when the taskbar service is not available (xp, vista)
804 if (!this.available || !this._prefenabled) {
808 win.gTaskbarTabGroup = new TabWindow(win);
812 // This occurs when the taskbar service is not available (xp, vista)
813 if (!this.available || !this._prefenabled) {
817 win.gTaskbarTabGroup.destroy();
818 delete win.gTaskbarTabGroup;
820 if (!this.windows.length) {
826 this.cacheTimer.cancel();
827 this.cacheTimer.init(
829 1000 * this.cacheLifespan,
830 Ci.nsITimer.TYPE_ONE_SHOT
835 observe(aSubject, aTopic, aData) {
836 if (aTopic == "nsPref:changed" && aData == TOGGLE_PREF_NAME) {
837 this._prefenabled = Services.prefs.getBoolPref(TOGGLE_PREF_NAME);
839 if (!this._prefenabled) {
843 case "nsPref:changed":
844 if (aData == CACHE_EXPIRATION_TIME_PREF_NAME) {
848 if (aData == DISABLE_THRESHOLD_PREF_NAME) {
849 this.maxpreviews = Services.prefs.getIntPref(
850 DISABLE_THRESHOLD_PREF_NAME
853 // Might need to enable/disable ourselves
854 this.checkPreviewCount();
856 case "timer-callback":
857 this.previews.forEach(function (preview) {
858 let controller = preview.controller.wrappedJSObject;
859 controller.resetCanvasPreview();
865 handlePlacesEvents(events) {
866 for (let event of events) {
867 switch (event.type) {
868 case "favicon-changed": {
869 for (let win of this.windows) {
870 for (let [tab] of win.previews) {
871 if (tab.getAttribute("image") == event.faviconUrl) {
872 win.updateFavicon(tab, event.faviconUrl);
881 QueryInterface: ChromeUtils.generateQI([
882 "nsISupportsWeakReference",
887 ChromeUtils.defineLazyGetter(AeroPeek, "cacheTimer", () =>
888 Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer)
891 AeroPeek.initialize();