Bug 581726 - Disable tab previews for windows that have panorama displayed. r=dao...
[mozilla-central.git] / browser / components / wintaskbar / WindowsPreviewPerTab.jsm
blob0083089fc33311bd4b2e85759e9e68a6e065b881
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
4  *
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/
9  *
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
13  * License.
14  *
15  * The Original Code is Mozilla code.
16  *
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.
21  *
22  * Contributor(s):
23  *    Rob Arnold <robarnold@cmu.edu> (original author)
24  *
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.
36  *
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).
46  *
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."
52  *
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.
63  *
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.
76  */
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",
99                                    "nsIIOService");
100 XPCOMUtils.defineLazyServiceGetter(this, "imgTools",
101                                    "@mozilla.org/image/tools;1",
102                                    "imgITools");
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))
112       return;
113     try {
114       let out_img = { value: null };
115       imgTools.decodeImageData(inputStream, channel.contentType, out_img);
116       callback(out_img.value);
117     } catch (e) {
118       // We failed, so use the default favicon (only if this wasn't the default
119       // favicon).
120       let defaultURI = faviconSvc.defaultFavicon;
121       if (!defaultURI.equals(uri))
122         _imageFromURI(defaultURI, callback);
123     }
124   });
127 // string? -> imgIContainer
128 function getFaviconAsImage(iconurl, callback) {
129   if (iconurl)
130     _imageFromURI(NetUtil.newURI(iconurl), callback);
131   else
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;
142   r.x = x / scale;
143   r.y = y / scale;
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.
159  * @param win
160  *        The TabWindow (see below) that owns the preview that this controls
161  * @param tab
162  *        The <tab> that this preview is associated with
163  */
164 function PreviewController(win, tab) {
165   this.win = win;
166   this.tab = 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;
178     return canvas;
179   });
181   XPCOMUtils.defineLazyGetter(this, "dirtyRegion",
182     function () {
183       let dirtyRegion = Cc["@mozilla.org/gfx/region;1"]
184                        .createInstance(Ci.nsIScriptableRegion);
185       dirtyRegion.init();
186       return dirtyRegion;
187     });
189   XPCOMUtils.defineLazyGetter(this, "winutils",
190     function () {
191       let win = tab.linkedBrowser.contentWindow;
192       return win.QueryInterface(Ci.nsIInterfaceRequestor)
193                 .getInterface(Ci.nsIDOMWindowUtils);
194   });
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
205     // attached to it.
206     delete this.win;
207     delete this.preview;
208     delete this.dirtyRegion;
209   },
210   get wrappedJSObject() {
211     return this;
212   },
214   get dirtyRects() {
215     let rectstream = this.dirtyRegion.getRects();
216     if (!rectstream)
217       return [];
218     let rects = [];
219     for (let i = 0; i < rectstream.length; i+= 4) {
220       let r = {x:      rectstream[i],
221                y:      rectstream[i+1],
222                width:  rectstream[i+2],
223                height: rectstream[i+3]};
224       rects.push(r);
225     }
226     return rects;
227   },
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;
235   },
237   get zoom() {
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;
241   },
243   // Updates the controller's canvas with the parts of the <browser> that need
244   // to be redrawn.
245   updateCanvasPreview: function () {
246     let win = this.linkedBrowser.contentWindow;
247     let bx = this.linkedBrowser.boxObject;
248     // Check for resize
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;
255     }
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
263     // canvas area.
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);
269       let x = r.x;
270       let y = r.y;
271       let width = r.width;
272       let height = r.height;
274       ctx.save();
275       ctx.scale(scale, scale);
276       ctx.translate(x, y);
277       ctx.drawWindow(win, x, y, width, height, "white", flags);
278       ctx.restore();
279     });
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();
285   },
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);
293   },
295   updateTitleAndTooltip: function () {
296     let title = this.win.tabbrowser.getWindowTitleForBrowser(this.linkedBrowser);
297     this.preview.title = title;
298     this.preview.tooltip = title;
299   },
301   //////////////////////////////////////////////////////////////////////////////
302   //// nsITaskbarPreviewController 
304   get width() {
305     return this.win.width;
306   },
308   get height() {
309     return this.win.height;
310   },
312   get thumbnailAspectRatio() {
313     let boxObject = this.tab.linkedBrowser.boxObject;
314     // Avoid returning 0
315     let tabWidth = boxObject.width || 1;
316     // Avoid divide by 0
317     let tabHeight = boxObject.height || 1;
318     return tabWidth / tabHeight;
319   },
321   drawPreview: function (ctx) {
322     let self = this;
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
326     return false;
327   },
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);
342   },
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
352     return false;
353   },
355   onClose: function () {
356     this.win.tabbrowser.removeTab(this.tab);
357   },
359   onActivate: function () {
360     this.win.tabbrowser.selectedTab = this.tab;
362     // Accept activation - this will restore the browser window
363     // if it's minimized
364     return true;
365   },
367   //// nsIDOMEventListener
368   handleEvent: function (evt) {
369     switch (evt.type) {
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);
376             this.onTabPaint(r);
377           }
378         }
379         let preview = this.preview;
380         if (preview.visible)
381           preview.invalidate();
382         break;
383       case "TabAttrModified":
384         this.updateTitleAndTooltip();
385         break;
386     }
387   }
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 ////////////////////////////////////////////////////////////////////////////////
399 //// TabWindow
402  * This class monitors a browser window for changes to its tabs
404  * @param win
405  *        The nsIDOMWindow browser window 
406  */
407 function TabWindow(win) {
408   this.win = win;
409   this.tabbrowser = win.gBrowser;
411   this.previews = [];
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 = {
430   _enabled: false,
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();
452   },
454   get width () {
455     return this.win.innerWidth;
456   },
457   get height () {
458     return this.win.innerHeight;
459   },
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);
468     let preview;
469     try {
470       preview = AeroPeek.taskbar.createTaskbarTabPreview(docShell, controller);
471     } catch (e) {
472       controller.destroy();
473       return;
474     }
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.
481       if (!preview.icon)
482         preview.icon = img;
483     });
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();
491   },
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;
498     preview.move(null);
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);
506   },
508   get enabled () {
509     return this._enabled;
510   },
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
516     // at once.
517     this.previews.forEach(function (preview) {
518       preview.move(null);
519       preview.visible = enable;
520     });
521     this.updateTabOrdering();
522   },
524   previewFromTab: function (tab) {
525     return this.previews[tab._tPos];
526   },
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];
536       p.move(next);
537     }
538   },
540   //// nsIDOMEventListener
541   handleEvent: function (evt) {
542     let tab = evt.originalTarget;
543     switch (evt.type) {
544       case "TabOpen":
545         this.newTab(tab);
546         this.updateTabOrdering();
547         break;
548       case "TabClose":
549         this.removeTab(tab);
550         this.updateTabOrdering();
551         break;
552       case "TabSelect":
553         this.previewFromTab(tab).active = true;
554         break;
555       case "TabMove":
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();
562         break;
563       case "tabviewshown":
564         this.enabled = false;
565         break;
566       case "tabviewhidden":
567         this.enabled = true;
568         break;
569     }
570   },
572   //// Browser progress listener
573   onLinkIconAvailable: function (aBrowser, aIconURL) {
574     let self = this;
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!
578       if (index != -1)
579         self.previews[index].icon = img;
580     });
581   }
584 ////////////////////////////////////////////////////////////////////////////////
585 //// AeroPeek
588  * This object acts as global storage and external interface for this feature.
589  * It maintains the values of the prefs.
590  */
591 var AeroPeek = {
592   available: false,
593   // Does the pref say we're enabled?
594   _prefenabled: true,
596   _enabled: true,
598   // nsITaskbarTabPreview array
599   previews: [],
601   // TabWindow array
602   windows: [],
604   // nsIWinTaskbar service
605   taskbar: null,
607   // Maximum number of previews
608   maxpreviews: 20,
610   // Length of time in seconds that previews are cached
611   cacheLifespan: 20,
613   initialize: function () {
614     if (!(WINTASKBAR_CONTRACTID in Cc))
615       return;
616     this.taskbar = Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar);
617     this.available = this.taskbar.available;
618     if (!this.available)
619       return;
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);
630   },
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);
639     if (this.cacheTimer)
640       this.cacheTimer.cancel();
641   },
643   get enabled() {
644     return this._enabled;
645   },
647   set enabled(enable) {
648     if (this._enabled == enable)
649       return;
651     this._enabled = enable;
653     this.windows.forEach(function (win) {
654       win.enabled = enable;
655     });
656   },
658   addPreview: function (preview) {
659     this.previews.push(preview);
660     this.checkPreviewCount();
661   },
663   removePreview: function (preview) {
664     let idx = this.previews.indexOf(preview);
665     this.previews.splice(idx, 1);
666     this.checkPreviewCount();
667   },
669   checkPreviewCount: function () {
670     if (this.previews.length > this.maxpreviews)
671       this.enabled = false;
672     else
673       this.enabled = this._prefenabled;
674   },
676   onOpenWindow: function (win) {
677     // This occurs when the taskbar service is not available (xp, vista)
678     if (!this.available)
679       return;
681     win.gTaskbarTabGroup = new TabWindow(win);
682   },
684   onCloseWindow: function (win) {
685     // This occurs when the taskbar service is not available (xp, vista)
686     if (!this.available)
687       return;
689     win.gTaskbarTabGroup.destroy();
690     delete win.gTaskbarTabGroup;
692     if (this.windows.length == 0)
693       this.destroy();
694   },
696   resetCacheTimer: function () {
697     this.cacheTimer.cancel();
698     this.cacheTimer.init(this, 1000*this.cacheLifespan, Ci.nsITimer.TYPE_ONE_SHOT);
699   },
701   //// nsIObserver
702   observe: function (aSubject, aTopic, aData) {
703     switch (aTopic) {
704       case "nsPref:changed":
705         if (aData == CACHE_EXPIRATION_TIME_PREF_NAME)
706           break;
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();
714         break;
715       case "timer-callback":
716         this.previews.forEach(function (preview) {
717           let controller = preview.controller.wrappedJSObject;
718           controller.resetCanvasPreview();
719         });
720         break;
721     }
722   }
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",
731                                    "nsIPrefBranch2");
733 AeroPeek.initialize();