Bug 395980 - Implement MRU order Ctrl+Tab panel with previews. r=mconnor
authorDão Gottwald <dao@mozilla.com>
Tue, 15 Jul 2008 18:11:00 +0200
changeset 15952 613a1fe1d5f9
parent 15951 6db0b5d30af1
child 15953 7b209553f0b5
push id634
push userdgottwald@mozilla.com
push dateTue, 15 Jul 2008 16:11:20 +0000
treeherdermozilla-central@613a1fe1d5f9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconnor
bugs395980
milestone1.9.1a1pre
Bug 395980 - Implement MRU order Ctrl+Tab panel with previews. r=mconnor
browser/app/profile/firefox.js
browser/base/content/browser-tabPreviews.js
browser/base/content/browser.css
browser/base/content/browser.js
browser/base/content/browser.xul
browser/themes/gnomestripe/browser/browser.css
browser/themes/pinstripe/browser/browser.css
browser/themes/winstripe/browser/browser.css
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -322,16 +322,19 @@ pref("browser.tabs.closeButtons", 1);
 
 // When tabs opened by links in other tabs via a combination of 
 // browser.link.open_newwindow being set to 3 and target="_blank" etc are
 // closed:
 // true   return to the tab that opened this tab (its owner)
 // false  return to the adjacent tab (old default)
 pref("browser.tabs.selectOwnerOnClose", true);
 
