Bug 582583 - Expose a small, simple UI for 'Undo Closed Tab' [r=mfinkle]
--- 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)