Bug 582583 - Expose a small, simple UI for 'Undo Closed Tab' [r=mfinkle]
authorVivien Nicolas <21@vingtetun.org>
Wed, 06 Oct 2010 17:08:56 +0200
changeset 2148 7bac809eef702657f4a61fb50b79e0b5747151a5
parent 2147 6aec315eafcdc498a30fbf90f4f2192fe8b657cf
child 2149 0ea707d62e819d557ef97b7a5713c85448c6ce76
push id1809
push uservnicolas@mozilla.com
push dateWed, 06 Oct 2010 15:09:10 +0000
reviewersmfinkle
bugs582583
Bug 582583 - Expose a small, simple UI for 'Undo Closed Tab' [r=mfinkle]
chrome/content/browser-ui.js
chrome/content/browser.css
chrome/content/browser.xul
chrome/content/tabs.xml
themes/core/browser.css
themes/core/images/reload-tab.png
themes/core/jar.mn
--- a/chrome/content/browser-ui.js
+++ b/chrome/content/browser-ui.js
@@ -404,16 +404,17 @@ var BrowserUI = {
     awesomePopup.addEventListener("popupshown", this, false);
     awesomePopup.addEventListener("popuphidden", this, false);
 
     document.getElementById("toolbar-main").ignoreDrag = true;
 
     let tabs = document.getElementById("tabs");
     tabs.addEventListener("TabSelect", this, true);
     tabs.addEventListener("TabOpen", this, true);
+    window.addEventListener("PanFinished", this, true);
 
     // listen content messages
     messageManager.addMessageListener("DOMLinkAdded", this);
     messageManager.addMessageListener("DOMTitleChanged", this);
     messageManager.addMessageListener("DOMWillOpenModalDialog", this);
     messageManager.addMessageListener("DOMWindowClose", this);
 
     messageManager.addMessageListener("Browser:OpenURI", this);
@@ -765,26 +766,31 @@ var BrowserUI = {
     switch (aEvent.type) {
       // Browser events
       case "TabSelect":
         this._tabSelect(aEvent);
         break;
       case "TabOpen":
       {
         let [tabsVisibility,,,] = Browser.computeSidebarVisibility();
-        if (!(tabsVisibility == 1.0) && Browser.selectedTab.chromeTab != aEvent.target)
-          NewTabPopup.show(aEvent.target);
+        if (!(tabsVisibility == 1.0) && Browser.selectedTab.chromeTab != aEvent.originalTarget)
+          NewTabPopup.show(aEvent.originalTarget);
 
         // Workaround to hide the tabstrip if it is partially visible
         // See bug 524469
         if (tabsVisibility > 0.0 && tabsVisibility < 1.0)
           Browser.hideSidebars();
 
         break;
       }
+      case "PanFinished":
+        let [tabsVisibility,,,] = Browser.computeSidebarVisibility();
+        if (tabsVisibility == 0.0)
+          document.getElementById("tabs").removeClosedTab();
+        break;
       // Window events
       case "keypress":
         if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE)
           this.handleEscape(aEvent);
         break;
       case "AppCommand":
         aEvent.stopPropagation();
         switch (aEvent.command) {
--- a/chrome/content/browser.css
+++ b/chrome/content/browser.css
@@ -13,17 +13,17 @@ browser[remote="true"] {
 #content-navigator {
   -moz-binding: url("chrome://browser/content/bindings.xml#content-navigator");
 }
 
 #tabs {
   -moz-binding: url("chrome://browser/content/tabs.xml#tablist");
 }
 
-box[type="documenttab"] {
+documenttab {
   -moz-binding: url("chrome://browser/content/tabs.xml#documenttab");
 }
 
 settings {
   -moz-binding: url("chrome://browser/content/bindings/setting.xml#settings");
 }
 
 setting[type="bool"] {
--- a/chrome/content/browser.xul
+++ b/chrome/content/browser.xul
@@ -206,17 +206,21 @@
   </keyset>
 
   <stack flex="1" id="stack">
     <scrollbox id="controls-scrollbox" style="overflow: hidden; -moz-box-orient: horizontal; position: relative;" flex="1">
       <vbox class="panel-dark">
         <spacer class="toolbar-height"/>
         <!-- Left toolbar -->
         <vbox id="tabs-container" class="panel-dark" flex="1">
-          <vbox id="tabs" onselect="BrowserUI.selectTab(this);" onclosetab="BrowserUI.closeTab(this)" flex="1"/>
+          <vbox id="tabs" flex="1"
+                onselect="BrowserUI.selectTab(this);"
+                onreloadtab="BrowserUI.undoCloseTab()"
+                onclosetab="BrowserUI.closeTab(this)"
+                onclosereloadtab="this._container.removeTab(this)"/>
           <hbox id="tabs-controls">
             <toolbarbutton id="newtab-button" class="button-image" command="cmd_newTab"/>
           </hbox>
         </vbox>
       </vbox>
 
       <!-- Page Area -->
       <stack class="window-width window-height">
--- a/chrome/content/tabs.xml
+++ b/chrome/content/tabs.xml
@@ -4,142 +4,191 @@
     xmlns="http://www.mozilla.org/xbl"
     xmlns:xbl="http://www.mozilla.org/xbl"
     xmlns:html="http://www.w3.org/1999/xhtml"
     xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <binding id="documenttab">
     <content>
       <xul:stack anonid="page" class="documenttab-container" flex="1">
-        <html:canvas anonid="canvas" class="documenttab-canvas" left="0" width="106" height="64" moz-opaque="true"
-          onclick="document.getBindingParent(this)._onClick()" xbl:inherits="selected"/>
-        <xul:hbox class="documenttab-close-container" left="0" top="10" height="64" width="55" align="center" onclick="document.getBindingParent(this)._close()">
+        <html:canvas anonid="thumbnail" class="documenttab-thumbnail" left="0" width="106" height="64" moz-opaque="true" empty="true"
+          onclick="document.getBindingParent(this)._onClick()"/>
+        <xul:hbox class="documenttab-reload" left="0" top="0" width="122" height="80" onclick="document.getBindingParent(this)._onUndo();"/>
+        <xul:hbox class="documenttab-close-container" left="0" top="10" height="64" width="55" align="center" onclick="document.getBindingParent(this)._onClose()">
           <xul:image anonid="close" class="documenttab-close" mousethrough="always"/>
         </xul:hbox>
       </xul:stack>
     </content>
-    
+
    <implementation>
+      <field name="thumbnail">document.getAnonymousElementByAttribute(this, "anonid", "thumbnail");</field>
+      <field name="_container">this.parentNode.parentNode;</field>
       <method name="_onClick">
         <body>
           <![CDATA[
-            this.parentNode.selectedTab = this;
+            this._container.selectedTab = this;
 
-            let selectFn = new Function("event", this.parentNode.getAttribute('onselect'));
+            let selectFn = new Function("event", this._container.getAttribute("onselect"));
             selectFn.call(this);
           ]]>
         </body>
       </method>
 
-      <method name="_close">
+      <method name="_onClose">
         <body>
           <![CDATA[
-            let closeFn = new Function("event", this.parentNode.getAttribute('onclosetab'));
+
+            let callbackFunc = this._container.getAttribute(this.hasAttribute("reload") ? "onclosereloadtab" : "onclosetab");
+            let closeFn = new Function("event", callbackFunc);
             closeFn.call(this);
           ]]>
         </body>
       </method>
 
+      <method name="_onUndo">
+        <body>
+          <![CDATA[
+            let closeFn = new Function("event", this._container.getAttribute("onreloadtab"));
+            closeFn.call(this);
+
+            this._container.removeTab(this);
+          ]]>
+        </body>
+      </method>
+
       <method name="updateThumbnail">
         <parameter name="browser"/>
         <parameter name="width"/>
         <parameter name="height"/>
         <body>
           <![CDATA[
             const tabWidth = 106;
             const tabHeight = 64;
 
             let ratio = tabHeight / tabWidth;
             height = width * ratio;
+            
+            let thumbnail = this.thumbnail;
+            thumbnail.removeAttribute("empty");
 
-            let canvas = document.getAnonymousElementByAttribute(this, "anonid", "canvas");
-            let renderer = rendererFactory(browser, canvas)
+            let renderer = rendererFactory(browser, thumbnail)
             renderer.drawContent(function(ctx, callback) {
               ctx.save();
               ctx.clearRect(0, 0, tabWidth, tabHeight);
               ctx.scale(tabWidth / width, tabHeight / height);
               callback(browser, 0, 0, width, height, "white");
               ctx.restore();
             });
           ]]>
         </body>
       </method>
     </implementation>
   </binding>
 
   <binding id="tablist">
+    <content>
+      <xul:vbox anonid="tabs-children" flex="1"/>
+      <xul:box anonid="tabs-undo"/>
+    </content>
     <implementation>
+      <field name="children">document.getAnonymousElementByAttribute(this, "anonid", "tabs-children");</field>
+      <field name="_tabsUndo">document.getAnonymousElementByAttribute(this, "anonid", "tabs-undo");</field>
+      <field name="_closedTab">null</field>
       <field name="_selectedTab">null</field>
 
-      <property name="selectedTab">
-        <getter>
-          <![CDATA[
-            return this._selectedTab;
-          ]]>
-        </getter>
+      <property name="selectedTab" onget="return this._selectedTab;">
         <setter>
           <![CDATA[
             if (this._selectedTab)
-              this._selectedTab.removeAttribute('selected');
+              this._selectedTab.removeAttribute("selected");
 
             if (val)
-              val.setAttribute('selected', 'true');
+              val.setAttribute("selected", "true");
 
             this._selectedTab = val;
           ]]>
         </setter>
       </property>
 
       <method name="addTab">
         <body>
           <![CDATA[
-            let tab = document.createElement("box");
-            tab.setAttribute("type", "documenttab");
-            this.appendChild(tab);
+            let tab = document.createElement("documenttab");
+            this.children.appendChild(tab);
             this._updateWidth();
             return tab;
           ]]>
         </body>
       </method>
 
       <method name="removeTab">
         <parameter name="aTab"/>
         <body>
           <![CDATA[
-            this.removeChild(aTab);
-            this._updateWidth();
+            let closedTab = this._closedTab;
+            if (closedTab) {
+              this._tabsUndo.removeChild(closedTab);
+              this._closedTab = null;
+            }
+
+            if (!closedTab || closedTab != aTab) {
+              if (aTab.thumbnail && !aTab.thumbnail.hasAttribute("empty")) {
+                // duplicate the old thumbnail to the new one because moving the
+                // tab in the dom clear the canvas
+                let oldThumbnail = aTab.thumbnail.toDataURL("image/png");
+                this._tabsUndo.appendChild(aTab);
+                let thumbnailImg = new Image();
+                thumbnailImg.onload = function() aTab.thumbnail.getContext("2d").drawImage(thumbnailImg, 0, 0);
+                thumbnailImg.src = oldThumbnail;
+              }
+              else
+                this._tabsUndo.appendChild(aTab);
+
+              aTab.setAttribute("reload", "true");
+              this._closedTab = aTab;
+            }
+
+            this.resize();
           ]]>
         </body>
       </method>
 
+      <method name="removeClosedTab">
+        <body><![CDATA[
+          if (!this._closedTab)
+            return;
+
+          this.removeTab(this._closedTab);
+        ]]></body>
+      </method>
+
       <method name="resize">
         <body>
           <![CDATA[
             let container = this.parentNode.getBoundingClientRect();
-            let element   = this.getBoundingClientRect();
-            
-            let height = (element.top - container.top) +
-                         ((container.top + container.height) - (element.top + element.height));
-            this.style.height = height + "px";
+            let element   = this.children.getBoundingClientRect();
+
+            let height = (element.top - container.top) + ((container.top + container.height) - (element.top + element.height));
+            this.children.style.height = height + "px";
 
             this._updateWidth();
           ]]>
         </body>
       </method>
 
       <field name="_columnsCount">1</field>
       <method name="_updateWidth">
         <body>
           <![CDATA[
-            let firstBox = this.firstChild.getBoundingClientRect();
-            let lastBox = this.lastChild.getBoundingClientRect();
-            let columnsCount = Math.ceil(this.childNodes.length / Math.floor(this.style.height / firstBox.height));
+            let firstBox = this.children.firstChild.getBoundingClientRect();
+            let lastBox = this.children.lastChild.getBoundingClientRect();
+            let columnsCount = Math.ceil(this.children.childNodes.length / Math.floor(this.children.style.height / firstBox.height));
             if (this._columnsCount != columnsCount) {
               let width = Math.max(lastBox.right - firstBox.left, firstBox.right - lastBox.left);
-              this.style.width = width + "px";
+              this.children.style.width = width + "px";
               this._columnsCount = columnsCount;
             }
           ]]>
         </body>
       </method>
 
     </implementation>
   </binding>
--- a/themes/core/browser.css
+++ b/themes/core/browser.css
@@ -1002,70 +1002,93 @@ autocompleteresult.noresults > .autocomp
 /* Left sidebar (tabs)  ---------------------------------------------------- */
 #tabs-container {
   -moz-padding-start: 4px; /* allow the thumbnails to get close to the edge */
   -moz-padding-end: 8px; /* core spacing */
   padding-bottom: 8px; /* core spacing */
   -moz-border-end: 3px solid #262629;
 }
 
-#tabs {
+#tabs > * {
   display: block;
   -moz-column-width: 128px;
   -moz-column-gap: 0;
   -moz-user-focus: ignore;
   margin: 0;
   padding: 0;
   background-color: transparent;
 }
 
+#tabs documenttab:only-child .documenttab-close {
+  display: none;
+}
+
 #tabs-controls {
   margin-top: 8px; /* core spacing */
   -moz-box-pack: start;
 }
 