+pref("browser.ctrlTab.previewsCount", 3);
+pref("browser.ctrlTab.smoothScroll", true);
+
 // Default bookmark sorting
 pref("browser.bookmarks.sort.direction", "descending");
 pref("browser.bookmarks.sort.resource", "rdf:http://home.netscape.com/NC-rdf#Name");
 
 // By default, do not export HTML at shutdown.
 // If true, at shutdown the bookmarks in your menu and toolbar will
 // be exported as HTML to the bookmarks.html file.
 pref("browser.bookmarks.autoExportHTML",          false);
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-tabPreviews.js
@@ -0,0 +1,518 @@
+/*
+#ifdef 0
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Tab Previews.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Dão Gottwald <dao@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK *****
+#endif
+ */
+
+/**
+ * Tab previews utility, produces thumbnails
+ */
+var tabPreviews = {
+  aspectRatio: 0.6875, // 16:11
+  init: function () {
+    this.width = Math.ceil(screen.availWidth / 7.5);
+    this.height = Math.round(this.width * this.aspectRatio);
+
+    gBrowser.tabContainer.addEventListener("TabSelect", this, false);
+    gBrowser.tabContainer.addEventListener("SSTabRestored", this, false);
+  },
+  uninit: function () {
+    gBrowser.tabContainer.removeEventListener("TabSelect", this, false);
+    gBrowser.tabContainer.removeEventListener("SSTabRestored", this, false);
+    this._selectedTab = null;
+  },
+  get: function (aTab) {
+    return aTab.__thumbnail || this.capture(aTab, !aTab.hasAttribute("busy"));
+  },
+  capture: function (aTab, aStore) {
+    var win = aTab.linkedBrowser.contentWindow;
+    var thumbnail = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+    thumbnail.mozOpaque = true;
+    thumbnail.height = this.height;
+    thumbnail.width = this.width;
+    var ctx = thumbnail.getContext("2d");
+    var widthScale = this.width / win.innerWidth;
+    ctx.scale(widthScale, widthScale);
+    ctx.drawWindow(win, win.scrollX, win.scrollY,
+                   win.innerWidth, win.innerWidth * this.aspectRatio, "rgb(255,255,255)");
+    var data = thumbnail.toDataURL("image/jpeg", "quality=60");
+    if (aStore)
+      aTab.__thumbnail = data;
+    return data;
+  },
+  handleEvent: function (event) {
+    switch (event.type) {
+      case "TabSelect":
+        if (this._selectedTab &&
+            this._selectedTab.parentNode &&
+            !this._pendingUpdate) {
+          // Generate a thumbnail for the tab that was selected.
+          // The timeout keeps the UI snappy and prevents us from generating thumbnails
+          // for tabs that will be closed. During that timeout, don't generate other
+          // thumbnails in case multiple TabSelect events occur fast in succession.
+          this._pendingUpdate = true;
+          setTimeout(function (self, aTab) {
+            self._pendingUpdate = false;
+            if (aTab.parentNode && !aTab.hasAttribute("busy"))
+              self.capture(aTab, true);
+          }, 2000, this, this._selectedTab);
+        }
+        this._selectedTab = event.target;
+        break;
+      case "SSTabRestored":
+        this.capture(event.target, true);
+        break;
+    }
+  }
+};
+
+/**
+ * Ctrl-Tab panel
+ */
+var ctrlTab = {
+  tabs: [],
+  _uniqid: 0,
+  get panel () {
+    delete this.panel;
+    return this.panel = document.getElementById("ctrlTab-panel");
+  },
+  get label () {
+    delete this.label;
+    return this.label = document.getElementById("ctrlTab-label");
+  },
+  get svgRoot () {
+    delete this.svgRoot;
+
+    let (groundFade = document.getElementById("ctrlTab-groundFade")) {
+      groundFade.setAttribute("height", Math.ceil(tabPreviews.height * .25) + 1);
+      groundFade.setAttribute("y", tabPreviews.height + 1);
+    }
+
+    this.svgRoot = document.getElementById("ctrlTab-svgRoot");
+    this.svgRoot.setAttribute("height", tabPreviews.height * 1.25 + 2);
+    return this.svgRoot;
+  },
+  get container () {
+    delete this.container;
+    return this.container = document.getElementById("ctrlTab-container");
+  },
+  get rtl () {
+    delete this.rtl;
+    return this.rtl = getComputedStyle(this.panel, "").direction == "rtl";
+  },
+  get iconSize () {
+    delete this.iconSize;
+    return this.iconSize = Math.round(tabPreviews.height / 4);
+  },
+  get smoothScroll () {
+    delete this.smoothScroll;
+    return this.smoothScroll = gPrefService.getBoolPref("browser.ctrlTab.smoothScroll");
+  },
+  get previewsCount () {
+    delete this.previewsCount;
+    return this.previewsCount = Math.max(gPrefService.getIntPref("browser.ctrlTab.previewsCount"), 3);
+  },
+  get visibleCount () {
+    return Math.min(this.previewsCount, this.tabs.length);
+  },
+  get offscreenStart () {
+    return Array.indexOf(this.container.childNodes, this.selected) - 1;
+  },
+  get offscreenEnd () {
+    return this.container.childNodes.length - this.visibleCount - this.offscreenStart;
+  },
+  get offsetX () {
+    return - tabPreviews.width * (this.rtl ? this.offscreenEnd : this.offscreenStart);
+  },
+  init: function () {
+    var tabContainer = gBrowser.tabContainer;
+
+    Array.forEach(tabContainer.childNodes, function (tab) {
+      this.attachTab(tab, tab == gBrowser.selectedTab);
+    }, this);
+
+    tabContainer.addEventListener("TabOpen", this, false);
+    tabContainer.addEventListener("TabSelect", this, false);
+    tabContainer.addEventListener("TabClose", this, false);
+
+    gBrowser.mTabBox.handleCtrlTab = false;
+    window.addEventListener("keydown", this, true);
+  },
+  uninit: function () {
+    var tabContainer = gBrowser.tabContainer;
+    tabContainer.removeEventListener("TabOpen", this, false);
+    tabContainer.removeEventListener("TabSelect", this, false);
+    tabContainer.removeEventListener("TabClose", this, false);
+
+    this.panel.removeEventListener("popuphiding", this, false);
+    window.removeEventListener("keydown", this, true);
+  },
+  addBox: function (aAtStart) {
+    const SVGNS = "http://www.w3.org/2000/svg";
+
+    var thumbnail = document.createElementNS(SVGNS, "image");
+    thumbnail.setAttribute("class", "ctrlTab-thumbnail");
+    thumbnail.setAttribute("height", tabPreviews.height);
+    thumbnail.setAttribute("width", tabPreviews.width);
+
+    var thumbnail_border = document.createElementNS(SVGNS, "rect");
+    thumbnail_border.setAttribute("class", "ctrlTab-thumbnailborder");
+    thumbnail_border.setAttribute("height", tabPreviews.height);
+    thumbnail_border.setAttribute("width", tabPreviews.width);
+
+    var icon = document.createElementNS(SVGNS, "image");
+    icon.setAttribute("class", "ctrlTab-icon");
+    icon.setAttribute("height", this.iconSize);
+    icon.setAttribute("width", this.iconSize);
+    icon.setAttribute("transform", "skewY(10)");
+    icon.setAttribute("x", - this.iconSize / 3);
+    icon.setAttribute("y", tabPreviews.height * .9 - this.iconSize);
+
+    var thumbnail_and_icon = document.createElementNS(SVGNS, "g");
+    thumbnail_and_icon.appendChild(thumbnail);
+    thumbnail_and_icon.appendChild(thumbnail_border);
+    thumbnail_and_icon.appendChild(icon);
+
+    var reflection = document.createElementNS(SVGNS, "use");
+    reflection.setAttribute("class", "ctrlTab-reflection");
+    var ref_scale = .5;
+    reflection.setAttribute("transform", "scale(1,-" + ref_scale + ")");
+    reflection.setAttribute("y", - ((1 / ref_scale + 1) * tabPreviews.height +
+                                    (1 / ref_scale) * 2));
+
+    var box = document.createElementNS(SVGNS, "g");
+    box.setAttribute("class", "ctrlTab-box");
+    box.appendChild(thumbnail_and_icon);
+    box.appendChild(reflection);
+
+    if (aAtStart)
+      this.container.insertBefore(box, this.container.firstChild);
+    else
+      this.container.appendChild(box);
+    return box;
+  },
+  removeBox: function (aBox) {
+    this.container.removeChild(aBox);
+    if (!Array.some(this.container.childNodes, function (box) box._tab == aBox._tab))
+      aBox._tab.removeEventListener("DOMAttrModified", this, false);
+    aBox._tab = null;
+  },
+  addPreview: function (aBox, aTab) {
+    const XLinkNS = "http://www.w3.org/1999/xlink";
+
+    aBox._tab = aTab;
+    let (thumbnail = aBox.firstChild.firstChild)
+      thumbnail.setAttributeNS(XLinkNS, "href", tabPreviews.get(aTab));
+    this.updateIcon(aBox);
+
+    aTab.addEventListener("DOMAttrModified", this, false);
+
+    if (!aBox.firstChild.hasAttribute("id")) {
+      // set up reflection
+      this._uniqid++;
+      aBox.firstChild.setAttribute("id", "ctrlTab-preview-" + this._uniqid);
+      aBox.lastChild.setAttributeNS(XLinkNS, "href", "#ctrlTab-preview-" + this._uniqid);
+    }
+  },
+  updateIcon: function (aBox) {
+    const XLinkNS = "http://www.w3.org/1999/xlink";
+    var url = aBox._tab.hasAttribute("busy") ?
+              "chrome://global/skin/icons/loading_16.png" :
+              aBox._tab.getAttribute("image");
+    var icon = aBox.firstChild.lastChild;
+    if (url)
+      icon.setAttributeNS(XLinkNS, "href", url);
+    else
+      icon.removeAttributeNS(XLinkNS, "href");
+  },
+  tabAttrModified: function (aTab, aAttrName) {
+    switch (aAttrName) {
+      case "busy":
+      case "image":
+        Array.forEach(this.container.childNodes, function (box) {
+          if (box._tab == aTab) {
+            if (aAttrName == "busy")
+              this.addPreview(box, aTab);
+            else
+              this.updateIcon(box);
+          }
+        }, this);
+        break;
+      case "label":
+      case "crop":
+        if (!this._scrollTimer) {
+          let boxes = this.container.childNodes;
+          for (let i = boxes.length - 1; i >= 0; i--) {
+            if (boxes[i]._tab == aTab && boxes[i] == this.selected) {
+              this.label[aAttrName == "label" ? "value" : aAttrName] =
+                aTab.getAttribute(aAttrName);
+              break;
+            }
+          }
+        }
+        break;
+    }
+  },
+  scroll: function () {
+    if (!this.smoothScroll) {
+      this._move = true;
+      this.stopScroll();
+      return;
+    }
+
+    this.stopScroll();
+    this._move = true;
+    let (next = this.invertDirection ? this.selected.previousSibling : this.selected.nextSibling) {
+      this.setStatusbarValue(next);
+      this.label.value = next._tab.label;
+      this.label.crop = next._tab.crop;
+    }
+
+    const FRAME_LENGTH = 40;
+    var x = this.offsetX;
+    var scrollAmounts = let (tenth = tabPreviews.width / (this.invertDirection == this.rtl ? -10 : 10))
+                        [3 * tenth, 4 * tenth, 2 * tenth, tenth];
+
+    function processFrame(self, lateness) {
+      lateness += FRAME_LENGTH / 2;
+      do {
+        x += scrollAmounts.shift();
+        lateness -= FRAME_LENGTH;
+      } while (lateness > 0 && scrollAmounts.length);
+      self.container.setAttribute("transform", "translate("+ x +",0)");
+      self.svgRoot.forceRedraw();
+      if (!scrollAmounts.length)
+        self.stopScroll();
+    }
+
+    this._scrollTimer = setInterval(processFrame, FRAME_LENGTH, this);
+    processFrame(this, 0);
+  },
+  stopScroll: function () {
+    if (this._scrollTimer) {
+      clearInterval(this._scrollTimer);
+      this._scrollTimer = 0;
+    }
+    if (this._move)
+      this.updateSelected();
+  },
+  updateSelected: function (aClosing) {
+    var index = 1;
+    if (this._move) {
+      this._move = false;
+      index += this.invertDirection ? -1 : 1;
+    }
+    if (this.selected) {
+      index += this.offscreenStart + this.tabs.length;
+      index %= this.tabs.length;
+      if (index < 2)
+        index += this.tabs.length;
+      if (index > this.container.childNodes.length - this.visibleCount + 1)
+        index -= this.tabs.length;
+    }
+    this.selected = this.container.childNodes[index];
+
+    if (aClosing)
+      return;
+
+    this.addOffscreenBox(this.invertDirection);
+    this.addOffscreenBox(!this.invertDirection);
+
+    // having lots of off-screen boxes reduce the scrolling speed, remove some
+    for (let i = this.offscreenStart; i > 1; i--)
+      this.removeBox(this.container.firstChild);
+    for (let i = this.offscreenEnd; i > 1; i--)
+      this.removeBox(this.container.lastChild);
+
+    this.container.setAttribute("transform", "translate("+ this.offsetX +", 0)");
+
+    for (let i = 0, l = this.container.childNodes.length; i < l; i++)
+      this.arrange(i);
+  },
+  addOffscreenBox: function (aAtStart) {
+    if (this.container.childNodes.length < this.tabs.length + this.visibleCount + 1 &&
+        !(aAtStart ? this.offscreenStart : this.offscreenEnd)) {
+      let i = aAtStart ?
+              this.tabs.indexOf(this.container.firstChild._tab) - 1:
+              this.tabs.indexOf(this.container.lastChild._tab) + 1;
+      i = (i + this.tabs.length) % this.tabs.length;
+      this.addPreview(this.addBox(aAtStart), this.tabs[i]);
+    }
+  },
+  arrange: function (aIndex) {
+    var box = this.container.childNodes[aIndex];
+    var selected = box == this.selected;
+    if (selected) {
+      box.setAttribute("selected", "true");
+      this.setStatusbarValue(box);
+      this.label.value = box._tab.label;
+      this.label.crop = box._tab.crop;
+    } else {
+      box.removeAttribute("selected");
+    }
+    var scale = selected ? 1 : .75;
+    var pos = this.rtl ? this.container.childNodes.length - 1 - aIndex : aIndex;
+    var trans_x = tabPreviews.width * (pos + (1 - scale) / 2) / scale;
+    var trans_y = (tabPreviews.height + 1) * (1 / scale - 1);
+    box.setAttribute("transform", "scale(" + scale + "," + scale + ") " +
+                                  "translate("+ trans_x + "," + trans_y + ")");
+  },
+  setStatusbarValue: function (aBox) {
+    var value = "";
+    if (aBox) {
+      value = aBox._tab.linkedBrowser.currentURI.spec;
+      if (value == "about:blank") {
+        // XXXhack: Passing a space here (and not "")
+        // to make sure the browser implementation would
+        // still consider it a hovered link.
+        value = " ";
+      } else {
+        try {
+          value = decodeURI(value);
+        } catch (e) {}
+      }
+    }
+    XULBrowserWindow.setOverLink(value, null);
+  },
+  attachTab: function (aTab, aSelected) {
+    if (aSelected)
+      this.tabs.unshift(aTab);
+    else
+      this.tabs.push(aTab);
+  },
+  detachTab: function (aTab) {
+    var i = this.tabs.indexOf(aTab);
+    if (i >= 0)
+      this.tabs.splice(i, 1);
+  },
+  open: function () {
+    window.addEventListener("keyup", this, true);
+    window.addEventListener("keypress", this, true);
+    this.panel.addEventListener("popuphiding", this, false);
+    this.panel.hidden = false;
+    this.panel.width = tabPreviews.width * this.visibleCount;
+    this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2,
+                                 screen.availTop + (screen.availHeight - this.svgRoot.getAttribute("height")) / 2,
+                                 false);
+
+    for (let index = this.invertDirection ? this.tabs.length - 2 : 0,
+             i = this.visibleCount; i > 0; i--)
+      this.addPreview(this.addBox(), this.tabs[index++ % this.tabs.length]);
+    this.updateSelected();
+  },
+  onKeyDown: function (event) {
+    var isOpen = this.panel.state == "open" || this.panel.state == "showing";
+    var propagate = !isOpen;
+    switch (event.keyCode) {
+      case event.DOM_VK_TAB:
+        if (event.ctrlKey && !event.altKey && !event.metaKey && this.tabs.length > 1) {
+          propagate = false;
+          this.invertDirection = event.shiftKey;
+          if (isOpen)
+            this.scroll();
+          else if (this.tabs.length == 2)
+            gBrowser.selectedTab = this.tabs[1];
+          else
+            this.open();
+        }
+        break;
+      case event.DOM_VK_ESCAPE:
+        if (isOpen)
+          this.panel.hidePopup();
+        break;
+    }
+    if (!propagate) {
+      event.stopPropagation();
+      event.preventDefault();
+    }
+  },
+  onKeyUp: function (event) {
+    if (event.keyCode == event.DOM_VK_CONTROL) {
+      if (this._move)
+        this.updateSelected(true);
+      let selectedTab = this.selected._tab;
+      this.panel.hidePopup();
+      gBrowser.selectedTab = selectedTab;
+    }
+  },
+  onPopupHiding: function () {
+    this.stopScroll();
+    window.removeEventListener("keyup", this, true);
+    window.removeEventListener("keypress", this, true);
+    while (this.container.childNodes.length)
+      this.removeBox(this.container.lastChild);
+    this.selected = null;
+    this.invertDirection = false;
+    this._move = false;
+    this._uniqid = 0;
+    this.label.value = "";
+    this.setStatusbarValue();
+    this.container.removeAttribute("transform");
+    this.svgRoot.forceRedraw();
+  },
+  handleEvent: function (event) {
+    switch (event.type) {
+      case "DOMAttrModified":
+        this.tabAttrModified(event.target, event.attrName);
+        break;
+      case "TabSelect":
+        this.detachTab(event.target);
+        this.attachTab(event.target, true);
+        break;
+      case "TabOpen":
+        this.attachTab(event.target);
+        break;
+      case "TabClose":
+        this.detachTab(event.target);
+        break;
+      case "keydown":
+        this.onKeyDown(event);
+        break;
+      case "keyup":
+      case "keypress":
+        // the panel is open; don't propagate any key events
+        event.stopPropagation();
+        event.preventDefault();
+      case "keyup":
+        this.onKeyUp(event);
+        break;
+      case "popuphiding":
+        this.onPopupHiding();
+        break;
+    }
+  }
+};
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1,8 +1,12 @@
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+@namespace svg url("http://www.w3.org/2000/svg");
+@namespace xlink url("http://www.w3.org/1999/xlink");
+
 searchbar {
   -moz-binding: url("chrome://browser/content/search/search.xml#searchbar");
 }
 
 tabbrowser {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser");
 }
 
