diff --git a/browser/base/content/tabview/groupitems.js b/browser/base/content/tabview/groupitems.js index 83cc7f1b3d11..b343490cb97a 100644 --- a/browser/base/content/tabview/groupitems.js +++ b/browser/base/content/tabview/groupitems.js @@ -882,6 +882,7 @@ GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), { addAppTab: function GroupItem_addAppTab(xulTab) { let self = this; + // add the icon let icon = xulTab.image || Utils.defaultFaviconURL; let $appTab = iQ("") .addClass("appTabIcon") @@ -897,6 +898,7 @@ GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), { UI.goToTab(iQ(this).data("xulTab")); }); + // adjust the tray let columnWidth = $appTab.width(); if (parseInt(this.$appTabTray.css("width")) != columnWidth) { this.$appTabTray.css({width: columnWidth}); @@ -904,6 +906,25 @@ GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), { } }, + // ---------- + // Removes the given xul:tab as an app tab in this group's apptab tray + removeAppTab: function GroupItem_removeAppTab(xulTab) { + // remove the icon + iQ(".appTabIcon", this.$appTabTray).each(function(icon) { + let $icon = iQ(icon); + if ($icon.data("xulTab") != xulTab) + return; + + $icon.remove(); + }); + + // adjust the tray + if (!iQ(".appTabIcon", this.$appTabTray).length) { + this.$appTabTray.css({width: 0}); + this.arrange(); + } + }, + // ---------- // Function: hideExpandControl // Hide the control which expands a stacked groupItem into a quick-look view. @@ -1502,12 +1523,14 @@ let GroupItems = { // ---------- // Function: uninit uninit : function GroupItems_uninit () { + // call our cleanup functions this._cleanupFunctions.forEach(function(func) { func(); }); this._cleanupFunctions = []; + // additional clean up this.groupItems = null; }, @@ -1530,6 +1553,22 @@ let GroupItems = { }); }, + // ---------- + // when a tab becomes pinned, add it to the app tab tray in all groups + handleTabPin: function GroupItems_handleTabPin(xulTab) { + this.groupItems.forEach(function(groupItem) { + groupItem.addAppTab(xulTab); + }); + }, + + // ---------- + // when a tab becomes unpinned, remove it from the app tab tray in all groups + handleTabUnpin: function GroupItems_handleTabUnpin(xulTab) { + this.groupItems.forEach(function(groupItem) { + groupItem.removeAppTab(xulTab); + }); + }, + // ---------- // Function: getNextID // Returns the next unused groupItem ID. diff --git a/browser/base/content/tabview/tabitems.js b/browser/base/content/tabview/tabitems.js index 94b87424f42a..2cd47bf4e5cb 100644 --- a/browser/base/content/tabview/tabitems.js +++ b/browser/base/content/tabview/tabitems.js @@ -831,8 +831,8 @@ let TabItems = { unlink: function TabItems_unlink(tab) { try { Utils.assertThrow(tab, "tab"); - Utils.assertThrow(!tab.pinned, "shouldn't be an app tab"); Utils.assertThrow(tab.tabItem, "should already be linked"); + // note that it's ok to unlink an app tab; see .handleTabUnpin this.unregister(tab.tabItem); tab.tabItem._sendToSubscribers("close"); @@ -851,6 +851,19 @@ let TabItems = { } }, + // ---------- + // when a tab becomes pinned, destroy its TabItem + handleTabPin: function TabItems_handleTabPin(xulTab) { + this.unlink(xulTab); + }, + + // ---------- + // when a tab becomes unpinned, create a TabItem for it + handleTabUnpin: function TabItems_handleTabUnpin(xulTab) { + this.link(xulTab); + this.update(xulTab); + }, + // ---------- // Function: heartbeat // Allows us to spreadout update calls over a period of time. diff --git a/browser/base/content/tabview/ui.js b/browser/base/content/tabview/ui.js index 3be498b5e848..854c5fd970b5 100644 --- a/browser/base/content/tabview/ui.js +++ b/browser/base/content/tabview/ui.js @@ -83,6 +83,10 @@ let UI = { // Keeps track of event listeners added to the AllTabs object. _eventListeners: {}, + // Variable: _cleanupFunctions + // An array of functions to be called at uninit time + _cleanupFunctions: [], + // ---------- // Function: init // Must be called after the object is created. @@ -233,6 +237,14 @@ let UI = { }, uninit: function UI_uninit() { + // call our cleanup functions + this._cleanupFunctions.forEach(function(func) { + func(); + }); + + this._cleanupFunctions = []; + + // additional clean up TabItems.uninit(); GroupItems.uninit(); Storage.uninit(); @@ -459,6 +471,28 @@ let UI = { for (let name in this._eventListeners) AllTabs.register(name, this._eventListeners[name]); + + // Start watching for tab pin events, and set up our uninit for same. + function handleTabPin(event) { + TabItems.handleTabPin(event.originalTarget); + GroupItems.handleTabPin(event.originalTarget); + } + + gBrowser.tabContainer.addEventListener("TabPinned", handleTabPin, false); + this._cleanupFunctions.push(function() { + gBrowser.tabContainer.removeEventListener("TabPinned", handleTabPin, false); + }); + + // Start watching for tab unpin events, and set up our uninit for same. + function handleTabUnpin(event) { + TabItems.handleTabUnpin(event.originalTarget); + GroupItems.handleTabUnpin(event.originalTarget); + } + + gBrowser.tabContainer.addEventListener("TabUnpinned", handleTabUnpin, false); + this._cleanupFunctions.push(function() { + gBrowser.tabContainer.removeEventListener("TabUnpinned", handleTabUnpin, false); + }); }, // ---------- @@ -582,7 +616,7 @@ let UI = { function getClosestTabBy(norm) { var centers = - [[item.bounds.center(), item] + [[item.bounds.center(), item] for each(item in TabItems.getItems()) if (!item.parent || !item.parent.hidden)]; var myCenter = self.getActiveTab().bounds.center(); var matches = centers diff --git a/browser/base/content/test/tabview/browser_tabview_apptabs.js b/browser/base/content/test/tabview/browser_tabview_apptabs.js index d66c0544b018..839bb83b1de6 100644 --- a/browser/base/content/test/tabview/browser_tabview_apptabs.js +++ b/browser/base/content/test/tabview/browser_tabview_apptabs.js @@ -52,38 +52,77 @@ function onTabViewWindowLoaded() { // establish initial state is(contentWindow.GroupItems.groupItems.length, 1, "we start with one group (the default)"); is(gBrowser.tabs.length, 1, "we start with one tab"); + let originalTab = gBrowser.tabs[0]; - // create an app tab - let appXulTab = gBrowser.loadOneTab("about:blank"); - gBrowser.pinTab(appXulTab); - is(gBrowser.tabs.length, 2, "we now have two tabs"); - - // Create a group + // create a group let box = new contentWindow.Rect(20, 20, 180, 180); - let groupItem = new contentWindow.GroupItem([], { bounds: box }); + let groupItemOne = new contentWindow.GroupItem([], { bounds: box, title: "test1" }); is(contentWindow.GroupItems.groupItems.length, 2, "we now have two groups"); + contentWindow.GroupItems.setActiveGroupItem(groupItemOne); + // create a tab + let xulTab = gBrowser.loadOneTab("about:blank"); + is(gBrowser.tabs.length, 2, "we now have two tabs"); + is(groupItemOne._children.length, 1, "the new tab was added to the group"); + + // make sure the group has no app tabs + let appTabIcons = groupItemOne.container.getElementsByClassName("appTabIcon"); + is(appTabIcons.length, 0, "there are no app tab icons"); + + // pin the tab, make sure the TabItem goes away and the icon comes on + gBrowser.pinTab(xulTab); + + is(groupItemOne._children.length, 0, "the app tab's TabItem was removed from the group"); + + appTabIcons = groupItemOne.container.getElementsByClassName("appTabIcon"); + is(appTabIcons.length, 1, "there's now one app tab icon"); + + // create a second group and make sure it gets the icon too + box.offset(box.width + 20, 0); + let groupItemTwo = new contentWindow.GroupItem([], { bounds: box, title: "test2" }); + is(contentWindow.GroupItems.groupItems.length, 3, "we now have three groups"); + appTabIcons = groupItemTwo.container.getElementsByClassName("appTabIcon"); + is(appTabIcons.length, 1, "there's an app tab icon in the second group"); + + // unpin the tab, make sure the icon goes away and the TabItem comes on + gBrowser.unpinTab(xulTab); + + is(groupItemOne._children.length, 1, "the app tab's TabItem is back"); + + appTabIcons = groupItemOne.container.getElementsByClassName("appTabIcon"); + is(appTabIcons.length, 0, "the icon is gone from group one"); + + appTabIcons = groupItemTwo.container.getElementsByClassName("appTabIcon"); + is(appTabIcons.length, 0, "the icon is gone from group 2"); + + // pin the tab again + gBrowser.pinTab(xulTab); + + // close the second group + groupItemTwo.close(); + // find app tab in group and hit it let onTabViewHidden = function() { window.removeEventListener("tabviewhidden", onTabViewHidden, false); ok(!TabView.isVisible(), "Tab View is hidden because we clicked on the app tab"); // clean up - gBrowser.unpinTab(appXulTab); - gBrowser.removeTab(appXulTab); + gBrowser.selectedTab = originalTab; + + gBrowser.unpinTab(xulTab); + gBrowser.removeTab(xulTab); is(gBrowser.tabs.length, 1, "we finish with one tab"); - groupItem.close(); + groupItemOne.close(); is(contentWindow.GroupItems.groupItems.length, 1, "we finish with one group"); - ok(!TabView.isVisible(), "Tab View is not visible"); + ok(!TabView.isVisible(), "we finish with Tab View not visible"); finish(); }; window.addEventListener("tabviewhidden", onTabViewHidden, false); - let appTabButtons = groupItem.$appTabTray[0].getElementsByTagName("img"); - ok(appTabButtons.length == 1, "there is one app tab button"); - EventUtils.sendMouseEvent({ type: "click" }, appTabButtons[0], contentWindow); + appTabIcons = groupItemOne.container.getElementsByClassName("appTabIcon"); + EventUtils.sendMouseEvent({ type: "click" }, appTabIcons[0], contentWindow); }