browser/modules/WindowsPreviewPerTab.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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/. */
     5 /*
     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).
    13  *
    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."
    19  *
    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.
    30  *
    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.
    43  */
    44 this.EXPORTED_SYMBOLS = ["AeroPeek"];
    46 const Cc = Components.classes;
    47 const Ci = Components.interfaces;
    48 const Cu = Components.utils;
    50 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    51 Cu.import("resource://gre/modules/NetUtil.jsm");
    52 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
    54 // Pref to enable/disable preview-per-tab
    55 const TOGGLE_PREF_NAME = "browser.taskbar.previews.enable";
    56 // Pref to determine the magic auto-disable threshold
    57 const DISABLE_THRESHOLD_PREF_NAME = "browser.taskbar.previews.max";
    58 // Pref to control the time in seconds that tab contents live in the cache
    59 const CACHE_EXPIRATION_TIME_PREF_NAME = "browser.taskbar.previews.cachetime";
    61 const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
    63 ////////////////////////////////////////////////////////////////////////////////
    64 //// Various utility properties
    65 XPCOMUtils.defineLazyServiceGetter(this, "ioSvc",
    66                                    "@mozilla.org/network/io-service;1",
    67                                    "nsIIOService");
    68 XPCOMUtils.defineLazyServiceGetter(this, "imgTools",
    69                                    "@mozilla.org/image/tools;1",
    70                                    "imgITools");
    71 XPCOMUtils.defineLazyServiceGetter(this, "faviconSvc",
    72                                    "@mozilla.org/browser/favicon-service;1",
    73                                    "nsIFaviconService");
    75 // nsIURI -> imgIContainer
    76 function _imageFromURI(uri, privateMode, callback) {
    77   let channel = ioSvc.newChannelFromURI(uri);
    78   try {
    79     channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
    80     channel.setPrivate(privateMode);
    81   } catch (e) {
    82     // Ignore channels which do not support nsIPrivateBrowsingChannel
    83   }
    84   NetUtil.asyncFetch(channel, function(inputStream, resultCode) {
    85     if (!Components.isSuccessCode(resultCode))
    86       return;
    87     try {
    88       let out_img = { value: null };
    89       imgTools.decodeImageData(inputStream, channel.contentType, out_img);
    90       callback(out_img.value);
    91     } catch (e) {
    92       // We failed, so use the default favicon (only if this wasn't the default
    93       // favicon).
    94       let defaultURI = faviconSvc.defaultFavicon;
    95       if (!defaultURI.equals(uri))
    96         _imageFromURI(defaultURI, callback);
    97     }
    98   });
    99 }
   101 // string? -> imgIContainer
   102 function getFaviconAsImage(iconurl, privateMode, callback) {
   103   if (iconurl)
   104     _imageFromURI(NetUtil.newURI(iconurl), privateMode, callback);
   105   else
   106     _imageFromURI(faviconSvc.defaultFavicon, privateMode, callback);
   107 }
   109 // Snaps the given rectangle to be pixel-aligned at the given scale
   110 function snapRectAtScale(r, scale) {
   111   let x = Math.floor(r.x * scale);
   112   let y = Math.floor(r.y * scale);
   113   let width = Math.ceil((r.x + r.width) * scale) - x;
   114   let height = Math.ceil((r.y + r.height) * scale) - y;
   116   r.x = x / scale;
   117   r.y = y / scale;
   118   r.width = width / scale;
   119   r.height = height / scale;
   120 }
   122 ////////////////////////////////////////////////////////////////////////////////
   123 //// PreviewController
   125 /*
   126  * This class manages the behavior of the preview.
   127  *
   128  * To give greater performance when drawing, the dirty areas of the content
   129  * window are tracked and drawn on demand into a canvas of the same size.
   130  * This provides a great increase in responsiveness when drawing a preview
   131  * for unchanged (or even only slightly changed) tabs.
   132  *
   133  * @param win
   134  *        The TabWindow (see below) that owns the preview that this controls
   135  * @param tab
   136  *        The <tab> that this preview is associated with
   137  */
   138 function PreviewController(win, tab) {
   139   this.win = win;
   140   this.tab = tab;
   141   this.linkedBrowser = tab.linkedBrowser;
   142   this.preview = this.win.createTabPreview(this);
   144   this.linkedBrowser.addEventListener("MozAfterPaint", this, false);
   145   this.tab.addEventListener("TabAttrModified", this, false);
   147   XPCOMUtils.defineLazyGetter(this, "canvasPreview", function () {
   148     let canvas = this.win.win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
   149     canvas.mozOpaque = true;
   150     return canvas;
   151   });
   153   XPCOMUtils.defineLazyGetter(this, "dirtyRegion",
   154     function () {
   155       let dirtyRegion = Cc["@mozilla.org/gfx/region;1"]
   156                        .createInstance(Ci.nsIScriptableRegion);
   157       dirtyRegion.init();
   158       return dirtyRegion;
   159     });
   161   XPCOMUtils.defineLazyGetter(this, "winutils",
   162     function () {
   163       let win = tab.linkedBrowser.contentWindow;
   164       return win.QueryInterface(Ci.nsIInterfaceRequestor)
   165                 .getInterface(Ci.nsIDOMWindowUtils);
   166   });
   167 }
   169 PreviewController.prototype = {
   170   QueryInterface: XPCOMUtils.generateQI([Ci.nsITaskbarPreviewController,
   171                                          Ci.nsIDOMEventListener]),
   172   destroy: function () {
   173     this.tab.removeEventListener("TabAttrModified", this, false);
   174     this.linkedBrowser.removeEventListener("MozAfterPaint", this, false);
   176     // Break cycles, otherwise we end up leaking the window with everything
   177     // attached to it.
   178     delete this.win;
   179     delete this.preview;
   180     delete this.dirtyRegion;
   181   },
   182   get wrappedJSObject() {
   183     return this;
   184   },
   186   get dirtyRects() {
   187     let rectstream = this.dirtyRegion.getRects();
   188     if (!rectstream)
   189       return [];
   190     let rects = [];
   191     for (let i = 0; i < rectstream.length; i+= 4) {
   192       let r = {x:      rectstream[i],
   193                y:      rectstream[i+1],
   194                width:  rectstream[i+2],
   195                height: rectstream[i+3]};
   196       rects.push(r);
   197     }
   198     return rects;
   199   },
   201   // Resizes the canvasPreview to 0x0, essentially freeing its memory.
   202   // updateCanvasPreview() will detect the size mismatch as a resize event
   203   // the next time it is called.
   204   resetCanvasPreview: function () {
   205     this.canvasPreview.width = 0;
   206     this.canvasPreview.height = 0;
   207   },
   209   get zoom() {
   210     // Note that winutils.fullZoom accounts for "quantization" of the zoom factor
   211     // from nsIMarkupDocumentViewer due to conversion through appUnits.
   212     // We do -not- want screenPixelsPerCSSPixel here, because that would -also-
   213     // incorporate any scaling that is applied due to hi-dpi resolution options.
   214     return this.winutils.fullZoom;
   215   },
   217   // Updates the controller's canvas with the parts of the <browser> that need
   218   // to be redrawn.
   219   updateCanvasPreview: function () {
   220     let win = this.linkedBrowser.contentWindow;
   221     let bx = this.linkedBrowser.boxObject;
   222     // Check for resize
   223     if (bx.width != this.canvasPreview.width ||
   224         bx.height != this.canvasPreview.height) {
   225       // Invalidate the entire area and repaint
   226       this.onTabPaint({left:0, top:0, right:win.innerWidth, bottom:win.innerHeight});
   227       this.canvasPreview.width = bx.width;
   228       this.canvasPreview.height = bx.height;
   229     }
   231     // Draw dirty regions
   232     let ctx = this.canvasPreview.getContext("2d");
   233     let scale = this.zoom;
   235     let flags = this.canvasPreviewFlags;
   236     // The dirty region may include parts that are offscreen so we clip to the
   237     // canvas area.
   238     this.dirtyRegion.intersectRect(0, 0, win.innerWidth, win.innerHeight);
   239     this.dirtyRects.forEach(function (r) {
   240       // We need to snap the rectangle to be pixel aligned in the destination
   241       // coordinate space. Otherwise natively themed widgets might not draw.
   242       snapRectAtScale(r, scale);
   243       let x = r.x;
   244       let y = r.y;
   245       let width = r.width;
   246       let height = r.height;
   248       ctx.save();
   249       ctx.scale(scale, scale);
   250       ctx.translate(x, y);
   251       ctx.drawWindow(win, x, y, width, height, "white", flags);
   252       ctx.restore();
   253     });
   254     this.dirtyRegion.setToRect(0,0,0,0);
   256     // If we're updating the canvas, then we're in the middle of a peek so
   257     // don't discard the cache of previews.
   258     AeroPeek.resetCacheTimer();
   259   },
   261   onTabPaint: function (rect) {
   262     let x = Math.floor(rect.left),
   263         y = Math.floor(rect.top),
   264         width = Math.ceil(rect.right) - x,
   265         height = Math.ceil(rect.bottom) - y;
   266     this.dirtyRegion.unionRect(x, y, width, height);
   267   },
   269   updateTitleAndTooltip: function () {
   270     let title = this.win.tabbrowser.getWindowTitleForBrowser(this.linkedBrowser);
   271     this.preview.title = title;
   272     this.preview.tooltip = title;
   273   },
   275   //////////////////////////////////////////////////////////////////////////////
   276   //// nsITaskbarPreviewController 
   278   get width() {
   279     return this.win.width;
   280   },
   282   get height() {
   283     return this.win.height;
   284   },
   286   get thumbnailAspectRatio() {
   287     let boxObject = this.tab.linkedBrowser.boxObject;
   288     // Avoid returning 0
   289     let tabWidth = boxObject.width || 1;
   290     // Avoid divide by 0
   291     let tabHeight = boxObject.height || 1;
   292     return tabWidth / tabHeight;
   293   },
   295   drawPreview: function (ctx) {
   296     let self = this;
   297     this.win.tabbrowser.previewTab(this.tab, function () self.previewTabCallback(ctx));
   299     // We must avoid having the frame drawn around the window. See bug 520807
   300     return false;
   301   },
   303   previewTabCallback: function (ctx) {
   304     // This will extract the resolution-scale component of the scaling we need,
   305     // which should be applied to both chrome and content;
   306     // the page zoom component is applied (to content only) within updateCanvasPreview.
   307     let scale = this.winutils.screenPixelsPerCSSPixel / this.winutils.fullZoom;
   308     ctx.save();
   309     ctx.scale(scale, scale);
   310     let width = this.win.width;
   311     let height = this.win.height;
   312     // Draw our toplevel window
   313     ctx.drawWindow(this.win.win, 0, 0, width, height, "transparent");
   315     // XXX (jfkthame): Pending tabs don't seem to draw with the proper scaling
   316     // unless we use this block of code; but doing this for "normal" (loaded) tabs
   317     // results in blurry rendering on hidpi systems, so we avoid it if possible.
   318     // I don't understand why pending and loaded tabs behave differently here...
   319     // (see bug 857061).
   320     if (this.tab.hasAttribute("pending")) {
   321       // Compositor, where art thou?
   322       // Draw the tab content on top of the toplevel window
   323       this.updateCanvasPreview();
   325       let boxObject = this.linkedBrowser.boxObject;
   326       ctx.translate(boxObject.x, boxObject.y);
   327       ctx.drawImage(this.canvasPreview, 0, 0);
   328     }
   330     ctx.restore();
   331   },
   333   drawThumbnail: function (ctx, width, height) {
   334     this.updateCanvasPreview();
   336     let scale = width/this.linkedBrowser.boxObject.width;
   337     ctx.scale(scale, scale);
   338     ctx.drawImage(this.canvasPreview, 0, 0);
   340     // Don't draw a frame around the thumbnail
   341     return false;
   342   },
   344   onClose: function () {
   345     this.win.tabbrowser.removeTab(this.tab);
   346   },
   348   onActivate: function () {
   349     this.win.tabbrowser.selectedTab = this.tab;
   351     // Accept activation - this will restore the browser window
   352     // if it's minimized
   353     return true;
   354   },
   356   //// nsIDOMEventListener
   357   handleEvent: function (evt) {
   358     switch (evt.type) {
   359       case "MozAfterPaint":
   360         if (evt.originalTarget === this.linkedBrowser.contentWindow) {
   361           let clientRects = evt.clientRects;
   362           let length = clientRects.length;
   363           for (let i = 0; i < length; i++) {
   364             let r = clientRects.item(i);
   365             this.onTabPaint(r);
   366           }
   367         }
   368         let preview = this.preview;
   369         if (preview.visible)
   370           preview.invalidate();
   371         break;
   372       case "TabAttrModified":
   373         this.updateTitleAndTooltip();
   374         break;
   375     }
   376   }
   377 };
   379 XPCOMUtils.defineLazyGetter(PreviewController.prototype, "canvasPreviewFlags",
   380   function () { let canvasInterface = Ci.nsIDOMCanvasRenderingContext2D;
   381                 return canvasInterface.DRAWWINDOW_DRAW_VIEW
   382                      | canvasInterface.DRAWWINDOW_DRAW_CARET
   383                      | canvasInterface.DRAWWINDOW_ASYNC_DECODE_IMAGES
   384                      | canvasInterface.DRAWWINDOW_DO_NOT_FLUSH;
   385 });
   387 ////////////////////////////////////////////////////////////////////////////////
   388 //// TabWindow
   390 /*
   391  * This class monitors a browser window for changes to its tabs
   392  *
   393  * @param win
   394  *        The nsIDOMWindow browser window 
   395  */
   396 function TabWindow(win) {
   397   this.win = win;
   398   this.tabbrowser = win.gBrowser;
   400   this.previews = new Map();
   402   for (let i = 0; i < this.tabEvents.length; i++)
   403     this.tabbrowser.tabContainer.addEventListener(this.tabEvents[i], this, false);
   404   this.tabbrowser.addTabsProgressListener(this);
   406   for (let i = 0; i < this.winEvents.length; i++)
   407     this.win.addEventListener(this.winEvents[i], this, false);
   409   AeroPeek.windows.push(this);
   410   let tabs = this.tabbrowser.tabs;
   411   for (let i = 0; i < tabs.length; i++)
   412     this.newTab(tabs[i]);
   414   this.updateTabOrdering();
   415   AeroPeek.checkPreviewCount();
   416 }
   418 TabWindow.prototype = {
   419   _enabled: false,
   420   tabEvents: ["TabOpen", "TabClose", "TabSelect", "TabMove"],
   421   winEvents: ["tabviewshown", "tabviewhidden"],
   423   destroy: function () {
   424     this._destroying = true;
   426     let tabs = this.tabbrowser.tabs;
   428     this.tabbrowser.removeTabsProgressListener(this);
   429     for (let i = 0; i < this.tabEvents.length; i++)
   430       this.tabbrowser.tabContainer.removeEventListener(this.tabEvents[i], this, false);
   432     for (let i = 0; i < this.winEvents.length; i++)
   433       this.win.removeEventListener(this.winEvents[i], this, false);
   435     for (let i = 0; i < tabs.length; i++)
   436       this.removeTab(tabs[i]);
   438     let idx = AeroPeek.windows.indexOf(this.win.gTaskbarTabGroup);
   439     AeroPeek.windows.splice(idx, 1);
   440     AeroPeek.checkPreviewCount();
   441   },
   443   get width () {
   444     return this.win.innerWidth;
   445   },
   446   get height () {
   447     return this.win.innerHeight;
   448   },
   450   // Invoked when the given tab is added to this window
   451   newTab: function (tab) {
   452     let controller = new PreviewController(this, tab);
   453     // It's OK to add the preview now while the favicon still loads.
   454     this.previews.set(tab, controller.preview);
   455     AeroPeek.addPreview(controller.preview);
   456     // updateTitleAndTooltip relies on having controller.preview which is lazily resolved.
   457     // Now that we've updated this.previews, it will resolve successfully.
   458     controller.updateTitleAndTooltip();
   459   },
   461   createTabPreview: function (controller) {
   462     let docShell = this.win
   463                   .QueryInterface(Ci.nsIInterfaceRequestor)
   464                   .getInterface(Ci.nsIWebNavigation)
   465                   .QueryInterface(Ci.nsIDocShell);
   466     let preview = AeroPeek.taskbar.createTaskbarTabPreview(docShell, controller);
   467     preview.visible = AeroPeek.enabled;
   468     preview.active = this.tabbrowser.selectedTab == controller.tab;
   469     // Grab the default favicon
   470     getFaviconAsImage(null, PrivateBrowsingUtils.isWindowPrivate(this.win), function (img) {
   471       // It is possible that we've already gotten the real favicon, so make sure
   472       // we have not set one before setting this default one.
   473       if (!preview.icon)
   474         preview.icon = img;
   475     });
   477     return preview;
   478   },
   480   // Invoked when the given tab is closed
   481   removeTab: function (tab) {
   482     let preview = this.previewFromTab(tab);
   483     preview.active = false;
   484     preview.visible = false;
   485     preview.move(null);
   486     preview.controller.wrappedJSObject.destroy();
   488     this.previews.delete(tab);
   489     AeroPeek.removePreview(preview);
   490   },
   492   get enabled () {
   493     return this._enabled;
   494   },
   496   set enabled (enable) {
   497     this._enabled = enable;
   498     // Because making a tab visible requires that the tab it is next to be
   499     // visible, it is far simpler to unset the 'next' tab and recreate them all
   500     // at once.
   501     for (let [tab, preview] of this.previews) {
   502       preview.move(null);
   503       preview.visible = enable;
   504     }
   505     this.updateTabOrdering();
   506   },
   508   previewFromTab: function (tab) {
   509     return this.previews.get(tab);
   510   },
   512   updateTabOrdering: function () {
   513     let previews = this.previews;
   514     let tabs = this.tabbrowser.tabs;
   516     // Previews are internally stored using a map, so we need to iterate the
   517     // tabbrowser's array of tabs to retrieve previews in the same order.
   518     let inorder = [previews.get(t) for (t of tabs) if (previews.has(t))];
   520     // Since the internal taskbar array has not yet been updated we must force
   521     // on it the sorting order of our local array.  To do so we must walk
   522     // the local array backwards, otherwise we would send move requests in the
   523     // wrong order.  See bug 522610 for details.
   524     for (let i = inorder.length - 1; i >= 0; i--) {
   525       inorder[i].move(inorder[i + 1] || null);
   526     }
   527   },
   529   //// nsIDOMEventListener
   530   handleEvent: function (evt) {
   531     let tab = evt.originalTarget;
   532     switch (evt.type) {
   533       case "TabOpen":
   534         this.newTab(tab);
   535         this.updateTabOrdering();
   536         break;
   537       case "TabClose":
   538         this.removeTab(tab);
   539         this.updateTabOrdering();
   540         break;
   541       case "TabSelect":
   542         this.previewFromTab(tab).active = true;
   543         break;
   544       case "TabMove":
   545         this.updateTabOrdering();
   546         break;
   547       case "tabviewshown":
   548         this.enabled = false;
   549         break;
   550       case "tabviewhidden":
   551         if (!AeroPeek._prefenabled)
   552           return;
   553         this.enabled = true;
   554         break;
   555     }
   556   },
   558   //// Browser progress listener
   559   onLinkIconAvailable: function (aBrowser, aIconURL) {
   560     let self = this;
   561     getFaviconAsImage(aIconURL, PrivateBrowsingUtils.isWindowPrivate(this.win), function (img) {
   562       let index = self.tabbrowser.browsers.indexOf(aBrowser);
   563       // Only add it if we've found the index.  The tab could have closed!
   564       if (index != -1) {
   565         let tab = self.tabbrowser.tabs[index];
   566         self.previews.get(tab).icon = img;
   567       }
   568     });
   569   }
   570 }
   572 ////////////////////////////////////////////////////////////////////////////////
   573 //// AeroPeek
   575 /*
   576  * This object acts as global storage and external interface for this feature.
   577  * It maintains the values of the prefs.
   578  */
   579 this.AeroPeek = {
   580   available: false,
   581   // Does the pref say we're enabled?
   582   _prefenabled: true,
   584   _enabled: true,
   586   // nsITaskbarTabPreview array
   587   previews: [],
   589   // TabWindow array
   590   windows: [],
   592   // nsIWinTaskbar service
   593   taskbar: null,
   595   // Maximum number of previews
   596   maxpreviews: 20,
   598   // Length of time in seconds that previews are cached
   599   cacheLifespan: 20,
   601   initialize: function () {
   602     if (!(WINTASKBAR_CONTRACTID in Cc))
   603       return;
   604     this.taskbar = Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar);
   605     this.available = this.taskbar.available;
   606     if (!this.available)
   607       return;
   609     this.prefs.addObserver(TOGGLE_PREF_NAME, this, false);
   610     this.prefs.addObserver(DISABLE_THRESHOLD_PREF_NAME, this, false);
   611     this.prefs.addObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this, false);
   613     this.cacheLifespan = this.prefs.getIntPref(CACHE_EXPIRATION_TIME_PREF_NAME);
   615     this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
   617     this.enabled = this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME);
   618   },
   620   destroy: function destroy() {
   621     this._enabled = false;
   623     this.prefs.removeObserver(TOGGLE_PREF_NAME, this);
   624     this.prefs.removeObserver(DISABLE_THRESHOLD_PREF_NAME, this);
   625     this.prefs.removeObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this);
   627     if (this.cacheTimer)
   628       this.cacheTimer.cancel();
   629   },
   631   get enabled() {
   632     return this._enabled;
   633   },
   635   set enabled(enable) {
   636     if (this._enabled == enable)
   637       return;
   639     this._enabled = enable;
   641     this.windows.forEach(function (win) {
   642       win.enabled = enable;
   643     });
   644   },
   646   addPreview: function (preview) {
   647     this.previews.push(preview);
   648     this.checkPreviewCount();
   649   },
   651   removePreview: function (preview) {
   652     let idx = this.previews.indexOf(preview);
   653     this.previews.splice(idx, 1);
   654     this.checkPreviewCount();
   655   },
   657   checkPreviewCount: function () {
   658     if (this.previews.length > this.maxpreviews)
   659       this.enabled = false;
   660     else
   661       this.enabled = this._prefenabled;
   662   },
   664   onOpenWindow: function (win) {
   665     // This occurs when the taskbar service is not available (xp, vista)
   666     if (!this.available)
   667       return;
   669     win.gTaskbarTabGroup = new TabWindow(win);
   670   },
   672   onCloseWindow: function (win) {
   673     // This occurs when the taskbar service is not available (xp, vista)
   674     if (!this.available)
   675       return;
   677     win.gTaskbarTabGroup.destroy();
   678     delete win.gTaskbarTabGroup;
   680     if (this.windows.length == 0)
   681       this.destroy();
   682   },
   684   resetCacheTimer: function () {
   685     this.cacheTimer.cancel();
   686     this.cacheTimer.init(this, 1000*this.cacheLifespan, Ci.nsITimer.TYPE_ONE_SHOT);
   687   },
   689   //// nsIObserver
   690   observe: function (aSubject, aTopic, aData) {
   691     switch (aTopic) {
   692       case "nsPref:changed":
   693         if (aData == CACHE_EXPIRATION_TIME_PREF_NAME)
   694           break;
   696         if (aData == TOGGLE_PREF_NAME)
   697           this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME);
   698         else if (aData == DISABLE_THRESHOLD_PREF_NAME)
   699           this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
   700         // Might need to enable/disable ourselves
   701         this.checkPreviewCount();
   702         break;
   703       case "timer-callback":
   704         this.previews.forEach(function (preview) {
   705           let controller = preview.controller.wrappedJSObject;
   706           controller.resetCanvasPreview();
   707         });
   708         break;
   709     }
   710   }
   711 };
   713 XPCOMUtils.defineLazyGetter(AeroPeek, "cacheTimer", function ()
   714   Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer)
   715 );
   717 XPCOMUtils.defineLazyServiceGetter(AeroPeek, "prefs",
   718                                    "@mozilla.org/preferences-service;1",
   719                                    "nsIPrefBranch");
   721 AeroPeek.initialize();

mercurial