@@ -77,8 +81,22 @@ window[chromehidden~="toolbar"] toolbar:
   display: none;
   min-height: 5px;
   height: 5px;
 }
 
 #navigator-toolbox[inFullscreen="true"] #fullscr-toggler {
   display: -moz-box;
 }
+
+/* Tab Previews */
+svg|*.ctrlTab-icon:not([xlink|href]) ,
+svg|*.ctrlTab-thumbnail:not([xlink|href]) {
+  display: none;
+}
+
+svg|*.ctrlTab-thumbnailborder {
+  fill: transparent;
+}
+
+svg|*.ctrlTab-icon {
+  filter: url(chrome://browser/content/browser.xul#ctrlTab-iconShadow);
+}
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1069,16 +1069,19 @@ function delayedStartup()
   gBookmarkAllTabsHandler = new BookmarkAllTabsHandler();
 
   // Attach a listener to watch for "command" events bubbling up from error
   // pages.  This lets us fix bugs like 401575 which require error page UI to
   // do privileged things, without letting error pages have any privilege
   // themselves.
   gBrowser.addEventListener("command", BrowserOnCommand, false);
 
+  tabPreviews.init();
+  ctrlTab.init();
+
   // Delayed initialization of the livemarks update timer.
   // Livemark updates don't need to start until after bookmark UI 
   // such as the toolbar has initialized. Starting 5 seconds after
   // delayedStartup in order to stagger this before the download
   // manager starts (see below).
   setTimeout(function() PlacesUtils.livemarks.start(), 5000);
 
   // Initialize the download manager some time after the app starts so that
@@ -1099,16 +1102,19 @@ function delayedStartup()
   let placesContext = document.getElementById("placesContext");
   placesContext.addEventListener("popupshowing", updateEditUIVisibility, false);
   placesContext.addEventListener("popuphiding", updateEditUIVisibility, false);
 #endif
 }
 
 function BrowserShutdown()
 {
+  tabPreviews.uninit();
+  ctrlTab.uninit();
+
   try {
     FullZoom.destroy();
   }
   catch(ex) {
     Components.utils.reportError(ex);
   }
 
   var os = Components.classes["@mozilla.org/observer-service;1"]
@@ -6134,16 +6140,18 @@ var FeedHandler = {
     }
   }
 };
 
 #include browser-places.js
 
 #include browser-textZoom.js
 
