michael@0: /* vim: se cin sw=2 ts=2 et filetype=javascript : michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: /* michael@0: * This module implements the front end behavior for AeroPeek. Starting in michael@0: * Windows Vista, the taskbar began showing live thumbnail previews of windows michael@0: * when the user hovered over the window icon in the taskbar. Starting with michael@0: * Windows 7, the taskbar allows an application to expose its tabbed interface michael@0: * in the taskbar by showing thumbnail previews rather than the default window michael@0: * preview. Additionally, when a user hovers over a thumbnail (tab or window), michael@0: * they are shown a live preview of the window (or tab + its containing window). michael@0: * michael@0: * In Windows 7, a title, icon, close button and optional toolbar are shown for michael@0: * each preview. This feature does not make use of the toolbar. For window michael@0: * previews, the title is the window title and the icon the window icon. For michael@0: * tab previews, the title is the page title and the page's favicon. In both michael@0: * cases, the close button "does the right thing." michael@0: * michael@0: * The primary objects behind this feature are nsITaskbarTabPreview and michael@0: * nsITaskbarPreviewController. Each preview has a controller. The controller michael@0: * responds to the user's interactions on the taskbar and provides the required michael@0: * data to the preview for determining the size of the tab and thumbnail. The michael@0: * PreviewController class implements this interface. The preview will request michael@0: * the controller to provide a thumbnail or preview when the user interacts with michael@0: * the taskbar. To reduce the overhead of drawing the tab area, the controller michael@0: * implementation caches the tab's contents in a element. If no michael@0: * previews or thumbnails have been requested for some time, the controller will michael@0: * discard its cached tab contents. michael@0: * michael@0: * Screen real estate is limited so when there are too many thumbnails to fit michael@0: * on the screen, the taskbar stops displaying thumbnails and instead displays michael@0: * just the title, icon and close button in a similar fashion to previous michael@0: * versions of the taskbar. If there are still too many previews to fit on the michael@0: * screen, the taskbar resorts to a scroll up and scroll down button pair to let michael@0: * the user scroll through the list of tabs. Since this is undoubtedly michael@0: * inconvenient for users with many tabs, the AeroPeek objects turns off all of michael@0: * the tab previews. This tells the taskbar to revert to one preview per window. michael@0: * If the number of tabs falls below this magic threshold, the preview-per-tab michael@0: * behavior returns. There is no reliable way to determine when the scroll michael@0: * buttons appear on the taskbar, so a magic pref-controlled number determines michael@0: * when this threshold has been crossed. michael@0: */ michael@0: this.EXPORTED_SYMBOLS = ["AeroPeek"]; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/NetUtil.jsm"); michael@0: Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: michael@0: // Pref to enable/disable preview-per-tab michael@0: const TOGGLE_PREF_NAME = "browser.taskbar.previews.enable"; michael@0: // Pref to determine the magic auto-disable threshold michael@0: const DISABLE_THRESHOLD_PREF_NAME = "browser.taskbar.previews.max"; michael@0: // Pref to control the time in seconds that tab contents live in the cache michael@0: const CACHE_EXPIRATION_TIME_PREF_NAME = "browser.taskbar.previews.cachetime"; michael@0: michael@0: const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1"; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Various utility properties michael@0: XPCOMUtils.defineLazyServiceGetter(this, "ioSvc", michael@0: "@mozilla.org/network/io-service;1", michael@0: "nsIIOService"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "imgTools", michael@0: "@mozilla.org/image/tools;1", michael@0: "imgITools"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "faviconSvc", michael@0: "@mozilla.org/browser/favicon-service;1", michael@0: "nsIFaviconService"); michael@0: michael@0: // nsIURI -> imgIContainer michael@0: function _imageFromURI(uri, privateMode, callback) { michael@0: let channel = ioSvc.newChannelFromURI(uri); michael@0: try { michael@0: channel.QueryInterface(Ci.nsIPrivateBrowsingChannel); michael@0: channel.setPrivate(privateMode); michael@0: } catch (e) { michael@0: // Ignore channels which do not support nsIPrivateBrowsingChannel michael@0: } michael@0: NetUtil.asyncFetch(channel, function(inputStream, resultCode) { michael@0: if (!Components.isSuccessCode(resultCode)) michael@0: return; michael@0: try { michael@0: let out_img = { value: null }; michael@0: imgTools.decodeImageData(inputStream, channel.contentType, out_img); michael@0: callback(out_img.value); michael@0: } catch (e) { michael@0: // We failed, so use the default favicon (only if this wasn't the default michael@0: // favicon). michael@0: let defaultURI = faviconSvc.defaultFavicon; michael@0: if (!defaultURI.equals(uri)) michael@0: _imageFromURI(defaultURI, callback); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: // string? -> imgIContainer michael@0: function getFaviconAsImage(iconurl, privateMode, callback) { michael@0: if (iconurl) michael@0: _imageFromURI(NetUtil.newURI(iconurl), privateMode, callback); michael@0: else michael@0: _imageFromURI(faviconSvc.defaultFavicon, privateMode, callback); michael@0: } michael@0: michael@0: // Snaps the given rectangle to be pixel-aligned at the given scale michael@0: function snapRectAtScale(r, scale) { michael@0: let x = Math.floor(r.x * scale); michael@0: let y = Math.floor(r.y * scale); michael@0: let width = Math.ceil((r.x + r.width) * scale) - x; michael@0: let height = Math.ceil((r.y + r.height) * scale) - y; michael@0: michael@0: r.x = x / scale; michael@0: r.y = y / scale; michael@0: r.width = width / scale; michael@0: r.height = height / scale; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// PreviewController michael@0: michael@0: /* michael@0: * This class manages the behavior of the preview. michael@0: * michael@0: * To give greater performance when drawing, the dirty areas of the content michael@0: * window are tracked and drawn on demand into a canvas of the same size. michael@0: * This provides a great increase in responsiveness when drawing a preview michael@0: * for unchanged (or even only slightly changed) tabs. michael@0: * michael@0: * @param win michael@0: * The TabWindow (see below) that owns the preview that this controls michael@0: * @param tab michael@0: * The that this preview is associated with michael@0: */ michael@0: function PreviewController(win, tab) { michael@0: this.win = win; michael@0: this.tab = tab; michael@0: this.linkedBrowser = tab.linkedBrowser; michael@0: this.preview = this.win.createTabPreview(this); michael@0: michael@0: this.linkedBrowser.addEventListener("MozAfterPaint", this, false); michael@0: this.tab.addEventListener("TabAttrModified", this, false); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "canvasPreview", function () { michael@0: let canvas = this.win.win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); michael@0: canvas.mozOpaque = true; michael@0: return canvas; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "dirtyRegion", michael@0: function () { michael@0: let dirtyRegion = Cc["@mozilla.org/gfx/region;1"] michael@0: .createInstance(Ci.nsIScriptableRegion); michael@0: dirtyRegion.init(); michael@0: return dirtyRegion; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "winutils", michael@0: function () { michael@0: let win = tab.linkedBrowser.contentWindow; michael@0: return win.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: }); michael@0: } michael@0: michael@0: PreviewController.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsITaskbarPreviewController, michael@0: Ci.nsIDOMEventListener]), michael@0: destroy: function () { michael@0: this.tab.removeEventListener("TabAttrModified", this, false); michael@0: this.linkedBrowser.removeEventListener("MozAfterPaint", this, false); michael@0: michael@0: // Break cycles, otherwise we end up leaking the window with everything michael@0: // attached to it. michael@0: delete this.win; michael@0: delete this.preview; michael@0: delete this.dirtyRegion; michael@0: }, michael@0: get wrappedJSObject() { michael@0: return this; michael@0: }, michael@0: michael@0: get dirtyRects() { michael@0: let rectstream = this.dirtyRegion.getRects(); michael@0: if (!rectstream) michael@0: return []; michael@0: let rects = []; michael@0: for (let i = 0; i < rectstream.length; i+= 4) { michael@0: let r = {x: rectstream[i], michael@0: y: rectstream[i+1], michael@0: width: rectstream[i+2], michael@0: height: rectstream[i+3]}; michael@0: rects.push(r); michael@0: } michael@0: return rects; michael@0: }, michael@0: michael@0: // Resizes the canvasPreview to 0x0, essentially freeing its memory. michael@0: // updateCanvasPreview() will detect the size mismatch as a resize event michael@0: // the next time it is called. michael@0: resetCanvasPreview: function () { michael@0: this.canvasPreview.width = 0; michael@0: this.canvasPreview.height = 0; michael@0: }, michael@0: michael@0: get zoom() { michael@0: // Note that winutils.fullZoom accounts for "quantization" of the zoom factor michael@0: // from nsIMarkupDocumentViewer due to conversion through appUnits. michael@0: // We do -not- want screenPixelsPerCSSPixel here, because that would -also- michael@0: // incorporate any scaling that is applied due to hi-dpi resolution options. michael@0: return this.winutils.fullZoom; michael@0: }, michael@0: michael@0: // Updates the controller's canvas with the parts of the that need michael@0: // to be redrawn. michael@0: updateCanvasPreview: function () { michael@0: let win = this.linkedBrowser.contentWindow; michael@0: let bx = this.linkedBrowser.boxObject; michael@0: // Check for resize michael@0: if (bx.width != this.canvasPreview.width || michael@0: bx.height != this.canvasPreview.height) { michael@0: // Invalidate the entire area and repaint michael@0: this.onTabPaint({left:0, top:0, right:win.innerWidth, bottom:win.innerHeight}); michael@0: this.canvasPreview.width = bx.width; michael@0: this.canvasPreview.height = bx.height; michael@0: } michael@0: michael@0: // Draw dirty regions michael@0: let ctx = this.canvasPreview.getContext("2d"); michael@0: let scale = this.zoom; michael@0: michael@0: let flags = this.canvasPreviewFlags; michael@0: // The dirty region may include parts that are offscreen so we clip to the michael@0: // canvas area. michael@0: this.dirtyRegion.intersectRect(0, 0, win.innerWidth, win.innerHeight); michael@0: this.dirtyRects.forEach(function (r) { michael@0: // We need to snap the rectangle to be pixel aligned in the destination michael@0: // coordinate space. Otherwise natively themed widgets might not draw. michael@0: snapRectAtScale(r, scale); michael@0: let x = r.x; michael@0: let y = r.y; michael@0: let width = r.width; michael@0: let height = r.height; michael@0: michael@0: ctx.save(); michael@0: ctx.scale(scale, scale); michael@0: ctx.translate(x, y); michael@0: ctx.drawWindow(win, x, y, width, height, "white", flags); michael@0: ctx.restore(); michael@0: }); michael@0: this.dirtyRegion.setToRect(0,0,0,0); michael@0: michael@0: // If we're updating the canvas, then we're in the middle of a peek so michael@0: // don't discard the cache of previews. michael@0: AeroPeek.resetCacheTimer(); michael@0: }, michael@0: michael@0: onTabPaint: function (rect) { michael@0: let x = Math.floor(rect.left), michael@0: y = Math.floor(rect.top), michael@0: width = Math.ceil(rect.right) - x, michael@0: height = Math.ceil(rect.bottom) - y; michael@0: this.dirtyRegion.unionRect(x, y, width, height); michael@0: }, michael@0: michael@0: updateTitleAndTooltip: function () { michael@0: let title = this.win.tabbrowser.getWindowTitleForBrowser(this.linkedBrowser); michael@0: this.preview.title = title; michael@0: this.preview.tooltip = title; michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsITaskbarPreviewController michael@0: michael@0: get width() { michael@0: return this.win.width; michael@0: }, michael@0: michael@0: get height() { michael@0: return this.win.height; michael@0: }, michael@0: michael@0: get thumbnailAspectRatio() { michael@0: let boxObject = this.tab.linkedBrowser.boxObject; michael@0: // Avoid returning 0 michael@0: let tabWidth = boxObject.width || 1; michael@0: // Avoid divide by 0 michael@0: let tabHeight = boxObject.height || 1; michael@0: return tabWidth / tabHeight; michael@0: }, michael@0: michael@0: drawPreview: function (ctx) { michael@0: let self = this; michael@0: this.win.tabbrowser.previewTab(this.tab, function () self.previewTabCallback(ctx)); michael@0: michael@0: // We must avoid having the frame drawn around the window. See bug 520807 michael@0: return false; michael@0: }, michael@0: michael@0: previewTabCallback: function (ctx) { michael@0: // This will extract the resolution-scale component of the scaling we need, michael@0: // which should be applied to both chrome and content; michael@0: // the page zoom component is applied (to content only) within updateCanvasPreview. michael@0: let scale = this.winutils.screenPixelsPerCSSPixel / this.winutils.fullZoom; michael@0: ctx.save(); michael@0: ctx.scale(scale, scale); michael@0: let width = this.win.width; michael@0: let height = this.win.height; michael@0: // Draw our toplevel window michael@0: ctx.drawWindow(this.win.win, 0, 0, width, height, "transparent"); michael@0: michael@0: // XXX (jfkthame): Pending tabs don't seem to draw with the proper scaling michael@0: // unless we use this block of code; but doing this for "normal" (loaded) tabs michael@0: // results in blurry rendering on hidpi systems, so we avoid it if possible. michael@0: // I don't understand why pending and loaded tabs behave differently here... michael@0: // (see bug 857061). michael@0: if (this.tab.hasAttribute("pending")) { michael@0: // Compositor, where art thou? michael@0: // Draw the tab content on top of the toplevel window michael@0: this.updateCanvasPreview(); michael@0: michael@0: let boxObject = this.linkedBrowser.boxObject; michael@0: ctx.translate(boxObject.x, boxObject.y); michael@0: ctx.drawImage(this.canvasPreview, 0, 0); michael@0: } michael@0: michael@0: ctx.restore(); michael@0: }, michael@0: michael@0: drawThumbnail: function (ctx, width, height) { michael@0: this.updateCanvasPreview(); michael@0: michael@0: let scale = width/this.linkedBrowser.boxObject.width; michael@0: ctx.scale(scale, scale); michael@0: ctx.drawImage(this.canvasPreview, 0, 0); michael@0: michael@0: // Don't draw a frame around the thumbnail michael@0: return false; michael@0: }, michael@0: michael@0: onClose: function () { michael@0: this.win.tabbrowser.removeTab(this.tab); michael@0: }, michael@0: michael@0: onActivate: function () { michael@0: this.win.tabbrowser.selectedTab = this.tab; michael@0: michael@0: // Accept activation - this will restore the browser window michael@0: // if it's minimized michael@0: return true; michael@0: }, michael@0: michael@0: //// nsIDOMEventListener michael@0: handleEvent: function (evt) { michael@0: switch (evt.type) { michael@0: case "MozAfterPaint": michael@0: if (evt.originalTarget === this.linkedBrowser.contentWindow) { michael@0: let clientRects = evt.clientRects; michael@0: let length = clientRects.length; michael@0: for (let i = 0; i < length; i++) { michael@0: let r = clientRects.item(i); michael@0: this.onTabPaint(r); michael@0: } michael@0: } michael@0: let preview = this.preview; michael@0: if (preview.visible) michael@0: preview.invalidate(); michael@0: break; michael@0: case "TabAttrModified": michael@0: this.updateTitleAndTooltip(); michael@0: break; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: XPCOMUtils.defineLazyGetter(PreviewController.prototype, "canvasPreviewFlags", michael@0: function () { let canvasInterface = Ci.nsIDOMCanvasRenderingContext2D; michael@0: return canvasInterface.DRAWWINDOW_DRAW_VIEW michael@0: | canvasInterface.DRAWWINDOW_DRAW_CARET michael@0: | canvasInterface.DRAWWINDOW_ASYNC_DECODE_IMAGES michael@0: | canvasInterface.DRAWWINDOW_DO_NOT_FLUSH; michael@0: }); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// TabWindow michael@0: michael@0: /* michael@0: * This class monitors a browser window for changes to its tabs michael@0: * michael@0: * @param win michael@0: * The nsIDOMWindow browser window michael@0: */ michael@0: function TabWindow(win) { michael@0: this.win = win; michael@0: this.tabbrowser = win.gBrowser; michael@0: michael@0: this.previews = new Map(); michael@0: michael@0: for (let i = 0; i < this.tabEvents.length; i++) michael@0: this.tabbrowser.tabContainer.addEventListener(this.tabEvents[i], this, false); michael@0: this.tabbrowser.addTabsProgressListener(this); michael@0: michael@0: for (let i = 0; i < this.winEvents.length; i++) michael@0: this.win.addEventListener(this.winEvents[i], this, false); michael@0: michael@0: AeroPeek.windows.push(this); michael@0: let tabs = this.tabbrowser.tabs; michael@0: for (let i = 0; i < tabs.length; i++) michael@0: this.newTab(tabs[i]); michael@0: michael@0: this.updateTabOrdering(); michael@0: AeroPeek.checkPreviewCount(); michael@0: } michael@0: michael@0: TabWindow.prototype = { michael@0: _enabled: false, michael@0: tabEvents: ["TabOpen", "TabClose", "TabSelect", "TabMove"], michael@0: winEvents: ["tabviewshown", "tabviewhidden"], michael@0: michael@0: destroy: function () { michael@0: this._destroying = true; michael@0: michael@0: let tabs = this.tabbrowser.tabs; michael@0: michael@0: this.tabbrowser.removeTabsProgressListener(this); michael@0: for (let i = 0; i < this.tabEvents.length; i++) michael@0: this.tabbrowser.tabContainer.removeEventListener(this.tabEvents[i], this, false); michael@0: michael@0: for (let i = 0; i < this.winEvents.length; i++) michael@0: this.win.removeEventListener(this.winEvents[i], this, false); michael@0: michael@0: for (let i = 0; i < tabs.length; i++) michael@0: this.removeTab(tabs[i]); michael@0: michael@0: let idx = AeroPeek.windows.indexOf(this.win.gTaskbarTabGroup); michael@0: AeroPeek.windows.splice(idx, 1); michael@0: AeroPeek.checkPreviewCount(); michael@0: }, michael@0: michael@0: get width () { michael@0: return this.win.innerWidth; michael@0: }, michael@0: get height () { michael@0: return this.win.innerHeight; michael@0: }, michael@0: michael@0: // Invoked when the given tab is added to this window michael@0: newTab: function (tab) { michael@0: let controller = new PreviewController(this, tab); michael@0: // It's OK to add the preview now while the favicon still loads. michael@0: this.previews.set(tab, controller.preview); michael@0: AeroPeek.addPreview(controller.preview); michael@0: // updateTitleAndTooltip relies on having controller.preview which is lazily resolved. michael@0: // Now that we've updated this.previews, it will resolve successfully. michael@0: controller.updateTitleAndTooltip(); michael@0: }, michael@0: michael@0: createTabPreview: function (controller) { michael@0: let docShell = this.win michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShell); michael@0: let preview = AeroPeek.taskbar.createTaskbarTabPreview(docShell, controller); michael@0: preview.visible = AeroPeek.enabled; michael@0: preview.active = this.tabbrowser.selectedTab == controller.tab; michael@0: // Grab the default favicon michael@0: getFaviconAsImage(null, PrivateBrowsingUtils.isWindowPrivate(this.win), function (img) { michael@0: // It is possible that we've already gotten the real favicon, so make sure michael@0: // we have not set one before setting this default one. michael@0: if (!preview.icon) michael@0: preview.icon = img; michael@0: }); michael@0: michael@0: return preview; michael@0: }, michael@0: michael@0: // Invoked when the given tab is closed michael@0: removeTab: function (tab) { michael@0: let preview = this.previewFromTab(tab); michael@0: preview.active = false; michael@0: preview.visible = false; michael@0: preview.move(null); michael@0: preview.controller.wrappedJSObject.destroy(); michael@0: michael@0: this.previews.delete(tab); michael@0: AeroPeek.removePreview(preview); michael@0: }, michael@0: michael@0: get enabled () { michael@0: return this._enabled; michael@0: }, michael@0: michael@0: set enabled (enable) { michael@0: this._enabled = enable; michael@0: // Because making a tab visible requires that the tab it is next to be michael@0: // visible, it is far simpler to unset the 'next' tab and recreate them all michael@0: // at once. michael@0: for (let [tab, preview] of this.previews) { michael@0: preview.move(null); michael@0: preview.visible = enable; michael@0: } michael@0: this.updateTabOrdering(); michael@0: }, michael@0: michael@0: previewFromTab: function (tab) { michael@0: return this.previews.get(tab); michael@0: }, michael@0: michael@0: updateTabOrdering: function () { michael@0: let previews = this.previews; michael@0: let tabs = this.tabbrowser.tabs; michael@0: michael@0: // Previews are internally stored using a map, so we need to iterate the michael@0: // tabbrowser's array of tabs to retrieve previews in the same order. michael@0: let inorder = [previews.get(t) for (t of tabs) if (previews.has(t))]; michael@0: michael@0: // Since the internal taskbar array has not yet been updated we must force michael@0: // on it the sorting order of our local array. To do so we must walk michael@0: // the local array backwards, otherwise we would send move requests in the michael@0: // wrong order. See bug 522610 for details. michael@0: for (let i = inorder.length - 1; i >= 0; i--) { michael@0: inorder[i].move(inorder[i + 1] || null); michael@0: } michael@0: }, michael@0: michael@0: //// nsIDOMEventListener michael@0: handleEvent: function (evt) { michael@0: let tab = evt.originalTarget; michael@0: switch (evt.type) { michael@0: case "TabOpen": michael@0: this.newTab(tab); michael@0: this.updateTabOrdering(); michael@0: break; michael@0: case "TabClose": michael@0: this.removeTab(tab); michael@0: this.updateTabOrdering(); michael@0: break; michael@0: case "TabSelect": michael@0: this.previewFromTab(tab).active = true; michael@0: break; michael@0: case "TabMove": michael@0: this.updateTabOrdering(); michael@0: break; michael@0: case "tabviewshown": michael@0: this.enabled = false; michael@0: break; michael@0: case "tabviewhidden": michael@0: if (!AeroPeek._prefenabled) michael@0: return; michael@0: this.enabled = true; michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: //// Browser progress listener michael@0: onLinkIconAvailable: function (aBrowser, aIconURL) { michael@0: let self = this; michael@0: getFaviconAsImage(aIconURL, PrivateBrowsingUtils.isWindowPrivate(this.win), function (img) { michael@0: let index = self.tabbrowser.browsers.indexOf(aBrowser); michael@0: // Only add it if we've found the index. The tab could have closed! michael@0: if (index != -1) { michael@0: let tab = self.tabbrowser.tabs[index]; michael@0: self.previews.get(tab).icon = img; michael@0: } michael@0: }); michael@0: } michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// AeroPeek michael@0: michael@0: /* michael@0: * This object acts as global storage and external interface for this feature. michael@0: * It maintains the values of the prefs. michael@0: */ michael@0: this.AeroPeek = { michael@0: available: false, michael@0: // Does the pref say we're enabled? michael@0: _prefenabled: true, michael@0: michael@0: _enabled: true, michael@0: michael@0: // nsITaskbarTabPreview array michael@0: previews: [], michael@0: michael@0: // TabWindow array michael@0: windows: [], michael@0: michael@0: // nsIWinTaskbar service michael@0: taskbar: null, michael@0: michael@0: // Maximum number of previews michael@0: maxpreviews: 20, michael@0: michael@0: // Length of time in seconds that previews are cached michael@0: cacheLifespan: 20, michael@0: michael@0: initialize: function () { michael@0: if (!(WINTASKBAR_CONTRACTID in Cc)) michael@0: return; michael@0: this.taskbar = Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar); michael@0: this.available = this.taskbar.available; michael@0: if (!this.available) michael@0: return; michael@0: michael@0: this.prefs.addObserver(TOGGLE_PREF_NAME, this, false); michael@0: this.prefs.addObserver(DISABLE_THRESHOLD_PREF_NAME, this, false); michael@0: this.prefs.addObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this, false); michael@0: michael@0: this.cacheLifespan = this.prefs.getIntPref(CACHE_EXPIRATION_TIME_PREF_NAME); michael@0: michael@0: this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME); michael@0: michael@0: this.enabled = this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME); michael@0: }, michael@0: michael@0: destroy: function destroy() { michael@0: this._enabled = false; michael@0: michael@0: this.prefs.removeObserver(TOGGLE_PREF_NAME, this); michael@0: this.prefs.removeObserver(DISABLE_THRESHOLD_PREF_NAME, this); michael@0: this.prefs.removeObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this); michael@0: michael@0: if (this.cacheTimer) michael@0: this.cacheTimer.cancel(); michael@0: }, michael@0: michael@0: get enabled() { michael@0: return this._enabled; michael@0: }, michael@0: michael@0: set enabled(enable) { michael@0: if (this._enabled == enable) michael@0: return; michael@0: michael@0: this._enabled = enable; michael@0: michael@0: this.windows.forEach(function (win) { michael@0: win.enabled = enable; michael@0: }); michael@0: }, michael@0: michael@0: addPreview: function (preview) { michael@0: this.previews.push(preview); michael@0: this.checkPreviewCount(); michael@0: }, michael@0: michael@0: removePreview: function (preview) { michael@0: let idx = this.previews.indexOf(preview); michael@0: this.previews.splice(idx, 1); michael@0: this.checkPreviewCount(); michael@0: }, michael@0: michael@0: checkPreviewCount: function () { michael@0: if (this.previews.length > this.maxpreviews) michael@0: this.enabled = false; michael@0: else michael@0: this.enabled = this._prefenabled; michael@0: }, michael@0: michael@0: onOpenWindow: function (win) { michael@0: // This occurs when the taskbar service is not available (xp, vista) michael@0: if (!this.available) michael@0: return; michael@0: michael@0: win.gTaskbarTabGroup = new TabWindow(win); michael@0: }, michael@0: michael@0: onCloseWindow: function (win) { michael@0: // This occurs when the taskbar service is not available (xp, vista) michael@0: if (!this.available) michael@0: return; michael@0: michael@0: win.gTaskbarTabGroup.destroy(); michael@0: delete win.gTaskbarTabGroup; michael@0: michael@0: if (this.windows.length == 0) michael@0: this.destroy(); michael@0: }, michael@0: michael@0: resetCacheTimer: function () { michael@0: this.cacheTimer.cancel(); michael@0: this.cacheTimer.init(this, 1000*this.cacheLifespan, Ci.nsITimer.TYPE_ONE_SHOT); michael@0: }, michael@0: michael@0: //// nsIObserver michael@0: observe: function (aSubject, aTopic, aData) { michael@0: switch (aTopic) { michael@0: case "nsPref:changed": michael@0: if (aData == CACHE_EXPIRATION_TIME_PREF_NAME) michael@0: break; michael@0: michael@0: if (aData == TOGGLE_PREF_NAME) michael@0: this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME); michael@0: else if (aData == DISABLE_THRESHOLD_PREF_NAME) michael@0: this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME); michael@0: // Might need to enable/disable ourselves michael@0: this.checkPreviewCount(); michael@0: break; michael@0: case "timer-callback": michael@0: this.previews.forEach(function (preview) { michael@0: let controller = preview.controller.wrappedJSObject; michael@0: controller.resetCanvasPreview(); michael@0: }); michael@0: break; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: XPCOMUtils.defineLazyGetter(AeroPeek, "cacheTimer", function () michael@0: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer) michael@0: ); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(AeroPeek, "prefs", michael@0: "@mozilla.org/preferences-service;1", michael@0: "nsIPrefBranch"); michael@0: michael@0: AeroPeek.initialize();