-box[type="documenttab"] {
+documenttab {
   /* display:block allow us to change the line-height, it won't work otherwise */
   display: block;
   width: 128px;
   -moz-margin-start: 8px; /* core spacing */
   line-height: 0;
 }
 
-.documenttab-canvas {
+documenttab .documenttab-thumbnail {
   /* keep the unselected thumbnails aligned with the selected one */
   border: 8px solid #36373b;
   background-color: white;
 }
 
-.documenttab-canvas[selected="true"] {
+documenttab .documenttab-close-container {
+  -moz-margin-end: 65px;
+}
+
+documenttab .documenttab-close-container > .documenttab-close {
+  width: 40px;
+  height: 40px;
+  list-style-image: url("chrome://browser/skin/images/close-default-40.png");
+}
+
+documenttab .documenttab-close-container:hover:active > .documenttab-close {
+  list-style-image: url("chrome://browser/skin/images/close-active-40.png");
+}
+
+documenttab .documenttab-reload {
+  display: none;
+}
+
+documenttab[selected="true"] .documenttab-thumbnail {
   border: 8px solid;
   -moz-border-radius: 3px;
   -moz-border-top-colors: #8db8d8 #8db8d8 #8db8d8 #8db8d8 #36373b;
   -moz-border-right-colors: #8db8d8 #8db8d8 #8db8d8 #8db8d8 #36373b;
   -moz-border-bottom-colors: #8db8d8 #8db8d8 #8db8d8 #8db8d8 #36373b;
   -moz-border-left-colors: #8db8d8 #8db8d8 #8db8d8 #8db8d8 #36373b;
 }
 
-.documenttab-close-container {
-  -moz-margin-end: 65px;
+documenttab[reload="true"] .documenttab-thumbnail {
+  border: 8px solid;
+  -moz-border-radius: 3px;
+  -moz-border-top-colors: #262629 #262629 #262629 #262629 #262629 !important;
+  -moz-border-right-colors: #262629 #262629 #262629 #262629 #262629 !important;
+  -moz-border-bottom-colors: #262629 #262629 #262629 #262629 #262629 !important;
+  -moz-border-left-colors: #262629 #262629 #262629 #262629 #262629 !important;
+  opacity: 0.5;
 }
 
-.documenttab-close {
-  width: 40px;
-  height: 40px;
-  list-style-image: url("chrome://browser/skin/images/close-default-40.png");
+documenttab[reload="true"] .documenttab-close-container {
+  display: none;
 }
 
-hbox:hover:active > .documenttab-close {
-  list-style-image: url("chrome://browser/skin/images/close-active-40.png");
-}
-
-box[type="documenttab"]:only-child > stack > hbox > .documenttab-close {
-  display: none;
+documenttab[reload="true"] .documenttab-reload {
+  background: url("chrome://browser/skin/images/reload-tab.png") no-repeat center center, -moz-radial-gradient(circle, rgba(137,215,21,0.8) 10%, rgba(68,108,17,0) 40%);
+  display: -moz-box;
 }
 
 #newtab-button {
   list-style-image: url("images/newtab-default-64.png");
 }
 
 #newtab-button:hover:active {
   list-style-image: url("images/newtab-active-64.png");
new file mode 100644
index 0000000000000000000000000000000000000000..fdd8d9feee3ffbc40798aa85a72e01a91b37f9ef
GIT binary patch
literal 1374
zc%17D@N?(olHy`uVBq!ia0vp^S|H591|*LjJ{b+9BuiW)N`mv#O3D+9QW+dm@{>{(
zJaZG%Q-e|yQz{EjrrIztFe_z-M3hAM`dB6B=jtVb)aX^@7BGN-jeSKyVsdtBi9%9p
zdS;%j()-=}l@u~lY?Z=IeGPmIoKrJ0J*tXQgRA^PlB=?lEmM^2?G$V(tbhjOrj{fs
zROII56<bx<DuK<l0<uBE`br95B_-LmN)Sgy_y#CA=NF|anCcnmCL5R;D3}@Q85*0I
zo15z>7#SEE=o=X68ye{vnp+tgSs558K!Fm_wxX0Ys~{IQs9ivwtx`rwNr9EVetCJh
zUb(Seeo?x<p{1oI$P6PRU7!lx;>x^|#0uTKVr7^KE~&-IMVSR9nfZANAbw&}erbuV
zk`l}dxdm`z^NOLt1Pn0!io^naLp=k1B!#}d_?717!c`ZS1f{0oS6v)ZS&*t9lv<o$
zT9gcoxHM&u<^n6{qSVBa{GyQj{2W*)24v)y<QHe;7brLfn=1GwCTHe>_+a(EzE+-j
z#U+V($*G<$wn{(|z0AxMD`RI@3kw%Z6K4Y_OG85!Hw#N=XA27>3nNoQM<a7XbC_P2
z{N&Qy)Vvay-V}shQ=EE1NdclewJ5VJHN~wcKUV?lWvfiwZZX2`7Kq*y+-@<(saGH9
z7=5&eh6w>v4~Pj*wm=R%;iu*SQ+p9GS!eAGl4W3EyyWTP7*cWT&5XTX%#I?h!jl`s
z1Rc3Ay9&HfX^mRpTEld?d%``Z4~!|bY;Qw1@He&26--(>DWWrU8&{M4kIO3rj~f@X
zmd?C6_szK$-ZPEsPuxBE=H#2XJIg1h^(<v`%*?(bwbgCy#gr_ML?6x&_Jc1ZZm>UH
zBK`NW<l1%d%3B_peqfl<9{p?L)Ej=4eQg10|EDgkV6#|o`pOrMRb1bNg^p$j>o84s
z4Q75)ZMtgH>(gIul)mCS(_AiWxoQ=E0Q-E#<g^1zSDqDF625_1Eu(mCLt?;!AJdPl
zNiH(}d6jLkVzEz?^vUntT9&JlUAq&^QqI`UV3s|&^=zHpui}%EuS3Pxa6Wqd*W}dm
z%pg(jpt&Ei=FIb2u!&18?ttmq6Xrh_u9~dYEICV^ecOSu6E(-1iq7=zD(u~+vA<+x
z>aJ_*`>HRkeY8L#R`kf7C8a-D4@Um-vA+MJ%gVOTUi3&=>C^y8_Yfw3kz3njR(N`4
z#D&{Sr35Y2eD<;L$dbjI<-cb0=Xk99*mU7TP23_Ug-fgvW<PhuEGm-RUKq!auhy>~
z7rD&AdeO<|INxJ;eKb!QhT1lIU8#O)<*r>a!T<NCzUQw+EqbCJzpeMbw!$>4H)?U!
c7wrQKC44(CerTR^5me%Ny85}Sb4q9e05DbhJ^%m!
--- a/themes/core/jar.mn
+++ b/themes/core/jar.mn
@@ -37,16 +37,17 @@ chrome.jar:
   skin/images/favicon-default-30.png        (images/favicon-default-30.png)
   skin/images/star-40.png                   (images/star-40.png)
   skin/images/star-24.png                   (images/star-24.png)
   skin/images/throbber.png                  (images/throbber.png)
   skin/images/navigation-magnifier-30.png   (images/navigation-magnifier-30.png)
   skin/images/folder-32.png                 (images/folder-32.png)
   skin/images/stop-30.png                   (images/stop-30.png)
   skin/images/reload-30.png                 (images/reload-30.png)
+  skin/images/reload-tab.png                (images/reload-tab.png)
   skin/images/alert-addons-30.png           (images/alert-addons-30.png)
   skin/images/alert-downloads-30.png        (images/alert-downloads-30.png)
   skin/images/addons-default-64.png         (images/addons-default-64.png)
   skin/images/addons-active-64.png          (images/addons-active-64.png)
   skin/images/back-default-64.png           (images/back-default-64.png)
   skin/images/back-active-64.png            (images/back-active-64.png)
   skin/images/back-disabled-64.png          (images/back-disabled-64.png)
   skin/images/history-48.png                (images/history-48.png)