+#include browser-tabPreviews.js
+
 HistoryMenu.toggleRecentlyClosedTabs = function PHM_toggleRecentlyClosedTabs() {
   // enable/disable the Recently Closed Tabs sub menu
   var undoPopup = document.getElementById("historyUndoPopup");
 
   // get closed-tabs from nsSessionStore
   var ss = Cc["@mozilla.org/browser/sessionstore;1"].
            getService(Ci.nsISessionStore);
   // no restorable tabs, so disable menu
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -24,16 +24,17 @@
 #
 # Contributor(s):
 #   Blake Ross <blake@cs.stanford.edu>
 #   David Hyatt <hyatt@mozilla.org>
 #   Joe Hewitt <hewitt@netscape.com>
 #   Pierre Chanial <chanial@noos.fr>
 #   Dean Tessman <dean_tessman@hotmail.com>
 #   Johnathan Nightingale <johnath@mozilla.com>
+#   Dão Gottwald <dao@mozilla.com>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either the GNU General Public License Version 2 or later (the "GPL"), or
 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -56,16 +57,17 @@
 <?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
 
 # All DTD information is stored in a separate file so that it can be shared by
 # hiddenWindow.xul.
 #include browser-doctype.inc
 
 <window id="main-window"
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+        xmlns:svg="http://www.w3.org/2000/svg"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="BrowserStartup()" onunload="BrowserShutdown()" onclose="return WindowIsClosing();"
         contenttitlesetting="true"
         title="&mainWindow.title;"
         titlemodifier="&mainWindow.title;"
         titlemenuseparator="&mainWindow.titlemodifiermenuseparator;"
         windowtype="navigator:browser"
         screenX="4" screenY="4"
@@ -222,16 +224,39 @@
           </hbox>
         </vbox>
       </hbox>
     </panel>
 
     <tooltip id="urlTooltip">
       <label crop="center" flex="1"/>
     </tooltip>
+
+    <panel id="ctrlTab-panel" class="KUI-panel" hidden="true">
+      <label id="ctrlTab-label" flex="1"/>
+      <svg:svg id="ctrlTab-svgRoot">
+        <svg:defs>
+          <svg:linearGradient id="ctrlTab-fadeGradient" x1="1" y1="1" x2="1" y2="0">
+            <svg:stop id="ctrlTab-fadeGradientBottom" offset="0"/>
+            <svg:stop id="ctrlTab-fadeGradientTop" offset="1"/>
+          </svg:linearGradient>
+          <svg:filter id="ctrlTab-iconShadow">
+            <svg:feOffset result="shadow" in="SourceAlpha" dx="2" dy="-1"/>
+            <svg:feColorMatrix result="transparent-shadow" in="shadow"
+                               values="1 0 0 0   0
+                                       0 1 0 0   0
+                                       0 0 1 0   0
+                                       0 0 0 0.5 0"/>
+            <svg:feBlend in="SourceGraphic" in2="transparent-shadow"/>
+          </svg:filter>
+        </svg:defs>
+        <svg:g id="ctrlTab-container"/>
+        <svg:rect id="ctrlTab-groundFade" fill="url(#ctrlTab-fadeGradient)" width="100%"/>
+      </svg:svg>
+    </panel>
   </popupset>
 
   <!-- bookmarks toolbar tooltip -->
   <tooltip id="btTooltip" noautohide="true"
            onpopupshowing="return BookmarksEventHandler.fillInBTTooltip(document.tooltipNode)">
     <vbox id="btTooltipTextBox" flex="1">
       <label id="btTitleText" class="tooltip-label" />
       <label id="btUrlText" class="tooltip-label" />
--- a/browser/themes/gnomestripe/browser/browser.css
+++ b/browser/themes/gnomestripe/browser/browser.css
@@ -21,17 +21,17 @@
  *
  * Contributor(s):
  *   Joe Hewitt (hewitt@netscape.com)
  *   Jason Kersey (kerz@netscape.com)
  *   Pierre Chanial (chanial@noos.fr)
  *   Dean Tessman (dean_tessman@hotmail.com)
  *   Blake Ross (blake@cs.stanford.edu)
  *   Pamela Greene (pamg.bugs@gmail.com)
- *   Dao Gottwald (dao@design-noir.de)
+ *   Dão Gottwald (dao@mozilla.com)
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -42,16 +42,17 @@
  *
  * ***** END LICENSE BLOCK ***** */
 %endif
 
 @import url("chrome://global/skin/");
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
+@namespace svg url("http://www.w3.org/2000/svg");
 
 #menubar-items {
   -moz-box-orient: vertical; /* for flex hack */
 }
 
 #menubar-items > menubar {
   -moz-box-flex: 1; /* make menu items expand to fill toolbar height */
 }
