fix for bug #318168, tab browsing improvements such as: 1) when we have "too many" tabs in a window, allow the user to scroll through the tabs. 2) add events for when adding and removing tabs initial patch by mconnor. final patch r=mconnor

This commit is contained in:
sspitzer@mozilla.org 2007-08-21 22:01:25 -07:00
Родитель 0a13acf605
Коммит b9b6389312
2 изменённых файлов: 253 добавлений и 49 удалений

Просмотреть файл

@ -106,7 +106,7 @@
<xul:tab selected="true" validate="never" <xul:tab selected="true" validate="never"
onerror="this.parentNode.parentNode.parentNode.parentNode.addToMissedIconCache(this.getAttribute('image')); onerror="this.parentNode.parentNode.parentNode.parentNode.addToMissedIconCache(this.getAttribute('image'));
this.removeAttribute('image');" this.removeAttribute('image');"
maxwidth="250" width="0" minwidth="30" flex="100" maxwidth="250" width="0" minwidth="140" flex="100"
class="tabbrowser-tab" label="&untitledTab;" crop="end"/> class="tabbrowser-tab" label="&untitledTab;" crop="end"/>
</xul:tabs> </xul:tabs>
</xul:hbox> </xul:hbox>
@ -1083,6 +1083,10 @@
if (!this.mTabbedMode) if (!this.mTabbedMode)
this.enterTabbedMode(); this.enterTabbedMode();
// if we're adding tabs, we're past interrupt mode, ditch the owner
if (this.mCurrentTab.owner)
this.mCurrentTab.owner = null;
var t = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", var t = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
"tab"); "tab");
@ -1095,13 +1099,17 @@
t.setAttribute("crop", "end"); t.setAttribute("crop", "end");
t.maxWidth = 250; t.maxWidth = 250;
t.minWidth = 30; t.minWidth = 140;
t.width = 0; t.width = 0;
t.setAttribute("flex", "100"); t.setAttribute("flex", "100");
t.setAttribute("validate", "never"); t.setAttribute("validate", "never");
t.setAttribute("onerror", "this.parentNode.parentNode.parentNode.parentNode.addToMissedIconCache(this.getAttribute('image')); this.removeAttribute('image');"); t.setAttribute("onerror", "this.parentNode.parentNode.parentNode.parentNode.addToMissedIconCache(this.getAttribute('image')); this.removeAttribute('image');");
t.className = "tabbrowser-tab"; t.className = "tabbrowser-tab";
this.mTabContainer.appendChild(t); this.mTabContainer.appendChild(t);
// invalidate cache, because mTabContainer is about to change
this._browsers = null;
// If this new tab is owned by another, assert that relationship // If this new tab is owned by another, assert that relationship
if (aOwner !== undefined && aOwner !== null) { if (aOwner !== undefined && aOwner !== null) {
t.owner = aOwner; t.owner = aOwner;
@ -1178,6 +1186,13 @@
b.loadURIWithFlags(aURI, flags, aReferrerURI, aCharset, aPostData); b.loadURIWithFlags(aURI, flags, aReferrerURI, aCharset, aPostData);
} }
// Dispatch a new tab notification. We do this once we're
// entirely done, so that things are in a consistent state
// even if the event listener opens or closes tabs.
var evt = document.createEvent("Events");
evt.initEvent("TabOpen", true, false);
t.dispatchEvent(evt);
return t; return t;
]]> ]]>
</body> </body>
@ -1291,6 +1306,14 @@
if (ds.contentViewer && !ds.contentViewer.permitUnload()) if (ds.contentViewer && !ds.contentViewer.permitUnload())
return; return;
// We're committed to closing the tab now.
// Dispatch a notification.
// We dispatch it before any teardown so that event listeners can
// inspect the tab that's about to close.
var evt = document.createEvent("Events");
evt.initEvent("TabClose", true, false);
aTab.dispatchEvent(evt);
if (l == 1) { if (l == 1) {
// add a new blank tab to replace the one being closed // add a new blank tab to replace the one being closed
// (this ensures that the remaining tab is as good as new) // (this ensures that the remaining tab is as good as new)
@ -1358,6 +1381,8 @@
// Remove the tab // Remove the tab
this.mTabContainer.removeChild(oldTab); this.mTabContainer.removeChild(oldTab);
// invalidate cache, because mTabContainer is about to change
this._browsers = null;
this.mPanelContainer.removeChild(oldBrowser.parentNode); this.mPanelContainer.removeChild(oldBrowser.parentNode);
// Find the tab to select // Find the tab to select
@ -1432,8 +1457,8 @@
<body> <body>
<![CDATA[ <![CDATA[
if (aEvent.button == 0 && if (aEvent.button == 0 &&
// Only capture clicks on tabbox.xml's <spacer> aEvent.originalTarget.localName == "box") {
aEvent.originalTarget.localName == "spacer") { // xxx this needs to check that we're in the empty area of the tabstrip
var e = document.createEvent("Events"); var e = document.createEvent("Events");
e.initEvent("NewTab", true, true); e.initEvent("NewTab", true, true);
this.dispatchEvent(e); this.dispatchEvent(e);
@ -1581,30 +1606,114 @@
<parameter name="aDragSession"/> <parameter name="aDragSession"/>
<body> <body>
<![CDATA[ <![CDATA[
if (aDragSession.canDrop && aDragSession.sourceNode && if (aDragSession.canDrop && aDragSession.sourceNode) {
aDragSession.sourceNode.parentNode == this.mTabContainer) { // autoscroll the tab strip if we drag over the autorepeat
// buttons, even if we aren't dragging a tab, but then
// return to avoid drawing the drop indicator
var isTabDrag = (aDragSession.sourceNode.parentNode == this.mTabContainer);
var pixelsToScroll = 0;
var tabStrip = this.mTabContainer.mTabstrip;
if (aEvent.originalTarget.localName == "autorepeatbutton") {
if (aEvent.originalTarget.getAttribute("class") ==
"autorepeatbutton-up")
pixelsToScroll = tabStrip.scrollIncrement * -1;
else
pixelsToScroll = tabStrip.scrollIncrement;
tabStrip.scrollByPixels(pixelsToScroll);
}
if (!isTabDrag)
return;
var newIndex = this.getNewIndex(aEvent); var newIndex = this.getNewIndex(aEvent);
var ib = this.mTabDropIndicatorBar; var ib = this.mTabDropIndicatorBar;
var ind = ib.firstChild; var ind = ib.firstChild;
ib.setAttribute('dragging','true'); ib.setAttribute('dragging','true');
if (window.getComputedStyle(this.parentNode, null).direction == "ltr") { var tabStripBoxObject = tabStrip.scrollBoxObject;
var halfIndWidth = Math.floor((ind.boxObject.width + 1) / 2);
if (window.getComputedStyle(this.parentNode, null)
.direction == "ltr") {
var newMarginLeft;
var minMarginLeft = tabStripBoxObject.x - halfIndWidth;
// make sure we don't place the tab drop indicator past the
// edge, or the containing box will flex and stretch
// the tab drop indicator bar, which will flex the url bar.
// XXX todo
// just use first value if you can figure out how to get
// the tab drop indicator to crop instead of flex and stretch
// the tab drop indicator bar.
var maxMarginLeft = Math.min(
(minMarginLeft + tabStripBoxObject.width),
(ib.boxObject.x + ib.boxObject.width - ind.boxObject.width));
// if we are scrolling, put the drop indicator at the edge
// so that it doesn't jump while scrolling
if (pixelsToScroll > 0)
newMarginLeft = maxMarginLeft;
else if (pixelsToScroll < 0)
newMarginLeft = minMarginLeft;
else {
if (newIndex == this.mTabs.length) { if (newIndex == this.mTabs.length) {
ind.style.marginLeft = this.mTabs[newIndex-1].boxObject.x + newMarginLeft = this.mTabs[newIndex-1].boxObject.screenX +
this.mTabs[newIndex-1].boxObject.width - this.boxObject.x - 7 + 'px'; this.mTabs[newIndex-1].boxObject.width -
this.boxObject.screenX - halfIndWidth;
} else { } else {
ind.style.marginLeft = this.mTabs[newIndex].boxObject.x - this.boxObject.x - 7 + 'px'; newMarginLeft = this.mTabs[newIndex].boxObject.screenX -
this.boxObject.screenX - halfIndWidth;
} }
// ensure we never place the drop indicator beyond
// our limits
if (newMarginLeft < minMarginLeft)
newMarginLeft = minMarginLeft;
else if (newMarginLeft > maxMarginLeft)
newMarginLeft = maxMarginLeft;
}
ind.style.marginLeft = newMarginLeft + 'px';
} else { } else {
var newMarginRight;
var minMarginRight = tabStripBoxObject.x - halfIndWidth;
// make sure we don't place the tab drop indicator past the
// edge, or the containing box will flex and stretch
// the tab drop indicator bar, which will flex the url bar.
// XXX todo
// just use first value if you can figure out how to get
// the tab drop indicator to crop instead of flex and stretch
// the tab drop indicator bar.
var maxMarginRight = Math.min(
(minMarginRight + tabStripBoxObject.width),
(ib.boxObject.x + ib.boxObject.width - ind.boxObject.width));
// if we are scrolling, put the drop indicator at the edge
// so that it doesn't jump while scrolling
if (pixelsToScroll > 0)
newMarginRight = maxMarginRight;
else if (pixelsToScroll < 0)
newMarginRight = minMarginRight;
else {
if (newIndex == this.mTabs.length) { if (newIndex == this.mTabs.length) {
ind.style.marginRight = this.boxObject.width + this.boxObject.x - newMarginRight = this.boxObject.width +
this.mTabs[newIndex-1].boxObject.x + 'px'; this.boxObject.screenX -
this.mTabs[newIndex-1].boxObject.screenX -
halfIndWidth;
} else { } else {
ind.style.marginRight = this.boxObject.width + this.boxObject.x - newMarginRight = this.boxObject.width +
this.mTabs[newIndex].boxObject.x - this.boxObject.screenX -
this.mTabs[newIndex].boxObject.width + 'px'; this.mTabs[newIndex].boxObject.screenX -
this.mTabs[newIndex].boxObject.width -
halfIndWidth;
} }
// ensure we never place the drop indicator beyond
// our limits
if (newMarginRight < minMarginRight)
newMarginRight = minMarginRight;
else if (newMarginRight > maxMarginRight)
newMarginRight = maxMarginRight;
}
ind.style.marginRight = newMarginRight + 'px';
} }
} }
]]> ]]>
@ -1696,15 +1805,24 @@
this.mTabFilters.splice(aIndex, 0, this.mTabFilters.splice(aTab._tPos, 1)[0]); this.mTabFilters.splice(aIndex, 0, this.mTabFilters.splice(aTab._tPos, 1)[0]);
this.mTabListeners.splice(aIndex, 0, this.mTabListeners.splice(aTab._tPos, 1)[0]); this.mTabListeners.splice(aIndex, 0, this.mTabListeners.splice(aTab._tPos, 1)[0]);
var oldPosition = aTab._tPos;
aIndex = aIndex < aTab._tPos ? aIndex: aIndex+1; aIndex = aIndex < aTab._tPos ? aIndex: aIndex+1;
this.mCurrentTab.selected = false; this.mCurrentTab.selected = false;
this.mTabContainer.insertBefore(aTab, this.mTabContainer.childNodes[aIndex]); this.mTabContainer.insertBefore(aTab, this.mTabContainer.childNodes[aIndex]);
// invalidate cache, because mTabContainer is about to change
this._browsers = null;
var i; var i;
for (i = 0; i < this.mTabContainer.childNodes.length; i++) { for (i = 0; i < this.mTabContainer.childNodes.length; i++) {
this.mTabContainer.childNodes[i]._tPos = i; this.mTabContainer.childNodes[i]._tPos = i;
} }
this.mCurrentTab.selected = true; this.mCurrentTab.selected = true;
var evt = document.createEvent("UIEvents");
evt.initUIEvent("TabMove", true, false, window, oldPosition);
aTab.dispatchEvent(evt);
return aTab; return aTab;
]]> ]]>
</body> </body>
@ -1717,11 +1835,11 @@
var i; var i;
if (window.getComputedStyle(this.parentNode, null).direction == "ltr") { if (window.getComputedStyle(this.parentNode, null).direction == "ltr") {
for (i = aEvent.target.localName == "tab" ? aEvent.target._tPos : 0; i < this.mTabs.length; i++) for (i = aEvent.target.localName == "tab" ? aEvent.target._tPos : 0; i < this.mTabs.length; i++)
if (aEvent.clientX < this.mTabs[i].boxObject.x + this.mTabs[i].boxObject.width / 2) if (aEvent.screenX < this.mTabs[i].boxObject.screenX + this.mTabs[i].boxObject.width / 2)
return i; return i;
} else { } else {
for (i = aEvent.target.localName == "tab" ? aEvent.target._tPos : 0; i < this.mTabs.length; i++) for (i = aEvent.target.localName == "tab" ? aEvent.target._tPos : 0; i < this.mTabs.length; i++)
if (aEvent.clientX > this.mTabs[i].boxObject.x + this.mTabs[i].boxObject.width / 2) if (aEvent.screenX > this.mTabs[i].boxObject.screenX + this.mTabs[i].boxObject.width / 2)
return i; return i;
} }
@ -2264,9 +2382,13 @@
<binding id="tabbrowser-tabs" <binding id="tabbrowser-tabs"
extends="chrome://global/content/bindings/tabbox.xml#tabs"> extends="chrome://global/content/bindings/tabbox.xml#tabs">
<content> <content>
<xul:hbox flex="1" style="min-width: 1px;"> <xul:arrowscrollbox anonid="arrowscrollbox" orient="horizontal" flex="1" style="min-width: 1px;" clicktoscroll="true">
<children includes="tab"/> <children includes="tab"/>
<xul:spacer class="tabs-right" flex="1"/> </xul:arrowscrollbox>
<xul:hbox class="tabs-closebutton-box" align="center" pack="end" anonid="tabstrip-closebutton">
<xul:toolbarbutton ondblclick="event.stopPropagation();"
class="close-button tabs-closebutton"
oncommand="this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.removeCurrentTab()"/>
</xul:hbox> </xul:hbox>
</content> </content>
<implementation> <implementation>
@ -2276,20 +2398,32 @@
getService(Components.interfaces.nsIPrefBranch2); getService(Components.interfaces.nsIPrefBranch2);
try { try {
this.mTabClipWidth = pb2.getIntPref("browser.tabs.tabClipWidth"); this.mTabClipWidth = pb2.getIntPref("browser.tabs.tabClipWidth");
this.mCloseButtons = pb2.getIntPref("browser.tabs.closeButtons");
} }
catch (e) { catch (e) {
} }
this._updateDisableBackgroundClose(); this._updateDisableBackgroundClose();
this.adjustTabstrip(false);
pb2.addObserver("browser.tabs.disableBackgroundClose", this._prefObserver, true); pb2.addObserver("browser.tabs.disableBackgroundClose", this._prefObserver, true);
pb2.addObserver("browser.tabs.closeButtons", this._prefObserver, true);
var self = this; var self = this;
function onResize() { function onResize() {
self.adjustCloseButtons(1); self.adjustTabstrip(false);
} }
window.addEventListener("resize", onResize, false); window.addEventListener("resize", onResize, false);
</constructor> </constructor>
<field name="mTabstrip">
document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox");
</field>
<field name="mTabstripClosebutton">
document.getAnonymousElementByAttribute(this, "anonid", "tabstrip-closebutton");
</field>
<method name="_updateDisableBackgroundClose"> <method name="_updateDisableBackgroundClose">
<body><![CDATA[ <body><![CDATA[
var prefs = var prefs =
@ -2306,13 +2440,26 @@
} }
]]></body> ]]></body>
</method> </method>
<field name="_prefObserver">({ <field name="_prefObserver">({
tabbox: this, tabbox: this,
observe: function(subject, topic, data) observe: function(subject, topic, data)
{ {
if (topic == "nsPref:changed") if (topic == "nsPref:changed") {
this.tabbox._updateDisableBackgroundClose(); switch (data) {
case "browser.tabs.disableBackgroundClose":
this._updateDisableBackgroundClose();
break;
case "browser.tabs.closeButtons":
var pb2 =
Components.classes['@mozilla.org/preferences-service;1'].
getService(Components.interfaces.nsIPrefBranch2);
this.mCloseButtons = pb2.getIntPref("browser.tabs.closeButtons");
this.adjustTabstrip(false);
break;
}
}
}, },
QueryInterface : function(aIID) QueryInterface : function(aIID)
@ -2325,44 +2472,97 @@
} }
}); });
</field> </field>
<field name="mTabClipWidth">140</field> <field name="mTabClipWidth">130</field>
<method name="adjustCloseButtons"> <field name="mCloseButtons">1</field>
<parameter name="aNumTabs"/>
<body><![CDATA[
// aNumTabs is the number of tabs that need to be present to cause
// the close button on the last visible tab to disappear when the
// pref for "always show the tab bar, even when only one tab is open"
// is set.
// When tabs are being removed from the tab strip, and the number of
// open tabs approaches 1 (i.e. when the number of open tabs is 2
// and one is removed), we need to set an attribute on the tabstrip
// that will cause the close button on the last item to be hidden.
// When tabs are being added to the tab strip - the number of open
// tabs is increasing (i.e. the number of open tabs is 1 and one is
// added) then we need to remove the attribute on the tab strip which
// will cause the close button to be shown on all tabs.
try {
if (this.childNodes.length == aNumTabs)
this.setAttribute("singlechild", "true");
else
this.removeAttribute("singlechild");
<method name="adjustTabstrip">
<parameter name="aRemovingTab"/>
<body><![CDATA[
// modes for tabstrip
// 0 - activetab = close button on active tab only
// 1 - alltabs = close buttons on all tabs
// 2 - noclose = no close buttons at all
// 3 - closeatend = close button at the end of the tabstrip
switch (this.mCloseButtons) {
case 0:
// TabClose fires before the tab closes, so if we have two tabs
// and we're removing the tab we should go to no closebutton
if ((aRemovingTab && this.childNodes.length == 2) ||
this.childNodes.length == 1)
this.setAttribute("closebuttons", "noclose");
else
this.setAttribute("closebuttons", "activetab");
break;
case 1:
try {
// if we have only one tab, hide the closebutton
if ((aRemovingTab && this.childNodes.length == 2) ||
this.childNodes.length == 1)
this.setAttribute("closebuttons", "noclose");
else {
var width = this.firstChild.boxObject.width; var width = this.firstChild.boxObject.width;
// 0 width is an invalid value and indicates an item without display, // 0 width is an invalid value and indicates an item without display,
// so ignore. // so ignore.
if (width > this.mTabClipWidth || width == 0) if (width > this.mTabClipWidth || width == 0)
this.removeAttribute("tiny"); this.setAttribute("closebuttons", "alltabs");
else else
this.setAttribute("tiny", "true"); this.setAttribute("closebuttons", "activetab");
}
} }
catch (e) { catch (e) {
} }
break;
case 2:
case 3:
this.setAttribute("closebuttons", "noclose");
break;
}
this.mTabstripClosebutton.collapsed = this.mCloseButtons != 3;
if (aRemovingTab) {
// if we're at the end of the tabstrip, we need to ensure
// that we stay completely scrolled to the end
// this is a hack to determine if that's where we are already
var tabWidth = this.firstChild.boxObject.width;
var scrollPos = {};
this.mTabstrip.scrollBoxObject.getPosition(scrollPos, {});
if (scrollPos.value + this.mTabstrip.boxObject.width > tabWidth * (this.childNodes.length - 1))
this.mTabstrip.scrollByPixels(-1 * this.firstChild.boxObject.width);
}
]]></body>
</method>
<field name="_mPrefs">null</field>
<property name="mPrefs" readonly="true">
<getter>
<![CDATA[
if (!this._mPrefs) {
this._mPrefs =
Components.classes['@mozilla.org/preferences-service;1'].
getService(Components.interfaces.nsIPrefBranch2);
}
return this._mPrefs;
]]>
</getter>
</property>
<method name="_handleTabSelect">
<body><![CDATA[
this.mTabstrip.scrollBoxObject.ensureElementIsVisible(this.selectedItem);
]]></body>
</method>
<method name="_handleUnderflow">
<body><![CDATA[
this.mTabstrip.scrollBoxObject.scrollBy(-2400, 0);
]]></body> ]]></body>
</method> </method>
</implementation> </implementation>
<handlers> <handlers>
<handler event="DOMNodeInserted" action="this.adjustCloseButtons(1);"/> <handler event="TabOpen" action="this.adjustTabstrip(false);"/>
<handler event="DOMNodeRemoved" action="this.adjustCloseButtons(2);"/> <handler event="TabClose" action="this.adjustTabstrip(true);"/>
<handler event="TabSelect" action="this._handleTabSelect()"/>
<handler event="underflow" action="this._handleUnderflow()"/>
</handlers> </handlers>
</binding> </binding>

Просмотреть файл

@ -59,9 +59,13 @@
<xul:stack> <xul:stack>
<xul:spacer class="tabs-left"/> <xul:spacer class="tabs-left"/>
</xul:stack> </xul:stack>
<xul:hbox flex="1" style="min-width: 1px;"> <xul:arrowscrollbox anonid="arrowscrollbox" orient="horizontal" flex="1" style="min-width: 1px;">
<children/> <children/>
<xul:spacer class="tabs-right" flex="1"/> </xul:arrowscrollbox>
<xul:hbox class="tabs-closebutton-box" align="center" pack="end" anonid="tabstrip-closebutton">
<xul:toolbarbutton ondblclick="event.stopPropagation();"
class="close-button tabs-closebutton"
oncommand="this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.removeCurrentTab()"/>
</xul:hbox> </xul:hbox>
</xul:hbox> </xul:hbox>
<xul:spacer class="tabs-bottom-spacer"/> <xul:spacer class="tabs-bottom-spacer"/>