1 /* vim: se cin sw=2 ts=2 et filetype=javascript :
2 * ***** BEGIN LICENSE BLOCK *****
3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 * The contents of this file are subject to the Mozilla Public License Version
6 * 1.1 (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 * http://www.mozilla.org/MPL/
10 * Software distributed under the License is distributed on an "AS IS" basis,
11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 * for the specific language governing rights and limitations under the
15 * The Original Code is Mozilla code.
17 * The Initial Developer of the Original Code is
18 * Mozilla Corporation.
19 * Portions created by the Initial Developer are Copyright (C) 2009
20 * the Initial Developer. All Rights Reserved.
23 * Rob Arnold <robarnold@cmu.edu> (original author)
25 * Alternatively, the contents of this file may be used under the terms of
26 * either the GNU General Public License Version 2 or later (the "GPL"), or
27 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28 * in which case the provisions of the GPL or the LGPL are applicable instead
29 * of those above. If you wish to allow use of your version of this file only
30 * under the terms of either the GPL or the LGPL, and not to allow others to
31 * use your version of this file under the terms of the MPL, indicate your
32 * decision by deleting the provisions above and replace them with the notice
33 * and other provisions required by the GPL or the LGPL. If you do not delete
34 * the provisions above, a recipient may use your version of this file under
35 * the terms of any one of the MPL, the GPL or the LGPL.
37 * ***** END LICENSE BLOCK ***** */
39 * This module implements the front end behavior for AeroPeek. Starting in
40 * Windows Vista, the taskbar began showing live thumbnail previews of windows
41 * when the user hovered over the window icon in the taskbar. Starting with
42 * Windows 7, the taskbar allows an application to expose its tabbed interface
43 * in the taskbar by showing thumbnail previews rather than the default window
44 * preview. Additionally, when a user hovers over a thumbnail (tab or window),
45 * they are shown a live preview of the window (or tab + its containing window).
47 * In Windows 7, a title, icon, close button and optional toolbar are shown for
48 * each preview. This feature does not make use of the toolbar. For window
49 * previews, the title is the window title and the icon the window icon. For
50 * tab previews, the title is the page title and the page's favicon. In both
51 * cases, the close button "does the right thing."
53 * The primary objects behind this feature are nsITaskbarTabPreview and
54 * nsITaskbarPreviewController. Each preview has a controller. The controller
55 * responds to the user's interactions on the taskbar and provides the required
56 * data to the preview for determining the size of the tab and thumbnail. The
57 * PreviewController class implements this interface. The preview will request
58 * the controller to provide a thumbnail or preview when the user interacts with
59 * the taskbar. To reduce the overhead of drawing the tab area, the controller
60 * implementation caches the tab's contents in a <canvas> element. If no
61 * previews or thumbnails have been requested for some time, the controller will
62 * discard its cached tab contents.
64 * Screen real estate is limited so when there are too many thumbnails to fit
65 * on the screen, the taskbar stops displaying thumbnails and instead displays
66 * just the title, icon and close button in a similar fashion to previous
67 * versions of the taskbar. If there are still too many previews to fit on the
68 * screen, the taskbar resorts to a scroll up and scroll down button pair to let
69 * the user scroll through the list of tabs. Since this is undoubtedly
70 * inconvenient for users with many tabs, the AeroPeek objects turns off all of
71 * the tab previews. This tells the taskbar to revert to one preview per window.
72 * If the number of tabs falls below this magic threshold, the preview-per-tab
73 * behavior returns. There is no reliable way to determine when the scroll
74 * buttons appear on the taskbar, so a magic pref-controlled number determines
75 * when this threshold has been crossed.
77 var EXPORTED_SYMBOLS = ["AeroPeek"];
79 const Cc = Components.classes;
80 const Ci = Components.interfaces;
81 const Cu = Components.utils;
83 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
84 Cu.import("resource://gre/modules/NetUtil.jsm");
86 // Pref to enable/disable preview-per-tab
87 const TOGGLE_PREF_NAME = "browser.taskbar.previews.enable";
88 // Pref to determine the magic auto-disable threshold
89 const DISABLE_THRESHOLD_PREF_NAME = "browser.taskbar.previews.max";
90 // Pref to control the time in seconds that tab contents live in the cache
91 const CACHE_EXPIRATION_TIME_PREF_NAME = "browser.taskbar.previews.cachetime";
93 const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
95 ////////////////////////////////////////////////////////////////////////////////
96 //// Various utility properties
97 XPCOMUtils.defineLazyServiceGetter(this, "ioSvc",
98 "@mozilla.org/network/io-service;1",
100 XPCOMUtils.defineLazyServiceGetter(this, "imgTools",
101 "@mozilla.org/image/tools;1",
103 XPCOMUtils.defineLazyServiceGetter(this, "faviconSvc",
104 "@mozilla.org/browser/favicon-service;1",
105 "nsIFaviconService");
107 // nsIURI -> imgIContainer
108 function _imageFromURI(uri, callback) {
109 let channel = ioSvc.newChannelFromURI(uri);
110 NetUtil.asyncFetch(channel, function(inputStream, resultCode) {
111 if (!Components.isSuccessCode(resultCode))
114 let out_img = { value: null };
115 imgTools.decodeImageData(inputStream, channel.contentType, out_img);
116 callback(out_img.value);
118 // We failed, so use the default favicon (only if this wasn't the default
120 let defaultURI = faviconSvc.defaultFavicon;
121 if (!defaultURI.equals(uri))
122 _imageFromURI(defaultURI, callback);
127 // string? -> imgIContainer
128 function getFaviconAsImage(iconurl, callback) {
130 _imageFromURI(NetUtil.newURI(iconurl), callback);
132 _imageFromURI(faviconSvc.defaultFavicon, callback);
135 // Snaps the given rectangle to be pixel-aligned at the given scale
136 function snapRectAtScale(r, scale) {
137 let x = Math.floor(r.x * scale);
138 let y = Math.floor(r.y * scale);
139 let width = Math.ceil((r.x + r.width) * scale) - x;
140 let height = Math.ceil((r.y + r.height) * scale) - y;
144 r.width = width / scale;
145 r.height = height / scale;
148 ////////////////////////////////////////////////////////////////////////////////
149 //// PreviewController
152 * This class manages the behavior of the preview.
154 * To give greater performance when drawing, the dirty areas of the content
155 * window are tracked and drawn on demand into a canvas of the same size.
156 * This provides a great increase in responsiveness when drawing a preview
157 * for unchanged (or even only slightly changed) tabs.
160 * The TabWindow (see below) that owns the preview that this controls
162 * The <tab> that this preview is associated with
164 function PreviewController(win, tab) {
167 this.linkedBrowser = tab.linkedBrowser;
169 this.linkedBrowser.addEventListener("MozAfterPaint", this, false);
170 this.tab.addEventListener("TabAttrModified", this, false);
172 // Cannot perform the lookup during construction. See TabWindow.newTab
173 XPCOMUtils.defineLazyGetter(this, "preview", function () this.win.previewFromTab(this.tab));
175 XPCOMUtils.defineLazyGetter(this, "canvasPreview", function () {
176 let canvas = this.win.win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
177 canvas.mozOpaque = true;
181 XPCOMUtils.defineLazyGetter(this, "dirtyRegion",
183 let dirtyRegion = Cc["@mozilla.org/gfx/region;1"]
184 .createInstance(Ci.nsIScriptableRegion);
189 XPCOMUtils.defineLazyGetter(this, "winutils",
191 let win = tab.linkedBrowser.contentWindow;
192 return win.QueryInterface(Ci.nsIInterfaceRequestor)
193 .getInterface(Ci.nsIDOMWindowUtils);
197 PreviewController.prototype = {
198 QueryInterface: XPCOMUtils.generateQI([Ci.nsITaskbarPreviewController,
199 Ci.nsIDOMEventListener]),
200 destroy: function () {
201 this.tab.removeEventListener("TabAttrModified", this, false);
202 this.linkedBrowser.removeEventListener("MozAfterPaint", this, false);
204 // Break cycles, otherwise we end up leaking the window with everything
208 delete this.dirtyRegion;
210 get wrappedJSObject() {
215 let rectstream = this.dirtyRegion.getRects();
219 for (let i = 0; i < rectstream.length; i+= 4) {
220 let r = {x: rectstream[i],
222 width: rectstream[i+2],
223 height: rectstream[i+3]};
229 // Resizes the canvasPreview to 0x0, essentially freeing its memory.
230 // updateCanvasPreview() will detect the size mismatch as a resize event
231 // the next time it is called.
232 resetCanvasPreview: function () {
233 this.canvasPreview.width = 0;
234 this.canvasPreview.height = 0;
238 // We use this property instead of the fullZoom property because this
239 // accurately reflects the actual zoom factor used when drawing.
240 return this.winutils.screenPixelsPerCSSPixel;
243 // Updates the controller's canvas with the parts of the <browser> that need
245 updateCanvasPreview: function () {
246 let win = this.linkedBrowser.contentWindow;
247 let bx = this.linkedBrowser.boxObject;
249 if (bx.width != this.canvasPreview.width ||
250 bx.height != this.canvasPreview.height) {
251 // Invalidate the entire area and repaint
252 this.onTabPaint({left:0, top:0, right:win.innerWidth, bottom:win.innerHeight});
253 this.canvasPreview.width = bx.width;
254 this.canvasPreview.height = bx.height;
257 // Draw dirty regions
258 let ctx = this.canvasPreview.getContext("2d");
259 let scale = this.zoom;
261 let flags = this.canvasPreviewFlags;
262 // The dirty region may include parts that are offscreen so we clip to the
264 this.dirtyRegion.intersectRect(0, 0, win.innerWidth, win.innerHeight);
265 this.dirtyRects.forEach(function (r) {
266 // We need to snap the rectangle to be pixel aligned in the destination
267 // coordinate space. Otherwise natively themed widgets might not draw.
268 snapRectAtScale(r, scale);
272 let height = r.height;
275 ctx.scale(scale, scale);
277 ctx.drawWindow(win, x, y, width, height, "white", flags);
280 this.dirtyRegion.setToRect(0,0,0,0);
282 // If we're updating the canvas, then we're in the middle of a peek so
283 // don't discard the cache of previews.
284 AeroPeek.resetCacheTimer();
287 onTabPaint: function (rect) {
288 let x = Math.floor(rect.left),
289 y = Math.floor(rect.top),
290 width = Math.ceil(rect.right) - x,
291 height = Math.ceil(rect.bottom) - y;
292 this.dirtyRegion.unionRect(x, y, width, height);
295 updateTitleAndTooltip: function () {
296 let title = this.win.tabbrowser.getWindowTitleForBrowser(this.linkedBrowser);
297 this.preview.title = title;
298 this.preview.tooltip = title;
301 //////////////////////////////////////////////////////////////////////////////
302 //// nsITaskbarPreviewController
305 return this.win.width;
309 return this.win.height;
312 get thumbnailAspectRatio() {
313 let boxObject = this.tab.linkedBrowser.boxObject;
315 let tabWidth = boxObject.width || 1;
317 let tabHeight = boxObject.height || 1;
318 return tabWidth / tabHeight;
321 drawPreview: function (ctx) {
323 this.win.tabbrowser.previewTab(this.tab, function () self.previewTabCallback(ctx));
325 // We must avoid having the frame drawn around the window. See bug 520807
329 previewTabCallback: function (ctx) {
330 let width = this.win.width;
331 let height = this.win.height;
332 // Draw our toplevel window
333 ctx.drawWindow(this.win.win, 0, 0, width, height, "transparent");
335 // Compositor, where art thou?
336 // Draw the tab content on top of the toplevel window
337 this.updateCanvasPreview();
339 let boxObject = this.linkedBrowser.boxObject;
340 ctx.translate(boxObject.x, boxObject.y);
341 ctx.drawImage(this.canvasPreview, 0, 0);
344 drawThumbnail: function (ctx, width, height) {
345 this.updateCanvasPreview();
347 let scale = width/this.linkedBrowser.boxObject.width;
348 ctx.scale(scale, scale);
349 ctx.drawImage(this.canvasPreview, 0, 0);
351 // Don't draw a frame around the thumbnail
355 onClose: function () {
356 this.win.tabbrowser.removeTab(this.tab);
359 onActivate: function () {
360 this.win.tabbrowser.selectedTab = this.tab;
362 // Accept activation - this will restore the browser window
367 //// nsIDOMEventListener
368 handleEvent: function (evt) {
370 case "MozAfterPaint":
371 if (evt.originalTarget === this.linkedBrowser.contentWindow) {
372 let clientRects = evt.clientRects;
373 let length = clientRects.length;
374 for (let i = 0; i < length; i++) {
375 let r = clientRects.item(i);
379 let preview = this.preview;
381 preview.invalidate();
383 case "TabAttrModified":
384 this.updateTitleAndTooltip();
390 XPCOMUtils.defineLazyGetter(PreviewController.prototype, "canvasPreviewFlags",
391 function () { let canvasInterface = Ci.nsIDOMCanvasRenderingContext2D;
392 return canvasInterface.DRAWWINDOW_DRAW_VIEW
393 | canvasInterface.DRAWWINDOW_DRAW_CARET
394 | canvasInterface.DRAWWINDOW_ASYNC_DECODE_IMAGES
395 | canvasInterface.DRAWWINDOW_DO_NOT_FLUSH;
398 ////////////////////////////////////////////////////////////////////////////////
402 * This class monitors a browser window for changes to its tabs
405 * The nsIDOMWindow browser window
407 function TabWindow(win) {
409 this.tabbrowser = win.gBrowser;
413 for (let i = 0; i < this.tabEvents.length; i++)
414 this.tabbrowser.tabContainer.addEventListener(this.tabEvents[i], this, false);
415 this.tabbrowser.addTabsProgressListener(this);
417 for (let i = 0; i < this.winEvents.length; i++)
418 this.win.addEventListener(this.winEvents[i], this, false);
420 AeroPeek.windows.push(this);
421 let tabs = this.tabbrowser.tabs;
422 for (let i = 0; i < tabs.length; i++)
423 this.newTab(tabs[i]);
425 this.updateTabOrdering();
426 AeroPeek.checkPreviewCount();
429 TabWindow.prototype = {
431 tabEvents: ["TabOpen", "TabClose", "TabSelect", "TabMove"],
432 winEvents: ["tabviewshown", "tabviewhidden"],
434 destroy: function () {
435 this._destroying = true;
437 let tabs = this.tabbrowser.tabs;
439 this.tabbrowser.removeTabsProgressListener(this);
440 for (let i = 0; i < this.tabEvents.length; i++)
441 this.tabbrowser.tabContainer.removeEventListener(this.tabEvents[i], this, false);
443 for (let i = 0; i < this.winEvents.length; i++)
444 this.win.removeEventListener(this.winEvents[i], this, false);
446 for (let i = 0; i < tabs.length; i++)
447 this.removeTab(tabs[i]);
449 let idx = AeroPeek.windows.indexOf(this.win.gTaskbarTabGroup);
450 AeroPeek.windows.splice(idx, 1);
451 AeroPeek.checkPreviewCount();
455 return this.win.innerWidth;
458 return this.win.innerHeight;
461 // Invoked when the given tab is added to this window
462 newTab: function (tab) {
463 let controller = new PreviewController(this, tab);
464 let docShell = this.win
465 .QueryInterface(Ci.nsIInterfaceRequestor)
466 .getInterface(Ci.nsIWebNavigation)
467 .QueryInterface(Ci.nsIDocShell);
470 preview = AeroPeek.taskbar.createTaskbarTabPreview(docShell, controller);
472 controller.destroy();
475 preview.visible = AeroPeek.enabled;
476 preview.active = this.tabbrowser.selectedTab == tab;
477 // Grab the default favicon
478 getFaviconAsImage(null, function (img) {
479 // It is possible that we've already gotten the real favicon, so make sure
480 // we have not set one before setting this default one.
485 // It's OK to add the preview now while the favicon still loads.
486 this.previews.splice(tab._tPos, 0, preview);
487 AeroPeek.addPreview(preview);
488 // updateTitleAndTooltip relies on having controller.preview which is lazily resolved.
489 // Now that we've updated this.previews, it will resolve successfully.
490 controller.updateTitleAndTooltip();
493 // Invoked when the given tab is closed
494 removeTab: function (tab) {
495 let preview = this.previewFromTab(tab);
496 preview.active = false;
497 preview.visible = false;
499 preview.controller.wrappedJSObject.destroy();
501 // We don't want to splice from the array if the tabs aren't being removed
502 // from the tab bar as well (as is the case when the window closes).
503 if (!this._destroying)
504 this.previews.splice(tab._tPos, 1);
505 AeroPeek.removePreview(preview);
509 return this._enabled;
512 set enabled (enable) {
513 this._enabled = enable;
514 // Because making a tab visible requires that the tab it is next to be
515 // visible, it is far simpler to unset the 'next' tab and recreate them all
517 this.previews.forEach(function (preview) {
519 preview.visible = enable;
521 this.updateTabOrdering();
524 previewFromTab: function (tab) {
525 return this.previews[tab._tPos];
528 updateTabOrdering: function () {
529 // Since the internal taskbar array has not yet been updated we must force
530 // on it the sorting order of our local array. To do so we must walk
531 // the local array backwards, otherwise we would send move requests in the
532 // wrong order. See bug 522610 for details.
533 for (let i = this.previews.length - 1; i >= 0; i--) {
534 let p = this.previews[i];
535 let next = i == this.previews.length - 1 ? null : this.previews[i+1];
540 //// nsIDOMEventListener
541 handleEvent: function (evt) {
542 let tab = evt.originalTarget;
546 this.updateTabOrdering();
550 this.updateTabOrdering();
553 this.previewFromTab(tab).active = true;
556 let oldPos = evt.detail;
557 let newPos = tab._tPos;
558 let preview = this.previews[oldPos];
559 this.previews.splice(oldPos, 1);
560 this.previews.splice(newPos, 0, preview);
561 this.updateTabOrdering();
564 this.enabled = false;
566 case "tabviewhidden":
572 //// Browser progress listener
573 onLinkIconAvailable: function (aBrowser, aIconURL) {
575 getFaviconAsImage(aIconURL, function (img) {
576 let index = self.tabbrowser.browsers.indexOf(aBrowser);
577 // Only add it if we've found the index. The tab could have closed!
579 self.previews[index].icon = img;
584 ////////////////////////////////////////////////////////////////////////////////
588 * This object acts as global storage and external interface for this feature.
589 * It maintains the values of the prefs.
593 // Does the pref say we're enabled?
598 // nsITaskbarTabPreview array
604 // nsIWinTaskbar service
607 // Maximum number of previews
610 // Length of time in seconds that previews are cached
613 initialize: function () {
614 if (!(WINTASKBAR_CONTRACTID in Cc))
616 this.taskbar = Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar);
617 this.available = this.taskbar.available;
621 this.prefs.addObserver(TOGGLE_PREF_NAME, this, false);
622 this.prefs.addObserver(DISABLE_THRESHOLD_PREF_NAME, this, false);
623 this.prefs.addObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this, false);
625 this.cacheLifespan = this.prefs.getIntPref(CACHE_EXPIRATION_TIME_PREF_NAME);
627 this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
629 this.enabled = this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME);
632 destroy: function destroy() {
633 this._enabled = false;
635 this.prefs.removeObserver(TOGGLE_PREF_NAME, this);
636 this.prefs.removeObserver(DISABLE_THRESHOLD_PREF_NAME, this);
637 this.prefs.removeObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this);
640 this.cacheTimer.cancel();
644 return this._enabled;
647 set enabled(enable) {
648 if (this._enabled == enable)
651 this._enabled = enable;
653 this.windows.forEach(function (win) {
654 win.enabled = enable;
658 addPreview: function (preview) {
659 this.previews.push(preview);
660 this.checkPreviewCount();
663 removePreview: function (preview) {
664 let idx = this.previews.indexOf(preview);
665 this.previews.splice(idx, 1);
666 this.checkPreviewCount();
669 checkPreviewCount: function () {
670 if (this.previews.length > this.maxpreviews)
671 this.enabled = false;
673 this.enabled = this._prefenabled;
676 onOpenWindow: function (win) {
677 // This occurs when the taskbar service is not available (xp, vista)
681 win.gTaskbarTabGroup = new TabWindow(win);
684 onCloseWindow: function (win) {
685 // This occurs when the taskbar service is not available (xp, vista)
689 win.gTaskbarTabGroup.destroy();
690 delete win.gTaskbarTabGroup;
692 if (this.windows.length == 0)
696 resetCacheTimer: function () {
697 this.cacheTimer.cancel();
698 this.cacheTimer.init(this, 1000*this.cacheLifespan, Ci.nsITimer.TYPE_ONE_SHOT);
702 observe: function (aSubject, aTopic, aData) {
704 case "nsPref:changed":
705 if (aData == CACHE_EXPIRATION_TIME_PREF_NAME)
708 if (aData == TOGGLE_PREF_NAME)
709 this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME);
710 else if (aData == DISABLE_THRESHOLD_PREF_NAME)
711 this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
712 // Might need to enable/disable ourselves
713 this.checkPreviewCount();
715 case "timer-callback":
716 this.previews.forEach(function (preview) {
717 let controller = preview.controller.wrappedJSObject;
718 controller.resetCanvasPreview();
725 XPCOMUtils.defineLazyGetter(AeroPeek, "cacheTimer", function ()
726 Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer)
729 XPCOMUtils.defineLazyServiceGetter(AeroPeek, "prefs",
730 "@mozilla.org/preferences-service;1",
733 AeroPeek.initialize();