@@ -1390,8 +1391,31 @@ toolbar[mode="text"] > #window-controls 
 #checkForUpdates[loading="true"] {
   list-style-image: url("chrome://global/skin/icons/loading_16.png");
 }
 
 #download-monitor {
   list-style-image: url("chrome://browser/skin/Toolbar-small.png");
   -moz-image-region: rect(0px 16px 16px 0px);
 }
+
+/* ::::: Ctrl-Tab Panel ::::: */
+#ctrlTab-label {
+  font-weight: bold;
+  text-align: center;
+  margin-top: .5em;
+  margin-bottom: .7em;
+}
+
+svg|*.ctrlTab-thumbnailborder {
+  stroke: ThreeDDarkShadow;
+  stroke-width: .7px;
+}
+
+svg|*#ctrlTab-fadeGradientTop {
+  stop-color: ThreeDShadow;
+  stop-opacity: .75;
+}
+
+svg|*#ctrlTab-fadeGradientBottom {
+  stop-color: -moz-dialog;
+  stop-opacity: 1;
+}
--- a/browser/themes/pinstripe/browser/browser.css
+++ b/browser/themes/pinstripe/browser/browser.css
@@ -19,17 +19,17 @@
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Joe Hewitt (hewitt@netscape.com)
  *   Jason Kersey (kerz@netscape.com)
  *   Pierre Chanial (chanial@noos.fr)
  *   Kevin Gerich (kevin@kmgerich.com)
  *   Pamela Greene (pamg.bugs@gmail.com)
