browser/modules/WindowsPreviewPerTab.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/modules/WindowsPreviewPerTab.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,721 @@
     1.4 +/* vim: se cin sw=2 ts=2 et filetype=javascript :
     1.5 + * This Source Code Form is subject to the terms of the Mozilla Public
     1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.8 +/*
     1.9 + * This module implements the front end behavior for AeroPeek. Starting in
    1.10 + * Windows Vista, the taskbar began showing live thumbnail previews of windows
    1.11 + * when the user hovered over the window icon in the taskbar. Starting with
    1.12 + * Windows 7, the taskbar allows an application to expose its tabbed interface
    1.13 + * in the taskbar by showing thumbnail previews rather than the default window
    1.14 + * preview. Additionally, when a user hovers over a thumbnail (tab or window),
    1.15 + * they are shown a live preview of the window (or tab + its containing window).
    1.16 + *
    1.17 + * In Windows 7, a title, icon, close button and optional toolbar are shown for
    1.18 + * each preview. This feature does not make use of the toolbar. For window
    1.19 + * previews, the title is the window title and the icon the window icon. For
    1.20 + * tab previews, the title is the page title and the page's favicon. In both
    1.21 + * cases, the close button "does the right thing."
    1.22 + *
    1.23 + * The primary objects behind this feature are nsITaskbarTabPreview and
    1.24 + * nsITaskbarPreviewController. Each preview has a controller. The controller
    1.25 + * responds to the user's interactions on the taskbar and provides the required
    1.26 + * data to the preview for determining the size of the tab and thumbnail. The
    1.27 + * PreviewController class implements this interface. The preview will request
    1.28 + * the controller to provide a thumbnail or preview when the user interacts with
    1.29 + * the taskbar. To reduce the overhead of drawing the tab area, the controller
    1.30 + * implementation caches the tab's contents in a <canvas> element. If no
    1.31 + * previews or thumbnails have been requested for some time, the controller will
    1.32 + * discard its cached tab contents.
    1.33 + *
    1.34 + * Screen real estate is limited so when there are too many thumbnails to fit
    1.35 + * on the screen, the taskbar stops displaying thumbnails and instead displays
    1.36 + * just the title, icon and close button in a similar fashion to previous
    1.37 + * versions of the taskbar. If there are still too many previews to fit on the 
    1.38 + * screen, the taskbar resorts to a scroll up and scroll down button pair to let
    1.39 + * the user scroll through the list of tabs. Since this is undoubtedly
    1.40 + * inconvenient for users with many tabs, the AeroPeek objects turns off all of
    1.41 + * the tab previews. This tells the taskbar to revert to one preview per window.
    1.42 + * If the number of tabs falls below this magic threshold, the preview-per-tab
    1.43 + * behavior returns. There is no reliable way to determine when the scroll
    1.44 + * buttons appear on the taskbar, so a magic pref-controlled number determines
    1.45 + * when this threshold has been crossed.
    1.46 + */
    1.47 +this.EXPORTED_SYMBOLS = ["AeroPeek"];
    1.48 +
    1.49 +const Cc = Components.classes;
    1.50 +const Ci = Components.interfaces;
    1.51 +const Cu = Components.utils;
    1.52 +
    1.53 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.54 +Cu.import("resource://gre/modules/NetUtil.jsm");
    1.55 +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
    1.56 +
    1.57 +// Pref to enable/disable preview-per-tab
    1.58 +const TOGGLE_PREF_NAME = "browser.taskbar.previews.enable";
    1.59 +// Pref to determine the magic auto-disable threshold
    1.60 +const DISABLE_THRESHOLD_PREF_NAME = "browser.taskbar.previews.max";
    1.61 +// Pref to control the time in seconds that tab contents live in the cache
    1.62 +const CACHE_EXPIRATION_TIME_PREF_NAME = "browser.taskbar.previews.cachetime";
    1.63 +
    1.64 +const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
    1.65 +
    1.66 +////////////////////////////////////////////////////////////////////////////////
    1.67 +//// Various utility properties
    1.68 +XPCOMUtils.defineLazyServiceGetter(this, "ioSvc",
    1.69 +                                   "@mozilla.org/network/io-service;1",
    1.70 +                                   "nsIIOService");
    1.71 +XPCOMUtils.defineLazyServiceGetter(this, "imgTools",
    1.72 +                                   "@mozilla.org/image/tools;1",
    1.73 +                                   "imgITools");
    1.74 +XPCOMUtils.defineLazyServiceGetter(this, "faviconSvc",
    1.75 +                                   "@mozilla.org/browser/favicon-service;1",
    1.76 +                                   "nsIFaviconService");
    1.77 +
    1.78 +// nsIURI -> imgIContainer
    1.79 +function _imageFromURI(uri, privateMode, callback) {
    1.80 +  let channel = ioSvc.newChannelFromURI(uri);
    1.81 +  try {
    1.82 +    channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
    1.83 +    channel.setPrivate(privateMode);
    1.84 +  } catch (e) {
    1.85 +    // Ignore channels which do not support nsIPrivateBrowsingChannel
    1.86 +  }
    1.87 +  NetUtil.asyncFetch(channel, function(inputStream, resultCode) {
    1.88 +    if (!Components.isSuccessCode(resultCode))
    1.89 +      return;
    1.90 +    try {
    1.91 +      let out_img = { value: null };
    1.92 +      imgTools.decodeImageData(inputStream, channel.contentType, out_img);
    1.93 +      callback(out_img.value);
    1.94 +    } catch (e) {
    1.95 +      // We failed, so use the default favicon (only if this wasn't the default
    1.96 +      // favicon).
    1.97 +      let defaultURI = faviconSvc.defaultFavicon;
    1.98 +      if (!defaultURI.equals(uri))
    1.99 +        _imageFromURI(defaultURI, callback);
   1.100 +    }
   1.101 +  });
   1.102 +}
   1.103 +
   1.104 +// string? -> imgIContainer
   1.105 +function getFaviconAsImage(iconurl, privateMode, callback) {
   1.106 +  if (iconurl)
   1.107 +    _imageFromURI(NetUtil.newURI(iconurl), privateMode, callback);
   1.108 +  else
   1.109 +    _imageFromURI(faviconSvc.defaultFavicon, privateMode, callback);
   1.110 +}
   1.111 +
   1.112 +// Snaps the given rectangle to be pixel-aligned at the given scale
   1.113 +function snapRectAtScale(r, scale) {
   1.114 +  let x = Math.floor(r.x * scale);
   1.115 +  let y = Math.floor(r.y * scale);
   1.116 +  let width = Math.ceil((r.x + r.width) * scale) - x;
   1.117 +  let height = Math.ceil((r.y + r.height) * scale) - y;
   1.118 +
   1.119 +  r.x = x / scale;
   1.120 +  r.y = y / scale;
   1.121 +  r.width = width / scale;
   1.122 +  r.height = height / scale;
   1.123 +}
   1.124 +
   1.125 +////////////////////////////////////////////////////////////////////////////////
   1.126 +//// PreviewController
   1.127 +
   1.128 +/*
   1.129 + * This class manages the behavior of the preview.
   1.130 + *
   1.131 + * To give greater performance when drawing, the dirty areas of the content
   1.132 + * window are tracked and drawn on demand into a canvas of the same size.
   1.133 + * This provides a great increase in responsiveness when drawing a preview
   1.134 + * for unchanged (or even only slightly changed) tabs.
   1.135 + *
   1.136 + * @param win
   1.137 + *        The TabWindow (see below) that owns the preview that this controls
   1.138 + * @param tab
   1.139 + *        The <tab> that this preview is associated with
   1.140 + */
   1.141 +function PreviewController(win, tab) {
   1.142 +  this.win = win;
   1.143 +  this.tab = tab;
   1.144 +  this.linkedBrowser = tab.linkedBrowser;
   1.145 +  this.preview = this.win.createTabPreview(this);
   1.146 +
   1.147 +  this.linkedBrowser.addEventListener("MozAfterPaint", this, false);
   1.148 +  this.tab.addEventListener("TabAttrModified", this, false);
   1.149 +
   1.150 +  XPCOMUtils.defineLazyGetter(this, "canvasPreview", function () {
   1.151 +    let canvas = this.win.win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
   1.152 +    canvas.mozOpaque = true;
   1.153 +    return canvas;
   1.154 +  });
   1.155 +
   1.156 +  XPCOMUtils.defineLazyGetter(this, "dirtyRegion",
   1.157 +    function () {
   1.158 +      let dirtyRegion = Cc["@mozilla.org/gfx/region;1"]
   1.159 +                       .createInstance(Ci.nsIScriptableRegion);
   1.160 +      dirtyRegion.init();
   1.161 +      return dirtyRegion;
   1.162 +    });
   1.163 +
   1.164 +  XPCOMUtils.defineLazyGetter(this, "winutils",
   1.165 +    function () {
   1.166 +      let win = tab.linkedBrowser.contentWindow;
   1.167 +      return win.QueryInterface(Ci.nsIInterfaceRequestor)
   1.168 +                .getInterface(Ci.nsIDOMWindowUtils);
   1.169 +  });
   1.170 +}
   1.171 +
   1.172 +PreviewController.prototype = {
   1.173 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsITaskbarPreviewController,
   1.174 +                                         Ci.nsIDOMEventListener]),
   1.175 +  destroy: function () {
   1.176 +    this.tab.removeEventListener("TabAttrModified", this, false);
   1.177 +    this.linkedBrowser.removeEventListener("MozAfterPaint", this, false);
   1.178 +
   1.179 +    // Break cycles, otherwise we end up leaking the window with everything
   1.180 +    // attached to it.
   1.181 +    delete this.win;
   1.182 +    delete this.preview;
   1.183 +    delete this.dirtyRegion;
   1.184 +  },
   1.185 +  get wrappedJSObject() {
   1.186 +    return this;
   1.187 +  },
   1.188 +
   1.189 +  get dirtyRects() {
   1.190 +    let rectstream = this.dirtyRegion.getRects();
   1.191 +    if (!rectstream)
   1.192 +      return [];
   1.193 +    let rects = [];
   1.194 +    for (let i = 0; i < rectstream.length; i+= 4) {
   1.195 +      let r = {x:      rectstream[i],
   1.196 +               y:      rectstream[i+1],
   1.197 +               width:  rectstream[i+2],
   1.198 +               height: rectstream[i+3]};
   1.199 +      rects.push(r);
   1.200 +    }
   1.201 +    return rects;
   1.202 +  },
   1.203 +
   1.204 +  // Resizes the canvasPreview to 0x0, essentially freeing its memory.
   1.205 +  // updateCanvasPreview() will detect the size mismatch as a resize event
   1.206 +  // the next time it is called.
   1.207 +  resetCanvasPreview: function () {
   1.208 +    this.canvasPreview.width = 0;
   1.209 +    this.canvasPreview.height = 0;
   1.210 +  },
   1.211 +
   1.212 +  get zoom() {
   1.213 +    // Note that winutils.fullZoom accounts for "quantization" of the zoom factor
   1.214 +    // from nsIMarkupDocumentViewer due to conversion through appUnits.
   1.215 +    // We do -not- want screenPixelsPerCSSPixel here, because that would -also-
   1.216 +    // incorporate any scaling that is applied due to hi-dpi resolution options.
   1.217 +    return this.winutils.fullZoom;
   1.218 +  },
   1.219 +
   1.220 +  // Updates the controller's canvas with the parts of the <browser> that need
   1.221 +  // to be redrawn.
   1.222 +  updateCanvasPreview: function () {
   1.223 +    let win = this.linkedBrowser.contentWindow;
   1.224 +    let bx = this.linkedBrowser.boxObject;
   1.225 +    // Check for resize
   1.226 +    if (bx.width != this.canvasPreview.width ||
   1.227 +        bx.height != this.canvasPreview.height) {
   1.228 +      // Invalidate the entire area and repaint
   1.229 +      this.onTabPaint({left:0, top:0, right:win.innerWidth, bottom:win.innerHeight});
   1.230 +      this.canvasPreview.width = bx.width;
   1.231 +      this.canvasPreview.height = bx.height;
   1.232 +    }
   1.233 +
   1.234 +    // Draw dirty regions
   1.235 +    let ctx = this.canvasPreview.getContext("2d");
   1.236 +    let scale = this.zoom;
   1.237 +
   1.238 +    let flags = this.canvasPreviewFlags;
   1.239 +    // The dirty region may include parts that are offscreen so we clip to the
   1.240 +    // canvas area.
   1.241 +    this.dirtyRegion.intersectRect(0, 0, win.innerWidth, win.innerHeight);
   1.242 +    this.dirtyRects.forEach(function (r) {
   1.243 +      // We need to snap the rectangle to be pixel aligned in the destination
   1.244 +      // coordinate space. Otherwise natively themed widgets might not draw.
   1.245 +      snapRectAtScale(r, scale);
   1.246 +      let x = r.x;
   1.247 +      let y = r.y;
   1.248 +      let width = r.width;
   1.249 +      let height = r.height;
   1.250 +
   1.251 +      ctx.save();
   1.252 +      ctx.scale(scale, scale);
   1.253 +      ctx.translate(x, y);
   1.254 +      ctx.drawWindow(win, x, y, width, height, "white", flags);
   1.255 +      ctx.restore();
   1.256 +    });
   1.257 +    this.dirtyRegion.setToRect(0,0,0,0);
   1.258 +
   1.259 +    // If we're updating the canvas, then we're in the middle of a peek so
   1.260 +    // don't discard the cache of previews.
   1.261 +    AeroPeek.resetCacheTimer();
   1.262 +  },
   1.263 +
   1.264 +  onTabPaint: function (rect) {
   1.265 +    let x = Math.floor(rect.left),
   1.266 +        y = Math.floor(rect.top),
   1.267 +        width = Math.ceil(rect.right) - x,
   1.268 +        height = Math.ceil(rect.bottom) - y;
   1.269 +    this.dirtyRegion.unionRect(x, y, width, height);
   1.270 +  },
   1.271 +
   1.272 +  updateTitleAndTooltip: function () {
   1.273 +    let title = this.win.tabbrowser.getWindowTitleForBrowser(this.linkedBrowser);
   1.274 +    this.preview.title = title;
   1.275 +    this.preview.tooltip = title;
   1.276 +  },
   1.277 +
   1.278 +  //////////////////////////////////////////////////////////////////////////////
   1.279 +  //// nsITaskbarPreviewController 
   1.280 +
   1.281 +  get width() {
   1.282 +    return this.win.width;
   1.283 +  },
   1.284 +
   1.285 +  get height() {
   1.286 +    return this.win.height;
   1.287 +  },
   1.288 +
   1.289 +  get thumbnailAspectRatio() {
   1.290 +    let boxObject = this.tab.linkedBrowser.boxObject;
   1.291 +    // Avoid returning 0
   1.292 +    let tabWidth = boxObject.width || 1;
   1.293 +    // Avoid divide by 0
   1.294 +    let tabHeight = boxObject.height || 1;
   1.295 +    return tabWidth / tabHeight;
   1.296 +  },
   1.297 +
   1.298 +  drawPreview: function (ctx) {
   1.299 +    let self = this;
   1.300 +    this.win.tabbrowser.previewTab(this.tab, function () self.previewTabCallback(ctx));
   1.301 +
   1.302 +    // We must avoid having the frame drawn around the window. See bug 520807
   1.303 +    return false;
   1.304 +  },
   1.305 +
   1.306 +  previewTabCallback: function (ctx) {
   1.307 +    // This will extract the resolution-scale component of the scaling we need,
   1.308 +    // which should be applied to both chrome and content;
   1.309 +    // the page zoom component is applied (to content only) within updateCanvasPreview.
   1.310 +    let scale = this.winutils.screenPixelsPerCSSPixel / this.winutils.fullZoom;
   1.311 +    ctx.save();
   1.312 +    ctx.scale(scale, scale);
   1.313 +    let width = this.win.width;
   1.314 +    let height = this.win.height;
   1.315 +    // Draw our toplevel window
   1.316 +    ctx.drawWindow(this.win.win, 0, 0, width, height, "transparent");
   1.317 +
   1.318 +    // XXX (jfkthame): Pending tabs don't seem to draw with the proper scaling
   1.319 +    // unless we use this block of code; but doing this for "normal" (loaded) tabs
   1.320 +    // results in blurry rendering on hidpi systems, so we avoid it if possible.
   1.321 +    // I don't understand why pending and loaded tabs behave differently here...
   1.322 +    // (see bug 857061).
   1.323 +    if (this.tab.hasAttribute("pending")) {
   1.324 +      // Compositor, where art thou?
   1.325 +      // Draw the tab content on top of the toplevel window
   1.326 +      this.updateCanvasPreview();
   1.327 +
   1.328 +      let boxObject = this.linkedBrowser.boxObject;
   1.329 +      ctx.translate(boxObject.x, boxObject.y);
   1.330 +      ctx.drawImage(this.canvasPreview, 0, 0);
   1.331 +    }
   1.332 +
   1.333 +    ctx.restore();
   1.334 +  },
   1.335 +
   1.336 +  drawThumbnail: function (ctx, width, height) {
   1.337 +    this.updateCanvasPreview();
   1.338 +
   1.339 +    let scale = width/this.linkedBrowser.boxObject.width;
   1.340 +    ctx.scale(scale, scale);
   1.341 +    ctx.drawImage(this.canvasPreview, 0, 0);
   1.342 +
   1.343 +    // Don't draw a frame around the thumbnail
   1.344 +    return false;
   1.345 +  },
   1.346 +
   1.347 +  onClose: function () {
   1.348 +    this.win.tabbrowser.removeTab(this.tab);
   1.349 +  },
   1.350 +
   1.351 +  onActivate: function () {
   1.352 +    this.win.tabbrowser.selectedTab = this.tab;
   1.353 +
   1.354 +    // Accept activation - this will restore the browser window
   1.355 +    // if it's minimized
   1.356 +    return true;
   1.357 +  },
   1.358 +
   1.359 +  //// nsIDOMEventListener
   1.360 +  handleEvent: function (evt) {
   1.361 +    switch (evt.type) {
   1.362 +      case "MozAfterPaint":
   1.363 +        if (evt.originalTarget === this.linkedBrowser.contentWindow) {
   1.364 +          let clientRects = evt.clientRects;
   1.365 +          let length = clientRects.length;
   1.366 +          for (let i = 0; i < length; i++) {
   1.367 +            let r = clientRects.item(i);
   1.368 +            this.onTabPaint(r);
   1.369 +          }
   1.370 +        }
   1.371 +        let preview = this.preview;
   1.372 +        if (preview.visible)
   1.373 +          preview.invalidate();
   1.374 +        break;
   1.375 +      case "TabAttrModified":
   1.376 +        this.updateTitleAndTooltip();
   1.377 +        break;
   1.378 +    }
   1.379 +  }
   1.380 +};
   1.381 +
   1.382 +XPCOMUtils.defineLazyGetter(PreviewController.prototype, "canvasPreviewFlags",
   1.383 +  function () { let canvasInterface = Ci.nsIDOMCanvasRenderingContext2D;
   1.384 +                return canvasInterface.DRAWWINDOW_DRAW_VIEW
   1.385 +                     | canvasInterface.DRAWWINDOW_DRAW_CARET
   1.386 +                     | canvasInterface.DRAWWINDOW_ASYNC_DECODE_IMAGES
   1.387 +                     | canvasInterface.DRAWWINDOW_DO_NOT_FLUSH;
   1.388 +});
   1.389 +
   1.390 +////////////////////////////////////////////////////////////////////////////////
   1.391 +//// TabWindow
   1.392 +
   1.393 +/*
   1.394 + * This class monitors a browser window for changes to its tabs
   1.395 + *
   1.396 + * @param win
   1.397 + *        The nsIDOMWindow browser window 
   1.398 + */
   1.399 +function TabWindow(win) {
   1.400 +  this.win = win;
   1.401 +  this.tabbrowser = win.gBrowser;
   1.402 +
   1.403 +  this.previews = new Map();
   1.404 +
   1.405 +  for (let i = 0; i < this.tabEvents.length; i++)
   1.406 +    this.tabbrowser.tabContainer.addEventListener(this.tabEvents[i], this, false);
   1.407 +  this.tabbrowser.addTabsProgressListener(this);
   1.408 +
   1.409 +  for (let i = 0; i < this.winEvents.length; i++)
   1.410 +    this.win.addEventListener(this.winEvents[i], this, false);
   1.411 +
   1.412 +  AeroPeek.windows.push(this);
   1.413 +  let tabs = this.tabbrowser.tabs;
   1.414 +  for (let i = 0; i < tabs.length; i++)
   1.415 +    this.newTab(tabs[i]);
   1.416 +
   1.417 +  this.updateTabOrdering();
   1.418 +  AeroPeek.checkPreviewCount();
   1.419 +}
   1.420 +
   1.421 +TabWindow.prototype = {
   1.422 +  _enabled: false,
   1.423 +  tabEvents: ["TabOpen", "TabClose", "TabSelect", "TabMove"],
   1.424 +  winEvents: ["tabviewshown", "tabviewhidden"],
   1.425 +
   1.426 +  destroy: function () {
   1.427 +    this._destroying = true;
   1.428 +
   1.429 +    let tabs = this.tabbrowser.tabs;
   1.430 +
   1.431 +    this.tabbrowser.removeTabsProgressListener(this);
   1.432 +    for (let i = 0; i < this.tabEvents.length; i++)
   1.433 +      this.tabbrowser.tabContainer.removeEventListener(this.tabEvents[i], this, false);
   1.434 +
   1.435 +    for (let i = 0; i < this.winEvents.length; i++)
   1.436 +      this.win.removeEventListener(this.winEvents[i], this, false);
   1.437 +
   1.438 +    for (let i = 0; i < tabs.length; i++)
   1.439 +      this.removeTab(tabs[i]);
   1.440 +
   1.441 +    let idx = AeroPeek.windows.indexOf(this.win.gTaskbarTabGroup);
   1.442 +    AeroPeek.windows.splice(idx, 1);
   1.443 +    AeroPeek.checkPreviewCount();
   1.444 +  },
   1.445 +
   1.446 +  get width () {
   1.447 +    return this.win.innerWidth;
   1.448 +  },
   1.449 +  get height () {
   1.450 +    return this.win.innerHeight;
   1.451 +  },
   1.452 +
   1.453 +  // Invoked when the given tab is added to this window
   1.454 +  newTab: function (tab) {
   1.455 +    let controller = new PreviewController(this, tab);
   1.456 +    // It's OK to add the preview now while the favicon still loads.
   1.457 +    this.previews.set(tab, controller.preview);
   1.458 +    AeroPeek.addPreview(controller.preview);
   1.459 +    // updateTitleAndTooltip relies on having controller.preview which is lazily resolved.
   1.460 +    // Now that we've updated this.previews, it will resolve successfully.
   1.461 +    controller.updateTitleAndTooltip();
   1.462 +  },
   1.463 +
   1.464 +  createTabPreview: function (controller) {
   1.465 +    let docShell = this.win
   1.466 +                  .QueryInterface(Ci.nsIInterfaceRequestor)
   1.467 +                  .getInterface(Ci.nsIWebNavigation)
   1.468 +                  .QueryInterface(Ci.nsIDocShell);
   1.469 +    let preview = AeroPeek.taskbar.createTaskbarTabPreview(docShell, controller);
   1.470 +    preview.visible = AeroPeek.enabled;
   1.471 +    preview.active = this.tabbrowser.selectedTab == controller.tab;
   1.472 +    // Grab the default favicon
   1.473 +    getFaviconAsImage(null, PrivateBrowsingUtils.isWindowPrivate(this.win), function (img) {
   1.474 +      // It is possible that we've already gotten the real favicon, so make sure
   1.475 +      // we have not set one before setting this default one.
   1.476 +      if (!preview.icon)
   1.477 +        preview.icon = img;
   1.478 +    });
   1.479 +
   1.480 +    return preview;
   1.481 +  },
   1.482 +
   1.483 +  // Invoked when the given tab is closed
   1.484 +  removeTab: function (tab) {
   1.485 +    let preview = this.previewFromTab(tab);
   1.486 +    preview.active = false;
   1.487 +    preview.visible = false;
   1.488 +    preview.move(null);
   1.489 +    preview.controller.wrappedJSObject.destroy();
   1.490 +
   1.491 +    this.previews.delete(tab);
   1.492 +    AeroPeek.removePreview(preview);
   1.493 +  },
   1.494 +
   1.495 +  get enabled () {
   1.496 +    return this._enabled;
   1.497 +  },
   1.498 +
   1.499 +  set enabled (enable) {
   1.500 +    this._enabled = enable;
   1.501 +    // Because making a tab visible requires that the tab it is next to be
   1.502 +    // visible, it is far simpler to unset the 'next' tab and recreate them all
   1.503 +    // at once.
   1.504 +    for (let [tab, preview] of this.previews) {
   1.505 +      preview.move(null);
   1.506 +      preview.visible = enable;
   1.507 +    }
   1.508 +    this.updateTabOrdering();
   1.509 +  },
   1.510 +
   1.511 +  previewFromTab: function (tab) {
   1.512 +    return this.previews.get(tab);
   1.513 +  },
   1.514 +
   1.515 +  updateTabOrdering: function () {
   1.516 +    let previews = this.previews;
   1.517 +    let tabs = this.tabbrowser.tabs;
   1.518 +
   1.519 +    // Previews are internally stored using a map, so we need to iterate the
   1.520 +    // tabbrowser's array of tabs to retrieve previews in the same order.
   1.521 +    let inorder = [previews.get(t) for (t of tabs) if (previews.has(t))];
   1.522 +
   1.523 +    // Since the internal taskbar array has not yet been updated we must force
   1.524 +    // on it the sorting order of our local array.  To do so we must walk
   1.525 +    // the local array backwards, otherwise we would send move requests in the
   1.526 +    // wrong order.  See bug 522610 for details.
   1.527 +    for (let i = inorder.length - 1; i >= 0; i--) {
   1.528 +      inorder[i].move(inorder[i + 1] || null);
   1.529 +    }
   1.530 +  },
   1.531 +
   1.532 +  //// nsIDOMEventListener
   1.533 +  handleEvent: function (evt) {
   1.534 +    let tab = evt.originalTarget;
   1.535 +    switch (evt.type) {
   1.536 +      case "TabOpen":
   1.537 +        this.newTab(tab);
   1.538 +        this.updateTabOrdering();
   1.539 +        break;
   1.540 +      case "TabClose":
   1.541 +        this.removeTab(tab);
   1.542 +        this.updateTabOrdering();
   1.543 +        break;
   1.544 +      case "TabSelect":
   1.545 +        this.previewFromTab(tab).active = true;
   1.546 +        break;
   1.547 +      case "TabMove":
   1.548 +        this.updateTabOrdering();
   1.549 +        break;
   1.550 +      case "tabviewshown":
   1.551 +        this.enabled = false;
   1.552 +        break;
   1.553 +      case "tabviewhidden":
   1.554 +        if (!AeroPeek._prefenabled)
   1.555 +          return;
   1.556 +        this.enabled = true;
   1.557 +        break;
   1.558 +    }
   1.559 +  },
   1.560 +
   1.561 +  //// Browser progress listener
   1.562 +  onLinkIconAvailable: function (aBrowser, aIconURL) {
   1.563 +    let self = this;
   1.564 +    getFaviconAsImage(aIconURL, PrivateBrowsingUtils.isWindowPrivate(this.win), function (img) {
   1.565 +      let index = self.tabbrowser.browsers.indexOf(aBrowser);
   1.566 +      // Only add it if we've found the index.  The tab could have closed!
   1.567 +      if (index != -1) {
   1.568 +        let tab = self.tabbrowser.tabs[index];
   1.569 +        self.previews.get(tab).icon = img;
   1.570 +      }
   1.571 +    });
   1.572 +  }
   1.573 +}
   1.574 +
   1.575 +////////////////////////////////////////////////////////////////////////////////
   1.576 +//// AeroPeek
   1.577 +
   1.578 +/*
   1.579 + * This object acts as global storage and external interface for this feature.
   1.580 + * It maintains the values of the prefs.
   1.581 + */
   1.582 +this.AeroPeek = {
   1.583 +  available: false,
   1.584 +  // Does the pref say we're enabled?
   1.585 +  _prefenabled: true,
   1.586 +
   1.587 +  _enabled: true,
   1.588 +
   1.589 +  // nsITaskbarTabPreview array
   1.590 +  previews: [],
   1.591 +
   1.592 +  // TabWindow array
   1.593 +  windows: [],
   1.594 +
   1.595 +  // nsIWinTaskbar service
   1.596 +  taskbar: null,
   1.597 +
   1.598 +  // Maximum number of previews
   1.599 +  maxpreviews: 20,
   1.600 +
   1.601 +  // Length of time in seconds that previews are cached
   1.602 +  cacheLifespan: 20,
   1.603 +
   1.604 +  initialize: function () {
   1.605 +    if (!(WINTASKBAR_CONTRACTID in Cc))
   1.606 +      return;
   1.607 +    this.taskbar = Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar);
   1.608 +    this.available = this.taskbar.available;
   1.609 +    if (!this.available)
   1.610 +      return;
   1.611 +
   1.612 +    this.prefs.addObserver(TOGGLE_PREF_NAME, this, false);
   1.613 +    this.prefs.addObserver(DISABLE_THRESHOLD_PREF_NAME, this, false);
   1.614 +    this.prefs.addObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this, false);
   1.615 +
   1.616 +    this.cacheLifespan = this.prefs.getIntPref(CACHE_EXPIRATION_TIME_PREF_NAME);
   1.617 +
   1.618 +    this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
   1.619 +
   1.620 +    this.enabled = this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME);
   1.621 +  },
   1.622 +
   1.623 +  destroy: function destroy() {
   1.624 +    this._enabled = false;
   1.625 +
   1.626 +    this.prefs.removeObserver(TOGGLE_PREF_NAME, this);
   1.627 +    this.prefs.removeObserver(DISABLE_THRESHOLD_PREF_NAME, this);
   1.628 +    this.prefs.removeObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this);
   1.629 +
   1.630 +    if (this.cacheTimer)
   1.631 +      this.cacheTimer.cancel();
   1.632 +  },
   1.633 +
   1.634 +  get enabled() {
   1.635 +    return this._enabled;
   1.636 +  },
   1.637 +
   1.638 +  set enabled(enable) {
   1.639 +    if (this._enabled == enable)
   1.640 +      return;
   1.641 +
   1.642 +    this._enabled = enable;
   1.643 +
   1.644 +    this.windows.forEach(function (win) {
   1.645 +      win.enabled = enable;
   1.646 +    });
   1.647 +  },
   1.648 +
   1.649 +  addPreview: function (preview) {
   1.650 +    this.previews.push(preview);
   1.651 +    this.checkPreviewCount();
   1.652 +  },
   1.653 +
   1.654 +  removePreview: function (preview) {
   1.655 +    let idx = this.previews.indexOf(preview);
   1.656 +    this.previews.splice(idx, 1);
   1.657 +    this.checkPreviewCount();
   1.658 +  },
   1.659 +
   1.660 +  checkPreviewCount: function () {
   1.661 +    if (this.previews.length > this.maxpreviews)
   1.662 +      this.enabled = false;
   1.663 +    else
   1.664 +      this.enabled = this._prefenabled;
   1.665 +  },
   1.666 +
   1.667 +  onOpenWindow: function (win) {
   1.668 +    // This occurs when the taskbar service is not available (xp, vista)
   1.669 +    if (!this.available)
   1.670 +      return;
   1.671 +
   1.672 +    win.gTaskbarTabGroup = new TabWindow(win);
   1.673 +  },
   1.674 +
   1.675 +  onCloseWindow: function (win) {
   1.676 +    // This occurs when the taskbar service is not available (xp, vista)
   1.677 +    if (!this.available)
   1.678 +      return;
   1.679 +
   1.680 +    win.gTaskbarTabGroup.destroy();
   1.681 +    delete win.gTaskbarTabGroup;
   1.682 +
   1.683 +    if (this.windows.length == 0)
   1.684 +      this.destroy();
   1.685 +  },
   1.686 +
   1.687 +  resetCacheTimer: function () {
   1.688 +    this.cacheTimer.cancel();
   1.689 +    this.cacheTimer.init(this, 1000*this.cacheLifespan, Ci.nsITimer.TYPE_ONE_SHOT);
   1.690 +  },
   1.691 +
   1.692 +  //// nsIObserver
   1.693 +  observe: function (aSubject, aTopic, aData) {
   1.694 +    switch (aTopic) {
   1.695 +      case "nsPref:changed":
   1.696 +        if (aData == CACHE_EXPIRATION_TIME_PREF_NAME)
   1.697 +          break;
   1.698 +
   1.699 +        if (aData == TOGGLE_PREF_NAME)
   1.700 +          this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME);
   1.701 +        else if (aData == DISABLE_THRESHOLD_PREF_NAME)
   1.702 +          this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
   1.703 +        // Might need to enable/disable ourselves
   1.704 +        this.checkPreviewCount();
   1.705 +        break;
   1.706 +      case "timer-callback":
   1.707 +        this.previews.forEach(function (preview) {
   1.708 +          let controller = preview.controller.wrappedJSObject;
   1.709 +          controller.resetCanvasPreview();
   1.710 +        });
   1.711 +        break;
   1.712 +    }
   1.713 +  }
   1.714 +};
   1.715 +
   1.716 +XPCOMUtils.defineLazyGetter(AeroPeek, "cacheTimer", function ()
   1.717 +  Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer)
   1.718 +);
   1.719 +
   1.720 +XPCOMUtils.defineLazyServiceGetter(AeroPeek, "prefs",
   1.721 +                                   "@mozilla.org/preferences-service;1",
   1.722 +                                   "nsIPrefBranch");
   1.723 +
   1.724 +AeroPeek.initialize();

mercurial