tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

WindowsPreviewPerTab.sys.mjs (25933B)


      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 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";
     48 
     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";
     55 
     56 const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
     57 
     58 const lazy = {};
     59 
     60 // Various utility properties
     61 XPCOMUtils.defineLazyServiceGetter(
     62  lazy,
     63  "imgTools",
     64  "@mozilla.org/image/tools;1",
     65  Ci.imgITools
     66 );
     67 ChromeUtils.defineESModuleGetters(lazy, {
     68  PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
     69 });
     70 
     71 // nsIURI -> imgIContainer
     72 function _imageFromURI(uri, privateMode, callback) {
     73  let channel = NetUtil.newChannel({
     74    uri,
     75    loadUsingSystemPrincipal: true,
     76    contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE,
     77  });
     78 
     79  try {
     80    channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
     81    channel.setPrivate(privateMode);
     82  } catch (e) {
     83    // Ignore channels which do not support nsIPrivateBrowsingChannel
     84  }
     85  NetUtil.asyncFetch(channel, function (inputStream, resultCode) {
     86    if (!Components.isSuccessCode(resultCode)) {
     87      return;
     88    }
     89 
     90    const decodeCallback = {
     91      onImageReady(image) {
     92        if (!image) {
     93          // We failed, so use the default favicon (only if this wasn't the
     94          // default favicon).
     95          let defaultURI = PlacesUtils.favicons.defaultFavicon;
     96          if (!defaultURI.equals(uri)) {
     97            _imageFromURI(defaultURI, privateMode, callback);
     98            return;
     99          }
    100        }
    101 
    102        callback(image);
    103      },
    104    };
    105 
    106    try {
    107      let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
    108      lazy.imgTools.decodeImageAsync(
    109        inputStream,
    110        channel.contentType,
    111        decodeCallback,
    112        threadManager.currentThread
    113      );
    114    } catch (e) {
    115      // We failed, so use the default favicon (only if this wasn't the default
    116      // favicon).
    117      let defaultURI = PlacesUtils.favicons.defaultFavicon;
    118      if (!defaultURI.equals(uri)) {
    119        _imageFromURI(defaultURI, privateMode, callback);
    120      }
    121    }
    122  });
    123 }
    124 
    125 // string? -> imgIContainer
    126 function getFaviconAsImage(iconurl, privateMode, callback) {
    127  if (iconurl) {
    128    _imageFromURI(NetUtil.newURI(iconurl), privateMode, callback);
    129  } else {
    130    _imageFromURI(PlacesUtils.favicons.defaultFavicon, privateMode, callback);
    131  }
    132 }
    133 
    134 // PreviewController
    135 
    136 /**
    137 * This class manages the behavior of thumbnails and previews. It has the following
    138 * responsibilities:
    139 * 1) responding to requests from Windows taskbar for a thumbnail or window
    140 *    preview.
    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.
    145 *
    146 * @param win
    147 *        The TabWindow (see below) that owns the preview that this controls
    148 * @param tab
    149 *        The <tab> that this preview is associated with
    150 */
    151 function PreviewController(win, tab) {
    152  this.win = win;
    153  this.tab = tab;
    154  this.linkedBrowser = tab.linkedBrowser;
    155  this.preview = this.win.createTabPreview(this);
    156 
    157  this.tab.addEventListener("TabAttrModified", this);
    158 
    159  ChromeUtils.defineLazyGetter(this, "canvasPreview", function () {
    160    let canvas = lazy.PageThumbs.createCanvas(this.win.win);
    161    canvas.mozOpaque = true;
    162    return canvas;
    163  });
    164 }
    165 
    166 PreviewController.prototype = {
    167  QueryInterface: ChromeUtils.generateQI(["nsITaskbarPreviewController"]),
    168 
    169  destroy() {
    170    this.tab.removeEventListener("TabAttrModified", this);
    171 
    172    // Break cycles, otherwise we end up leaking the window with everything
    173    // attached to it.
    174    delete this.win;
    175    delete this.preview;
    176  },
    177 
    178  get wrappedJSObject() {
    179    return this;
    180  },
    181 
    182  // Resizes the canvasPreview to 0x0, essentially freeing its memory.
    183  resetCanvasPreview() {
    184    this.canvasPreview.width = 0;
    185    this.canvasPreview.height = 0;
    186  },
    187 
    188  /**
    189   * Set the canvas dimensions.
    190   */
    191  resizeCanvasPreview(aRequestedWidth, aRequestedHeight) {
    192    this.canvasPreview.width = aRequestedWidth;
    193    this.canvasPreview.height = aRequestedHeight;
    194  },
    195 
    196  get browserDims() {
    197    return this.tab.linkedBrowser.getBoundingClientRect();
    198  },
    199 
    200  cacheBrowserDims() {
    201    let dims = this.browserDims;
    202    this._cachedWidth = dims.width;
    203    this._cachedHeight = dims.height;
    204  },
    205 
    206  testCacheBrowserDims() {
    207    let dims = this.browserDims;
    208    return this._cachedWidth == dims.width && this._cachedHeight == dims.height;
    209  },
    210 
    211  /**
    212   * Capture a new thumbnail image for this preview. Called by the controller
    213   * in response to a request for a new thumbnail image.
    214   */
    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(
    221      this.linkedBrowser,
    222      this.canvasPreview,
    223      {
    224        fullScale: aFullScale,
    225      }
    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.
    229  },
    230 
    231  updateTitleAndTooltip() {
    232    let title = this.win.tabbrowser.getWindowTitleForBrowser(
    233      this.linkedBrowser
    234    );
    235    this.preview.title = title;
    236    this.preview.tooltip = title;
    237  },
    238 
    239  // nsITaskbarPreviewController
    240 
    241  // window width and height, not browser
    242  get width() {
    243    return this.win.width;
    244  },
    245 
    246  // window width and height, not browser
    247  get height() {
    248    return this.win.height;
    249  },
    250 
    251  get thumbnailAspectRatio() {
    252    let browserDims = this.browserDims;
    253    // Avoid returning 0
    254    let tabWidth = browserDims.width || 1;
    255    // Avoid divide by 0
    256    let tabHeight = browserDims.height || 1;
    257    return tabWidth / tabHeight;
    258  },
    259 
    260  /**
    261   * Responds to taskbar requests for window previews. Returns the results asynchronously
    262   * through updateCanvasPreview.
    263   *
    264   * @param aTaskbarCallback nsITaskbarPreviewCallback results callback
    265   */
    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;
    272 
    273      let composite = lazy.PageThumbs.createCanvas(this.win.win);
    274 
    275      // Use transparency, Aero glass is drawn black without it.
    276      composite.mozOpaque = false;
    277 
    278      let ctx = composite.getContext("2d");
    279      let scale = this.win.win.devicePixelRatio;
    280 
    281      composite.width = winWidth * scale;
    282      composite.height = winHeight * scale;
    283 
    284      ctx.save();
    285      ctx.scale(scale, scale);
    286 
    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)");
    290 
    291      // Draw the content are into the composite canvas at the right location.
    292      ctx.drawImage(
    293        aPreviewCanvas,
    294        this.browserDims.x,
    295        this.browserDims.y,
    296        aPreviewCanvas.width,
    297        aPreviewCanvas.height
    298      );
    299      ctx.restore();
    300 
    301      // Deliver the resulting composite canvas to Windows
    302      this.win.tabbrowser.previewTab(this.tab, function () {
    303        aTaskbarCallback.done(composite, false);
    304      });
    305    });
    306  },
    307 
    308  /**
    309   * Responds to taskbar requests for tab thumbnails. Returns the results asynchronously
    310   * through updateCanvasPreview.
    311   *
    312   * Note Windows requests a specific width and height here, if the resulting thumbnail
    313   * does not match these dimensions thumbnail display will fail.
    314   *
    315   * @param aTaskbarCallback nsITaskbarPreviewCallback results callback
    316   * @param aRequestedWidth width of the requested thumbnail
    317   * @param aRequestedHeight height of the requested thumbnail
    318   */
    319  requestThumbnail(aTaskbarCallback, aRequestedWidth, aRequestedHeight) {
    320    this.resizeCanvasPreview(aRequestedWidth, aRequestedHeight);
    321    this.updateCanvasPreview(false).then(aThumbnailCanvas => {
    322      aTaskbarCallback.done(aThumbnailCanvas, false);
    323    });
    324  },
    325 
    326  // Event handling
    327 
    328  onClose() {
    329    this.win.tabbrowser.removeTab(this.tab);
    330  },
    331 
    332  onActivate() {
    333    this.win.tabbrowser.selectedTab = this.tab;
    334 
    335    // Accept activation - this will restore the browser window
    336    // if it's minimized
    337    return true;
    338  },
    339 
    340  // EventListener
    341  handleEvent(evt) {
    342    switch (evt.type) {
    343      case "TabAttrModified":
    344        this.updateTitleAndTooltip();
    345        break;
    346    }
    347  },
    348 };
    349 
    350 // TabWindow
    351 
    352 /**
    353 * This class monitors a browser window for changes to its tabs
    354 *
    355 * @param win
    356 *        The nsIDOMWindow browser window
    357 */
    358 function TabWindow(win) {
    359  this.win = win;
    360  this.tabbrowser = win.gBrowser;
    361 
    362  this.previews = new Map();
    363 
    364  for (let i = 0; i < this.tabEvents.length; i++) {
    365    this.tabbrowser.tabContainer.addEventListener(this.tabEvents[i], this);
    366  }
    367 
    368  for (let i = 0; i < this.winEvents.length; i++) {
    369    this.win.addEventListener(this.winEvents[i], this);
    370  }
    371 
    372  this.tabbrowser.addTabsProgressListener(this);
    373 
    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]);
    378  }
    379 
    380  this.updateTabOrdering();
    381  AeroPeek.checkPreviewCount();
    382 }
    383 
    384 TabWindow.prototype = {
    385  _enabled: false,
    386  _cachedWidth: 0,
    387  _cachedHeight: 0,
    388  tabEvents: ["TabOpen", "TabClose", "TabSelect", "TabMove"],
    389  winEvents: ["resize"],
    390 
    391  destroy() {
    392    this._destroying = true;
    393 
    394    let tabs = this.tabbrowser.tabs;
    395 
    396    this.tabbrowser.removeTabsProgressListener(this);
    397 
    398    for (let i = 0; i < this.winEvents.length; i++) {
    399      this.win.removeEventListener(this.winEvents[i], this);
    400    }
    401 
    402    for (let i = 0; i < this.tabEvents.length; i++) {
    403      this.tabbrowser.tabContainer.removeEventListener(this.tabEvents[i], this);
    404    }
    405 
    406    for (let i = 0; i < tabs.length; i++) {
    407      this.removeTab(tabs[i]);
    408    }
    409 
    410    let idx = AeroPeek.windows.indexOf(this.win.gTaskbarTabGroup);
    411    AeroPeek.windows.splice(idx, 1);
    412    AeroPeek.checkPreviewCount();
    413  },
    414 
    415  get width() {
    416    return this.win.innerWidth;
    417  },
    418  get height() {
    419    return this.win.innerHeight;
    420  },
    421 
    422  cacheDims() {
    423    this._cachedWidth = this.width;
    424    this._cachedHeight = this.height;
    425  },
    426 
    427  testCacheDims() {
    428    return this._cachedWidth == this.width && this._cachedHeight == this.height;
    429  },
    430 
    431  // Invoked when the given tab is added to this window
    432  newTab(tab) {
    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();
    440  },
    441 
    442  createTabPreview(controller) {
    443    let docShell = this.win.docShell;
    444    let preview = AeroPeek.taskbar.createTaskbarTabPreview(
    445      docShell,
    446      controller
    447    );
    448    preview.visible = AeroPeek.enabled;
    449    let { tab } = controller;
    450    preview.active = this.tabbrowser.selectedTab == tab;
    451    this.updateFavicon(tab, tab.getAttribute("image"));
    452    return preview;
    453  },
    454 
    455  // Invoked when the given tab is closed
    456  removeTab(tab) {
    457    let preview = this.previewFromTab(tab);
    458    preview.active = false;
    459    preview.visible = false;
    460    preview.move(null);
    461    preview.controller.wrappedJSObject.destroy();
    462 
    463    this.previews.delete(tab);
    464    AeroPeek.removePreview(preview);
    465  },
    466 
    467  get enabled() {
    468    return this._enabled;
    469  },
    470 
    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
    475    // at once.
    476    for (let [, preview] of this.previews) {
    477      preview.move(null);
    478      preview.visible = enable;
    479    }
    480    this.updateTabOrdering();
    481  },
    482 
    483  previewFromTab(tab) {
    484    return this.previews.get(tab);
    485  },
    486 
    487  updateTabOrdering() {
    488    let previews = this.previews;
    489    let tabs = this.tabbrowser.tabs;
    490 
    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.
    493    let inorder = [];
    494    for (let t of tabs) {
    495      if (previews.has(t)) {
    496        inorder.push(previews.get(t));
    497      }
    498    }
    499 
    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);
    506    }
    507  },
    508 
    509  // EventListener
    510  handleEvent(evt) {
    511    let tab = evt.originalTarget;
    512    switch (evt.type) {
    513      case "TabOpen":
    514        this.newTab(tab);
    515        this.updateTabOrdering();
    516        break;
    517      case "TabClose":
    518        this.removeTab(tab);
    519        this.updateTabOrdering();
    520        break;
    521      case "TabSelect":
    522        this.previewFromTab(tab).active = true;
    523        break;
    524      case "TabMove":
    525        this.updateTabOrdering();
    526        break;
    527      case "resize":
    528        if (!AeroPeek._prefenabled) {
    529          return;
    530        }
    531        this.onResize();
    532        break;
    533    }
    534  },
    535 
    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(
    540        Ci.nsITimer
    541      );
    542    }
    543    this.invalidateTimer.cancel();
    544 
    545    // delay 1 second before invalidating
    546    this.invalidateTimer.initWithCallback(
    547      () => {
    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();
    555          }
    556        });
    557      },
    558      1000,
    559      Ci.nsITimer.TYPE_ONE_SHOT
    560    );
    561  },
    562 
    563  onResize() {
    564    // Specific to a window.
    565 
    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.
    569 
    570    if (this.testCacheDims()) {
    571      return;
    572    }
    573 
    574    // update the window dims on our TabWindow object.
    575    this.cacheDims();
    576 
    577    // invalidate soon
    578    this.setInvalidationTimer();
    579  },
    580 
    581  invalidateTabPreview(aBrowser) {
    582    for (let [tab, preview] of this.previews) {
    583      if (aBrowser == tab.linkedBrowser) {
    584        preview.invalidate();
    585        break;
    586      }
    587    }
    588  },
    589 
    590  // Browser progress listener
    591 
    592  onLocationChange() {
    593    // I'm not sure we need this, onStateChange does a really good job
    594    // of picking up page changes.
    595    // this.invalidateTabPreview(aBrowser);
    596  },
    597 
    598  onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags) {
    599    if (
    600      aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
    601      aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
    602    ) {
    603      this.invalidateTabPreview(aBrowser);
    604    }
    605  },
    606 
    607  onLinkIconAvailable(aBrowser, aIconURL) {
    608    let tab = this.win.gBrowser.getTabForBrowser(aBrowser);
    609    this.updateFavicon(tab, aIconURL);
    610  },
    611  updateFavicon(aTab, aIconURL) {
    612    let requestURL = null;
    613    if (aIconURL) {
    614      try {
    615        requestURL = PlacesUtils.favicons.getFaviconLinkForIcon(
    616          Services.io.newURI(aIconURL)
    617        ).spec;
    618      } catch (ex) {
    619        requestURL = aIconURL;
    620      }
    621    }
    622    let isDefaultFavicon = !requestURL;
    623    getFaviconAsImage(
    624      requestURL,
    625      PrivateBrowsingUtils.isWindowPrivate(this.win),
    626      img => {
    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) {
    630          return;
    631        }
    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);
    640        if (
    641          aTab.getAttribute("image") == aIconURL ||
    642          (!preview.icon && isDefaultFavicon)
    643        ) {
    644          preview.icon = img;
    645        }
    646      }
    647    );
    648  },
    649 };
    650 
    651 // AeroPeek
    652 
    653 /*
    654 * This object acts as global storage and external interface for this feature.
    655 * It maintains the values of the prefs.
    656 */
    657 export var AeroPeek = {
    658  available: false,
    659  // Does the pref say we're enabled?
    660  __prefenabled: false,
    661 
    662  _enabled: true,
    663 
    664  initialized: false,
    665 
    666  // nsITaskbarTabPreview array
    667  previews: [],
    668 
    669  // TabWindow array
    670  windows: [],
    671 
    672  // nsIWinTaskbar service
    673  taskbar: null,
    674 
    675  // Maximum number of previews
    676  maxpreviews: 20,
    677 
    678  // Length of time in seconds that previews are cached
    679  cacheLifespan: 20,
    680 
    681  initialize() {
    682    if (!(WINTASKBAR_CONTRACTID in Cc)) {
    683      return;
    684    }
    685    this.taskbar = Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar);
    686    this.available = this.taskbar.available;
    687    if (!this.available) {
    688      return;
    689    }
    690 
    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;
    695  },
    696 
    697  destroy: function destroy() {
    698    this._enabled = false;
    699 
    700    if (this.cacheTimer) {
    701      this.cacheTimer.cancel();
    702    }
    703  },
    704 
    705  get enabled() {
    706    return this._enabled;
    707  },
    708 
    709  set enabled(enable) {
    710    if (this._enabled == enable) {
    711      return;
    712    }
    713 
    714    this._enabled = enable;
    715 
    716    this.windows.forEach(function (win) {
    717      win.enabled = enable;
    718    });
    719  },
    720 
    721  get _prefenabled() {
    722    return this.__prefenabled;
    723  },
    724 
    725  set _prefenabled(enable) {
    726    if (enable == this.__prefenabled) {
    727      return;
    728    }
    729    this.__prefenabled = enable;
    730 
    731    if (enable) {
    732      this.enable();
    733    } else {
    734      this.disable();
    735    }
    736  },
    737 
    738  _observersAdded: false,
    739 
    740  enable() {
    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(
    746        ["favicon-changed"],
    747        this._placesListener
    748      );
    749      this._observersAdded = true;
    750    }
    751 
    752    this.cacheLifespan = Services.prefs.getIntPref(
    753      CACHE_EXPIRATION_TIME_PREF_NAME
    754    );
    755 
    756    this.maxpreviews = Services.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
    757 
    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")) {
    763        if (!win.closed) {
    764          this.onOpenWindow(win);
    765        }
    766      }
    767    }
    768  },
    769 
    770  disable() {
    771    while (this.windows.length) {
    772      // We can't call onCloseWindow here because it'll bail if we're not
    773      // enabled.
    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.
    777    }
    778    PlacesUtils.observers.removeListener(
    779      ["favicon-changed"],
    780      this._placesListener
    781    );
    782  },
    783 
    784  addPreview(preview) {
    785    this.previews.push(preview);
    786    this.checkPreviewCount();
    787  },
    788 
    789  removePreview(preview) {
    790    let idx = this.previews.indexOf(preview);
    791    this.previews.splice(idx, 1);
    792    this.checkPreviewCount();
    793  },
    794 
    795  checkPreviewCount() {
    796    if (!this._prefenabled) {
    797      return;
    798    }
    799    this.enabled = this.previews.length <= this.maxpreviews;
    800  },
    801 
    802  onOpenWindow(win) {
    803    // This occurs when the taskbar service is not available (xp, vista)
    804    if (!this.available || !this._prefenabled) {
    805      return;
    806    }
    807 
    808    win.gTaskbarTabGroup = new TabWindow(win);
    809  },
    810 
    811  onCloseWindow(win) {
    812    // This occurs when the taskbar service is not available (xp, vista)
    813    if (!this.available || !this._prefenabled) {
    814      return;
    815    }
    816 
    817    win.gTaskbarTabGroup.destroy();
    818    delete win.gTaskbarTabGroup;
    819 
    820    if (!this.windows.length) {
    821      this.destroy();
    822    }
    823  },
    824 
    825  resetCacheTimer() {
    826    this.cacheTimer.cancel();
    827    this.cacheTimer.init(
    828      this,
    829      1000 * this.cacheLifespan,
    830      Ci.nsITimer.TYPE_ONE_SHOT
    831    );
    832  },
    833 
    834  // nsIObserver
    835  observe(aSubject, aTopic, aData) {
    836    if (aTopic == "nsPref:changed" && aData == TOGGLE_PREF_NAME) {
    837      this._prefenabled = Services.prefs.getBoolPref(TOGGLE_PREF_NAME);
    838    }
    839    if (!this._prefenabled) {
    840      return;
    841    }
    842    switch (aTopic) {
    843      case "nsPref:changed":
    844        if (aData == CACHE_EXPIRATION_TIME_PREF_NAME) {
    845          break;
    846        }
    847 
    848        if (aData == DISABLE_THRESHOLD_PREF_NAME) {
    849          this.maxpreviews = Services.prefs.getIntPref(
    850            DISABLE_THRESHOLD_PREF_NAME
    851          );
    852        }
    853        // Might need to enable/disable ourselves
    854        this.checkPreviewCount();
    855        break;
    856      case "timer-callback":
    857        this.previews.forEach(function (preview) {
    858          let controller = preview.controller.wrappedJSObject;
    859          controller.resetCanvasPreview();
    860        });
    861        break;
    862    }
    863  },
    864 
    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);
    873              }
    874            }
    875          }
    876        }
    877      }
    878    }
    879  },
    880 
    881  QueryInterface: ChromeUtils.generateQI([
    882    "nsISupportsWeakReference",
    883    "nsIObserver",
    884  ]),
    885 };
    886 
    887 ChromeUtils.defineLazyGetter(AeroPeek, "cacheTimer", () =>
    888  Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer)
    889 );
    890 
    891 AeroPeek.initialize();