- *   Dao Gottwald (dao@design-noir.de)
+ *   Dão Gottwald (dao@mozilla.com)
  *   Stephen Horlander (stephen@noved.org)
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
@@ -40,16 +40,17 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 @import url("chrome://global/skin/");
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
+@namespace svg url("http://www.w3.org/2000/svg");
 
 #main-window {
   -moz-binding: url("chrome://global/skin/globalBindings.xml#unifiedWindow");
 }
 
 #main-window[chromehidden~="toolbar"][chromehidden~="location"][chromehidden~="directories"] {
   border-top: 1px solid rgba(0,0,0,0.65);
   -moz-appearance: none;
@@ -2257,8 +2258,53 @@ tabpanels.plain {
 
 #identity-popup-more-info-button:hover:active {
   background-color: #86888B;
 }
 
 #download-monitor {
   list-style-image: url("chrome://mozapps/skin/downloads/downloadStatusIcon.png");
 }
+
+/* ::::: Keyboard UI Panel ::::: */
+.KUI-panel {
+  -moz-appearance: none;
+  background: rgba(0, 0, 0, .8);
+  color: white;
+  border: 3px solid #ccc;
+  -moz-border-top-colors: #ccc #ccc black;
+  -moz-border-right-colors: #ccc #ccc black;
+  -moz-border-bottom-colors: #ccc #ccc black;
+  -moz-border-left-colors: #ccc #ccc black;
+  -moz-border-radius: 10px;
+  margin-top: -3px;
+  margin-left: -3px;
+}
+
+/* ::::: Ctrl-Tab Panel ::::: */
+#ctrlTab-panel {
+  margin-top: -18px;
+  border-bottom-width: 5px;
+  -moz-border-bottom-colors: #ccc #ccc black black black;
+}
+
+#ctrlTab-label {
+  font-weight: bold;
+  font-size: 14px;
+  text-align: center;
+  margin-top: 6px;
+  margin-bottom: 10px;
+}
+
+svg|*.ctrlTab-thumbnailborder {
+  stroke: white;
+  stroke-width: 1px;
+}
+
+svg|*#ctrlTab-fadeGradientTop {
+  stop-color: black;
+  stop-opacity: .5;
+}
+
+svg|*#ctrlTab-fadeGradientBottom {
+  stop-color: black;
+  stop-opacity: 1;
+}
--- a/browser/themes/winstripe/browser/browser.css
+++ b/browser/themes/winstripe/browser/browser.css
@@ -20,17 +20,17 @@
  *
  * Contributor(s):
  *   Joe Hewitt (hewitt@netscape.com)
  *   Jason Kersey (kerz@netscape.com)
  *   Pierre Chanial (chanial@noos.fr)
  *   Dean Tessman (dean_tessman@hotmail.com)
  *   Blake Ross (blake@cs.stanford.edu)
  *   Pamela Greene (pamg.bugs@gmail.com)
