browser/components/tabview/groupitems.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 // **********
     6 // Title: groupItems.js
     8 // ##########
     9 // Class: GroupItem
    10 // A single groupItem in the TabView window. Descended from <Item>.
    11 // Note that it implements the <Subscribable> interface.
    12 //
    13 // ----------
    14 // Constructor: GroupItem
    15 //
    16 // Parameters:
    17 //   listOfEls - an array of DOM elements for tabs to be added to this groupItem
    18 //   options - various options for this groupItem (see below). In addition, gets passed
    19 //     to <add> along with the elements provided.
    20 //
    21 // Possible options:
    22 //   id - specifies the groupItem's id; otherwise automatically generated
    23 //   userSize - see <Item.userSize>; default is null
    24 //   bounds - a <Rect>; otherwise based on the locations of the provided elements
    25 //   container - a DOM element to use as the container for this groupItem; otherwise will create
    26 //   title - the title for the groupItem; otherwise blank
    27 //   focusTitle - focus the title's input field after creation
    28 //   dontPush - true if this groupItem shouldn't push away or snap on creation; default is false
    29 //   immediately - true if we want all placement immediately, not with animation
    30 function GroupItem(listOfEls, options) {
    31   if (!options)
    32     options = {};
    34   this._inited = false;
    35   this._uninited = false;
    36   this._children = []; // an array of Items
    37   this.isAGroupItem = true;
    38   this.id = options.id || GroupItems.getNextID();
    39   this._isStacked = false;
    40   this.expanded = null;
    41   this.hidden = false;
    42   this.fadeAwayUndoButtonDelay = 15000;
    43   this.fadeAwayUndoButtonDuration = 300;
    45   this.keepProportional = false;
    46   this._frozenItemSizeData = {};
    48   this._onChildClose = this._onChildClose.bind(this);
    50   // Variable: _activeTab
    51   // The <TabItem> for the groupItem's active tab.
    52   this._activeTab = null;
    54   if (Utils.isPoint(options.userSize))
    55     this.userSize = new Point(options.userSize);
    57   var self = this;
    59   var rectToBe;
    60   if (options.bounds) {
    61     Utils.assert(Utils.isRect(options.bounds), "options.bounds must be a Rect");
    62     rectToBe = new Rect(options.bounds);
    63   }
    65   if (!rectToBe) {
    66     rectToBe = GroupItems.getBoundingBox(listOfEls);
    67     rectToBe.inset(-42, -42);
    68   }
    70   var $container = options.container;
    71   let immediately = options.immediately || $container ? true : false;
    72   if (!$container) {
    73     $container = iQ('<div>')
    74       .addClass('groupItem')
    75       .css({position: 'absolute'})
    76       .css(rectToBe);
    77   }
    79   this.bounds = $container.bounds();
    81   this.isDragging = false;
    82   $container
    83     .css({zIndex: -100})
    84     .attr("data-id", this.id)
    85     .appendTo("body");
    87   // ___ Resizer
    88   this.$resizer = iQ("<div>")
    89     .addClass('resizer')
    90     .appendTo($container)
    91     .hide();
    93   // ___ Titlebar
    94   var html =
    95     "<div class='title-container'>" +
    96       "<input class='name' />" +
    97       "<div class='title-shield' />" +
    98     "</div>";
   100   this.$titlebar = iQ('<div>')
   101     .addClass('titlebar')
   102     .html(html)
   103     .appendTo($container);
   105   this.$closeButton = iQ('<div>')
   106     .addClass('close')
   107     .click(function() {
   108       self.closeAll();
   109     })
   110     .attr("title", tabviewString("groupItem.closeGroup"))
   111     .appendTo($container);
   113   // ___ Title
   114   this.$titleContainer = iQ('.title-container', this.$titlebar);
   115   this.$title = iQ('.name', this.$titlebar).attr('placeholder', this.defaultName);
   116   this.$titleShield = iQ('.title-shield', this.$titlebar);
   117   this.setTitle(options.title);
   119   var handleKeyPress = function (e) {
   120     if (e.keyCode == KeyEvent.DOM_VK_ESCAPE ||
   121         e.keyCode == KeyEvent.DOM_VK_RETURN) {
   122       (self.$title)[0].blur();
   123       self.$title
   124         .addClass("transparentBorder")
   125         .one("mouseout", function() {
   126           self.$title.removeClass("transparentBorder");
   127         });
   128       e.stopPropagation();
   129       e.preventDefault();
   130     }
   131   };
   133   var handleKeyUp = function(e) {
   134     // NOTE: When user commits or cancels IME composition, the last key
   135     //       event fires only a keyup event.  Then, we shouldn't take any
   136     //       reactions but we should update our status.
   137     self.save();
   138   };
   140   this.$title
   141     .blur(function() {
   142       self._titleFocused = false;
   143       self.$title[0].setSelectionRange(0, 0);
   144       self.$titleShield.show();
   145       if (self.getTitle())
   146         gTabView.firstUseExperienced = true;
   147       self.save();
   148     })
   149     .focus(function() {
   150       self._unfreezeItemSize();
   151       if (!self._titleFocused) {
   152         (self.$title)[0].select();
   153         self._titleFocused = true;
   154       }
   155     })
   156     .mousedown(function(e) {
   157       e.stopPropagation();
   158     })
   159     .keypress(handleKeyPress)
   160     .keyup(handleKeyUp)
   161     .attr("title", tabviewString("groupItem.defaultName"));
   163   this.$titleShield
   164     .mousedown(function(e) {
   165       self.lastMouseDownTarget = (Utils.isLeftClick(e) ? e.target : null);
   166     })
   167     .mouseup(function(e) {
   168       var same = (e.target == self.lastMouseDownTarget);
   169       self.lastMouseDownTarget = null;
   170       if (!same)
   171         return;
   173       if (!self.isDragging)
   174         self.focusTitle();
   175     })
   176     .attr("title", tabviewString("groupItem.defaultName"));
   178   if (options.focusTitle)
   179     this.focusTitle();
   181   // ___ Stack Expander
   182   this.$expander = iQ("<div/>")
   183     .addClass("stackExpander")
   184     .appendTo($container)
   185     .hide();
   187   // ___ app tabs: create app tab tray and populate it
   188   let appTabTrayContainer = iQ("<div/>")
   189     .addClass("appTabTrayContainer")
   190     .appendTo($container);
   191   this.$appTabTray = iQ("<div/>")
   192     .addClass("appTabTray")
   193     .appendTo(appTabTrayContainer);
   195   let pinnedTabCount = gBrowser._numPinnedTabs;
   196   AllTabs.tabs.forEach(function (xulTab, index) {
   197     // only adjust tray when it's the last app tab.
   198     if (xulTab.pinned)
   199       this.addAppTab(xulTab, {dontAdjustTray: index + 1 < pinnedTabCount});
   200   }, this);
   202   // ___ Undo Close
   203   this.$undoContainer = null;
   204   this._undoButtonTimeoutId = null;
   206   // ___ Superclass initialization
   207   this._init($container[0]);
   209   // ___ Children
   210   // We explicitly set dontArrange=true to prevent the groupItem from
   211   // re-arranging its children after a tabItem has been added. This saves us a
   212   // group.arrange() call per child and therefore some tab.setBounds() calls.
   213   options.dontArrange = true;
   214   listOfEls.forEach(function (el) {
   215     self.add(el, options);
   216   });
   218   // ___ Finish Up
   219   this._addHandlers($container);
   221   this.setResizable(true, immediately);
   223   GroupItems.register(this);
   225   // ___ Position
   226   this.setBounds(rectToBe, immediately);
   227   if (options.dontPush) {
   228     this.setZ(drag.zIndex);
   229     drag.zIndex++; 
   230   } else {
   231     // Calling snap will also trigger pushAway
   232     this.snap(immediately);
   233   }
   235   if (!options.immediately && listOfEls.length > 0)
   236     $container.hide().fadeIn();
   238   this._inited = true;
   239   this.save();
   241   GroupItems.updateGroupCloseButtons();
   242 };
   244 // ----------
   245 GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), {
   246   // ----------
   247   // Function: toString
   248   // Prints [GroupItem id=id] for debug use
   249   toString: function GroupItem_toString() {
   250     return "[GroupItem id=" + this.id + "]";
   251   },
   253   // ----------
   254   // Variable: defaultName
   255   // The prompt text for the title field.
   256   defaultName: tabviewString('groupItem.defaultName'),
   258   // -----------
   259   // Function: setActiveTab
   260   // Sets the active <TabItem> for this groupItem; can be null, but only
   261   // if there are no children.
   262   setActiveTab: function GroupItem_setActiveTab(tab) {
   263     Utils.assertThrow((!tab && this._children.length == 0) || tab.isATabItem,
   264         "tab must be null (if no children) or a TabItem");
   266     this._activeTab = tab;
   268     if (this.isStacked())
   269       this.arrange({immediately: true});
   270   },
   272   // -----------
   273   // Function: getActiveTab
   274   // Gets the active <TabItem> for this groupItem; can be null, but only
   275   // if there are no children.
   276   getActiveTab: function GroupItem_getActiveTab() {
   277     return this._activeTab;
   278   },
   280   // ----------
   281   // Function: getStorageData
   282   // Returns all of the info worth storing about this groupItem.
   283   getStorageData: function GroupItem_getStorageData() {
   284     var data = {
   285       bounds: this.getBounds(),
   286       userSize: null,
   287       title: this.getTitle(),
   288       id: this.id
   289     };
   291     if (Utils.isPoint(this.userSize))
   292       data.userSize = new Point(this.userSize);
   294     return data;
   295   },
   297   // ----------
   298   // Function: isEmpty
   299   // Returns true if the tab groupItem is empty and unnamed.
   300   isEmpty: function GroupItem_isEmpty() {
   301     return !this._children.length && !this.getTitle();
   302   },
   304   // ----------
   305   // Function: isStacked
   306   // Returns true if this item is in a stacked groupItem.
   307   isStacked: function GroupItem_isStacked() {
   308     return this._isStacked;
   309   },
   311   // ----------
   312   // Function: isTopOfStack
   313   // Returns true if the item is showing on top of this group's stack,
   314   // determined by whether the tab is this group's topChild, or
   315   // if it doesn't have one, its first child.
   316   isTopOfStack: function GroupItem_isTopOfStack(item) {
   317     return this.isStacked() && item == this.getTopChild();
   318   },
   320   // ----------
   321   // Function: save
   322   // Saves this groupItem to persistent storage.
   323   save: function GroupItem_save() {
   324     if (!this._inited || this._uninited) // too soon/late to save
   325       return;
   327     var data = this.getStorageData();
   328     if (GroupItems.groupItemStorageSanity(data))
   329       Storage.saveGroupItem(gWindow, data);
   330   },
   332   // ----------
   333   // Function: deleteData
   334   // Deletes the groupItem in the persistent storage.
   335   deleteData: function GroupItem_deleteData() {
   336     this._uninited = true;
   337     Storage.deleteGroupItem(gWindow, this.id);
   338   },
   340   // ----------
   341   // Function: getTitle
   342   // Returns the title of this groupItem as a string.
   343   getTitle: function GroupItem_getTitle() {
   344     return this.$title ? this.$title.val() : '';
   345   },
   347   // ----------
   348   // Function: setTitle
   349   // Sets the title of this groupItem with the given string
   350   setTitle: function GroupItem_setTitle(value) {
   351     this.$title.val(value);
   352     this.save();
   353   },
   355   // ----------
   356   // Function: focusTitle
   357   // Hide the title's shield and focus the underlying input field.
   358   focusTitle: function GroupItem_focusTitle() {
   359     this.$titleShield.hide();
   360     this.$title[0].focus();
   361   },
   363   // ----------
   364   // Function: adjustAppTabTray
   365   // Used to adjust the appTabTray size, to split the appTabIcons across
   366   // multiple columns when needed - if the groupItem size is too small.
   367   //
   368   // Parameters:
   369   //   arrangeGroup - rearrange the groupItem if the number of appTab columns
   370   //   changes. If true, then this.arrange() is called, otherwise not.
   371   adjustAppTabTray: function GroupItem_adjustAppTabTray(arrangeGroup) {
   372     let icons = iQ(".appTabIcon", this.$appTabTray);
   373     let container = iQ(this.$appTabTray[0].parentNode);
   374     if (!icons.length) {
   375       // There are no icons, so hide the appTabTray if needed.
   376       if (parseInt(container.css("width")) != 0) {
   377         this.$appTabTray.css("-moz-column-count", "auto");
   378         this.$appTabTray.css("height", 0);
   379         container.css("width", 0);
   380         container.css("height", 0);
   382         if (container.hasClass("appTabTrayContainerTruncated"))
   383           container.removeClass("appTabTrayContainerTruncated");
   385         if (arrangeGroup)
   386           this.arrange();
   387       }
   388       return;
   389     }
   391     let iconBounds = iQ(icons[0]).bounds();
   392     let boxBounds = this.getBounds();
   393     let contentHeight = boxBounds.height -
   394                         parseInt(container.css("top")) -
   395                         this.$resizer.height();
   396     let rows = Math.floor(contentHeight / iconBounds.height);
   397     let columns = Math.ceil(icons.length / rows);
   398     let columnsGap = parseInt(this.$appTabTray.css("-moz-column-gap"));
   399     let iconWidth = iconBounds.width + columnsGap;
   400     let maxColumns = Math.floor((boxBounds.width * 0.20) / iconWidth);
   402     Utils.assert(rows > 0 && columns > 0 && maxColumns > 0,
   403       "make sure the calculated rows, columns and maxColumns are correct");
   405     if (columns > maxColumns)
   406       container.addClass("appTabTrayContainerTruncated");
   407     else if (container.hasClass("appTabTrayContainerTruncated"))
   408       container.removeClass("appTabTrayContainerTruncated");
   410     // Need to drop the -moz- prefix when Gecko makes it obsolete.
   411     // See bug 629452.
   412     if (parseInt(this.$appTabTray.css("-moz-column-count")) != columns)
   413       this.$appTabTray.css("-moz-column-count", columns);
   415     if (parseInt(this.$appTabTray.css("height")) != contentHeight) {
   416       this.$appTabTray.css("height", contentHeight + "px");
   417       container.css("height", contentHeight + "px");
   418     }
   420     let fullTrayWidth = iconWidth * columns - columnsGap;
   421     if (parseInt(this.$appTabTray.css("width")) != fullTrayWidth)
   422       this.$appTabTray.css("width", fullTrayWidth + "px");
   424     let trayWidth = iconWidth * Math.min(columns, maxColumns) - columnsGap;
   425     if (parseInt(container.css("width")) != trayWidth) {
   426       container.css("width", trayWidth + "px");
   428       // Rearrange the groupItem if the width changed.
   429       if (arrangeGroup)
   430         this.arrange();
   431     }
   432   },
   434   // ----------
   435   // Function: getContentBounds
   436   // Returns a <Rect> for the groupItem's content area (which doesn't include the title, etc).
   437   //
   438   // Parameters:
   439   //   options - an object with additional parameters, see below
   440   //
   441   // Possible options:
   442   //   stacked - true to get content bounds for stacked mode
   443   getContentBounds: function GroupItem_getContentBounds(options) {
   444     let box = this.getBounds();
   445     let titleHeight = this.$titlebar.height();
   446     box.top += titleHeight;
   447     box.height -= titleHeight;
   449     let appTabTrayContainer = iQ(this.$appTabTray[0].parentNode);
   450     let appTabTrayWidth = appTabTrayContainer.width();
   451     if (appTabTrayWidth)
   452       appTabTrayWidth += parseInt(appTabTrayContainer.css(UI.rtl ? "left" : "right"));
   454     box.width -= appTabTrayWidth;
   455     if (UI.rtl) {
   456       box.left += appTabTrayWidth;
   457     }
   459     // Make the computed bounds' "padding" and expand button margin actually be
   460     // themeable --OR-- compute this from actual bounds. Bug 586546
   461     box.inset(6, 6);
   463     // make some room for the expand button in stacked mode
   464     if (options && options.stacked)
   465       box.height -= this.$expander.height() + 9; // the button height plus padding
   467     return box;
   468   },
   470   // ----------
   471   // Function: setBounds
   472   // Sets the bounds with the given <Rect>, animating unless "immediately" is false.
   473   //
   474   // Parameters:
   475   //   inRect - a <Rect> giving the new bounds
   476   //   immediately - true if it should not animate; default false
   477   //   options - an object with additional parameters, see below
   478   //
   479   // Possible options:
   480   //   force - true to always update the DOM even if the bounds haven't changed; default false
   481   setBounds: function GroupItem_setBounds(inRect, immediately, options) {
   482       Utils.assert(Utils.isRect(inRect), 'GroupItem.setBounds: rect is not a real rectangle!');
   484     // Validate and conform passed in size
   485     let validSize = GroupItems.calcValidSize(
   486       new Point(inRect.width, inRect.height));
   487     let rect = new Rect(inRect.left, inRect.top, validSize.x, validSize.y);
   489     if (!options)
   490       options = {};
   492     var titleHeight = this.$titlebar.height();
   494     // ___ Determine what has changed
   495     var css = {};
   496     var titlebarCSS = {};
   497     var contentCSS = {};
   499     if (rect.left != this.bounds.left || options.force)
   500       css.left = rect.left;
   502     if (rect.top != this.bounds.top || options.force)
   503       css.top = rect.top;
   505     if (rect.width != this.bounds.width || options.force) {
   506       css.width = rect.width;
   507       titlebarCSS.width = rect.width;
   508       contentCSS.width = rect.width;
   509     }
   511     if (rect.height != this.bounds.height || options.force) {
   512       css.height = rect.height;
   513       contentCSS.height = rect.height - titleHeight;
   514     }
   516     if (Utils.isEmptyObject(css))
   517       return;
   519     var offset = new Point(rect.left - this.bounds.left, rect.top - this.bounds.top);
   520     this.bounds = new Rect(rect);
   522     // Make sure the AppTab icons fit the new groupItem size.
   523     if (css.width || css.height)
   524       this.adjustAppTabTray();
   526     // ___ Deal with children
   527     if (css.width || css.height) {
   528       this.arrange({animate: !immediately}); //(immediately ? 'sometimes' : true)});
   529     } else if (css.left || css.top) {
   530       this._children.forEach(function(child) {
   531         if (!child.getHidden()) {
   532           var box = child.getBounds();
   533           child.setPosition(box.left + offset.x, box.top + offset.y, immediately);
   534         }
   535       });
   536     }
   538     // ___ Update our representation
   539     if (immediately) {
   540       iQ(this.container).css(css);
   541       this.$titlebar.css(titlebarCSS);
   542     } else {
   543       TabItems.pausePainting();
   544       iQ(this.container).animate(css, {
   545         duration: 350,
   546         easing: "tabviewBounce",
   547         complete: function() {
   548           TabItems.resumePainting();
   549         }
   550       });
   552       this.$titlebar.animate(titlebarCSS, {
   553         duration: 350
   554       });
   555     }
   557     UI.clearShouldResizeItems();
   558     this.setTrenches(rect);
   559     this.save();
   560   },
   562   // ----------
   563   // Function: setZ
   564   // Set the Z order for the groupItem's container, as well as its children.
   565   setZ: function GroupItem_setZ(value) {
   566     this.zIndex = value;
   568     iQ(this.container).css({zIndex: value});
   570     var count = this._children.length;
   571     if (count) {
   572       var topZIndex = value + count + 1;
   573       var zIndex = topZIndex;
   574       var self = this;
   575       this._children.forEach(function(child) {
   576         if (child == self.getTopChild())
   577           child.setZ(topZIndex + 1);
   578         else {
   579           child.setZ(zIndex);
   580           zIndex--;
   581         }
   582       });
   583     }
   584   },
   586   // ----------
   587   // Function: close
   588   // Closes the groupItem, removing (but not closing) all of its children.
   589   //
   590   // Parameters:
   591   //   options - An object with optional settings for this call.
   592   //
   593   // Options:
   594   //   immediately - (bool) if true, no animation will be used
   595   close: function GroupItem_close(options) {
   596     this.removeAll({dontClose: true});
   597     GroupItems.unregister(this);
   599     // remove unfreeze event handlers, if item size is frozen
   600     this._unfreezeItemSize({dontArrange: true});
   602     let self = this;
   603     let destroyGroup = function () {
   604       iQ(self.container).remove();
   605       if (self.$undoContainer) {
   606         self.$undoContainer.remove();
   607         self.$undoContainer = null;
   608       }
   609       self.removeTrenches();
   610       Items.unsquish();
   611       self._sendToSubscribers("close");
   612       GroupItems.updateGroupCloseButtons();
   613     }
   615     if (this.hidden || (options && options.immediately)) {
   616       destroyGroup();
   617     } else {
   618       iQ(this.container).animate({
   619         opacity: 0,
   620         "transform": "scale(.3)",
   621       }, {
   622         duration: 170,
   623         complete: destroyGroup
   624       });
   625     }
   627     this.deleteData();
   628   },
   630   // ----------
   631   // Function: closeAll
   632   // Closes the groupItem and all of its children.
   633   closeAll: function GroupItem_closeAll() {
   634     if (this._children.length > 0) {
   635       this._unfreezeItemSize();
   636       this._children.forEach(function(child) {
   637         iQ(child.container).hide();
   638       });
   640       iQ(this.container).animate({
   641          opacity: 0,
   642          "transform": "scale(.3)",
   643       }, {
   644         duration: 170,
   645         complete: function() {
   646           iQ(this).hide();
   647         }
   648       });
   650       this.droppable(false);
   651       this.removeTrenches();
   652       this._createUndoButton();
   653     } else
   654       this.close();
   656     this._makeLastActiveGroupItemActive();
   657   },
   659   // ----------
   660   // Function: _makeClosestTabActive
   661   // Make the closest tab external to this group active.
   662   // Used when closing the group.
   663   _makeClosestTabActive: function GroupItem__makeClosestTabActive() {
   664     let closeCenter = this.getBounds().center();
   665     // Find closest tab to make active
   666     let closestTabItem = UI.getClosestTab(closeCenter);
   667     if (closestTabItem)
   668       UI.setActive(closestTabItem);
   669   },
   671   // ----------
   672   // Function: _makeLastActiveGroupItemActive
   673   // Makes the last active group item active.
   674   _makeLastActiveGroupItemActive: function GroupItem__makeLastActiveGroupItemActive() {
   675     let groupItem = GroupItems.getLastActiveGroupItem();
   676     if (groupItem)
   677       UI.setActive(groupItem);
   678     else
   679       this._makeClosestTabActive();
   680   },
   682   // ----------
   683   // Function: closeIfEmpty
   684   // Closes the group if it's empty, is closable, and autoclose is enabled
   685   // (see pauseAutoclose()). Returns true if the close occurred and false
   686   // otherwise.
   687   closeIfEmpty: function GroupItem_closeIfEmpty() {
   688     if (this.isEmpty() && !UI._closedLastVisibleTab &&
   689         !GroupItems.getUnclosableGroupItemId() && !GroupItems._autoclosePaused) {
   690       this.close();
   691       return true;
   692     }
   693     return false;
   694   },
   696   // ----------
   697   // Function: _unhide
   698   // Shows the hidden group.
   699   //
   700   // Parameters:
   701   //   options - various options (see below)
   702   //
   703   // Possible options:
   704   //   immediately - true when no animations should be used
   705   _unhide: function GroupItem__unhide(options) {
   706     this._cancelFadeAwayUndoButtonTimer();
   707     this.hidden = false;
   708     this.$undoContainer.remove();
   709     this.$undoContainer = null;
   710     this.droppable(true);
   711     this.setTrenches(this.bounds);
   713     let self = this;
   715     let finalize = function () {
   716       self._children.forEach(function(child) {
   717         iQ(child.container).show();
   718       });
   720       UI.setActive(self);
   721       self._sendToSubscribers("groupShown");
   722     };
   724     let $container = iQ(this.container).show();
   726     if (!options || !options.immediately) {
   727       $container.animate({
   728         "transform": "scale(1)",
   729         "opacity": 1
   730       }, {
   731         duration: 170,
   732         complete: finalize
   733       });
   734     } else {
   735       $container.css({"transform": "none", opacity: 1});
   736       finalize();
   737     }
   739     GroupItems.updateGroupCloseButtons();
   740   },
   742   // ----------
   743   // Function: closeHidden
   744   // Removes the group item, its children and its container.
   745   closeHidden: function GroupItem_closeHidden() {
   746     let self = this;
   748     this._cancelFadeAwayUndoButtonTimer();
   750     // When the last non-empty groupItem is closed and there are no
   751     // pinned tabs then create a new group with a blank tab.
   752     let remainingGroups = GroupItems.groupItems.filter(function (groupItem) {
   753       return (groupItem != self && groupItem.getChildren().length);
   754     });
   756     let tab = null;
   758     if (!gBrowser._numPinnedTabs && !remainingGroups.length) {
   759       let emptyGroups = GroupItems.groupItems.filter(function (groupItem) {
   760         return (groupItem != self && !groupItem.getChildren().length);
   761       });
   762       let group = (emptyGroups.length ? emptyGroups[0] : GroupItems.newGroup());
   763       tab = group.newTab(null, {dontZoomIn: true});
   764     }
   766     let closed = this.destroy();
   768     if (!tab)
   769       return;
   771     if (closed) {
   772       // Let's make the new tab the selected tab.
   773       UI.goToTab(tab);
   774     } else {
   775       // Remove the new tab and group, if this group is no longer closed.
   776       tab._tabViewTabItem.parent.destroy({immediately: true});
   777     }
   778   },
   780   // ----------
   781   // Function: destroy
   782   // Close all tabs linked to children (tabItems), removes all children and 
   783   // close the groupItem.
   784   //
   785   // Parameters:
   786   //   options - An object with optional settings for this call.
   787   //
   788   // Options:
   789   //   immediately - (bool) if true, no animation will be used
   790   //
   791   // Returns true if the groupItem has been closed, or false otherwise. A group
   792   // could not have been closed due to a tab with an onUnload handler (that
   793   // waits for user interaction).
   794   destroy: function GroupItem_destroy(options) {
   795     let self = this;
   797     // when "TabClose" event is fired, the browser tab is about to close and our 
   798     // item "close" event is fired.  And then, the browser tab gets closed. 
   799     // In other words, the group "close" event is fired before all browser
   800     // tabs in the group are closed.  The below code would fire the group "close"
   801     // event only after all browser tabs in that group are closed.
   802     this._children.concat().forEach(function(child) {
   803       child.removeSubscriber("close", self._onChildClose);
   805       if (child.close(true)) {
   806         self.remove(child, { dontArrange: true });
   807       } else {
   808         // child.removeSubscriber() must be called before child.close(), 
   809         // therefore we call child.addSubscriber() if the tab is not removed.
   810         child.addSubscriber("close", self._onChildClose);
   811       }
   812     });
   814     if (this._children.length) {
   815       if (this.hidden)
   816         this.$undoContainer.fadeOut(function() { self._unhide() });
   818       return false;
   819     } else {
   820       this.close(options);
   821       return true;
   822     }
   823   },
   825   // ----------
   826   // Function: _fadeAwayUndoButton
   827   // Fades away the undo button
   828   _fadeAwayUndoButton: function GroupItem__fadeAwayUndoButton() {
   829     let self = this;
   831     if (this.$undoContainer) {
   832       // if there is more than one group and other groups are not empty,
   833       // fade away the undo button.
   834       let shouldFadeAway = false;
   836       if (GroupItems.groupItems.length > 1) {
   837         shouldFadeAway = 
   838           GroupItems.groupItems.some(function(groupItem) {
   839             return (groupItem != self && groupItem.getChildren().length > 0);
   840           });
   841       }
   843       if (shouldFadeAway) {
   844         self.$undoContainer.animate({
   845           color: "transparent",
   846           opacity: 0
   847         }, {
   848           duration: this._fadeAwayUndoButtonDuration,
   849           complete: function() { self.closeHidden(); }
   850         });
   851       }
   852     }
   853   },
   855   // ----------
   856   // Function: _createUndoButton
   857   // Makes the affordance for undo a close group action
   858   _createUndoButton: function GroupItem__createUndoButton() {
   859     let self = this;
   860     this.$undoContainer = iQ("<div/>")
   861       .addClass("undo")
   862       .attr("type", "button")
   863       .attr("data-group-id", this.id)
   864       .appendTo("body");
   865     iQ("<span/>")
   866       .text(tabviewString("groupItem.undoCloseGroup"))
   867       .appendTo(this.$undoContainer);
   868     let undoClose = iQ("<span/>")
   869       .addClass("close")
   870       .attr("title", tabviewString("groupItem.discardClosedGroup"))
   871       .appendTo(this.$undoContainer);
   873     this.$undoContainer.css({
   874       left: this.bounds.left + this.bounds.width/2 - iQ(self.$undoContainer).width()/2,
   875       top:  this.bounds.top + this.bounds.height/2 - iQ(self.$undoContainer).height()/2,
   876       "transform": "scale(.1)",
   877       opacity: 0
   878     });
   879     this.hidden = true;
   881     // hide group item and show undo container.
   882     setTimeout(function() {
   883       self.$undoContainer.animate({
   884         "transform": "scale(1)",
   885         "opacity": 1
   886       }, {
   887         easing: "tabviewBounce",
   888         duration: 170,
   889         complete: function() {
   890           self._sendToSubscribers("groupHidden");
   891         }
   892       });
   893     }, 50);
   895     // add click handlers
   896     this.$undoContainer.click(function(e) {
   897       // don't do anything if the close button is clicked.
   898       if (e.target == undoClose[0])
   899         return;
   901       self.$undoContainer.fadeOut(function() { self._unhide(); });
   902     });
   904     undoClose.click(function() {
   905       self.$undoContainer.fadeOut(function() { self.closeHidden(); });
   906     });
   908     this.setupFadeAwayUndoButtonTimer();
   909     // Cancel the fadeaway if you move the mouse over the undo
   910     // button, and restart the countdown once you move out of it.
   911     this.$undoContainer.mouseover(function() { 
   912       self._cancelFadeAwayUndoButtonTimer();
   913     });
   914     this.$undoContainer.mouseout(function() {
   915       self.setupFadeAwayUndoButtonTimer();
   916     });
   918     GroupItems.updateGroupCloseButtons();
   919   },
   921   // ----------
   922   // Sets up fade away undo button timeout. 
   923   setupFadeAwayUndoButtonTimer: function GroupItem_setupFadeAwayUndoButtonTimer() {
   924     let self = this;
   926     if (!this._undoButtonTimeoutId) {
   927       this._undoButtonTimeoutId = setTimeout(function() { 
   928         self._fadeAwayUndoButton(); 
   929       }, this.fadeAwayUndoButtonDelay);
   930     }
   931   },
   933   // ----------
   934   // Cancels the fade away undo button timeout. 
   935   _cancelFadeAwayUndoButtonTimer: function GroupItem__cancelFadeAwayUndoButtonTimer() {
   936     clearTimeout(this._undoButtonTimeoutId);
   937     this._undoButtonTimeoutId = null;
   938   }, 
   940   // ----------
   941   // Function: add
   942   // Adds an item to the groupItem.
   943   // Parameters:
   944   //
   945   //   a - The item to add. Can be an <Item>, a DOM element or an iQ object.
   946   //       The latter two must refer to the container of an <Item>.
   947   //   options - An object with optional settings for this call.
   948   //
   949   // Options:
   950   //
   951   //   index - (int) if set, add this tab at this index
   952   //   immediately - (bool) if true, no animation will be used
   953   //   dontArrange - (bool) if true, will not trigger an arrange on the group
   954   add: function GroupItem_add(a, options) {
   955     try {
   956       var item;
   957       var $el;
   958       if (a.isAnItem) {
   959         item = a;
   960         $el = iQ(a.container);
   961       } else {
   962         $el = iQ(a);
   963         item = Items.item($el);
   964       }
   966       // safeguard to remove the item from its previous group
   967       if (item.parent && item.parent !== this)
   968         item.parent.remove(item);
   970       item.removeTrenches();
   972       if (!options)
   973         options = {};
   975       var self = this;
   977       var wasAlreadyInThisGroupItem = false;
   978       var oldIndex = this._children.indexOf(item);
   979       if (oldIndex != -1) {
   980         this._children.splice(oldIndex, 1);
   981         wasAlreadyInThisGroupItem = true;
   982       }
   984       // Insert the tab into the right position.
   985       var index = ("index" in options) ? options.index : this._children.length;
   986       this._children.splice(index, 0, item);
   988       item.setZ(this.getZ() + 1);
   990       if (!wasAlreadyInThisGroupItem) {
   991         item.droppable(false);
   992         item.groupItemData = {};
   994         item.addSubscriber("close", this._onChildClose);
   995         item.setParent(this);
   996         $el.attr("data-group-id", this.id);
   998         if (typeof item.setResizable == 'function')
   999           item.setResizable(false, options.immediately);
  1001         if (item == UI.getActiveTab() || !this._activeTab)
  1002           this.setActiveTab(item);
  1004         // if it matches the selected tab or no active tab and the browser
  1005         // tab is hidden, the active group item would be set.
  1006         if (item.tab.selected ||
  1007             (!GroupItems.getActiveGroupItem() && !item.tab.hidden))
  1008           UI.setActive(this);
  1011       if (!options.dontArrange)
  1012         this.arrange({animate: !options.immediately});
  1014       this._unfreezeItemSize({dontArrange: true});
  1015       this._sendToSubscribers("childAdded", { item: item });
  1017       UI.setReorderTabsOnHide(this);
  1018     } catch(e) {
  1019       Utils.log('GroupItem.add error', e);
  1021   },
  1023   // ----------
  1024   // Function: _onChildClose
  1025   // Handles "close" events from the group's children.
  1026   //
  1027   // Parameters:
  1028   //   tabItem - The tabItem that is closed.
  1029   _onChildClose: function GroupItem__onChildClose(tabItem) {
  1030     let count = this._children.length;
  1031     let dontArrange = tabItem.closedManually &&
  1032                       (this.expanded || !this.shouldStack(count));
  1033     let dontClose = !tabItem.closedManually && gBrowser._numPinnedTabs > 0;
  1034     this.remove(tabItem, {dontArrange: dontArrange, dontClose: dontClose});
  1036     if (dontArrange)
  1037       this._freezeItemSize(count);
  1039     if (this._children.length > 0 && this._activeTab && tabItem.closedManually)
  1040       UI.setActive(this);
  1041   },
  1043   // ----------
  1044   // Function: remove
  1045   // Removes an item from the groupItem.
  1046   // Parameters:
  1047   //
  1048   //   a - The item to remove. Can be an <Item>, a DOM element or an iQ object.
  1049   //       The latter two must refer to the container of an <Item>.
  1050   //   options - An optional object with settings for this call. See below.
  1051   //
  1052   // Possible options: 
  1053   //   dontArrange - don't rearrange the remaining items
  1054   //   dontClose - don't close the group even if it normally would
  1055   //   immediately - don't animate
  1056   remove: function GroupItem_remove(a, options) {
  1057     try {
  1058       let $el;
  1059       let item;
  1061       if (a.isAnItem) {
  1062         item = a;
  1063         $el = iQ(item.container);
  1064       } else {
  1065         $el = iQ(a);
  1066         item = Items.item($el);
  1069       if (!options)
  1070         options = {};
  1072       let index = this._children.indexOf(item);
  1073       if (index != -1)
  1074         this._children.splice(index, 1);
  1076       if (item == this._activeTab || !this._activeTab) {
  1077         if (this._children.length > 0)
  1078           this._activeTab = this._children[0];
  1079         else
  1080           this._activeTab = null;
  1083       $el[0].removeAttribute("data-group-id");
  1084       item.setParent(null);
  1085       item.removeClass("stacked");
  1086       item.isStacked = false;
  1087       item.setHidden(false);
  1088       item.removeClass("stack-trayed");
  1089       item.setRotation(0);
  1091       // Force tabItem resize if it's dragged out of a stacked groupItem.
  1092       // The tabItems's title will be visible and that's why we need to
  1093       // recalculate its height.
  1094       if (item.isDragging && this.isStacked())
  1095         item.setBounds(item.getBounds(), true, {force: true});
  1097       item.droppable(true);
  1098       item.removeSubscriber("close", this._onChildClose);
  1100       if (typeof item.setResizable == 'function')
  1101         item.setResizable(true, options.immediately);
  1103       // if a blank tab is selected while restoring a tab the blank tab gets
  1104       // removed. we need to keep the group alive for the restored tab.
  1105       if (item.isRemovedAfterRestore)
  1106         options.dontClose = true;
  1108       let closed = options.dontClose ? false : this.closeIfEmpty();
  1109       if (closed ||
  1110           (this._children.length == 0 && !gBrowser._numPinnedTabs &&
  1111            !item.isDragging)) {
  1112         this._makeLastActiveGroupItemActive();
  1113       } else if (!options.dontArrange) {
  1114         this.arrange({animate: !options.immediately});
  1115         this._unfreezeItemSize({dontArrange: true});
  1118       this._sendToSubscribers("childRemoved", { item: item });
  1119     } catch(e) {
  1120       Utils.log(e);
  1122   },
  1124   // ----------
  1125   // Function: removeAll
  1126   // Removes all of the groupItem's children.
  1127   // The optional "options" param is passed to each remove call. 
  1128   removeAll: function GroupItem_removeAll(options) {
  1129     let self = this;
  1130     let newOptions = {dontArrange: true};
  1131     if (options)
  1132       Utils.extend(newOptions, options);
  1134     let toRemove = this._children.concat();
  1135     toRemove.forEach(function(child) {
  1136       self.remove(child, newOptions);
  1137     });
  1138   },
  1140   // ----------
  1141   // Adds the given xul:tab as an app tab in this group's apptab tray
  1142   //
  1143   // Parameters:
  1144   //   xulTab - the xul:tab.
  1145   //   options - change how the app tab is added.
  1146   //
  1147   // Options:
  1148   //   position - the position of the app tab should be added to.
  1149   //   dontAdjustTray - (boolean) if true, do not adjust the tray.
  1150   addAppTab: function GroupItem_addAppTab(xulTab, options) {
  1151     GroupItems.getAppTabFavIconUrl(xulTab, function(iconUrl) {
  1152       // The tab might have been removed or unpinned while waiting.
  1153       if (!Utils.isValidXULTab(xulTab) || !xulTab.pinned)
  1154         return;
  1156       let self = this;
  1157       let $appTab = iQ("<img>")
  1158         .addClass("appTabIcon")
  1159         .attr("src", iconUrl)
  1160         .data("xulTab", xulTab)
  1161         .mousedown(function GroupItem_addAppTab_onAppTabMousedown(event) {
  1162           // stop mousedown propagation to disable group dragging on app tabs
  1163           event.stopPropagation();
  1164         })
  1165         .click(function GroupItem_addAppTab_onAppTabClick(event) {
  1166           if (!Utils.isLeftClick(event))
  1167             return;
  1169           UI.setActive(self, { dontSetActiveTabInGroup: true });
  1170           UI.goToTab(iQ(this).data("xulTab"));
  1171         });
  1173       if (options && "position" in options) {
  1174         let children = this.$appTabTray[0].childNodes;
  1176         if (options.position >= children.length)
  1177           $appTab.appendTo(this.$appTabTray);
  1178         else
  1179           this.$appTabTray[0].insertBefore($appTab[0], children[options.position]);
  1180       } else {
  1181         $appTab.appendTo(this.$appTabTray);
  1183       if (!options || !options.dontAdjustTray)
  1184         this.adjustAppTabTray(true);
  1186       this._sendToSubscribers("appTabIconAdded", { item: $appTab });
  1187     }.bind(this));
  1188   },
  1190   // ----------
  1191   // Removes the given xul:tab as an app tab in this group's apptab tray
  1192   removeAppTab: function GroupItem_removeAppTab(xulTab) {
  1193     // remove the icon
  1194     iQ(".appTabIcon", this.$appTabTray).each(function(icon) {
  1195       let $icon = iQ(icon);
  1196       if ($icon.data("xulTab") != xulTab)
  1197         return true;
  1199       $icon.remove();
  1200       return false;
  1201     });
  1203     // adjust the tray
  1204     this.adjustAppTabTray(true);
  1205   },
  1207   // ----------
  1208   // Arranges the given xul:tab as an app tab in the group's apptab tray
  1209   arrangeAppTab: function GroupItem_arrangeAppTab(xulTab) {
  1210     let self = this;
  1212     let elements = iQ(".appTabIcon", this.$appTabTray);
  1213     let length = elements.length;
  1215     elements.each(function(icon) {
  1216       let $icon = iQ(icon);
  1217       if ($icon.data("xulTab") != xulTab)
  1218         return true;
  1220       let targetIndex = xulTab._tPos;
  1222       $icon.remove({ preserveEventHandlers: true });
  1223       if (targetIndex < (length - 1))
  1224         self.$appTabTray[0].insertBefore(
  1225           icon,
  1226           iQ(".appTabIcon:nth-child(" + (targetIndex + 1) + ")", self.$appTabTray)[0]);
  1227       else
  1228         $icon.appendTo(self.$appTabTray);
  1229       return false;
  1230     });
  1231   },
  1233   // ----------
  1234   // Function: hideExpandControl
  1235   // Hide the control which expands a stacked groupItem into a quick-look view.
  1236   hideExpandControl: function GroupItem_hideExpandControl() {
  1237     this.$expander.hide();
  1238   },
  1240   // ----------
  1241   // Function: showExpandControl
  1242   // Show the control which expands a stacked groupItem into a quick-look view.
  1243   showExpandControl: function GroupItem_showExpandControl() {
  1244     let parentBB = this.getBounds();
  1245     let childBB = this.getChild(0).getBounds();
  1246     this.$expander
  1247         .show()
  1248         .css({
  1249           left: parentBB.width/2 - this.$expander.width()/2
  1250         });
  1251   },
  1253   // ----------
  1254   // Function: shouldStack
  1255   // Returns true if the groupItem, given "count", should stack (instead of 
  1256   // grid).
  1257   shouldStack: function GroupItem_shouldStack(count) {
  1258     let bb = this.getContentBounds();
  1259     let options = {
  1260       return: 'widthAndColumns',
  1261       count: count || this._children.length,
  1262       hideTitle: false
  1263     };
  1264     let arrObj = Items.arrange(this._children, bb, options);
  1266     let shouldStack = arrObj.childWidth < TabItems.minTabWidth * 1.35;
  1267     this._columns = shouldStack ? null : arrObj.columns;
  1269     return shouldStack;
  1270   },
  1272   // ----------
  1273   // Function: _freezeItemSize
  1274   // Freezes current item size (when removing a child).
  1275   //
  1276   // Parameters:
  1277   //   itemCount - the number of children before the last one was removed
  1278   _freezeItemSize: function GroupItem__freezeItemSize(itemCount) {
  1279     let data = this._frozenItemSizeData;
  1281     if (!data.lastItemCount) {
  1282       let self = this;
  1283       data.lastItemCount = itemCount;
  1285       // unfreeze item size when tabview is hidden
  1286       data.onTabViewHidden = function () self._unfreezeItemSize();
  1287       window.addEventListener('tabviewhidden', data.onTabViewHidden, false);
  1289       // we don't need to observe mouse movement when expanded because the
  1290       // tray is closed when we leave it and collapse causes unfreezing
  1291       if (!self.expanded) {
  1292         // unfreeze item size when cursor is moved out of group bounds
  1293         data.onMouseMove = function (e) {
  1294           let cursor = new Point(e.pageX, e.pageY);
  1295           if (!self.bounds.contains(cursor))
  1296             self._unfreezeItemSize();
  1298         iQ(window).mousemove(data.onMouseMove);
  1302     this.arrange({animate: true, count: data.lastItemCount});
  1303   },
  1305   // ----------
  1306   // Function: _unfreezeItemSize
  1307   // Unfreezes and updates item size.
  1308   //
  1309   // Parameters:
  1310   //   options - various options (see below)
  1311   //
  1312   // Possible options:
  1313   //   dontArrange - do not arrange items when unfreezing
  1314   _unfreezeItemSize: function GroupItem__unfreezeItemSize(options) {
  1315     let data = this._frozenItemSizeData;
  1316     if (!data.lastItemCount)
  1317       return;
  1319     if (!options || !options.dontArrange)
  1320       this.arrange({animate: true});
  1322     // unbind event listeners
  1323     window.removeEventListener('tabviewhidden', data.onTabViewHidden, false);
  1324     if (data.onMouseMove)
  1325       iQ(window).unbind('mousemove', data.onMouseMove);
  1327     // reset freeze status
  1328     this._frozenItemSizeData = {};
  1329   },
  1331   // ----------
  1332   // Function: arrange
  1333   // Lays out all of the children.
  1334   //
  1335   // Parameters:
  1336   //   options - passed to <Items.arrange> or <_stackArrange>, except those below
  1337   //
  1338   // Options:
  1339   //   addTab - (boolean) if true, we add one to the child count
  1340   //   oldDropIndex - if set, we will only set any bounds if the dropIndex has
  1341   //                  changed
  1342   //   dropPos - (<Point>) a position where a tab is currently positioned, above
  1343   //             this group.
  1344   //   animate - (boolean) if true, movement of children will be animated.
  1345   //
  1346   // Returns:
  1347   //   dropIndex - an index value for where an item would be dropped, if 
  1348   //               options.dropPos is given.
  1349   arrange: function GroupItem_arrange(options) {
  1350     if (!options)
  1351       options = {};
  1353     let childrenToArrange = [];
  1354     this._children.forEach(function(child) {
  1355       if (child.isDragging)
  1356         options.addTab = true;
  1357       else
  1358         childrenToArrange.push(child);
  1359     });
  1361     if (GroupItems._arrangePaused) {
  1362       GroupItems.pushArrange(this, options);
  1363       return false;
  1366     let shouldStack = this.shouldStack(childrenToArrange.length + (options.addTab ? 1 : 0));
  1367     let shouldStackArrange = (shouldStack && !this.expanded);
  1368     let box;
  1370     // if we should stack and we're not expanded
  1371     if (shouldStackArrange) {
  1372       this.showExpandControl();
  1373       box = this.getContentBounds({stacked: true});
  1374       this._stackArrange(childrenToArrange, box, options);
  1375       return false;
  1376     } else {
  1377       this.hideExpandControl();
  1378       box = this.getContentBounds();
  1379       // a dropIndex is returned
  1380       return this._gridArrange(childrenToArrange, box, options);
  1382   },
  1384   // ----------
  1385   // Function: _stackArrange
  1386   // Arranges the children in a stack.
  1387   //
  1388   // Parameters:
  1389   //   childrenToArrange - array of <TabItem> children
  1390   //   bb - <Rect> to arrange within
  1391   //   options - see below
  1392   //
  1393   // Possible "options" properties:
  1394   //   animate - whether to animate; default: true.
  1395   _stackArrange: function GroupItem__stackArrange(childrenToArrange, bb, options) {
  1396     if (!options)
  1397       options = {};
  1398     var animate = "animate" in options ? options.animate : true;
  1400     var count = childrenToArrange.length;
  1401     if (!count)
  1402       return;
  1404     let itemAspect = TabItems.tabHeight / TabItems.tabWidth;
  1405     let zIndex = this.getZ() + count + 1;
  1406     let maxRotation = 35; // degress
  1407     let scale = 0.7;
  1408     let newTabsPad = 10;
  1409     let bbAspect = bb.height / bb.width;
  1410     let numInPile = 6;
  1411     let angleDelta = 3.5; // degrees
  1413     // compute size of the entire stack, modulo rotation.
  1414     let size;
  1415     if (bbAspect > itemAspect) { // Tall, thin groupItem
  1416       size = TabItems.calcValidSize(new Point(bb.width * scale, -1),
  1417         {hideTitle:true});
  1418      } else { // Short, wide groupItem
  1419       size = TabItems.calcValidSize(new Point(-1, bb.height * scale),
  1420         {hideTitle:true});
  1423     // x is the left margin that the stack will have, within the content area (bb)
  1424     // y is the vertical margin
  1425     var x = (bb.width - size.x) / 2;
  1426     var y = Math.min(size.x, (bb.height - size.y) / 2);
  1427     var box = new Rect(bb.left + x, bb.top + y, size.x, size.y);
  1429     var self = this;
  1430     var children = [];
  1432     // ensure topChild is the first item in childrenToArrange
  1433     let topChild = this.getTopChild();
  1434     let topChildPos = childrenToArrange.indexOf(topChild);
  1435     if (topChildPos > 0) {
  1436       childrenToArrange.splice(topChildPos, 1);
  1437       childrenToArrange.unshift(topChild);
  1440     childrenToArrange.forEach(function GroupItem__stackArrange_order(child) {
  1441       // Children are still considered stacked even if they're hidden later.
  1442       child.addClass("stacked");
  1443       child.isStacked = true;
  1444       if (numInPile-- > 0) {
  1445         children.push(child);
  1446       } else {
  1447         child.setHidden(true);
  1449     });
  1451     self._isStacked = true;
  1453     let angleAccum = 0;
  1454     children.forEach(function GroupItem__stackArrange_apply(child, index) {
  1455       child.setZ(zIndex);
  1456       zIndex--;
  1458       // Force a recalculation of height because we've changed how the title
  1459       // is shown.
  1460       child.setBounds(box, !animate || child.getHidden(), {force:true});
  1461       child.setRotation((UI.rtl ? -1 : 1) * angleAccum);
  1462       child.setHidden(false);
  1463       angleAccum += angleDelta;
  1464     });
  1465   },
  1467   // ----------
  1468   // Function: _gridArrange
  1469   // Arranges the children into a grid.
  1470   //
  1471   // Parameters:
  1472   //   childrenToArrange - array of <TabItem> children
  1473   //   box - <Rect> to arrange within
  1474   //   options - see below
  1475   //
  1476   // Possible "options" properties:
  1477   //   animate - whether to animate; default: true.
  1478   //   z - (int) a z-index to assign the children
  1479   //   columns - the number of columns to use in the layout, if known in advance
  1480   //
  1481   // Returns:
  1482   //   dropIndex - (int) the index at which a dragged item (if there is one) should be added
  1483   //               if it is dropped. Otherwise (boolean) false.
  1484   _gridArrange: function GroupItem__gridArrange(childrenToArrange, box, options) {
  1485     let arrangeOptions;
  1486     if (this.expanded) {
  1487       // if we're expanded, we actually want to use the expanded tray's bounds.
  1488       box = new Rect(this.expanded.bounds);
  1489       box.inset(8, 8);
  1490       arrangeOptions = Utils.extend({}, options, {z: 99999});
  1491     } else {
  1492       this._isStacked = false;
  1493       arrangeOptions = Utils.extend({}, options, {
  1494         columns: this._columns
  1495       });
  1497       childrenToArrange.forEach(function(child) {
  1498         child.removeClass("stacked");
  1499         child.isStacked = false;
  1500         child.setHidden(false);
  1501       });
  1504     if (!childrenToArrange.length)
  1505       return false;
  1507     // Items.arrange will determine where/how the child items should be
  1508     // placed, but will *not* actually move them for us. This is our job.
  1509     let result = Items.arrange(childrenToArrange, box, arrangeOptions);
  1510     let {dropIndex, rects, columns} = result;
  1511     if ("oldDropIndex" in options && options.oldDropIndex === dropIndex)
  1512       return dropIndex;
  1514     this._columns = columns;
  1515     let index = 0;
  1516     let self = this;
  1517     childrenToArrange.forEach(function GroupItem_arrange_children_each(child, i) {
  1518       // If dropIndex spacing is active and this is a child after index,
  1519       // bump it up one so we actually use the correct rect
  1520       // (and skip one for the dropPos)
  1521       if (self._dropSpaceActive && index === dropIndex)
  1522         index++;
  1523       child.setBounds(rects[index], !options.animate);
  1524       child.setRotation(0);
  1525       if (arrangeOptions.z)
  1526         child.setZ(arrangeOptions.z);
  1527       index++;
  1528     });
  1530     return dropIndex;
  1531   },
  1533   expand: function GroupItem_expand() {
  1534     var self = this;
  1535     // ___ we're stacked, and command is held down so expand
  1536     UI.setActive(this.getTopChild());
  1538     var startBounds = this.getChild(0).getBounds();
  1539     var $tray = iQ("<div>").css({
  1540       top: startBounds.top,
  1541       left: startBounds.left,
  1542       width: startBounds.width,
  1543       height: startBounds.height,
  1544       position: "absolute",
  1545       zIndex: 99998
  1546     }).appendTo("body");
  1547     $tray[0].id = "expandedTray";
  1549     var w = 180;
  1550     var h = w * (TabItems.tabHeight / TabItems.tabWidth) * 1.1;
  1551     var padding = 20;
  1552     var col = Math.ceil(Math.sqrt(this._children.length));
  1553     var row = Math.ceil(this._children.length/col);
  1555     var overlayWidth = Math.min(window.innerWidth - (padding * 2), w*col + padding*(col+1));
  1556     var overlayHeight = Math.min(window.innerHeight - (padding * 2), h*row + padding*(row+1));
  1558     var pos = {left: startBounds.left, top: startBounds.top};
  1559     pos.left -= overlayWidth / 3;
  1560     pos.top  -= overlayHeight / 3;
  1562     if (pos.top < 0)
  1563       pos.top = 20;
  1564     if (pos.left < 0)
  1565       pos.left = 20;
  1566     if (pos.top + overlayHeight > window.innerHeight)
  1567       pos.top = window.innerHeight - overlayHeight - 20;
  1568     if (pos.left + overlayWidth > window.innerWidth)
  1569       pos.left = window.innerWidth - overlayWidth - 20;
  1571     $tray
  1572       .animate({
  1573         width:  overlayWidth,
  1574         height: overlayHeight,
  1575         top: pos.top,
  1576         left: pos.left
  1577       }, {
  1578         duration: 200,
  1579         easing: "tabviewBounce",
  1580         complete: function GroupItem_expand_animate_complete() {
  1581           self._sendToSubscribers("expanded");
  1583       })
  1584       .addClass("overlay");
  1586     this._children.forEach(function(child) {
  1587       child.addClass("stack-trayed");
  1588       child.setHidden(false);
  1589     });
  1591     var $shield = iQ('<div>')
  1592       .addClass('shield')
  1593       .css({
  1594         zIndex: 99997
  1595       })
  1596       .appendTo('body')
  1597       .click(function() { // just in case
  1598         self.collapse();
  1599       });
  1601     // There is a race-condition here. If there is
  1602     // a mouse-move while the shield is coming up
  1603     // it will collapse, which we don't want. Thus,
  1604     // we wait a little bit before adding this event
  1605     // handler.
  1606     setTimeout(function() {
  1607       $shield.mouseover(function() {
  1608         self.collapse();
  1609       });
  1610     }, 200);
  1612     this.expanded = {
  1613       $tray: $tray,
  1614       $shield: $shield,
  1615       bounds: new Rect(pos.left, pos.top, overlayWidth, overlayHeight)
  1616     };
  1618     this.arrange();
  1619   },
  1621   // ----------
  1622   // Function: collapse
  1623   // Collapses the groupItem from the expanded "tray" mode.
  1624   collapse: function GroupItem_collapse() {
  1625     if (this.expanded) {
  1626       var z = this.getZ();
  1627       var box = this.getBounds();
  1628       let self = this;
  1629       this.expanded.$tray
  1630         .css({
  1631           zIndex: z + 1
  1632         })
  1633         .animate({
  1634           width:  box.width,
  1635           height: box.height,
  1636           top: box.top,
  1637           left: box.left,
  1638           opacity: 0
  1639         }, {
  1640           duration: 350,
  1641           easing: "tabviewBounce",
  1642           complete: function GroupItem_collapse_animate_complete() {
  1643             iQ(this).remove();
  1644             self._sendToSubscribers("collapsed");
  1646         });
  1648       this.expanded.$shield.remove();
  1649       this.expanded = null;
  1651       this._children.forEach(function(child) {
  1652         child.removeClass("stack-trayed");
  1653       });
  1655       this.arrange({z: z + 2});
  1656       this._unfreezeItemSize({dontArrange: true});
  1658   },
  1660   // ----------
  1661   // Function: _addHandlers
  1662   // Helper routine for the constructor; adds various event handlers to the container.
  1663   _addHandlers: function GroupItem__addHandlers(container) {
  1664     let self = this;
  1665     let lastMouseDownTarget;
  1667     container.mousedown(function(e) {
  1668       let target = e.target;
  1669       // only set the last mouse down target if it is a left click, not on the
  1670       // close button, not on the expand button, not on the title bar and its
  1671       // elements
  1672       if (Utils.isLeftClick(e) &&
  1673           self.$closeButton[0] != target &&
  1674           self.$titlebar[0] != target &&
  1675           self.$expander[0] != target &&
  1676           !self.$titlebar.contains(target) &&
  1677           !self.$appTabTray.contains(target)) {
  1678         lastMouseDownTarget = target;
  1679       } else {
  1680         lastMouseDownTarget = null;
  1682     });
  1683     container.mouseup(function(e) {
  1684       let same = (e.target == lastMouseDownTarget);
  1685       lastMouseDownTarget = null;
  1687       if (same && !self.isDragging) {
  1688         if (gBrowser.selectedTab.pinned &&
  1689             UI.getActiveTab() != self.getActiveTab() &&
  1690             self.getChildren().length > 0) {
  1691           UI.setActive(self, { dontSetActiveTabInGroup: true });
  1692           UI.goToTab(gBrowser.selectedTab);
  1693         } else {
  1694           let tabItem = self.getTopChild();
  1695           if (tabItem)
  1696             tabItem.zoomIn();
  1697           else
  1698             self.newTab();
  1701     });
  1703     let dropIndex = false;
  1704     let dropSpaceTimer = null;
  1706     // When the _dropSpaceActive flag is turned on on a group, and a tab is
  1707     // dragged on top, a space will open up.
  1708     this._dropSpaceActive = false;
  1710     this.dropOptions.over = function GroupItem_dropOptions_over(event) {
  1711       iQ(this.container).addClass("acceptsDrop");
  1712     };
  1713     this.dropOptions.move = function GroupItem_dropOptions_move(event) {
  1714       let oldDropIndex = dropIndex;
  1715       let dropPos = drag.info.item.getBounds().center();
  1716       let options = {dropPos: dropPos,
  1717                      addTab: self._dropSpaceActive && drag.info.item.parent != self,
  1718                      oldDropIndex: oldDropIndex};
  1719       let newDropIndex = self.arrange(options);
  1720       // If this is a new drop index, start a timer!
  1721       if (newDropIndex !== oldDropIndex) {
  1722         dropIndex = newDropIndex;
  1723         if (this._dropSpaceActive)
  1724           return;
  1726         if (dropSpaceTimer) {
  1727           clearTimeout(dropSpaceTimer);
  1728           dropSpaceTimer = null;
  1731         dropSpaceTimer = setTimeout(function GroupItem_arrange_evaluateDropSpace() {
  1732           // Note that dropIndex's scope is GroupItem__addHandlers, but
  1733           // newDropIndex's scope is GroupItem_dropOptions_move. Thus,
  1734           // dropIndex may change with other movement events before we come
  1735           // back and check this. If it's still the same dropIndex, activate
  1736           // drop space display!
  1737           if (dropIndex === newDropIndex) {
  1738             self._dropSpaceActive = true;
  1739             dropIndex = self.arrange({dropPos: dropPos,
  1740                                       addTab: drag.info.item.parent != self,
  1741                                       animate: true});
  1743           dropSpaceTimer = null;
  1744         }, 250);
  1746     };
  1747     this.dropOptions.drop = function GroupItem_dropOptions_drop(event) {
  1748       iQ(this.container).removeClass("acceptsDrop");
  1749       let options = {};
  1750       if (this._dropSpaceActive)
  1751         this._dropSpaceActive = false;
  1753       if (dropSpaceTimer) {
  1754         clearTimeout(dropSpaceTimer);
  1755         dropSpaceTimer = null;
  1756         // If we drop this item before the timed rearrange was executed,
  1757         // we won't have an accurate dropIndex value. Get that now.
  1758         let dropPos = drag.info.item.getBounds().center();
  1759         dropIndex = self.arrange({dropPos: dropPos,
  1760                                   addTab: drag.info.item.parent != self,
  1761                                   animate: true});
  1764       if (dropIndex !== false)
  1765         options = {index: dropIndex};
  1766       this.add(drag.info.$el, options);
  1767       UI.setActive(this);
  1768       dropIndex = false;
  1769     };
  1770     this.dropOptions.out = function GroupItem_dropOptions_out(event) {
  1771       dropIndex = false;
  1772       if (this._dropSpaceActive)
  1773         this._dropSpaceActive = false;
  1775       if (dropSpaceTimer) {
  1776         clearTimeout(dropSpaceTimer);
  1777         dropSpaceTimer = null;
  1779       self.arrange();
  1780       var groupItem = drag.info.item.parent;
  1781       if (groupItem)
  1782         groupItem.remove(drag.info.$el, {dontClose: true});
  1783       iQ(this.container).removeClass("acceptsDrop");
  1786     this.draggable();
  1787     this.droppable(true);
  1789     this.$expander.click(function() {
  1790       self.expand();
  1791     });
  1792   },
  1794   // ----------
  1795   // Function: setResizable
  1796   // Sets whether the groupItem is resizable and updates the UI accordingly.
  1797   setResizable: function GroupItem_setResizable(value, immediately) {
  1798     var self = this;
  1800     this.resizeOptions.minWidth = GroupItems.minGroupWidth;
  1801     this.resizeOptions.minHeight = GroupItems.minGroupHeight;
  1803     let start = this.resizeOptions.start;
  1804     this.resizeOptions.start = function (event) {
  1805       start.call(self, event);
  1806       self._unfreezeItemSize();
  1809     if (value) {
  1810       immediately ? this.$resizer.show() : this.$resizer.fadeIn();
  1811       this.resizable(true);
  1812     } else {
  1813       immediately ? this.$resizer.hide() : this.$resizer.fadeOut();
  1814       this.resizable(false);
  1816   },
  1818   // ----------
  1819   // Function: newTab
  1820   // Creates a new tab within this groupItem.
  1821   // Parameters:
  1822   //  url - the new tab should open this url as well
  1823   //  options - the options object
  1824   //    dontZoomIn - set to true to not zoom into the newly created tab
  1825   //    closedLastTab - boolean indicates the last tab has just been closed
  1826   newTab: function GroupItem_newTab(url, options) {
  1827     if (options && options.closedLastTab)
  1828       UI.closedLastTabInTabView = true;
  1830     UI.setActive(this, { dontSetActiveTabInGroup: true });
  1832     let dontZoomIn = !!(options && options.dontZoomIn);
  1833     return gBrowser.loadOneTab(url || gWindow.BROWSER_NEW_TAB_URL, { inBackground: dontZoomIn });
  1834   },
  1836   // ----------
  1837   // Function: reorderTabItemsBasedOnTabOrder
  1838   // Reorders the tabs in a groupItem based on the arrangment of the tabs
  1839   // shown in the tab bar. It does it by sorting the children
  1840   // of the groupItem by the positions of their respective tabs in the
  1841   // tab bar.
  1842   reorderTabItemsBasedOnTabOrder: function GroupItem_reorderTabItemsBasedOnTabOrder() {
  1843     this._children.sort(function(a,b) a.tab._tPos - b.tab._tPos);
  1845     this.arrange({animate: false});
  1846     // this.arrange calls this.save for us
  1847   },
  1849   // Function: reorderTabsBasedOnTabItemOrder
  1850   // Reorders the tabs in the tab bar based on the arrangment of the tabs
  1851   // shown in the groupItem.
  1852   reorderTabsBasedOnTabItemOrder: function GroupItem_reorderTabsBasedOnTabItemOrder() {
  1853     let indices;
  1854     let tabs = this._children.map(function (tabItem) tabItem.tab);
  1856     tabs.forEach(function (tab, index) {
  1857       if (!indices)
  1858         indices = tabs.map(function (tab) tab._tPos);
  1860       let start = index ? indices[index - 1] + 1 : 0;
  1861       let end = index + 1 < indices.length ? indices[index + 1] - 1 : Infinity;
  1862       let targetRange = new Range(start, end);
  1864       if (!targetRange.contains(tab._tPos)) {
  1865         gBrowser.moveTabTo(tab, start);
  1866         indices = null;
  1868     });
  1869   },
  1871   // ----------
  1872   // Function: getTopChild
  1873   // Gets the <Item> that should be displayed on top when in stack mode.
  1874   getTopChild: function GroupItem_getTopChild() {
  1875     if (!this.getChildren().length) {
  1876       return null;
  1879     return this.getActiveTab() || this.getChild(0);
  1880   },
  1882   // ----------
  1883   // Function: getChild
  1884   // Returns the nth child tab or null if index is out of range.
  1885   //
  1886   // Parameters:
  1887   //  index - the index of the child tab to return, use negative
  1888   //          numbers to index from the end (-1 is the last child)
  1889   getChild: function GroupItem_getChild(index) {
  1890     if (index < 0)
  1891       index = this._children.length + index;
  1892     if (index >= this._children.length || index < 0)
  1893       return null;
  1894     return this._children[index];
  1895   },
  1897   // ----------
  1898   // Function: getChildren
  1899   // Returns all children.
  1900   getChildren: function GroupItem_getChildren() {
  1901     return this._children;
  1903 });
  1905 // ##########
  1906 // Class: GroupItems
  1907 // Singleton for managing all <GroupItem>s.
  1908 let GroupItems = {
  1909   groupItems: [],
  1910   nextID: 1,
  1911   _inited: false,
  1912   _activeGroupItem: null,
  1913   _cleanupFunctions: [],
  1914   _arrangePaused: false,
  1915   _arrangesPending: [],
  1916   _removingHiddenGroups: false,
  1917   _delayedModUpdates: [],
  1918   _autoclosePaused: false,
  1919   minGroupHeight: 110,
  1920   minGroupWidth: 125,
  1921   _lastActiveList: null,
  1923   // ----------
  1924   // Function: toString
  1925   // Prints [GroupItems] for debug use
  1926   toString: function GroupItems_toString() {
  1927     return "[GroupItems count=" + this.groupItems.length + "]";
  1928   },
  1930   // ----------
  1931   // Function: init
  1932   init: function GroupItems_init() {
  1933     let self = this;
  1935     // setup attr modified handler, and prepare for its uninit
  1936     function handleAttrModified(event) {
  1937       self._handleAttrModified(event.target);
  1940     // make sure any closed tabs are removed from the delay update list
  1941     function handleClose(event) {
  1942       let idx = self._delayedModUpdates.indexOf(event.target);
  1943       if (idx != -1)
  1944         self._delayedModUpdates.splice(idx, 1);
  1947     this._lastActiveList = new MRUList();
  1949     AllTabs.register("attrModified", handleAttrModified);
  1950     AllTabs.register("close", handleClose);
  1951     this._cleanupFunctions.push(function() {
  1952       AllTabs.unregister("attrModified", handleAttrModified);
  1953       AllTabs.unregister("close", handleClose);
  1954     });
  1955   },
  1957   // ----------
  1958   // Function: uninit
  1959   uninit: function GroupItems_uninit() {
  1960     // call our cleanup functions
  1961     this._cleanupFunctions.forEach(function(func) {
  1962       func();
  1963     });
  1965     this._cleanupFunctions = [];
  1967     // additional clean up
  1968     this.groupItems = null;
  1969   },
  1971   // ----------
  1972   // Function: newGroup
  1973   // Creates a new empty group.
  1974   newGroup: function GroupItems_newGroup() {
  1975     let bounds = new Rect(20, 20, 250, 200);
  1976     return new GroupItem([], {bounds: bounds, immediately: true});
  1977   },
  1979   // ----------
  1980   // Function: pauseArrange
  1981   // Bypass arrange() calls and collect for resolution in
  1982   // resumeArrange()
  1983   pauseArrange: function GroupItems_pauseArrange() {
  1984     Utils.assert(this._arrangePaused == false, 
  1985       "pauseArrange has been called while already paused");
  1986     Utils.assert(this._arrangesPending.length == 0, 
  1987       "There are bypassed arrange() calls that haven't been resolved");
  1988     this._arrangePaused = true;
  1989   },
  1991   // ----------
  1992   // Function: pushArrange
  1993   // Push an arrange() call and its arguments onto an array
  1994   // to be resolved in resumeArrange()
  1995   pushArrange: function GroupItems_pushArrange(groupItem, options) {
  1996     Utils.assert(this._arrangePaused, 
  1997       "Ensure pushArrange() called while arrange()s aren't paused"); 
  1998     let i;
  1999     for (i = 0; i < this._arrangesPending.length; i++)
  2000       if (this._arrangesPending[i].groupItem === groupItem)
  2001         break;
  2002     let arrangeInfo = {
  2003       groupItem: groupItem,
  2004       options: options
  2005     };
  2006     if (i < this._arrangesPending.length)
  2007       this._arrangesPending[i] = arrangeInfo;
  2008     else
  2009       this._arrangesPending.push(arrangeInfo);
  2010   },
  2012   // ----------
  2013   // Function: resumeArrange
  2014   // Resolve bypassed and collected arrange() calls
  2015   resumeArrange: function GroupItems_resumeArrange() {
  2016     this._arrangePaused = false;
  2017     for (let i = 0; i < this._arrangesPending.length; i++) {
  2018       let g = this._arrangesPending[i];
  2019       g.groupItem.arrange(g.options);
  2021     this._arrangesPending = [];
  2022   },
  2024   // ----------
  2025   // Function: _handleAttrModified
  2026   // watch for icon changes on app tabs
  2027   _handleAttrModified: function GroupItems__handleAttrModified(xulTab) {
  2028     if (!UI.isTabViewVisible()) {
  2029       if (this._delayedModUpdates.indexOf(xulTab) == -1) {
  2030         this._delayedModUpdates.push(xulTab);
  2032     } else
  2033       this._updateAppTabIcons(xulTab); 
  2034   },
  2036   // ----------
  2037   // Function: flushTabUpdates
  2038   // Update apptab icons based on xulTabs which have been updated
  2039   // while the TabView hasn't been visible 
  2040   flushAppTabUpdates: function GroupItems_flushAppTabUpdates() {
  2041     let self = this;
  2042     this._delayedModUpdates.forEach(function(xulTab) {
  2043       self._updateAppTabIcons(xulTab);
  2044     });
  2045     this._delayedModUpdates = [];
  2046   },
  2048   // ----------
  2049   // Function: _updateAppTabIcons
  2050   // Update images of any apptab icons that point to passed in xultab 
  2051   _updateAppTabIcons: function GroupItems__updateAppTabIcons(xulTab) {
  2052     if (!xulTab.pinned)
  2053       return;
  2055     this.getAppTabFavIconUrl(xulTab, function(iconUrl) {
  2056       iQ(".appTabIcon").each(function GroupItems__updateAppTabIcons_forEach(icon) {
  2057          let $icon = iQ(icon);
  2058          if ($icon.data("xulTab") == xulTab && iconUrl != $icon.attr("src"))
  2059            $icon.attr("src", iconUrl);
  2060       });
  2061     });
  2062   },
  2064   // ----------
  2065   // Function: getAppTabFavIconUrl
  2066   // Gets the fav icon url for app tab.
  2067   getAppTabFavIconUrl: function GroupItems_getAppTabFavIconUrl(xulTab, callback) {
  2068     FavIcons.getFavIconUrlForTab(xulTab, function GroupItems_getAppTabFavIconUrl_getFavIconUrlForTab(iconUrl) {
  2069       callback(iconUrl || FavIcons.defaultFavicon);
  2070     });
  2071   },
  2073   // ----------
  2074   // Function: addAppTab
  2075   // Adds the given xul:tab to the app tab tray in all groups
  2076   addAppTab: function GroupItems_addAppTab(xulTab) {
  2077     this.groupItems.forEach(function(groupItem) {
  2078       groupItem.addAppTab(xulTab);
  2079     });
  2080     this.updateGroupCloseButtons();
  2081   },
  2083   // ----------
  2084   // Function: removeAppTab
  2085   // Removes the given xul:tab from the app tab tray in all groups
  2086   removeAppTab: function GroupItems_removeAppTab(xulTab) {
  2087     this.groupItems.forEach(function(groupItem) {
  2088       groupItem.removeAppTab(xulTab);
  2089     });
  2090     this.updateGroupCloseButtons();
  2091   },
  2093   // ----------
  2094   // Function: arrangeAppTab
  2095   // Arranges the given xul:tab as an app tab from app tab tray in all groups
  2096   arrangeAppTab: function GroupItems_arrangeAppTab(xulTab) {
  2097     this.groupItems.forEach(function(groupItem) {
  2098       groupItem.arrangeAppTab(xulTab);
  2099     });
  2100   },
  2102   // ----------
  2103   // Function: getNextID
  2104   // Returns the next unused groupItem ID.
  2105   getNextID: function GroupItems_getNextID() {
  2106     var result = this.nextID;
  2107     this.nextID++;
  2108     this._save();
  2109     return result;
  2110   },
  2112   // ----------
  2113   // Function: saveAll
  2114   // Saves GroupItems state, as well as the state of all of the groupItems.
  2115   saveAll: function GroupItems_saveAll() {
  2116     this._save();
  2117     this.groupItems.forEach(function(groupItem) {
  2118       groupItem.save();
  2119     });
  2120   },
  2122   // ----------
  2123   // Function: _save
  2124   // Saves GroupItems state.
  2125   _save: function GroupItems__save() {
  2126     if (!this._inited) // too soon to save now
  2127       return;
  2129     let activeGroupId = this._activeGroupItem ? this._activeGroupItem.id : null;
  2130     Storage.saveGroupItemsData(
  2131       gWindow,
  2132       { nextID: this.nextID, activeGroupId: activeGroupId,
  2133         totalNumber: this.groupItems.length });
  2134   },
  2136   // ----------
  2137   // Function: getBoundingBox
  2138   // Given an array of DOM elements, returns a <Rect> with (roughly) the union of their locations.
  2139   getBoundingBox: function GroupItems_getBoundingBox(els) {
  2140     var bounds = [iQ(el).bounds() for each (el in els)];
  2141     var left   = Math.min.apply({},[ b.left   for each (b in bounds) ]);
  2142     var top    = Math.min.apply({},[ b.top    for each (b in bounds) ]);
  2143     var right  = Math.max.apply({},[ b.right  for each (b in bounds) ]);
  2144     var bottom = Math.max.apply({},[ b.bottom for each (b in bounds) ]);
  2146     return new Rect(left, top, right-left, bottom-top);
  2147   },
  2149   // ----------
  2150   // Function: reconstitute
  2151   // Restores to stored state, creating groupItems as needed.
  2152   reconstitute: function GroupItems_reconstitute(groupItemsData, groupItemData) {
  2153     try {
  2154       let activeGroupId;
  2156       if (groupItemsData) {
  2157         if (groupItemsData.nextID)
  2158           this.nextID = Math.max(this.nextID, groupItemsData.nextID);
  2159         if (groupItemsData.activeGroupId)
  2160           activeGroupId = groupItemsData.activeGroupId;
  2163       if (groupItemData) {
  2164         var toClose = this.groupItems.concat();
  2165         for (var id in groupItemData) {
  2166           let data = groupItemData[id];
  2167           if (this.groupItemStorageSanity(data)) {
  2168             let groupItem = this.groupItem(data.id); 
  2169             if (groupItem && !groupItem.hidden) {
  2170               groupItem.userSize = data.userSize;
  2171               groupItem.setTitle(data.title);
  2172               groupItem.setBounds(data.bounds, true);
  2174               let index = toClose.indexOf(groupItem);
  2175               if (index != -1)
  2176                 toClose.splice(index, 1);
  2177             } else {
  2178               var options = {
  2179                 dontPush: true,
  2180                 immediately: true
  2181               };
  2183               new GroupItem([], Utils.extend({}, data, options));
  2188         toClose.forEach(function(groupItem) {
  2189           // all tabs still existing in closed groups will be moved to new
  2190           // groups. prepare them to be reconnected later.
  2191           groupItem.getChildren().forEach(function (tabItem) {
  2192             if (tabItem.parent.hidden)
  2193               iQ(tabItem.container).show();
  2195             tabItem._reconnected = false;
  2197             // sanity check the tab's groupID
  2198             let tabData = Storage.getTabData(tabItem.tab);
  2200             if (tabData) {
  2201               let parentGroup = GroupItems.groupItem(tabData.groupID);
  2203               // the tab's group id could be invalid or point to a non-existing
  2204               // group. correct it by assigning the active group id or the first
  2205               // group of the just restored session.
  2206               if (!parentGroup || -1 < toClose.indexOf(parentGroup)) {
  2207                 tabData.groupID = activeGroupId || Object.keys(groupItemData)[0];
  2208                 Storage.saveTab(tabItem.tab, tabData);
  2211           });
  2213           // this closes the group but not its children
  2214           groupItem.close({immediately: true});
  2215         });
  2218       // set active group item
  2219       if (activeGroupId) {
  2220         let activeGroupItem = this.groupItem(activeGroupId);
  2221         if (activeGroupItem)
  2222           UI.setActive(activeGroupItem);
  2225       this._inited = true;
  2226       this._save(); // for nextID
  2227     } catch(e) {
  2228       Utils.log("error in recons: "+e);
  2230   },
  2232   // ----------
  2233   // Function: load
  2234   // Loads the storage data for groups. 
  2235   // Returns true if there was global group data.
  2236   load: function GroupItems_load() {
  2237     let groupItemsData = Storage.readGroupItemsData(gWindow);
  2238     let groupItemData = Storage.readGroupItemData(gWindow);
  2239     this.reconstitute(groupItemsData, groupItemData);
  2241     return (groupItemsData && !Utils.isEmptyObject(groupItemsData));
  2242   },
  2244   // ----------
  2245   // Function: groupItemStorageSanity
  2246   // Given persistent storage data for a groupItem, returns true if it appears to not be damaged.
  2247   groupItemStorageSanity: function GroupItems_groupItemStorageSanity(groupItemData) {
  2248     let sane = true;
  2249     if (!groupItemData.bounds || !Utils.isRect(groupItemData.bounds)) {
  2250       Utils.log('GroupItems.groupItemStorageSanity: bad bounds', groupItemData.bounds);
  2251       sane = false;
  2252     } else if ((groupItemData.userSize && 
  2253                !Utils.isPoint(groupItemData.userSize)) ||
  2254                !groupItemData.id) {
  2255       sane = false;
  2258     return sane;
  2259   },
  2261   // ----------
  2262   // Function: register
  2263   // Adds the given <GroupItem> to the list of groupItems we're tracking.
  2264   register: function GroupItems_register(groupItem) {
  2265     Utils.assert(groupItem, 'groupItem');
  2266     Utils.assert(this.groupItems.indexOf(groupItem) == -1, 'only register once per groupItem');
  2267     this.groupItems.push(groupItem);
  2268     UI.updateTabButton();
  2269   },
  2271   // ----------
  2272   // Function: unregister
  2273   // Removes the given <GroupItem> from the list of groupItems we're tracking.
  2274   unregister: function GroupItems_unregister(groupItem) {
  2275     var index = this.groupItems.indexOf(groupItem);
  2276     if (index != -1)
  2277       this.groupItems.splice(index, 1);
  2279     if (groupItem == this._activeGroupItem)
  2280       this._activeGroupItem = null;
  2282     this._arrangesPending = this._arrangesPending.filter(function (pending) {
  2283       return groupItem != pending.groupItem;
  2284     });
  2286     this._lastActiveList.remove(groupItem);
  2287     UI.updateTabButton();
  2288   },
  2290   // ----------
  2291   // Function: groupItem
  2292   // Given some sort of identifier, returns the appropriate groupItem.
  2293   // Currently only supports groupItem ids.
  2294   groupItem: function GroupItems_groupItem(a) {
  2295     if (!this.groupItems) {
  2296       // uninit has been called
  2297       return null;
  2299     var result = null;
  2300     this.groupItems.forEach(function(candidate) {
  2301       if (candidate.id == a)
  2302         result = candidate;
  2303     });
  2305     return result;
  2306   },
  2308   // ----------
  2309   // Function: removeAll
  2310   // Removes all tabs from all groupItems (which automatically closes all unnamed groupItems).
  2311   removeAll: function GroupItems_removeAll() {
  2312     var toRemove = this.groupItems.concat();
  2313     toRemove.forEach(function(groupItem) {
  2314       groupItem.removeAll();
  2315     });
  2316   },
  2318   // ----------
  2319   // Function: newTab
  2320   // Given a <TabItem>, files it in the appropriate groupItem.
  2321   newTab: function GroupItems_newTab(tabItem, options) {
  2322     let activeGroupItem = this.getActiveGroupItem();
  2324     // 1. Active group
  2325     // 2. First visible non-app tab (that's not the tab in question)
  2326     // 3. First group
  2327     // 4. At this point there should be no groups or tabs (except for app tabs and the
  2328     // tab in question): make a new group
  2330     if (activeGroupItem && !activeGroupItem.hidden) {
  2331       activeGroupItem.add(tabItem, options);
  2332       return;
  2335     let targetGroupItem;
  2336     // find first non-app visible tab belongs a group, and add the new tabItem
  2337     // to that group
  2338     gBrowser.visibleTabs.some(function(tab) {
  2339       if (!tab.pinned && tab != tabItem.tab) {
  2340         if (tab._tabViewTabItem && tab._tabViewTabItem.parent &&
  2341             !tab._tabViewTabItem.parent.hidden) {
  2342           targetGroupItem = tab._tabViewTabItem.parent;
  2344         return true;
  2346       return false;
  2347     });
  2349     let visibleGroupItems;
  2350     if (targetGroupItem) {
  2351       // add the new tabItem to the first group item
  2352       targetGroupItem.add(tabItem);
  2353       UI.setActive(targetGroupItem);
  2354       return;
  2355     } else {
  2356       // find the first visible group item
  2357       visibleGroupItems = this.groupItems.filter(function(groupItem) {
  2358         return (!groupItem.hidden);
  2359       });
  2360       if (visibleGroupItems.length > 0) {
  2361         visibleGroupItems[0].add(tabItem);
  2362         UI.setActive(visibleGroupItems[0]);
  2363         return;
  2367     // create new group for the new tabItem
  2368     tabItem.setPosition(60, 60, true);
  2369     let newGroupItemBounds = tabItem.getBounds();
  2371     newGroupItemBounds.inset(-40,-40);
  2372     let newGroupItem = new GroupItem([tabItem], { bounds: newGroupItemBounds });
  2373     newGroupItem.snap();
  2374     UI.setActive(newGroupItem);
  2375   },
  2377   // ----------
  2378   // Function: getActiveGroupItem
  2379   // Returns the active groupItem. Active means its tabs are
  2380   // shown in the tab bar when not in the TabView interface.
  2381   getActiveGroupItem: function GroupItems_getActiveGroupItem() {
  2382     return this._activeGroupItem;
  2383   },
  2385   // ----------
  2386   // Function: setActiveGroupItem
  2387   // Sets the active groupItem, thereby showing only the relevant tabs and
  2388   // setting the groupItem which will receive new tabs.
  2389   //
  2390   // Paramaters:
  2391   //  groupItem - the active <GroupItem>
  2392   setActiveGroupItem: function GroupItems_setActiveGroupItem(groupItem) {
  2393     Utils.assert(groupItem, "groupItem must be given");
  2395     if (this._activeGroupItem)
  2396       iQ(this._activeGroupItem.container).removeClass('activeGroupItem');
  2398     iQ(groupItem.container).addClass('activeGroupItem');
  2400     this._lastActiveList.update(groupItem);
  2401     this._activeGroupItem = groupItem;
  2402     this._save();
  2403   },
  2405   // ----------
  2406   // Function: getLastActiveGroupItem
  2407   // Gets last active group item.
  2408   // Returns the <groupItem>. If nothing is found, return null.
  2409   getLastActiveGroupItem: function GroupItem_getLastActiveGroupItem() {
  2410     return this._lastActiveList.peek(function(groupItem) {
  2411       return (groupItem && !groupItem.hidden && groupItem.getChildren().length > 0)
  2412     });
  2413   },
  2415   // ----------
  2416   // Function: _updateTabBar
  2417   // Hides and shows tabs in the tab bar based on the active groupItem
  2418   _updateTabBar: function GroupItems__updateTabBar() {
  2419     if (!window.UI)
  2420       return; // called too soon
  2422     Utils.assert(this._activeGroupItem, "There must be something to show in the tab bar!");
  2424     let tabItems = this._activeGroupItem._children;
  2425     gBrowser.showOnlyTheseTabs(tabItems.map(function(item) item.tab));
  2426   },
  2428   // ----------
  2429   // Function: updateActiveGroupItemAndTabBar
  2430   // Sets active TabItem and GroupItem, and updates tab bar appropriately.
  2431   // Parameters:
  2432   // tabItem - the tab item
  2433   // options - is passed to UI.setActive() directly
  2434   updateActiveGroupItemAndTabBar: 
  2435     function GroupItems_updateActiveGroupItemAndTabBar(tabItem, options) {
  2436     Utils.assertThrow(tabItem && tabItem.isATabItem, "tabItem must be a TabItem");
  2438     UI.setActive(tabItem, options);
  2439     this._updateTabBar();
  2440   },
  2442   // ----------
  2443   // Function: getNextGroupItemTab
  2444   // Paramaters:
  2445   //  reverse - the boolean indicates the direction to look for the next groupItem.
  2446   // Returns the <tabItem>. If nothing is found, return null.
  2447   getNextGroupItemTab: function GroupItems_getNextGroupItemTab(reverse) {
  2448     var groupItems = Utils.copy(GroupItems.groupItems);
  2449     var activeGroupItem = GroupItems.getActiveGroupItem();
  2450     var tabItem = null;
  2452     if (reverse)
  2453       groupItems = groupItems.reverse();
  2455     if (!activeGroupItem) {
  2456       if (groupItems.length > 0) {
  2457         groupItems.some(function(groupItem) {
  2458           if (!groupItem.hidden) {
  2459             // restore the last active tab in the group
  2460             let activeTab = groupItem.getActiveTab();
  2461             if (activeTab) {
  2462               tabItem = activeTab;
  2463               return true;
  2465             // if no tab is active, use the first one
  2466             var child = groupItem.getChild(0);
  2467             if (child) {
  2468               tabItem = child;
  2469               return true;
  2472           return false;
  2473         });
  2475     } else {
  2476       var currentIndex;
  2477       groupItems.some(function(groupItem, index) {
  2478         if (!groupItem.hidden && groupItem == activeGroupItem) {
  2479           currentIndex = index;
  2480           return true;
  2482         return false;
  2483       });
  2484       var firstGroupItems = groupItems.slice(currentIndex + 1);
  2485       firstGroupItems.some(function(groupItem) {
  2486         if (!groupItem.hidden) {
  2487           // restore the last active tab in the group
  2488           let activeTab = groupItem.getActiveTab();
  2489           if (activeTab) {
  2490             tabItem = activeTab;
  2491             return true;
  2493           // if no tab is active, use the first one
  2494           var child = groupItem.getChild(0);
  2495           if (child) {
  2496             tabItem = child;
  2497             return true;
  2500         return false;
  2501       });
  2502       if (!tabItem) {
  2503         var secondGroupItems = groupItems.slice(0, currentIndex);
  2504         secondGroupItems.some(function(groupItem) {
  2505           if (!groupItem.hidden) {
  2506             // restore the last active tab in the group
  2507             let activeTab = groupItem.getActiveTab();
  2508             if (activeTab) {
  2509               tabItem = activeTab;
  2510               return true;
  2512             // if no tab is active, use the first one
  2513             var child = groupItem.getChild(0);
  2514             if (child) {
  2515               tabItem = child;
  2516               return true;
  2519           return false;
  2520         });
  2523     return tabItem;
  2524   },
  2526   // ----------
  2527   // Function: moveTabToGroupItem
  2528   // Used for the right click menu in the tab strip; moves the given tab
  2529   // into the given group. Does nothing if the tab is an app tab.
  2530   // Paramaters:
  2531   //  tab - the <xul:tab>.
  2532   //  groupItemId - the <groupItem>'s id.  If nothing, create a new <groupItem>.
  2533   moveTabToGroupItem : function GroupItems_moveTabToGroupItem(tab, groupItemId) {
  2534     if (tab.pinned)
  2535       return;
  2537     Utils.assertThrow(tab._tabViewTabItem, "tab must be linked to a TabItem");
  2539     // given tab is already contained in target group
  2540     if (tab._tabViewTabItem.parent && tab._tabViewTabItem.parent.id == groupItemId)
  2541       return;
  2543     let shouldUpdateTabBar = false;
  2544     let shouldShowTabView = false;
  2545     let groupItem;
  2547     // switch to the appropriate tab first.
  2548     if (tab.selected) {
  2549       if (gBrowser.visibleTabs.length > 1) {
  2550         gBrowser._blurTab(tab);
  2551         shouldUpdateTabBar = true;
  2552       } else {
  2553         shouldShowTabView = true;
  2555     } else {
  2556       shouldUpdateTabBar = true
  2559     // remove tab item from a groupItem
  2560     if (tab._tabViewTabItem.parent)
  2561       tab._tabViewTabItem.parent.remove(tab._tabViewTabItem);
  2563     // add tab item to a groupItem
  2564     if (groupItemId) {
  2565       groupItem = GroupItems.groupItem(groupItemId);
  2566       groupItem.add(tab._tabViewTabItem);
  2567       groupItem.reorderTabsBasedOnTabItemOrder()
  2568     } else {
  2569       let pageBounds = Items.getPageBounds();
  2570       pageBounds.inset(20, 20);
  2572       let box = new Rect(pageBounds);
  2573       box.width = 250;
  2574       box.height = 200;
  2576       new GroupItem([ tab._tabViewTabItem ], { bounds: box, immediately: true });
  2579     if (shouldUpdateTabBar)
  2580       this._updateTabBar();
  2581     else if (shouldShowTabView)
  2582       UI.showTabView();
  2583   },
  2585   // ----------
  2586   // Function: removeHiddenGroups
  2587   // Removes all hidden groups' data and its browser tabs.
  2588   removeHiddenGroups: function GroupItems_removeHiddenGroups() {
  2589     if (this._removingHiddenGroups)
  2590       return;
  2591     this._removingHiddenGroups = true;
  2593     let groupItems = this.groupItems.concat();
  2594     groupItems.forEach(function(groupItem) {
  2595       if (groupItem.hidden)
  2596         groupItem.closeHidden();
  2597      });
  2599     this._removingHiddenGroups = false;
  2600   },
  2602   // ----------
  2603   // Function: getUnclosableGroupItemId
  2604   // If there's only one (non-hidden) group, and there are app tabs present, 
  2605   // returns that group.
  2606   // Return the <GroupItem>'s Id
  2607   getUnclosableGroupItemId: function GroupItems_getUnclosableGroupItemId() {
  2608     let unclosableGroupItemId = null;
  2610     if (gBrowser._numPinnedTabs > 0) {
  2611       let hiddenGroupItems = 
  2612         this.groupItems.concat().filter(function(groupItem) {
  2613           return !groupItem.hidden;
  2614         });
  2615       if (hiddenGroupItems.length == 1)
  2616         unclosableGroupItemId = hiddenGroupItems[0].id;
  2619     return unclosableGroupItemId;
  2620   },
  2622   // ----------
  2623   // Function: updateGroupCloseButtons
  2624   // Updates group close buttons.
  2625   updateGroupCloseButtons: function GroupItems_updateGroupCloseButtons() {
  2626     let unclosableGroupItemId = this.getUnclosableGroupItemId();
  2628     if (unclosableGroupItemId) {
  2629       let groupItem = this.groupItem(unclosableGroupItemId);
  2631       if (groupItem) {
  2632         groupItem.$closeButton.hide();
  2634     } else {
  2635       this.groupItems.forEach(function(groupItem) {
  2636         groupItem.$closeButton.show();
  2637       });
  2639   },
  2641   // ----------
  2642   // Function: calcValidSize
  2643   // Basic measure rules. Assures that item is a minimum size.
  2644   calcValidSize: function GroupItems_calcValidSize(size, options) {
  2645     Utils.assert(Utils.isPoint(size), 'input is a Point');
  2646     Utils.assert((size.x>0 || size.y>0) && (size.x!=0 && size.y!=0), 
  2647       "dimensions are valid:"+size.x+","+size.y);
  2648     return new Point(
  2649       Math.max(size.x, GroupItems.minGroupWidth),
  2650       Math.max(size.y, GroupItems.minGroupHeight));
  2651   },
  2653   // ----------
  2654   // Function: pauseAutoclose()
  2655   // Temporarily disable the behavior that closes groups when they become
  2656   // empty. This is used when entering private browsing, to avoid trashing the
  2657   // user's groups while private browsing is shuffling things around.
  2658   pauseAutoclose: function GroupItems_pauseAutoclose() {
  2659     this._autoclosePaused = true;
  2660   },
  2662   // ----------
  2663   // Function: unpauseAutoclose()
  2664   // Re-enables the auto-close behavior.
  2665   resumeAutoclose: function GroupItems_resumeAutoclose() {
  2666     this._autoclosePaused = false;
  2668 };

mercurial