Bug 578553 - Implement App-Tab experience in Panorama; [a+r=dietrich]

This commit is contained in:
Ian Gilman
2010-09-10 17:50:14 +08:00
parent dc4493ab30
commit 36f1f942af
10 changed files with 239 additions and 37 deletions

View File

@@ -64,7 +64,6 @@
// title - the title for the groupItem; otherwise blank
// dontPush - true if this groupItem shouldn't push away on creation; default is false
function GroupItem(listOfEls, options) {
try {
if (typeof options == 'undefined')
options = {};
@@ -253,6 +252,16 @@ function GroupItem(listOfEls, options) {
.appendTo($container)
.hide();
// ___ app tabs: create app tab tray and populate it
this.$appTabTray = iQ("<div/>")
.addClass("appTabTray")
.appendTo($container);
AllTabs.tabs.forEach(function(xulTab) {
if (xulTab.pinned && xulTab.ownerDocument.defaultView == gWindow)
self.addAppTab(xulTab);
});
// ___ locking
if (this.locked.bounds)
$container.css({cursor: 'default'});
@@ -292,10 +301,6 @@ function GroupItem(listOfEls, options) {
this._inited = true;
this.save();
} catch(e) {
Utils.log("Error in GroupItem()");
Utils.log(e.stack);
}
};
// ----------
@@ -307,15 +312,19 @@ GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), {
// -----------
// Function: setActiveTab
// Sets the active <TabItem> for this groupItem
// Sets the active <TabItem> for this groupItem; can be null, but only
// if there are no children.
setActiveTab: function GroupItem_setActiveTab(tab) {
Utils.assert(tab && tab.isATabItem, 'tab must be a TabItem');
Utils.assertThrow((!tab && this._children.length == 0) || tab.isATabItem,
"tab must be null (if no children) or a TabItem");
this._activeTab = tab;
},
// -----------
// Function: getActiveTab
// Gets the active <TabItem> for this groupItem
// Gets the active <TabItem> for this groupItem; can be null, but only
// if there are no children.
getActiveTab: function GroupItem_getActiveTab() {
return this._activeTab;
},
@@ -397,6 +406,8 @@ GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), {
box.top += titleHeight;
box.height -= titleHeight;
box.width -= this.$appTabTray.width();
// Make the computed bounds' "padding" and new tab button margin actually be
// themeable --OR-- compute this from actual bounds. Bug 586546
box.inset(6, 6);
@@ -695,6 +706,13 @@ GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), {
var index = this._children.indexOf(item);
if (index != -1)
this._children.splice(index, 1);
if (item == this._activeTab) {
if (this._children.length)
this._activeTab = this._children[0];
else
this._activeTab = null;
}
item.setParent(null);
item.removeClass("tabInGroupItem");
@@ -732,6 +750,33 @@ GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), {
});
},
// ----------
// Adds the given xul:tab as an app tab in this group's apptab tray
addAppTab: function GroupItem_addAppTab(xulTab) {
let self = this;
let icon = xulTab.image || Utils.defaultFaviconURL;
let $appTab = iQ("<img>")
.addClass("appTabIcon")
.attr("src", icon)
.data("xulTab", xulTab)
.appendTo(this.$appTabTray)
.click(function(event) {
if (Utils.isRightClick(event))
return;
GroupItems.setActiveGroupItem(self);
GroupItems._updateTabBar();
UI.goToTab(iQ(this).data("xulTab"));
});
let columnWidth = $appTab.width();
if (parseInt(this.$appTabTray.css("width")) != columnWidth) {
this.$appTabTray.css({width: columnWidth});
this.arrange();
}
},
// ----------
// Function: hideExpandControl
// Hide the control which expands a stacked groupItem into a quick-look view.
@@ -1129,6 +1174,8 @@ GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), {
className.indexOf('name') != -1 ||
className.indexOf('close') != -1 ||
className.indexOf('newTabButton') != -1 ||
className.indexOf('appTabTray') != -1 ||
className.indexOf('appTabIcon') != -1 ||
className.indexOf('stackExpander') != -1) {
return;
}
@@ -1181,7 +1228,9 @@ GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), {
GroupItems.setActiveGroupItem(this);
let newTab = gBrowser.loadOneTab(url || "about:blank", {inBackground: true});
// TabItems will have handled the new tab and added the tabItem property
// TabItems will have handled the new tab and added the tabItem property.
// We don't have to check if it's an app tab (and therefore wouldn't have a
// TabItem), since we've just created it.
let newItem = newTab.tabItem;
var self = this;
@@ -1604,16 +1653,18 @@ let GroupItems = {
// ----------
// Function: updateActiveGroupItemAndTabBar
// Sets active group item and updates tab bar
// Sets active TabItem and GroupItem, and updates tab bar appropriately.
updateActiveGroupItemAndTabBar: function GroupItems_updateActiveGroupItemAndTabBar(tabItem) {
if (tabItem.parent) {
let groupItem = tabItem.parent;
this.setActiveGroupItem(groupItem);
Utils.assertThrow(tabItem && tabItem.isATabItem, "tabItem must be a TabItem");
let groupItem = tabItem.parent;
this.setActiveGroupItem(groupItem);
if (groupItem)
groupItem.setActiveTab(tabItem);
} else {
this.setActiveGroupItem(null);
else
this.setActiveOrphanTab(tabItem);
}
this._updateTabBar();
},
@@ -1693,10 +1744,17 @@ let GroupItems = {
// ----------
// Function: moveTabToGroupItem
// Used for the right click menu in the tab strip; moves the given tab
// into the given group. Does nothing if the tab is an app tab.
// Paramaters:
// tab - the <xul:tab>.
// groupItemId - the <groupItem>'s id. If nothing, create a new <groupItem>.
moveTabToGroupItem : function GroupItems_moveTabToGroupItem (tab, groupItemId) {
if (tab.pinned)
return;
Utils.assertThrow(tab.tabItem, "tab must be linked to a TabItem");
let shouldUpdateTabBar = false;
let shouldShowTabView = false;
let groupItem;

View File

@@ -478,8 +478,9 @@ Subscribable.prototype = {
// Class: Utils
// Singelton with common utility functions.
let Utils = {
// ___ Logging
defaultFaviconURL: "chrome://mozapps/skin/places/defaultFavicon.png",
// ___ Logging
useConsole: true, // as opposed to dump
showTime: false,

View File

@@ -525,11 +525,7 @@ TabItem.prototype = Utils.extend(new Item(), new Subscribable(), {
.css(orig.css())
.removeClass("front");
// If it's not focused, the onFocus lsitener would handle it.
if (gBrowser.selectedTab == tab)
UI.onTabSelect(tab);
else
gBrowser.selectedTab = tab;
UI.goToTab(tab);
if (isNewBlankTab)
gWindow.gURLBar.focus();
@@ -663,7 +659,7 @@ let TabItems = {
// When a tab is opened, create the TabItem
this._eventListeners["open"] = function(tab) {
if (tab.ownerDocument.defaultView != gWindow)
if (tab.ownerDocument.defaultView != gWindow || tab.pinned)
return;
self.link(tab);
@@ -671,14 +667,14 @@ let TabItems = {
// When a tab's content is loaded, show the canvas and hide the cached data
// if necessary.
this._eventListeners["attrModified"] = function(tab) {
if (tab.ownerDocument.defaultView != gWindow)
if (tab.ownerDocument.defaultView != gWindow || tab.pinned)
return;
self.update(tab);
}
// When a tab is closed, unlink.
this._eventListeners["close"] = function(tab) {
if (tab.ownerDocument.defaultView != gWindow)
if (tab.ownerDocument.defaultView != gWindow || tab.pinned)
return;
self.unlink(tab);
@@ -689,7 +685,7 @@ let TabItems = {
// For each tab, create the link.
AllTabs.tabs.forEach(function(tab) {
if (tab.ownerDocument.defaultView != gWindow)
if (tab.ownerDocument.defaultView != gWindow || tab.pinned)
return;
self.link(tab);
@@ -722,6 +718,8 @@ let TabItems = {
update: function TabItems_update(tab) {
try {
Utils.assertThrow(tab, "tab");
Utils.assertThrow(!tab.pinned, "shouldn't be an app tab");
Utils.assertThrow(tab.tabItem, "should already be linked");
let shouldDefer = (
this.isPaintingPaused() ||
@@ -763,7 +761,7 @@ let TabItems = {
// ___ icon
let iconUrl = tab.image;
if (iconUrl == null)
iconUrl = "chrome://mozapps/skin/places/defaultFavicon.png";
iconUrl = Utils.defaultFaviconURL;
if (iconUrl != tabItem.favEl.src)
tabItem.favEl.src = iconUrl;
@@ -812,10 +810,11 @@ let TabItems = {
// ----------
// Function: link
// Takes in a xul:tab.
// Takes in a xul:tab, creates a TabItem for it and adds it to the scene.
link: function TabItems_link(tab){
try {
Utils.assertThrow(tab, "tab");
Utils.assertThrow(!tab.pinned, "shouldn't be an app tab");
Utils.assertThrow(!tab.tabItem, "shouldn't already be linked");
new TabItem(tab); // sets tab.tabItem to itself
} catch(e) {
@@ -825,10 +824,11 @@ let TabItems = {
// ----------
// Function: unlink
// Takes in a xul:tab.
// Takes in a xul:tab and destroys the TabItem associated with it.
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");
this.unregister(tab.tabItem);

View File

@@ -1,4 +1,4 @@
/* Platform-independent structural styling for
/* Platform-independent structural styling for
* <strike>Tab Candy</strike> Panorama
----------------------------------*/
@@ -88,10 +88,20 @@ body {
image-rendering: -moz-crisp-edges;
}
/* Groups
----------------------------------*/
.groupItem {
position: absolute;
}
.appTabTray {
position: absolute;
}
/* Other Items
----------------------------------*/
.groupItem,
.info-item {
position: absolute;
}

View File

@@ -106,9 +106,9 @@ let UI = {
// ___ Dev Menu
// This dev menu is not meant for shipping, nor is it of general
// interest, but we still need it for the time being. Change the
// false below to enable; just remember to change back before
// committing. Bug 586721 will track the ultimate removal.
// interest, but we still need it for the time being. Change the
// false below to enable; just remember to change back before
// committing. Bug 586721 will track the ultimate removal.
if (false)
this._addDevMenu();
@@ -466,6 +466,16 @@ let UI = {
AllTabs.unregister(name, this._eventListeners[name]);
},
// ----------
// Selects the given xul:tab in the browser.
goToTab: function UI_goToTab(xulTab) {
// If it's not focused, the onFocus listener would handle it.
if (gBrowser.selectedTab == xulTab)
this.onTabSelect(xulTab);
else
gBrowser.selectedTab = xulTab;
},
// ----------
// Function: onTabSelect
// Called when the user switches from one tab to another outside of the TabView UI.
@@ -491,7 +501,7 @@ let UI = {
let oldItem = null;
let newItem = null;
if (currentTab && currentTab.tabItem)
oldItem = currentTab.tabItem;
if (tab && tab.tabItem) {
@@ -614,13 +624,13 @@ let UI = {
event.stopPropagation();
event.preventDefault();
}
} else if (event.keyCode == KeyEvent.DOM_VK_ESCAPE ||
} else if (event.keyCode == KeyEvent.DOM_VK_ESCAPE ||
event.keyCode == KeyEvent.DOM_VK_RETURN ||
event.keyCode == KeyEvent.DOM_VK_ENTER) {
let activeTab = self.getActiveTab();
let activeGroupItem = GroupItems.getActiveGroupItem();
if (activeGroupItem && activeGroupItem.expanded &&
if (activeGroupItem && activeGroupItem.expanded &&
event.keyCode == KeyEvent.DOM_VK_ESCAPE)
activeGroupItem.collapse();
else if (activeTab)

View File

@@ -50,6 +50,7 @@ _BROWSER_FILES = \
browser_tabview_search.js \
browser_tabview_snapping.js \
browser_tabview_bug591706.js \
browser_tabview_apptabs.js \
$(NULL)
libs:: $(_BROWSER_FILES)

View File

@@ -0,0 +1,89 @@
/* ***** 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 tabview group test.
*
* The Initial Developer of the Original Code is
* Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Raymond Lee <raymond@appcoast.com>
* Ian Gilman <ian@iangilman.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 ***** */
function test() {
waitForExplicitFinish();
window.addEventListener("tabviewshown", onTabViewWindowLoaded, false);
TabView.toggle();
}
function onTabViewWindowLoaded() {
window.removeEventListener("tabviewshown", onTabViewWindowLoaded, false);
ok(TabView.isVisible(), "Tab View is visible");
let contentWindow = document.getElementById("tab-view").contentWindow;
// 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");
// 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
let box = new contentWindow.Rect(20, 20, 180, 180);
let groupItem = new contentWindow.GroupItem([], { bounds: box });
is(contentWindow.GroupItems.groupItems.length, 2, "we now have two groups");
// 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);
is(gBrowser.tabs.length, 1, "we finish with one tab");
groupItem.close();
is(contentWindow.GroupItems.groupItems.length, 1, "we finish with one group");
ok(!TabView.isVisible(), "Tab View is 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);
}

View File

@@ -183,6 +183,17 @@ body {
inset rgba(255, 255, 255, 0.6) 0 0 0 2px; */
}
.appTabTray {
top: 34px;
right: 1px;
}
.appTabIcon {
width: 16px;
height: 16px;
cursor: pointer;
}
/* InfoItems
----------------------------------*/

View File

@@ -190,6 +190,17 @@ body {
inset rgba(255, 255, 255, 0.6) 0 0 0 2px; */
}
.appTabTray {
top: 34px;
right: 1px;
}
.appTabIcon {
width: 16px;
height: 16px;
cursor: pointer;
}
/* InfoItems
----------------------------------*/

View File

@@ -195,6 +195,17 @@ body {
inset rgba(255, 255, 255, 0.6) 0 0 0 2px; */
}
.appTabTray {
top: 34px;
right: 1px;
}
.appTabIcon {
width: 16px;
height: 16px;
cursor: pointer;
}
/* InfoItems
----------------------------------*/