- *   Dao Gottwald (dao@design-noir.de)
+ *   Dão Gottwald (dao@mozilla.com)
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -40,16 +40,17 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 @import url("chrome://global/skin/");
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
+@namespace svg url("http://www.w3.org/2000/svg");
 
 #menubar-items {
   -moz-box-orient: vertical; /* for flex hack */
 }
 
 #menubar-items > menubar {
   -moz-box-flex: 1; /* make menu items expand to fill toolbar height */
 }
@@ -2053,8 +2054,53 @@ toolbarbutton.bookmark-item[dragover="tr
 #nav-bar[mode="full"]:not([currentset]):-moz-system-metric(windows-default-theme),
 #nav-bar[mode="full"][currentset$=",urlbar-container"]:-moz-system-metric(windows-default-theme),
 #nav-bar[mode="full"][currentset$=",search-container"]:-moz-system-metric(windows-default-theme),
 #nav-bar[iconsize="large"][mode="icons"]:not([currentset]):-moz-system-metric(windows-default-theme),
 #nav-bar[iconsize="large"][mode="icons"][currentset$=",urlbar-container"]:-moz-system-metric(windows-default-theme),
 #nav-bar[iconsize="large"][mode="icons"][currentset$=",search-container"]:-moz-system-metric(windows-default-theme) {
   -moz-padding-end: 3px;
 }
