Bug 625443 - Arranging of expanded stacked groups is broken [r=ian, a=blocking2.0-final+]
authorMichael Yoshitaka Erlewine <mitcho@mitcho.com>
Sun, 16 Jan 2011 10:38:48 -0500
changeset 60700 7c9e4303c3c7b545e02cb405a0894dd4f1d7701e
parent 60699 9698210ea3c66a935750a597ce6b48e1736fc9ca
child 60701 c22c9543a44959b24069d44dcb8dfb41e360c1c9
push idunknown
push userunknown
push dateunknown
reviewersian, blocking2
bugs625443
milestone2.0b10pre
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 625443 - Arranging of expanded stacked groups is broken [r=ian, a=blocking2.0-final+]
browser/base/content/tabview/groupitems.js
browser/base/content/test/tabview/Makefile.in
browser/base/content/test/tabview/browser_tabview_expander.js
--- a/browser/base/content/tabview/groupitems.js
+++ b/browser/base/content/tabview/groupitems.js
@@ -1087,100 +1087,56 @@ GroupItem.prototype = Utils.extend(new I
       if (child.isDragging)
         options.addTab = true;
       else
         childrenToArrange.push(child);
     });
 
     if (GroupItems._arrangePaused) {
       GroupItems.pushArrange(this, options);
-      return;
+      return false;
     }
-    var dropIndex = false;
-    if (this.expanded) {
-      this.topChild = null;
-      var box = new Rect(this.expanded.bounds);
-      box.inset(8, 8);
-      let result = Items.arrange(childrenToArrange, box, Utils.extend({}, options, {z: 99999}));
-      dropIndex = result.dropIndex;
+    
+    let shouldStack = this.shouldStack(childrenToArrange.length + (options.addTab ? 1 : 0));
+    let box = this.getContentBounds();
+    
+    // if we should stack and we're not expanded
+    if (shouldStack && !this.expanded) {
+      this.showExpandControl();
+      this._stackArrange(childrenToArrange, box, options);
+      return false;
     } else {
-      var count = childrenToArrange.length;
-      var bb = this.getContentBounds();
-      if (!this.shouldStack(count + (options.addTab ? 1 : 0))) {
-        childrenToArrange.forEach(function(child) {
-          child.removeClass("stacked")
-        });
-
-        this.topChild = null;
-
-        if (!childrenToArrange.length)
-          return;
-
-        var arrangeOptions = Utils.extend({}, options, {
-          columns: this._columns
-        });
-
-        // Items.arrange will rearrange the children, but also return an array
-        // of the Rect's used.
-
-        let result = Items.arrange(childrenToArrange, bb, arrangeOptions);
-        dropIndex = result.dropIndex;
-        if ("oldDropIndex" in options && options.oldDropIndex === dropIndex)
-          return dropIndex;
-        var rects = result.rects;
-
-        let index = 0;
-        let self = this;
-        childrenToArrange.forEach(function GroupItem_arrange_children_each(child, i) {
-          // If dropIndex spacing is active and this is a child after index,
-          // bump it up one so we actually use the correct rect
-          // (and skip one for the dropPos)
-          if (self._dropSpaceActive && index === dropIndex)
-            index++;
-          if (!child.locked.bounds) {
-            child.setBounds(rects[index], !options.animate);
-            child.setRotation(0);
-            if (options.z)
-              child.setZ(options.z);
-          }
-          index++;
-        });
-
-        this._isStacked = false;
-      } else
-        this._stackArrange(bb, options);
+      this.hideExpandControl();
+      // a dropIndex is returned
+      return this._gridArrange(childrenToArrange, box, options);
     }
-
-    if (this._isStacked && !this.expanded) this.showExpandControl();
-    else this.hideExpandControl();
-    
-    return dropIndex;
   },
 
   // ----------
   // Function: _stackArrange
   // Arranges the children in a stack.
   //
   // Parameters:
+  //   childrenToArrange - array of <TabItem> children
   //   bb - <Rect> to arrange within
   //   options - see below
   //
   // Possible "options" properties:
   //   animate - whether to animate; default: true.
