Bug 637840 - after closing a group in panorama, focus should go to the last used tab in the last used group r=tim
authorRaymond Lee <raymond@raysquare.com>
Tue, 06 Sep 2011 13:04:38 +0800
changeset 77974 e4ab06518d81c805bf0b6f80ca97b93b29761278
parent 77973 02f42784ccb28b942b345d44d3465daf4ed78376
child 77975 183fb86c9ac4858045a9ca5d9e4997cda3c3f258
push id78
push userclegnitto@mozilla.com
push dateFri, 16 Dec 2011 17:32:24 +0000
treeherdermozilla-release@79d24e644fdd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstim
bugs637840
milestone9.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 637840 - after closing a group in panorama, focus should go to the last used tab in the last used group r=tim
browser/base/content/tabview/groupitems.js
browser/base/content/tabview/modules/utils.jsm
browser/base/content/test/tabview/Makefile.in
browser/base/content/test/tabview/browser_tabview_bug637840.js
--- a/browser/base/content/tabview/groupitems.js
+++ b/browser/base/content/tabview/groupitems.js
@@ -706,33 +706,44 @@ GroupItem.prototype = Utils.extend(new I
         }
       });
 
       this.droppable(false);
       this.removeTrenches();
       this._createUndoButton();
     } else
       this.close();
-    
-    this._makeClosestTabActive();
+
+    this._makeLastActiveGroupItemActive();
   },
   
   // ----------
   // Function: _makeClosestTabActive
   // Make the closest tab external to this group active.
   // Used when closing the group.
   _makeClosestTabActive: function GroupItem__makeClosestTabActive() {
     let closeCenter = this.getBounds().center();
     // Find closest tab to make active
     let closestTabItem = UI.getClosestTab(closeCenter);
     if (closestTabItem)
       UI.setActive(closestTabItem);
   },
 
   // ----------