+
+/* ::::: Keyboard UI Panel ::::: */
+.KUI-panel {
+  -moz-appearance: none;
+  background: rgba(0, 0, 0, .8);
+  color: white;
+  border: 3px solid #ccc;
+  -moz-border-top-colors: #ccc #ccc black;
+  -moz-border-right-colors: #ccc #ccc black;
+  -moz-border-bottom-colors: #ccc #ccc black;
+  -moz-border-left-colors: #ccc #ccc black;
+  -moz-border-radius: 10px;
+  margin-top: -3px;
+  margin-left: -3px;
+}
+
+/* ::::: Ctrl-Tab Panel ::::: */
+#ctrlTab-panel {
+  margin-top: -18px;
+  border-bottom-width: 5px;
+  -moz-border-bottom-colors: #ccc #ccc black black black;
+}
+
+#ctrlTab-label {
+  font-weight: bold;
+  font-size: 14px;
+  text-align: center;
+  margin-top: 6px;
+  margin-bottom: 10px;
+}
+
+svg|*.ctrlTab-thumbnailborder {
+  stroke: white;
+  stroke-width: 1px;
+}
+
+svg|*#ctrlTab-fadeGradientTop {
+  stop-color: black;
+  stop-opacity: .5;
+}
+
+svg|*#ctrlTab-fadeGradientBottom {
+  stop-color: black;
+  stop-opacity: 1;
+}