-  _stackArrange: function GroupItem__stackArrange(bb, options) {
+  _stackArrange: function GroupItem__stackArrange(childrenToArrange, bb, options) {
     var animate;
     if (!options || typeof options.animate == 'undefined')
       animate = true;
     else
       animate = options.animate;
 
     if (typeof options == 'undefined')
       options = {};
 
-    var count = this._children.length;
+    var count = childrenToArrange.length;
     if (!count)
       return;
 
     var zIndex = this.getZ() + count + 1;
 
     var maxRotation = 35; // degress
     var scale = 0.8;
     var newTabsPad = 10;
@@ -1204,17 +1160,17 @@ GroupItem.prototype = Utils.extend(new I
     // y is the vertical margin
     var x = (bb.width - w) / 2;
 
     var y = Math.min(x, (bb.height - h) / 2);
     var box = new Rect(bb.left + x, bb.top + y, w, h);
 
     var self = this;
     var children = [];
-    this._children.forEach(function GroupItem__stackArrange_order(child) {
+    childrenToArrange.forEach(function GroupItem__stackArrange_order(child) {
       if (child == self.topChild)
         children.unshift(child);
       else
         children.push(child);
     });
 
     children.forEach(function GroupItem__stackArrange_apply(child, index) {
       if (!child.locked.bounds) {
@@ -1224,16 +1180,82 @@ GroupItem.prototype = Utils.extend(new I
         child.addClass("stacked");
         child.setBounds(box, !animate);
         child.setRotation((UI.rtl ? -1 : 1) * self._randRotate(maxRotation, index));
       }
     });
 
     self._isStacked = true;
   },
+  
+  // ----------
+  // Function: _gridArrange
+  // Arranges the children into a grid.
+  //
+  // Parameters:
+  //   childrenToArrange - array of <TabItem> children
+  //   box - <Rect> to arrange within
+  //   options - see below
+  //
+  // Possible "options" properties:
+  //   animate - whether to animate; default: true.
+  //   z - (int) a z-index to assign the children
+  //   columns - the number of columns to use in the layout, if known in advance
+  //
+  // Returns:
+  //   dropIndex - (int) the index at which a dragged item (if there is one) should be added
+  //               if it is dropped. Otherwise (boolean) false.
+  _gridArrange: function GroupItem__gridArrange(childrenToArrange, box, options) {
+    this.topChild = null;
+    let arrangeOptions;
+    if (this.expanded) {
+      // if we're expanded, we actually want to use the expanded tray's bounds.
+      box = new Rect(this.expanded.bounds);
+      box.inset(8, 8);
+      arrangeOptions = Utils.extend({}, options, {z: 99999});
+    } else {
+      this._isStacked = false;
+      arrangeOptions = Utils.extend({}, options, {
+        columns: this._columns
+      });
+
+      childrenToArrange.forEach(function(child) {
+        child.removeClass("stacked")
+      });
+    }
+  
+    if (!childrenToArrange.length)
+      return false;
+
+    // Items.arrange will determine where/how the child items should be
+    // placed, but will *not* actually move them for us. This is our job.
+    let result = Items.arrange(childrenToArrange, box, arrangeOptions);
+    let {dropIndex, rects} = result;
+    if ("oldDropIndex" in options && options.oldDropIndex === dropIndex)
+      return dropIndex;
+
+    let index = 0;
+    let self = this;
+    childrenToArrange.forEach(function GroupItem_arrange_children_each(child, i) {
+      // If dropIndex spacing is active and this is a child after index,
+      // bump it up one so we actually use the correct rect
+      // (and skip one for the dropPos)
+      if (self._dropSpaceActive && index === dropIndex)
+        index++;
+      if (!child.locked.bounds) {
+        child.setBounds(rects[index], !options.animate);
+        child.setRotation(0);
+        if (arrangeOptions.z)
+          child.setZ(arrangeOptions.z);
+      }
+      index++;
+    });
+
+    return dropIndex;
+  },
 
   // ----------
   // Function: _randRotate
   // Random rotation generator for <_stackArrange>
   _randRotate: function GroupItem__randRotate(spread, index) {
     if (index >= this._stackAngles.length) {
       var randAngle = 5*index + parseInt((Math.random()-.5)*1);
       this._stackAngles.push(randAngle);
@@ -1277,17 +1299,17 @@ GroupItem.prototype = Utils.extend(new I
     var $tray = iQ("<div>").css({
       top: startBounds.top,
       left: startBounds.left,
       width: startBounds.width,
       height: startBounds.height,
       position: "absolute",
       zIndex: 99998
     }).appendTo("body");
-
+    $tray[0].id = "expandedTray";
 
     var w = 180;
     var h = w * (TabItems.tabHeight / TabItems.tabWidth) * 1.1;
     var padding = 20;
     var col = Math.ceil(Math.sqrt(this._children.length));
     var row = Math.ceil(this._children.length/col);
 
     var overlayWidth = Math.min(window.innerWidth - (padding * 2), w*col + padding*(col+1));
@@ -1309,17 +1331,20 @@ GroupItem.prototype = Utils.extend(new I
     $tray
       .animate({
         width:  overlayWidth,
         height: overlayHeight,
         top: pos.top,
         left: pos.left
       }, {
         duration: 200,
-        easing: "tabviewBounce"
+        easing: "tabviewBounce",
+        complete: function GroupItem_expand_animate_complete() {
+          self._sendToSubscribers("expanded");
+        }
       })
       .addClass("overlay");
 
     this._children.forEach(function(child) {
       child.addClass("stack-trayed");
     });
 
     var $shield = iQ('<div>')
@@ -1354,31 +1379,33 @@ GroupItem.prototype = Utils.extend(new I
 
   // ----------
   // Function: collapse
   // Collapses the groupItem from the expanded "tray" mode.
   collapse: function GroupItem_collapse() {
     if (this.expanded) {
       var z = this.getZ();
       var box = this.getBounds();
+      let self = this;
       this.expanded.$tray
         .css({
           zIndex: z + 1
         })
         .animate({
           width:  box.width,
           height: box.height,
           top: box.top,
           left: box.left,
           opacity: 0
         }, {
           duration: 350,
           easing: "tabviewBounce",
-          complete: function() {
+          complete: function GroupItem_collapse_animate_complete() {
             iQ(this).remove();
+            self._sendToSubscribers("collapsed");
           }
         });
 
       this.expanded.$shield.remove();
       this.expanded = null;
 
       this._children.forEach(function(child) {
         child.removeClass("stack-trayed");
--- a/browser/base/content/test/tabview/Makefile.in
+++ b/browser/base/content/test/tabview/Makefile.in
@@ -80,16 +80,17 @@ include $(topsrcdir)/config/rules.mk
                  browser_tabview_bug608158.js \
                  browser_tabview_bug610242.js \
                  browser_tabview_bug616967.js \
                  browser_tabview_bug618828.js \
                  browser_tabview_bug619937.js \
                  browser_tabview_bug624265.js \
                  browser_tabview_dragdrop.js \
                  browser_tabview_exit_button.js \
+                 browser_tabview_expander.js \
                  browser_tabview_group.js \
                  browser_tabview_launch.js \
                  browser_tabview_multiwindow_search.js \
                  browser_tabview_orphaned_tabs.js \
                  browser_tabview_privatebrowsing.js \
                  browser_tabview_rtl.js \
                  browser_tabview_search.js \
                  browser_tabview_snapping.js \
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabview/browser_tabview_expander.js
@@ -0,0 +1,225 @@
+/* ***** 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 Panorama expander 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):
+ * Michael Yoshitaka Erlewine <mitcho@mitcho.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();
+  requestLongerTimeout(2);
+  newWindowWithTabView(onTabViewWindowLoaded);
+}
+
+function onTabViewWindowLoaded(win) {
+  ok(win.TabView.isVisible(), "Tab View is visible");
+
+  let contentWindow = win.document.getElementById("tab-view").contentWindow;
+  let [originalTab] = win.gBrowser.visibleTabs;
+  let currentGroup = contentWindow.GroupItems.getActiveGroupItem();
+  
+  // let's create a group small enough to get stacked
+  let group = new contentWindow.GroupItem([], {
+    immediately: true,
+    bounds: {left: 20, top: 300, width: 300, height: 300}
+  });
+
+  let expander = contentWindow.iQ(group.container).find(".stackExpander");
+  ok("length" in expander && expander.length == 1, "The group has an expander.");
+
+  // procreate!
+  contentWindow.GroupItems.setActiveGroupItem(group);
+  for (var i=0; i<7; i++) {
+    win.gBrowser.loadOneTab('about:blank#' + i, {inBackground: true});
+  }
+  let children = group.getChildren();
+  
+  // Wait until they all update because, once updated, they will notice that they
+  // don't have favicons and this will change their styling at some unknown time.
+  afterAllTabItemsUpdated(function() {
+    
+    ok(!group.shouldStack(group._children.length), "The group should not stack.");
+    is(expander[0].style.display, "none", "The expander is hidden.");
+    
+    // now resize the group down.
+    group.setSize(100, 100, true);
+  
+    ok(group.shouldStack(group._children.length), "The group should stack.");
+    isnot(expander[0].style.display, "none", "The expander is now visible!");
+    let expanderBounds = expander.bounds();
+    ok(group.getBounds().contains(expanderBounds), "The expander lies in the group.");
+    let stackCenter = children[0].getBounds().center();
+    ok(stackCenter.y < expanderBounds.center().y, "The expander is below the stack.");
+  
+    // STAGE 1:
+    // Here, we just expand the group, click elsewhere, and make sure
+    // it collapsed.
+    let stage1expanded = function() {
+      group.removeSubscriber("test stage 1", "expanded", stage1expanded);
+    
+      ok(group.expanded, "The group is now expanded.");
+      is(expander[0].style.display, "none", "The expander is hidden!");
+      
+      let overlay = contentWindow.document.getElementById("expandedTray");    
+      ok(overlay, "The expanded tray exists.");
+      let $overlay = contentWindow.iQ(overlay);
+      
+      group.addSubscriber("test stage 1", "collapsed", stage1collapsed);
+      // null type means "click", for some reason...
+      EventUtils.synthesizeMouse(contentWindow.document.body, 10, $overlay.bounds().bottom + 5,
+                                 {type: null}, contentWindow);
+    };
+    
+    let stage1collapsed = function() {
+      group.removeSubscriber("test stage 1", "collapsed", stage1collapsed);
+      ok(!group.expanded, "The group is no longer expanded.");
+      isnot(expander[0].style.display, "none", "The expander is visible!");
+      let expanderBounds = expander.bounds();
+      ok(group.getBounds().contains(expanderBounds), "The expander still lies in the group.");
+      let stackCenter = children[0].getBounds().center();
+      ok(stackCenter.y < expanderBounds.center().y, "The expander is below the stack.");
+  
+      // now, try opening it up again.
+      group.addSubscriber("test stage 2", "expanded", stage2expanded);
+      EventUtils.sendMouseEvent({ type: "click" }, expander[0], contentWindow);
+    };
+  
+    // STAGE 2:
+    // Now make sure every child of the group shows up within this "tray", and
+    // click on one of them and make sure we go into the tab and the tray collapses.
+    let stage2expanded = function() {
+      group.removeSubscriber("test stage 2", "expanded", stage2expanded);
+    
+      ok(group.expanded, "The group is now expanded.");
+      is(expander[0].style.display, "none", "The expander is hidden!");
+      
+      let overlay = contentWindow.document.getElementById("expandedTray");    
+      ok(overlay, "The expanded tray exists.");
+      let $overlay = contentWindow.iQ(overlay);
+      let overlayBounds = $overlay.bounds();
+  
+      children.forEach(function(child, i) {
+        ok(overlayBounds.contains(child.getBounds()), "Child " + i + " is in the overlay");
+      });
+      
+      win.addEventListener("tabviewhidden", stage2hidden, false);
+      // again, null type means "click", for some reason...
+      EventUtils.synthesizeMouse(children[0].container, 2, 2, {type: null}, contentWindow);
+    };
+  
+    let stage2hidden = function() {
+      win.removeEventListener("tabviewhidden", stage2hidden, false);
+      
+      is(win.gBrowser.selectedTab, children[0].tab, "We clicked on the first child.");
+      
+      win.addEventListener("tabviewshown", stage2shown, false);
+      win.TabView.toggle();
+    };
+    
+    let stage2shown = function() {
+      win.removeEventListener("tabviewshown", stage2shown, false);
+      ok(!group.expanded, "The group is not expanded.");
+      isnot(expander[0].style.display, "none", "The expander is visible!");
+      let expanderBounds = expander.bounds();
+      ok(group.getBounds().contains(expanderBounds), "The expander still lies in the group.");
+      let stackCenter = children[0].getBounds().center();
+      ok(stackCenter.y < expanderBounds.center().y, "The expander is below the stack.");
+
+      // In preparation for Stage 3, find that original tab and make it the active tab.
+      let originalTabItem = originalTab._tabViewTabItem;
+      contentWindow.UI.setActiveTab(originalTabItem);
+  
+      // okay, expand this group one last time
+      group.addSubscriber("test stage 3", "expanded", stage3expanded);
+      EventUtils.sendMouseEvent({ type: "click" }, expander[0], contentWindow);
+    }
+  
+    // STAGE 3:
+    // Activate another tab not in this group, expand our stacked group, but then
+    // enter Panorama (i.e., zoom into this other group), and make sure we can go to
+    // it and that the tray gets collapsed.
+    let stage3expanded = function() {
+      group.removeSubscriber("test stage 3", "expanded", stage3expanded);
+    
+      ok(group.expanded, "The group is now expanded.");
+      is(expander[0].style.display, "none", "The expander is hidden!");
+      let overlay = contentWindow.document.getElementById("expandedTray");
+      ok(overlay, "The expanded tray exists.");
+      
+      let activeTab = contentWindow.UI.getActiveTab();
+      ok(activeTab, "There is an active tab.");
+      let originalTabItem = originalTab._tabViewTabItem;
+
+      // TODO: bug 625654
+      todo_isnot(activeTab, originalTabItem, "But it's not what it was a moment ago.");
+      let someChildIsActive = group.getChildren().some(function(child)
+                              child == activeTab);
+      todo(someChildIsActive, "Now one of the children in the group is active.");
+            
+      // now activate Panorama...
+      win.addEventListener("tabviewhidden", stage3hidden, false);
+      win.TabView.toggle();
+    };
+  
+    let stage3hidden = function() {
+      win.removeEventListener("tabviewhidden", stage3hidden, false);
+      
+      // TODO: bug 625654
+      todo_isnot(win.gBrowser.selectedTab, originalTab, "We did not enter the original tab.");
+
+      let someChildIsSelected = group.getChildren().some(function(child)
+                                  child.tab == win.gBrowser.selectedTab);
+      todo(someChildIsSelected, "Instead we're in one of the stack's children.");
+      
+      win.addEventListener("tabviewshown", stage3shown, false);
+      win.TabView.toggle();
+    };
+    
+    let stage3shown = function() {
+      win.removeEventListener("tabviewshown", stage3shown, false);
+  
+      // TODO: bug 625654
+      let overlay = contentWindow.document.getElementById("expandedTray");
+      todo(!group.expanded, "The group is no longer expanded.");
+      todo(!overlay, "The expanded tray should be gone after looking at another tab.");
+      todo_isnot(expander[0].style.display, "none", "The expander is visible!");
+
+      win.close();
+      finish();
+    }
+  
+    // get the ball rolling
+    group.addSubscriber("test stage 1", "expanded", stage1expanded);
+    EventUtils.sendMouseEvent({ type: "click" }, expander[0], contentWindow);
+  }, win);
+}