+  // Function: _makeLastActiveGroupItemActive
+  // Makes the last active group item active.
+  _makeLastActiveGroupItemActive: function GroupItem__makeLastActiveGroupItemActive() {
+    let groupItem = GroupItems.getLastActiveGroupItem();
+    if (groupItem)
+      UI.setActive(groupItem);
+    else
+      this._makeClosestTabActive();
+  },
+
+  // ----------
   // Function: closeIfEmpty
   // Closes the group if it's empty, is closable, and autoclose is enabled
   // (see pauseAutoclose()). Returns true if the close occurred and false
   // otherwise.
   closeIfEmpty: function GroupItem_closeIfEmpty() {
     if (this.isEmpty() && !UI._closedLastVisibleTab &&
         !GroupItems.getUnclosableGroupItemId() && !GroupItems._autoclosePaused) {
       this.close();
@@ -1144,19 +1155,19 @@ GroupItem.prototype = Utils.extend(new I
         item.setResizable(true, options.immediately);
 
       // if a blank tab is selected while restoring a tab the blank tab gets
       // removed. we need to keep the group alive for the restored tab.
       if (item.isRemovedAfterRestore)
         options.dontClose = true;
 
       let closed = options.dontClose ? false : this.closeIfEmpty();
-      if (closed)
-        this._makeClosestTabActive();
-      else if (!options.dontArrange) {
+      if (closed) {
+        this._makeLastActiveGroupItemActive();
+      } else if (!options.dontArrange) {
         this.arrange({animate: !options.immediately});
         this._unfreezeItemSize({dontArrange: true});
       }
 
       this._sendToSubscribers("childRemoved",{ groupItemId: this.id, item: item });
     } catch(e) {
       Utils.log(e);
     }
@@ -1939,16 +1950,17 @@ let GroupItems = {
   _cleanupFunctions: [],
   _arrangePaused: false,
   _arrangesPending: [],
   _removingHiddenGroups: false,
   _delayedModUpdates: [],
   _autoclosePaused: false,
   minGroupHeight: 110,
   minGroupWidth: 125,
+  _lastActiveList: null,
 
   // ----------
   // Function: toString
   // Prints [GroupItems] for debug use
   toString: function GroupItems_toString() {
     return "[GroupItems count=" + this.groupItems.length + "]";
   },
 
@@ -1964,16 +1976,18 @@ let GroupItems = {
 
     // make sure any closed tabs are removed from the delay update list
     function handleClose(event) {
       let idx = self._delayedModUpdates.indexOf(event.target);
       if (idx != -1)
         self._delayedModUpdates.splice(idx, 1);
     }
 
+    this._lastActiveList = new MRUList();
+
     AllTabs.register("attrModified", handleAttrModified);
     AllTabs.register("close", handleClose);
     this._cleanupFunctions.push(function() {
       AllTabs.unregister("attrModified", handleAttrModified);
       AllTabs.unregister("close", handleClose);
     });
   },
 
@@ -2307,16 +2321,17 @@ let GroupItems = {
 
     if (groupItem == this._activeGroupItem)
       this._activeGroupItem = null;
 
     this._arrangesPending = this._arrangesPending.filter(function (pending) {
       return groupItem != pending.groupItem;
     });
 
+    this._lastActiveList.remove(groupItem);
     UI.updateTabButton();
   },
 
   // ----------
   // Function: groupItem
   // Given some sort of identifier, returns the appropriate groupItem.
   // Currently only supports groupItem ids.
   groupItem: function GroupItems_groupItem(a) {
@@ -2418,21 +2433,32 @@ let GroupItems = {
   setActiveGroupItem: function GroupItems_setActiveGroupItem(groupItem) {
     Utils.assert(groupItem, "groupItem must be given");
 
     if (this._activeGroupItem)
       iQ(this._activeGroupItem.container).removeClass('activeGroupItem');
 
     iQ(groupItem.container).addClass('activeGroupItem');
 
+    this._lastActiveList.update(groupItem);
     this._activeGroupItem = groupItem;
     this._save();
   },
 
   // ----------
+  // Function: getLastActiveGroupItem
+  // Gets last active group item.
+  // Returns the <groupItem>. If nothing is found, return null.
+  getLastActiveGroupItem: function GroupItem_getLastActiveGroupItem() {
+    return this._lastActiveList.peek(function(groupItem) {
+      return (groupItem && !groupItem.hidden && groupItem.getChildren().length > 0)
+    });
+  },
+
+  // ----------
   // Function: _updateTabBar
   // Hides and shows tabs in the tab bar based on the active groupItem
   _updateTabBar: function GroupItems__updateTabBar() {
     if (!window.UI)
       return; // called too soon
 
     Utils.assert(this._activeGroupItem, "There must be something to show in the tab bar!");
 
--- a/browser/base/content/tabview/modules/utils.jsm
+++ b/browser/base/content/tabview/modules/utils.jsm
@@ -45,17 +45,17 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 "use strict";
 
 // **********
 // Title: utils.js
 
-let EXPORTED_SYMBOLS = ["Point", "Rect", "Range", "Subscribable", "Utils"];
+let EXPORTED_SYMBOLS = ["Point", "Rect", "Range", "Subscribable", "Utils", "MRUList"];
 
 // #########
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 // ##########
@@ -790,8 +790,68 @@ let Utils = {
       try {
         return args[i]();
       } catch (e) {}
     }
 
     return null;
   }
 };
+
+// ##########
+// Class: MRUList
+// A most recently used list.
+//
+// Constructor: MRUList
+// If a is an array of entries, creates a copy of it.
+function MRUList(a) {
+  if (Array.isArray(a))
+    this._list = a.concat();
+  else
+    this._list = [];
+};
+
+MRUList.prototype = {
+  // ----------
+  // Function: toString
+  // Prints [List (entry1, entry2, ...)] for debug use
+  toString: function MRUList_toString() {
+    return "[List (" + this._list.join(", ") + ")]";
+  },
+
+  // ----------
+  // Function: update
+  // Updates/inserts the given entry as the most recently used one in the list.
+  update: function MRUList_update(entry) {
+    this.remove(entry);
+    this._list.unshift(entry);
+  },
+
+  // ----------
+  // Function: remove
+  // Removes the given entry from the list.
+  remove: function MRUList_remove(entry) {
+    let index = this._list.indexOf(entry);
+    if (index > -1)
+      this._list.splice(index, 1);
+  },
+
+  // ----------
+  // Function: peek
+  // Returns the most recently used entry.  If a filter exists, gets the most 
+  // recently used entry which matches the filter.
+  peek: function MRUList_peek(filter) {
+    let match = null;
+    if (filter && typeof filter == "function")
+      this._list.some(function MRUList_peek_getEntry(entry) {
+        if (filter(entry)) {
+          match = entry
+          return true;
+        }
+        return false;
+      });
+    else 
+      match = this._list.length > 0 ? this._list[0] : null;
+
+    return match;
+  },
+};
+
--- a/browser/base/content/test/tabview/Makefile.in
+++ b/browser/base/content/test/tabview/Makefile.in
@@ -125,16 +125,17 @@ include $(topsrcdir)/config/rules.mk
                  browser_tabview_bug630157.js \
                  browser_tabview_bug631662.js \
                  browser_tabview_bug631752.js \
                  browser_tabview_bug633788.js \
                  browser_tabview_bug634077.js \
                  browser_tabview_bug634085.js \
                  browser_tabview_bug634672.js \
                  browser_tabview_bug635696.js \
+                 browser_tabview_bug637840.js \
                  browser_tabview_bug640765.js \
                  browser_tabview_bug641802.js \
                  browser_tabview_bug642793.js \
                  browser_tabview_bug643392.js \
                  browser_tabview_bug644097.js \
                  browser_tabview_bug648882.js \
                  browser_tabview_bug649006.js \
                  browser_tabview_bug649307.js \
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabview/browser_tabview_bug637840.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let cw;
+
+function test() {
+  waitForExplicitFinish();
+
+  newWindowWithTabView(function(win) {
+    registerCleanupFunction(function() {
+      win.close();
+    });
+
+    cw = win.TabView.getContentWindow();
+
+    let groupItemOne = cw.GroupItems.groupItems[0];
+    is(groupItemOne.getChildren().length, 1, "Group one has 1 tab item");
+
+    let groupItemTwo = createGroupItemWithBlankTabs(win, 300, 300, 40, 2);
+    is(groupItemTwo.getChildren().length, 2, "Group two has 2 tab items");
+
+    let groupItemThree = createGroupItemWithBlankTabs(win, 300, 300, 40, 2);
+    is(groupItemThree.getChildren().length, 2, "Group three has 2 tab items");
+
+    testMoreRecentlyUsedGroup(groupItemOne, groupItemTwo, function() {
+      testMoreRecentlyUsedGroup(groupItemOne, groupItemThree, function() {
+        testRemoveGroupAndCheckMoreRecentlyUsedGroup(groupItemOne, groupItemTwo);
+      });
+    });
+  });
+}
+
+function testMoreRecentlyUsedGroup(groupItemOne, otherGroupItem, callback) {
+  let tabItem = otherGroupItem.getChild(1);
+  cw.UI.setActive(tabItem);
+  is(otherGroupItem.getActiveTab(), tabItem, "The second item in the other group is active");
+  is(cw.GroupItems.getActiveGroupItem(), otherGroupItem, "The other group is active");
+
+  let tabItemInGroupItemOne = groupItemOne.getChild(0);
+  cw.UI.setActive(tabItemInGroupItemOne);
+  is(groupItemOne.getActiveTab(), tabItemInGroupItemOne, "The first item in group one is active");
+  is(cw.GroupItems.getActiveGroupItem(), groupItemOne, "The group one is active");
+
+  groupItemOne.addSubscriber("groupHidden", function onHide() {
+    groupItemOne.removeSubscriber("groupHidden", onHide);
+
+    // group item three should have the focus
+    is(otherGroupItem.getActiveTab(), tabItem, "The second item in the other group is active after group one is hidden");
+    is(cw.GroupItems.getActiveGroupItem(), otherGroupItem, "The other group is active active after group one is hidden");
+
+    groupItemOne.addSubscriber("groupShown", function onShown() {
+      groupItemOne.removeSubscriber("groupShown", onShown);
+
+      is(groupItemOne.getActiveTab(), tabItemInGroupItemOne, "The first item in group one is active after it is shown");
+      is(cw.GroupItems.getActiveGroupItem(), groupItemOne, "The group one is active after it is shown");
+
+      callback();
+    });
+    // click on the undo button
+    EventUtils.sendMouseEvent(
+      { type: "click" }, groupItemOne.$undoContainer[0], cw);
+  });
+  // click on the close button of group item one
+  let closeButton = groupItemOne.container.getElementsByClassName("close");
+  ok(closeButton[0], "Group item one close button exists");
+  EventUtils.sendMouseEvent({ type: "click" }, closeButton[0], cw);
+}
+
+function testRemoveGroupAndCheckMoreRecentlyUsedGroup(groupItemOne, groupItemTwo) {
+  let tabItem = groupItemTwo.getChild(0);
+  cw.UI.setActive(tabItem);
+
+  is(groupItemTwo.getActiveTab(), tabItem, "The first item in the group two is active");
+  is(cw.GroupItems.getActiveGroupItem(), groupItemTwo, "The group two is active");
+
+  let tabItemInGroupItemOne = groupItemOne.getChild(0);
+
+  tabItemInGroupItemOne.addSubscriber("close", function onClose() {
+    tabItemInGroupItemOne.removeSubscriber("close", onClose);
+
+    is(groupItemTwo.getActiveTab(), tabItem, "The first item in the group two is still active after group one is closed");
+    is(cw.GroupItems.getActiveGroupItem(), groupItemTwo, "The group two is still active after group one is closed");
+
+    finish();
+  });
+  // close the tab item and the group item
+  let closeButton = tabItemInGroupItemOne.container.getElementsByClassName("close");
+  ok(closeButton[0], "Tab item close button exists");
+  EventUtils.sendMouseEvent({ type: "mousedown" }, closeButton[0], cw);
+  EventUtils.sendMouseEvent({ type: "mouseup" }, closeButton[0], cw);
+}
+