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();