WindowsPreviewPerTab.jsm
Go to the documentation of this file.
1 /* vim: se cin sw=2 ts=2 et filetype=javascript :
2  * ***** BEGIN LICENSE BLOCK *****
3  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4  *
5  * The contents of this file are subject to the Mozilla Public License Version
6  * 1.1 (the "License"); you may not use this file except in compliance with
7  * the License. You may obtain a copy of the License at
8  * http://www.mozilla.org/MPL/
9  *
10  * Software distributed under the License is distributed on an "AS IS" basis,
11  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12  * for the specific language governing rights and limitations under the
13  * License.
14  *
15  * The Original Code is Mozilla code.
16  *
17  * The Initial Developer of the Original Code is
18  * Mozilla Corporation.
19  * Portions created by the Initial Developer are Copyright (C) 2009
20  * the Initial Developer. All Rights Reserved.
21  *
22  * Contributor(s):
23  * Rob Arnold <robarnold@cmu.edu> (original author)
24  *
25  * Alternatively, the contents of this file may be used under the terms of
26  * either the GNU General Public License Version 2 or later (the "GPL"), or
27  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28  * in which case the provisions of the GPL or the LGPL are applicable instead
29  * of those above. If you wish to allow use of your version of this file only
30  * under the terms of either the GPL or the LGPL, and not to allow others to
31  * use your version of this file under the terms of the MPL, indicate your
32  * decision by deleting the provisions above and replace them with the notice
33  * and other provisions required by the GPL or the LGPL. If you do not delete
34  * the provisions above, a recipient may use your version of this file under
35  * the terms of any one of the MPL, the GPL or the LGPL.
36  *
37  * ***** END LICENSE BLOCK ***** */
38 /*
39  * This module implements the front end behavior for AeroPeek. Starting in
40  * Windows Vista, the taskbar began showing live thumbnail previews of windows
41  * when the user hovered over the window icon in the taskbar. Starting with
42  * Windows 7, the taskbar allows an application to expose its tabbed interface
43  * in the taskbar by showing thumbnail previews rather than the default window
44  * preview. Additionally, when a user hovers over a thumbnail (tab or window),
45  * they are shown a live preview of the window (or tab + its containing window).
46  *
47  * In Windows 7, a title, icon, close button and optional toolbar are shown for
48  * each preview. This feature does not make use of the toolbar. For window
49  * previews, the title is the window title and the icon the window icon. For
50  * tab previews, the title is the page title and the page's favicon. In both
51  * cases, the close button "does the right thing."
52  *
53  * The primary objects behind this feature are nsITaskbarTabPreview and
54  * nsITaskbarPreviewController. Each preview has a controller. The controller
55  * responds to the user's interactions on the taskbar and provides the required
56  * data to the preview for determining the size of the tab and thumbnail. The
57  * PreviewController class implements this interface. The preview will request
58  * the controller to provide a thumbnail or preview when the user interacts with
59  * the taskbar. To reduce the overhead of drawing the tab area, the controller
60  * implementation caches the tab's contents in a <canvas> element. If no
61  * previews or thumbnails have been requested for some time, the controller will
62  * discard its cached tab contents.
63  *
64  * Screen real estate is limited so when there are too many thumbnails to fit
65  * on the screen, the taskbar stops displaying thumbnails and instead displays
66  * just the title, icon and close button in a similar fashion to previous
67  * versions of the taskbar. If there are still too many previews to fit on the
68  * screen, the taskbar resorts to a scroll up and scroll down button pair to let
69  * the user scroll through the list of tabs. Since this is undoubtedly
70  * inconvenient for users with many tabs, the AeroPeek objects turns off all of
71  * the tab previews. This tells the taskbar to revert to one preview per window.
72  * If the number of tabs falls below this magic threshold, the preview-per-tab
73  * behavior returns. There is no reliable way to determine when the scroll
74  * buttons appear on the taskbar, so a magic pref-controlled number determines
75  * when this threshold has been crossed.
76  */
77 var EXPORTED_SYMBOLS = ["AeroPeek"];
78 
79 const Cc = Components.classes;
80 const Ci = Components.interfaces;
81 const Cu = Components.utils;
82 
83 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
84 Cu.import("resource://gre/modules/NetUtil.jsm");
85 
86 // Pref to enable/disable preview-per-tab
87 const TOGGLE_PREF_NAME = "browser.taskbar.previews.enable";
88 // Pref to determine the magic auto-disable threshold
89 const DISABLE_THRESHOLD_PREF_NAME = "browser.taskbar.previews.max";
90 // Pref to control the time in seconds that tab contents live in the cache
91 const CACHE_EXPIRATION_TIME_PREF_NAME = "browser.taskbar.previews.cachetime";
92 
93 const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
94 
97 XPCOMUtils.defineLazyServiceGetter(this, "ioSvc",
98  "@mozilla.org/network/io-service;1",
99  "nsIIOService");
100 XPCOMUtils.defineLazyServiceGetter(this, "imgTools",
101  "@mozilla.org/image/tools;1",
102  "imgITools");
103 XPCOMUtils.defineLazyServiceGetter(this, "faviconSvc",
104  "@mozilla.org/browser/favicon-service;1",
105  "nsIFaviconService");
106 
107 // nsIURI -> imgIContainer
109  let channel = ioSvc.newChannelFromURI(uri);
110  NetUtil.asyncFetch(channel, function(inputStream, resultCode) {
111  if (!Components.isSuccessCode(resultCode))
112  return;
113  try {
114  let out_img = { value: null };
115  imgTools.decodeImageData(inputStream, channel.contentType, out_img);
116  callback(out_img.value);
117  } catch (e) {
118  // We failed, so use the default favicon (only if this wasn't the default
119  // favicon).
120  let defaultURI = faviconSvc.defaultFavicon;
121  if (!defaultURI.equals(uri))
122  _imageFromURI(defaultURI, callback);
123  }
124  });
125 }
126 
127 // string? -> imgIContainer
128 function getFaviconAsImage(iconurl, callback) {
129  if (iconurl)
130  _imageFromURI(NetUtil.newURI(iconurl), callback);
131  else
132  _imageFromURI(faviconSvc.defaultFavicon, callback);
133 }
134 
137 
138 /*
139  * This class manages the behavior of the preview.
140  *
141  * To give greater performance when drawing, the dirty areas of the content
142  * window are tracked and drawn on demand into a canvas of the same size.
143  * This provides a great increase in responsiveness when drawing a preview
144  * for unchanged (or even only slightly changed) tabs.
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  */
152  this.win = win;
153  this.tab = tab;
154  this.linkedBrowser = tab.linkedBrowser;
155 
156  this.linkedBrowser.addEventListener("MozAfterPaint", this, false);
157  this.linkedBrowser.addEventListener("DOMTitleChanged", this, false);
158  // pageshow is needed for when a tab is dragged across windows.
159  this.linkedBrowser.addEventListener("pageshow", this, false);
160 
161  // Cannot perform the lookup during construction. See TabWindow.newTab
162  XPCOMUtils.defineLazyGetter(this, "preview", function () this.win.previewFromTab(this.tab));
163 
164  XPCOMUtils.defineLazyGetter(this, "canvasPreview", function ()
165  this.win.win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"));
166 
167  XPCOMUtils.defineLazyGetter(this, "dirtyRegion",
168  function () {
169  let dirtyRegion = Cc["@mozilla.org/gfx/region;1"]
170  .createInstance(Ci.nsIScriptableRegion);
171  dirtyRegion.init();
172  return dirtyRegion;
173  });
174 }
175 
176 PreviewController.prototype = {
177  QueryInterface: XPCOMUtils.generateQI([Ci.nsITaskbarPreviewController,
178  Ci.nsIDOMEventListener]),
179  destroy: function () {
180  this.linkedBrowser.removeEventListener("pageshow", this, false);
181  this.linkedBrowser.removeEventListener("DOMTitleChanged", this, false);
182  this.linkedBrowser.removeEventListener("MozAfterPaint", this, false);
183  },
184  get wrappedJSObject() {
185  return this;
186  },
187 
188  get dirtyRects() {
189  let rectstream = this.dirtyRegion.getRects();
190  if (!rectstream)
191  return [];
192  let rects = [];
193  for (let i = 0; i < rectstream.length; i+= 4) {
194  let r = {x: rectstream[i],
195  y: rectstream[i+1],
196  width: rectstream[i+2],
197  height: rectstream[i+3]};
198  rects.push(r);
199  }
200  return rects;
201  },
202 
203  // Resizes the canvasPreview to 0x0, essentially freeing its memory.
204  // updateCanvasPreview() will detect the size mismatch as a resize event
205  // the next time it is called.
206  resetCanvasPreview: function () {
207  this.canvasPreview.width = 0;
208  this.canvasPreview.height = 0;
209  },
210 
211  // Updates the controller's canvas with the parts of the <browser> that need
212  // to be redrawn.
213  updateCanvasPreview: function () {
214  let win = this.linkedBrowser.contentWindow;
215  let bx = this.linkedBrowser.boxObject;
216  // Check for resize
217  if (bx.width != this.canvasPreview.width ||
218  bx.height != this.canvasPreview.height) {
219  // Invalidate the entire area and repaint
220  this.onTabPaint({left:0, top:0, width:bx.width, height:bx.height});
221  this.canvasPreview.width = bx.width;
222  this.canvasPreview.height = bx.height;
223  }
224 
225  // Draw dirty regions
226  let ctx = this.canvasPreview.getContext("2d");
227  let flags = this.canvasPreviewFlags;
228  // width/height are occasionally bogus and too large for drawWindow
229  // so we clip to the canvas region
230  this.dirtyRegion.intersectRect(0, 0, bx.width, bx.height);
231  this.dirtyRects.forEach(function (r) {
232  let x = r.x;
233  let y = r.y;
234  let width = r.width;
235  let height = r.height;
236  ctx.save();
237  ctx.translate(x, y);
238  ctx.drawWindow(win, x, y, width, height, "white", flags);
239  ctx.restore();
240  });
241  this.dirtyRegion.setToRect(0,0,0,0);
242 
243  // If we're updating the canvas, then we're in the middle of a peek so
244  // don't discard the cache of previews.
245  AeroPeek.resetCacheTimer();
246  },
247 
248  onTabPaint: function (rect) {
249  // Ignore spurious dirty rects
250  if (!rect.width || !rect.height)
251  return;
252 
253  let r = { x: Math.floor(rect.left),
254  y: Math.floor(rect.top),
255  width: Math.ceil(rect.width),
256  height: Math.ceil(rect.height)
257  };
258  this.dirtyRegion.unionRect(r.x, r.y, r.width, r.height);
259  },
260 
263 
264  get width() {
265  return this.win.width;
266  },
267 
268  get height() {
269  return this.win.height;
270  },
271 
272  get thumbnailAspectRatio() {
273  let boxObject = this.tab.linkedBrowser.boxObject;
274  // Avoid returning 0
275  let tabWidth = boxObject.width || 1;
276  // Avoid divide by 0
277  let tabHeight = boxObject.height || 1;
278  return tabWidth / tabHeight;
279  },
280 
281  drawPreview: function (ctx) {
282  let self = this;
283  this.win.tabbrowser.previewTab(this.tab, function () self.previewTabCallback(ctx));
284 
285  // We want a frame drawn around the preview
286  return true;
287  },
288 
289  previewTabCallback: function (ctx) {
290  let width = this.win.width;
291  let height = this.win.height;
292  // Draw our toplevel window
293  ctx.drawWindow(this.win.win, 0, 0, width, height, "transparent");
294 
295  // Compositor, where art thou?
296  // Draw the tab content on top of the toplevel window
297  this.updateCanvasPreview();
298 
299  let boxObject = this.linkedBrowser.boxObject;
300  ctx.translate(boxObject.x, boxObject.y);
301  ctx.drawImage(this.canvasPreview, 0, 0);
302  },
303 
304  drawThumbnail: function (ctx, width, height) {
305  this.updateCanvasPreview();
306 
307  let scale = width/this.linkedBrowser.boxObject.width;
308  ctx.scale(scale, scale);
309  ctx.drawImage(this.canvasPreview, 0, 0);
310 
311  // Don't draw a frame around the thumbnail
312  return false;
313  },
314 
315  onClose: function () {
316  this.win.tabbrowser.removeTab(this.tab);
317  },
318 
319  onActivate: function () {
320  this.win.tabbrowser.selectedTab = this.tab;
321 
322  // Accept activation - this will restore the browser window
323  // if it's minimized
324  return true;
325  },
326 
328  handleEvent: function (evt) {
329  switch (evt.type) {
330  case "MozAfterPaint":
331  if (evt.originalTarget === this.linkedBrowser.contentWindow) {
332  let clientRects = evt.clientRects;
333  let length = clientRects.length;
334  for (let i = 0; i < length; i++) {
335  let r = clientRects.item(i);
336  this.onTabPaint(r);
337  }
338  }
339  let preview = this.preview;
340  if (preview.visible)
341  preview.invalidate();
342  break;
343  case "pageshow":
344  case "DOMTitleChanged":
345  // The tab's label is sometimes empty when dragging tabs between windows
346  // so we force the tab title to be updated (see bug 520579)
347  this.win.tabbrowser.setTabTitle(this.tab);
348  let title = this.tab.label;
349  this.preview.title = title;
350  this.preview.tooltip = title;
351  break;
352  }
353  }
354 };
355 
356 XPCOMUtils.defineLazyGetter(PreviewController.prototype, "canvasPreviewFlags",
357  function () { let canvasInterface = Ci.nsIDOMCanvasRenderingContext2D;
358  return canvasInterface.DRAWWINDOW_DRAW_VIEW
359  | canvasInterface.DRAWWINDOW_DRAW_CARET
360  | canvasInterface.DRAWWINDOW_DO_NOT_FLUSH;
361 });
362 
365 
366 /*
367  * This class monitors a browser window for changes to its tabs
368  *
369  * @param win
370  * The nsIDOMWindow browser window
371  */
372 function TabWindow(win) {
373  this.win = win;
374  this.tabbrowser = win.gBrowser;
375 
376  this.previews = [];
377 
378  for (let i = 0; i < this.events.length; i++)
379  this.tabbrowser.tabContainer.addEventListener(this.events[i], this, false);
380  this.tabbrowser.addTabsProgressListener(this);
381 
382 
383  AeroPeek.windows.push(this);
384  let tabs = this.tabbrowser.mTabs;
385  for (let i = 0; i < tabs.length; i++)
386  this.newTab(tabs[i]);
387 
388  this.updateTabOrdering();
389  AeroPeek.checkPreviewCount();
390 }
391 
392 TabWindow.prototype = {
393  _enabled: false,
394  events: ["TabOpen", "TabClose", "TabSelect", "TabMove"],
395 
396  destroy: function () {
397  this._destroying = true;
398 
399  let tabs = this.tabbrowser.mTabs;
400 
401  for (let i = 0; i < this.events.length; i++)
402  this.tabbrowser.tabContainer.removeEventListener(this.events[i], this, false);
403 
404  for (let i = 0; i < tabs.length; i++)
405  this.removeTab(tabs[i]);
406 
407  let idx = AeroPeek.windows.indexOf(this.win.gTaskbarTabGroup);
408  AeroPeek.windows.splice(idx, 1);
409  AeroPeek.checkPreviewCount();
410  },
411 
412  get width () {
413  return this.win.innerWidth;
414  },
415  get height () {
416  return this.win.innerHeight;
417  },
418 
419  // Invoked when the given tab is added to this window
420  newTab: function (tab) {
421  let controller = new PreviewController(this, tab);
422  let preview = AeroPeek.taskbar.createTaskbarTabPreview(this.tabbrowser.docShell, controller);
423  preview.title = tab.label;
424  preview.tooltip = tab.label;
425  preview.visible = AeroPeek.enabled;
426  preview.active = this.tabbrowser.selectedTab == tab;
427  // Grab the default favicon
428  getFaviconAsImage(null, function (img) {
429  // It is possible that we've already gotten the real favicon, so make sure
430  // we have not set one before setting this default one.
431  if (!preview.icon)
432  preview.icon = img;
433  });
434 
435  // It's OK to add the preview now while the favicon still loads.
436  this.previews.splice(tab._tPos, 0, preview);
437  AeroPeek.addPreview(preview);
438  },
439 
440  // Invoked when the given tab is closed
441  removeTab: function (tab) {
442  let preview = this.previewFromTab(tab);
443  preview.active = false;
444  preview.visible = false;
445  preview.move(null);
446  preview.controller.wrappedJSObject.destroy();
447 
448  // We don't want to splice from the array if the tabs aren't being removed
449  // from the tab bar as well (as is the case when the window closes).
450  if (!this._destroying)
451  this.previews.splice(tab._tPos, 1);
452  AeroPeek.removePreview(preview);
453  },
454 
455  get enabled () {
456  return this._enabled;
457  },
458 
459  set enabled (enable) {
460  this._enabled = enable;
461  // Because making a tab visible requires that the tab it is next to be
462  // visible, it is far simpler to unset the 'next' tab and recreate them all
463  // at once.
464  this.previews.forEach(function (preview) {
465  preview.move(null);
466  preview.visible = enable;
467  });
468  this.updateTabOrdering();
469  },
470 
471  previewFromTab: function (tab) {
472  return this.previews[tab._tPos];
473  },
474 
475  updateTabOrdering: function () {
476  for (let i = 0; i < this.previews.length; i++) {
477  let p = this.previews[i];
478  let next = i == this.previews.length - 1 ? null : this.previews[i+1];
479  p.move(next);
480  }
481  },
482 
484  handleEvent: function (evt) {
485  let tab = evt.originalTarget;
486  switch (evt.type) {
487  case "TabOpen":
488  this.newTab(tab);
489  this.updateTabOrdering();
490  break;
491  case "TabClose":
492  this.removeTab(tab);
493  this.updateTabOrdering();
494  break;
495  case "TabSelect":
496  this.previewFromTab(tab).active = true;
497  break;
498  case "TabMove":
499  let oldPos = evt.detail;
500  let newPos = tab._tPos;
501  let preview = this.previews[oldPos];
502  this.previews.splice(oldPos, 1);
503  this.previews.splice(newPos, 0, preview);
504  this.updateTabOrdering();
505  break;
506  }
507  },
508 
510  onLocationChange: function () {
511  },
512  onProgressChange: function () {
513  },
514  onSecurityChange: function () {
515  },
516  onStateChange: function () {
517  },
518  onStatusChange: function () {
519  },
520  onLinkIconAvailable: function (aBrowser) {
521  let self = this;
522  getFaviconAsImage(aBrowser.mIconURL, function (img) {
523  let index = self.tabbrowser.browsers.indexOf(aBrowser);
524  // Only add it if we've found the index. The tab could have closed!
525  if (index != -1)
526  self.previews[index].icon = img;
527  });
528  }
529 }
530 
533 
534 /*
535  * This object acts as global storage and external interface for this feature.
536  * It maintains the values of the prefs.
537  */
538 var AeroPeek = {
539  available: false,
540  // Does the pref say we're enabled?
541  _prefenabled: true,
542 
543  _enabled: true,
544 
545  // nsITaskbarTabPreview array
546  previews: [],
547 
548  // TabWindow array
549  windows: [],
550 
551  // nsIWinTaskbar service
552  taskbar: null,
553 
554  // Maximum number of previews
555  maxpreviews: 20,
556 
557  // Length of time in seconds that previews are cached
558  cacheLifespan: 20,
559 
560  initialize: function () {
561  if (!(WINTASKBAR_CONTRACTID in Cc))
562  return;
563  this.taskbar = Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar);
564  this.available = this.taskbar.available;
565  if (!this.available)
566  return;
567 
568  this.prefs.addObserver(TOGGLE_PREF_NAME, this, false);
569  this.prefs.addObserver(DISABLE_THRESHOLD_PREF_NAME, this, false);
570  this.prefs.addObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this, false);
571 
572  this.cacheLifespan = this.prefs.getIntPref(CACHE_EXPIRATION_TIME_PREF_NAME);
573 
574  this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
575 
576  this.enabled = this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME);
577  },
578 
579  get enabled() {
580  return this._enabled;
581  },
582 
583  set enabled(enable) {
584  if (this._enabled == enable)
585  return;
586 
587  this._enabled = enable;
588 
589  this.windows.forEach(function (win) {
590  win.enabled = enable;
591  });
592  },
593 
594  addPreview: function (preview) {
595  this.previews.push(preview);
596  this.checkPreviewCount();
597  },
598 
599  removePreview: function (preview) {
600  let idx = this.previews.indexOf(preview);
601  this.previews.splice(idx, 1);
602  this.checkPreviewCount();
603  },
604 
605  checkPreviewCount: function () {
606  if (this.previews.length > this.maxpreviews)
607  this.enabled = false;
608  else
609  this.enabled = this._prefenabled;
610  },
611 
612  onOpenWindow: function (win) {
613  // This occurs when the taskbar service is not available (xp, vista)
614  if (!this.available)
615  return;
616 
617  win.gTaskbarTabGroup = new TabWindow(win);
618  },
619 
620  onCloseWindow: function (win) {
621  // This occurs when the taskbar service is not available (xp, vista)
622  if (!this.available)
623  return;
624 
625  win.gTaskbarTabGroup.destroy();
626  win.gTaskbarTabGroup = null;
627  },
628 
629  resetCacheTimer: function () {
630  this.cacheTimer.cancel();
631  this.cacheTimer.init(this, 1000*this.cacheLifespan, Ci.nsITimer.TYPE_ONE_SHOT);
632  },
633 
635  observe: function (aSubject, aTopic, aData) {
636  switch (aTopic) {
637  case "nsPref:changed":
639  break;
640 
641  if (aData == TOGGLE_PREF_NAME)
642  this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME);
644  this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
645  // Might need to enable/disable ourselves
646  this.checkPreviewCount();
647  break;
648  case "timer-callback":
649  this.previews.forEach(function (preview) {
650  let controller = preview.controller.wrappedJSObject;
651  controller.resetCanvasPreview();
652  });
653  break;
654  }
655  }
656 };
657 
658 XPCOMUtils.defineLazyGetter(AeroPeek, "cacheTimer", function ()
659  Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer)
660 );
661 
662 XPCOMUtils.defineLazyServiceGetter(AeroPeek, "prefs",
663  "@mozilla.org/preferences-service;1",
664  "nsIPrefBranch2");
665 
666 AeroPeek.initialize();
const WINTASKBAR_CONTRACTID
AeroPeek initialize()
var windows
sbDeviceFirmwareAutoCheckForUpdate prototype flags
var EXPORTED_SYMBOLS
const Cc
Element Properties events
sidebarFactory createInstance
Definition: nsSidebar.js:351
sbOSDControlService prototype QueryInterface
const TOGGLE_PREF_NAME
function PreviewController(win, tab)
var tab
function width(ele) rect(ele).width
const Ci
function getFaviconAsImage(iconurl, callback)
var tabbrowser
var tabs
function TabWindow(win)
function rect(ele) ele.getBoundingClientRect()
grep callback
return null
Definition: FeedWriter.js:1143
_updateDatepicker height
var uri
Definition: FeedWriter.js:1135
_updateTextAndScrollDataForTab aBrowser
var prefs
Definition: FeedWriter.js:1169
countRef value
Definition: FeedWriter.js:1423
ContinuingWebProgressListener prototype onStateChange
const DISABLE_THRESHOLD_PREF_NAME
const CACHE_EXPIRATION_TIME_PREF_NAME
_getSelectedPageStyle s i
const Cu
_updateTextAndScrollDataForFrame aData
function next()
sbDeviceFirmwareAutoCheckForUpdate prototype observe
function _imageFromURI(uri, callback)