browser/components/tabview/groupitems.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/components/tabview/groupitems.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,2668 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +// **********
     1.9 +// Title: groupItems.js
    1.10 +
    1.11 +// ##########
    1.12 +// Class: GroupItem
    1.13 +// A single groupItem in the TabView window. Descended from <Item>.
    1.14 +// Note that it implements the <Subscribable> interface.
    1.15 +//
    1.16 +// ----------
    1.17 +// Constructor: GroupItem
    1.18 +//
    1.19 +// Parameters:
    1.20 +//   listOfEls - an array of DOM elements for tabs to be added to this groupItem
    1.21 +//   options - various options for this groupItem (see below). In addition, gets passed
    1.22 +//     to <add> along with the elements provided.
    1.23 +//
    1.24 +// Possible options:
    1.25 +//   id - specifies the groupItem's id; otherwise automatically generated
    1.26 +//   userSize - see <Item.userSize>; default is null
    1.27 +//   bounds - a <Rect>; otherwise based on the locations of the provided elements
    1.28 +//   container - a DOM element to use as the container for this groupItem; otherwise will create
    1.29 +//   title - the title for the groupItem; otherwise blank
    1.30 +//   focusTitle - focus the title's input field after creation
    1.31 +//   dontPush - true if this groupItem shouldn't push away or snap on creation; default is false
    1.32 +//   immediately - true if we want all placement immediately, not with animation
    1.33 +function GroupItem(listOfEls, options) {
    1.34 +  if (!options)
    1.35 +    options = {};
    1.36 +
    1.37 +  this._inited = false;
    1.38 +  this._uninited = false;
    1.39 +  this._children = []; // an array of Items
    1.40 +  this.isAGroupItem = true;
    1.41 +  this.id = options.id || GroupItems.getNextID();
    1.42 +  this._isStacked = false;
    1.43 +  this.expanded = null;
    1.44 +  this.hidden = false;
    1.45 +  this.fadeAwayUndoButtonDelay = 15000;
    1.46 +  this.fadeAwayUndoButtonDuration = 300;
    1.47 +
    1.48 +  this.keepProportional = false;
    1.49 +  this._frozenItemSizeData = {};
    1.50 +
    1.51 +  this._onChildClose = this._onChildClose.bind(this);
    1.52 +
    1.53 +  // Variable: _activeTab
    1.54 +  // The <TabItem> for the groupItem's active tab.
    1.55 +  this._activeTab = null;
    1.56 +
    1.57 +  if (Utils.isPoint(options.userSize))
    1.58 +    this.userSize = new Point(options.userSize);
    1.59 +
    1.60 +  var self = this;
    1.61 +
    1.62 +  var rectToBe;
    1.63 +  if (options.bounds) {
    1.64 +    Utils.assert(Utils.isRect(options.bounds), "options.bounds must be a Rect");
    1.65 +    rectToBe = new Rect(options.bounds);
    1.66 +  }
    1.67 +
    1.68 +  if (!rectToBe) {
    1.69 +    rectToBe = GroupItems.getBoundingBox(listOfEls);
    1.70 +    rectToBe.inset(-42, -42);
    1.71 +  }
    1.72 +
    1.73 +  var $container = options.container;
    1.74 +  let immediately = options.immediately || $container ? true : false;
    1.75 +  if (!$container) {
    1.76 +    $container = iQ('<div>')
    1.77 +      .addClass('groupItem')
    1.78 +      .css({position: 'absolute'})
    1.79 +      .css(rectToBe);
    1.80 +  }
    1.81 +
    1.82 +  this.bounds = $container.bounds();
    1.83 +
    1.84 +  this.isDragging = false;
    1.85 +  $container
    1.86 +    .css({zIndex: -100})
    1.87 +    .attr("data-id", this.id)
    1.88 +    .appendTo("body");
    1.89 +
    1.90 +  // ___ Resizer
    1.91 +  this.$resizer = iQ("<div>")
    1.92 +    .addClass('resizer')
    1.93 +    .appendTo($container)
    1.94 +    .hide();
    1.95 +
    1.96 +  // ___ Titlebar
    1.97 +  var html =
    1.98 +    "<div class='title-container'>" +
    1.99 +      "<input class='name' />" +
   1.100 +      "<div class='title-shield' />" +
   1.101 +    "</div>";
   1.102 +
   1.103 +  this.$titlebar = iQ('<div>')
   1.104 +    .addClass('titlebar')
   1.105 +    .html(html)
   1.106 +    .appendTo($container);
   1.107 +
   1.108 +  this.$closeButton = iQ('<div>')
   1.109 +    .addClass('close')
   1.110 +    .click(function() {
   1.111 +      self.closeAll();
   1.112 +    })
   1.113 +    .attr("title", tabviewString("groupItem.closeGroup"))
   1.114 +    .appendTo($container);
   1.115 +
   1.116 +  // ___ Title
   1.117 +  this.$titleContainer = iQ('.title-container', this.$titlebar);
   1.118 +  this.$title = iQ('.name', this.$titlebar).attr('placeholder', this.defaultName);
   1.119 +  this.$titleShield = iQ('.title-shield', this.$titlebar);
   1.120 +  this.setTitle(options.title);
   1.121 +
   1.122 +  var handleKeyPress = function (e) {
   1.123 +    if (e.keyCode == KeyEvent.DOM_VK_ESCAPE ||
   1.124 +        e.keyCode == KeyEvent.DOM_VK_RETURN) {
   1.125 +      (self.$title)[0].blur();
   1.126 +      self.$title
   1.127 +        .addClass("transparentBorder")
   1.128 +        .one("mouseout", function() {
   1.129 +          self.$title.removeClass("transparentBorder");
   1.130 +        });
   1.131 +      e.stopPropagation();
   1.132 +      e.preventDefault();
   1.133 +    }
   1.134 +  };
   1.135 +
   1.136 +  var handleKeyUp = function(e) {
   1.137 +    // NOTE: When user commits or cancels IME composition, the last key
   1.138 +    //       event fires only a keyup event.  Then, we shouldn't take any
   1.139 +    //       reactions but we should update our status.
   1.140 +    self.save();
   1.141 +  };
   1.142 +
   1.143 +  this.$title
   1.144 +    .blur(function() {
   1.145 +      self._titleFocused = false;
   1.146 +      self.$title[0].setSelectionRange(0, 0);
   1.147 +      self.$titleShield.show();
   1.148 +      if (self.getTitle())
   1.149 +        gTabView.firstUseExperienced = true;
   1.150 +      self.save();
   1.151 +    })
   1.152 +    .focus(function() {
   1.153 +      self._unfreezeItemSize();
   1.154 +      if (!self._titleFocused) {
   1.155 +        (self.$title)[0].select();
   1.156 +        self._titleFocused = true;
   1.157 +      }
   1.158 +    })
   1.159 +    .mousedown(function(e) {
   1.160 +      e.stopPropagation();
   1.161 +    })
   1.162 +    .keypress(handleKeyPress)
   1.163 +    .keyup(handleKeyUp)
   1.164 +    .attr("title", tabviewString("groupItem.defaultName"));
   1.165 +
   1.166 +  this.$titleShield
   1.167 +    .mousedown(function(e) {
   1.168 +      self.lastMouseDownTarget = (Utils.isLeftClick(e) ? e.target : null);
   1.169 +    })
   1.170 +    .mouseup(function(e) {
   1.171 +      var same = (e.target == self.lastMouseDownTarget);
   1.172 +      self.lastMouseDownTarget = null;
   1.173 +      if (!same)
   1.174 +        return;
   1.175 +
   1.176 +      if (!self.isDragging)
   1.177 +        self.focusTitle();
   1.178 +    })
   1.179 +    .attr("title", tabviewString("groupItem.defaultName"));
   1.180 +
   1.181 +  if (options.focusTitle)
   1.182 +    this.focusTitle();
   1.183 +
   1.184 +  // ___ Stack Expander
   1.185 +  this.$expander = iQ("<div/>")
   1.186 +    .addClass("stackExpander")
   1.187 +    .appendTo($container)
   1.188 +    .hide();
   1.189 +
   1.190 +  // ___ app tabs: create app tab tray and populate it
   1.191 +  let appTabTrayContainer = iQ("<div/>")
   1.192 +    .addClass("appTabTrayContainer")
   1.193 +    .appendTo($container);
   1.194 +  this.$appTabTray = iQ("<div/>")
   1.195 +    .addClass("appTabTray")
   1.196 +    .appendTo(appTabTrayContainer);
   1.197 +
   1.198 +  let pinnedTabCount = gBrowser._numPinnedTabs;
   1.199 +  AllTabs.tabs.forEach(function (xulTab, index) {
   1.200 +    // only adjust tray when it's the last app tab.
   1.201 +    if (xulTab.pinned)
   1.202 +      this.addAppTab(xulTab, {dontAdjustTray: index + 1 < pinnedTabCount});
   1.203 +  }, this);
   1.204 +
   1.205 +  // ___ Undo Close
   1.206 +  this.$undoContainer = null;
   1.207 +  this._undoButtonTimeoutId = null;
   1.208 +
   1.209 +  // ___ Superclass initialization
   1.210 +  this._init($container[0]);
   1.211 +
   1.212 +  // ___ Children
   1.213 +  // We explicitly set dontArrange=true to prevent the groupItem from
   1.214 +  // re-arranging its children after a tabItem has been added. This saves us a
   1.215 +  // group.arrange() call per child and therefore some tab.setBounds() calls.
   1.216 +  options.dontArrange = true;
   1.217 +  listOfEls.forEach(function (el) {
   1.218 +    self.add(el, options);
   1.219 +  });
   1.220 +
   1.221 +  // ___ Finish Up
   1.222 +  this._addHandlers($container);
   1.223 +
   1.224 +  this.setResizable(true, immediately);
   1.225 +
   1.226 +  GroupItems.register(this);
   1.227 +
   1.228 +  // ___ Position
   1.229 +  this.setBounds(rectToBe, immediately);
   1.230 +  if (options.dontPush) {
   1.231 +    this.setZ(drag.zIndex);
   1.232 +    drag.zIndex++; 
   1.233 +  } else {
   1.234 +    // Calling snap will also trigger pushAway
   1.235 +    this.snap(immediately);
   1.236 +  }
   1.237 +
   1.238 +  if (!options.immediately && listOfEls.length > 0)
   1.239 +    $container.hide().fadeIn();
   1.240 +
   1.241 +  this._inited = true;
   1.242 +  this.save();
   1.243 +
   1.244 +  GroupItems.updateGroupCloseButtons();
   1.245 +};
   1.246 +
   1.247 +// ----------
   1.248 +GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), {
   1.249 +  // ----------
   1.250 +  // Function: toString
   1.251 +  // Prints [GroupItem id=id] for debug use
   1.252 +  toString: function GroupItem_toString() {
   1.253 +    return "[GroupItem id=" + this.id + "]";
   1.254 +  },
   1.255 +
   1.256 +  // ----------
   1.257 +  // Variable: defaultName
   1.258 +  // The prompt text for the title field.
   1.259 +  defaultName: tabviewString('groupItem.defaultName'),
   1.260 +
   1.261 +  // -----------
   1.262 +  // Function: setActiveTab
   1.263 +  // Sets the active <TabItem> for this groupItem; can be null, but only
   1.264 +  // if there are no children.
   1.265 +  setActiveTab: function GroupItem_setActiveTab(tab) {
   1.266 +    Utils.assertThrow((!tab && this._children.length == 0) || tab.isATabItem,
   1.267 +        "tab must be null (if no children) or a TabItem");
   1.268 +
   1.269 +    this._activeTab = tab;
   1.270 +
   1.271 +    if (this.isStacked())
   1.272 +      this.arrange({immediately: true});
   1.273 +  },
   1.274 +
   1.275 +  // -----------
   1.276 +  // Function: getActiveTab
   1.277 +  // Gets the active <TabItem> for this groupItem; can be null, but only
   1.278 +  // if there are no children.
   1.279 +  getActiveTab: function GroupItem_getActiveTab() {
   1.280 +    return this._activeTab;
   1.281 +  },
   1.282 +
   1.283 +  // ----------
   1.284 +  // Function: getStorageData
   1.285 +  // Returns all of the info worth storing about this groupItem.
   1.286 +  getStorageData: function GroupItem_getStorageData() {
   1.287 +    var data = {
   1.288 +      bounds: this.getBounds(),
   1.289 +      userSize: null,
   1.290 +      title: this.getTitle(),
   1.291 +      id: this.id
   1.292 +    };
   1.293 +
   1.294 +    if (Utils.isPoint(this.userSize))
   1.295 +      data.userSize = new Point(this.userSize);
   1.296 +
   1.297 +    return data;
   1.298 +  },
   1.299 +
   1.300 +  // ----------
   1.301 +  // Function: isEmpty
   1.302 +  // Returns true if the tab groupItem is empty and unnamed.
   1.303 +  isEmpty: function GroupItem_isEmpty() {
   1.304 +    return !this._children.length && !this.getTitle();
   1.305 +  },
   1.306 +
   1.307 +  // ----------
   1.308 +  // Function: isStacked
   1.309 +  // Returns true if this item is in a stacked groupItem.
   1.310 +  isStacked: function GroupItem_isStacked() {
   1.311 +    return this._isStacked;
   1.312 +  },
   1.313 +
   1.314 +  // ----------
   1.315 +  // Function: isTopOfStack
   1.316 +  // Returns true if the item is showing on top of this group's stack,
   1.317 +  // determined by whether the tab is this group's topChild, or
   1.318 +  // if it doesn't have one, its first child.
   1.319 +  isTopOfStack: function GroupItem_isTopOfStack(item) {
   1.320 +    return this.isStacked() && item == this.getTopChild();
   1.321 +  },
   1.322 +
   1.323 +  // ----------
   1.324 +  // Function: save
   1.325 +  // Saves this groupItem to persistent storage.
   1.326 +  save: function GroupItem_save() {
   1.327 +    if (!this._inited || this._uninited) // too soon/late to save
   1.328 +      return;
   1.329 +
   1.330 +    var data = this.getStorageData();
   1.331 +    if (GroupItems.groupItemStorageSanity(data))
   1.332 +      Storage.saveGroupItem(gWindow, data);
   1.333 +  },
   1.334 +
   1.335 +  // ----------
   1.336 +  // Function: deleteData
   1.337 +  // Deletes the groupItem in the persistent storage.
   1.338 +  deleteData: function GroupItem_deleteData() {
   1.339 +    this._uninited = true;
   1.340 +    Storage.deleteGroupItem(gWindow, this.id);
   1.341 +  },
   1.342 +
   1.343 +  // ----------
   1.344 +  // Function: getTitle
   1.345 +  // Returns the title of this groupItem as a string.
   1.346 +  getTitle: function GroupItem_getTitle() {
   1.347 +    return this.$title ? this.$title.val() : '';
   1.348 +  },
   1.349 +
   1.350 +  // ----------
   1.351 +  // Function: setTitle
   1.352 +  // Sets the title of this groupItem with the given string
   1.353 +  setTitle: function GroupItem_setTitle(value) {
   1.354 +    this.$title.val(value);
   1.355 +    this.save();
   1.356 +  },
   1.357 +
   1.358 +  // ----------
   1.359 +  // Function: focusTitle
   1.360 +  // Hide the title's shield and focus the underlying input field.
   1.361 +  focusTitle: function GroupItem_focusTitle() {
   1.362 +    this.$titleShield.hide();
   1.363 +    this.$title[0].focus();
   1.364 +  },
   1.365 +
   1.366 +  // ----------
   1.367 +  // Function: adjustAppTabTray
   1.368 +  // Used to adjust the appTabTray size, to split the appTabIcons across
   1.369 +  // multiple columns when needed - if the groupItem size is too small.
   1.370 +  //
   1.371 +  // Parameters:
   1.372 +  //   arrangeGroup - rearrange the groupItem if the number of appTab columns
   1.373 +  //   changes. If true, then this.arrange() is called, otherwise not.
   1.374 +  adjustAppTabTray: function GroupItem_adjustAppTabTray(arrangeGroup) {
   1.375 +    let icons = iQ(".appTabIcon", this.$appTabTray);
   1.376 +    let container = iQ(this.$appTabTray[0].parentNode);
   1.377 +    if (!icons.length) {
   1.378 +      // There are no icons, so hide the appTabTray if needed.
   1.379 +      if (parseInt(container.css("width")) != 0) {
   1.380 +        this.$appTabTray.css("-moz-column-count", "auto");
   1.381 +        this.$appTabTray.css("height", 0);
   1.382 +        container.css("width", 0);
   1.383 +        container.css("height", 0);
   1.384 +
   1.385 +        if (container.hasClass("appTabTrayContainerTruncated"))
   1.386 +          container.removeClass("appTabTrayContainerTruncated");
   1.387 +
   1.388 +        if (arrangeGroup)
   1.389 +          this.arrange();
   1.390 +      }
   1.391 +      return;
   1.392 +    }
   1.393 +
   1.394 +    let iconBounds = iQ(icons[0]).bounds();
   1.395 +    let boxBounds = this.getBounds();
   1.396 +    let contentHeight = boxBounds.height -
   1.397 +                        parseInt(container.css("top")) -
   1.398 +                        this.$resizer.height();
   1.399 +    let rows = Math.floor(contentHeight / iconBounds.height);
   1.400 +    let columns = Math.ceil(icons.length / rows);
   1.401 +    let columnsGap = parseInt(this.$appTabTray.css("-moz-column-gap"));
   1.402 +    let iconWidth = iconBounds.width + columnsGap;
   1.403 +    let maxColumns = Math.floor((boxBounds.width * 0.20) / iconWidth);
   1.404 +
   1.405 +    Utils.assert(rows > 0 && columns > 0 && maxColumns > 0,
   1.406 +      "make sure the calculated rows, columns and maxColumns are correct");
   1.407 +
   1.408 +    if (columns > maxColumns)
   1.409 +      container.addClass("appTabTrayContainerTruncated");
   1.410 +    else if (container.hasClass("appTabTrayContainerTruncated"))
   1.411 +      container.removeClass("appTabTrayContainerTruncated");
   1.412 +
   1.413 +    // Need to drop the -moz- prefix when Gecko makes it obsolete.
   1.414 +    // See bug 629452.
   1.415 +    if (parseInt(this.$appTabTray.css("-moz-column-count")) != columns)
   1.416 +      this.$appTabTray.css("-moz-column-count", columns);
   1.417 +
   1.418 +    if (parseInt(this.$appTabTray.css("height")) != contentHeight) {
   1.419 +      this.$appTabTray.css("height", contentHeight + "px");
   1.420 +      container.css("height", contentHeight + "px");
   1.421 +    }
   1.422 +
   1.423 +    let fullTrayWidth = iconWidth * columns - columnsGap;
   1.424 +    if (parseInt(this.$appTabTray.css("width")) != fullTrayWidth)
   1.425 +      this.$appTabTray.css("width", fullTrayWidth + "px");
   1.426 +
   1.427 +    let trayWidth = iconWidth * Math.min(columns, maxColumns) - columnsGap;
   1.428 +    if (parseInt(container.css("width")) != trayWidth) {
   1.429 +      container.css("width", trayWidth + "px");
   1.430 +
   1.431 +      // Rearrange the groupItem if the width changed.
   1.432 +      if (arrangeGroup)
   1.433 +        this.arrange();
   1.434 +    }
   1.435 +  },
   1.436 +
   1.437 +  // ----------
   1.438 +  // Function: getContentBounds
   1.439 +  // Returns a <Rect> for the groupItem's content area (which doesn't include the title, etc).
   1.440 +  //
   1.441 +  // Parameters:
   1.442 +  //   options - an object with additional parameters, see below
   1.443 +  //
   1.444 +  // Possible options:
   1.445 +  //   stacked - true to get content bounds for stacked mode
   1.446 +  getContentBounds: function GroupItem_getContentBounds(options) {
   1.447 +    let box = this.getBounds();
   1.448 +    let titleHeight = this.$titlebar.height();
   1.449 +    box.top += titleHeight;
   1.450 +    box.height -= titleHeight;
   1.451 +
   1.452 +    let appTabTrayContainer = iQ(this.$appTabTray[0].parentNode);
   1.453 +    let appTabTrayWidth = appTabTrayContainer.width();
   1.454 +    if (appTabTrayWidth)
   1.455 +      appTabTrayWidth += parseInt(appTabTrayContainer.css(UI.rtl ? "left" : "right"));
   1.456 +
   1.457 +    box.width -= appTabTrayWidth;
   1.458 +    if (UI.rtl) {
   1.459 +      box.left += appTabTrayWidth;
   1.460 +    }
   1.461 +
   1.462 +    // Make the computed bounds' "padding" and expand button margin actually be
   1.463 +    // themeable --OR-- compute this from actual bounds. Bug 586546
   1.464 +    box.inset(6, 6);
   1.465 +
   1.466 +    // make some room for the expand button in stacked mode
   1.467 +    if (options && options.stacked)
   1.468 +      box.height -= this.$expander.height() + 9; // the button height plus padding
   1.469 +
   1.470 +    return box;
   1.471 +  },
   1.472 +
   1.473 +  // ----------
   1.474 +  // Function: setBounds
   1.475 +  // Sets the bounds with the given <Rect>, animating unless "immediately" is false.
   1.476 +  //
   1.477 +  // Parameters:
   1.478 +  //   inRect - a <Rect> giving the new bounds
   1.479 +  //   immediately - true if it should not animate; default false
   1.480 +  //   options - an object with additional parameters, see below
   1.481 +  //
   1.482 +  // Possible options:
   1.483 +  //   force - true to always update the DOM even if the bounds haven't changed; default false
   1.484 +  setBounds: function GroupItem_setBounds(inRect, immediately, options) {
   1.485 +      Utils.assert(Utils.isRect(inRect), 'GroupItem.setBounds: rect is not a real rectangle!');
   1.486 +
   1.487 +    // Validate and conform passed in size
   1.488 +    let validSize = GroupItems.calcValidSize(
   1.489 +      new Point(inRect.width, inRect.height));
   1.490 +    let rect = new Rect(inRect.left, inRect.top, validSize.x, validSize.y);
   1.491 +
   1.492 +    if (!options)
   1.493 +      options = {};
   1.494 +
   1.495 +    var titleHeight = this.$titlebar.height();
   1.496 +
   1.497 +    // ___ Determine what has changed
   1.498 +    var css = {};
   1.499 +    var titlebarCSS = {};
   1.500 +    var contentCSS = {};
   1.501 +
   1.502 +    if (rect.left != this.bounds.left || options.force)
   1.503 +      css.left = rect.left;
   1.504 +
   1.505 +    if (rect.top != this.bounds.top || options.force)
   1.506 +      css.top = rect.top;
   1.507 +
   1.508 +    if (rect.width != this.bounds.width || options.force) {
   1.509 +      css.width = rect.width;
   1.510 +      titlebarCSS.width = rect.width;
   1.511 +      contentCSS.width = rect.width;
   1.512 +    }
   1.513 +
   1.514 +    if (rect.height != this.bounds.height || options.force) {
   1.515 +      css.height = rect.height;
   1.516 +      contentCSS.height = rect.height - titleHeight;
   1.517 +    }
   1.518 +
   1.519 +    if (Utils.isEmptyObject(css))
   1.520 +      return;
   1.521 +
   1.522 +    var offset = new Point(rect.left - this.bounds.left, rect.top - this.bounds.top);
   1.523 +    this.bounds = new Rect(rect);
   1.524 +
   1.525 +    // Make sure the AppTab icons fit the new groupItem size.
   1.526 +    if (css.width || css.height)
   1.527 +      this.adjustAppTabTray();
   1.528 +
   1.529 +    // ___ Deal with children
   1.530 +    if (css.width || css.height) {
   1.531 +      this.arrange({animate: !immediately}); //(immediately ? 'sometimes' : true)});
   1.532 +    } else if (css.left || css.top) {
   1.533 +      this._children.forEach(function(child) {
   1.534 +        if (!child.getHidden()) {
   1.535 +          var box = child.getBounds();
   1.536 +          child.setPosition(box.left + offset.x, box.top + offset.y, immediately);
   1.537 +        }
   1.538 +      });
   1.539 +    }
   1.540 +
   1.541 +    // ___ Update our representation
   1.542 +    if (immediately) {
   1.543 +      iQ(this.container).css(css);
   1.544 +      this.$titlebar.css(titlebarCSS);
   1.545 +    } else {
   1.546 +      TabItems.pausePainting();
   1.547 +      iQ(this.container).animate(css, {
   1.548 +        duration: 350,
   1.549 +        easing: "tabviewBounce",
   1.550 +        complete: function() {
   1.551 +          TabItems.resumePainting();
   1.552 +        }
   1.553 +      });
   1.554 +
   1.555 +      this.$titlebar.animate(titlebarCSS, {
   1.556 +        duration: 350
   1.557 +      });
   1.558 +    }
   1.559 +
   1.560 +    UI.clearShouldResizeItems();
   1.561 +    this.setTrenches(rect);
   1.562 +    this.save();
   1.563 +  },
   1.564 +
   1.565 +  // ----------
   1.566 +  // Function: setZ
   1.567 +  // Set the Z order for the groupItem's container, as well as its children.
   1.568 +  setZ: function GroupItem_setZ(value) {
   1.569 +    this.zIndex = value;
   1.570 +
   1.571 +    iQ(this.container).css({zIndex: value});
   1.572 +
   1.573 +    var count = this._children.length;
   1.574 +    if (count) {
   1.575 +      var topZIndex = value + count + 1;
   1.576 +      var zIndex = topZIndex;
   1.577 +      var self = this;
   1.578 +      this._children.forEach(function(child) {
   1.579 +        if (child == self.getTopChild())
   1.580 +          child.setZ(topZIndex + 1);
   1.581 +        else {
   1.582 +          child.setZ(zIndex);
   1.583 +          zIndex--;
   1.584 +        }
   1.585 +      });
   1.586 +    }
   1.587 +  },
   1.588 +
   1.589 +  // ----------
   1.590 +  // Function: close
   1.591 +  // Closes the groupItem, removing (but not closing) all of its children.
   1.592 +  //
   1.593 +  // Parameters:
   1.594 +  //   options - An object with optional settings for this call.
   1.595 +  //
   1.596 +  // Options:
   1.597 +  //   immediately - (bool) if true, no animation will be used
   1.598 +  close: function GroupItem_close(options) {
   1.599 +    this.removeAll({dontClose: true});
   1.600 +    GroupItems.unregister(this);
   1.601 +
   1.602 +    // remove unfreeze event handlers, if item size is frozen
   1.603 +    this._unfreezeItemSize({dontArrange: true});
   1.604 +
   1.605 +    let self = this;
   1.606 +    let destroyGroup = function () {
   1.607 +      iQ(self.container).remove();
   1.608 +      if (self.$undoContainer) {
   1.609 +        self.$undoContainer.remove();
   1.610 +        self.$undoContainer = null;
   1.611 +      }
   1.612 +      self.removeTrenches();
   1.613 +      Items.unsquish();
   1.614 +      self._sendToSubscribers("close");
   1.615 +      GroupItems.updateGroupCloseButtons();
   1.616 +    }
   1.617 +
   1.618 +    if (this.hidden || (options && options.immediately)) {
   1.619 +      destroyGroup();
   1.620 +    } else {
   1.621 +      iQ(this.container).animate({
   1.622 +        opacity: 0,
   1.623 +        "transform": "scale(.3)",
   1.624 +      }, {
   1.625 +        duration: 170,
   1.626 +        complete: destroyGroup
   1.627 +      });
   1.628 +    }
   1.629 +
   1.630 +    this.deleteData();
   1.631 +  },
   1.632 +
   1.633 +  // ----------
   1.634 +  // Function: closeAll
   1.635 +  // Closes the groupItem and all of its children.
   1.636 +  closeAll: function GroupItem_closeAll() {
   1.637 +    if (this._children.length > 0) {
   1.638 +      this._unfreezeItemSize();
   1.639 +      this._children.forEach(function(child) {
   1.640 +        iQ(child.container).hide();
   1.641 +      });
   1.642 +
   1.643 +      iQ(this.container).animate({
   1.644 +         opacity: 0,
   1.645 +         "transform": "scale(.3)",
   1.646 +      }, {
   1.647 +        duration: 170,
   1.648 +        complete: function() {
   1.649 +          iQ(this).hide();
   1.650 +        }
   1.651 +      });
   1.652 +
   1.653 +      this.droppable(false);
   1.654 +      this.removeTrenches();
   1.655 +      this._createUndoButton();
   1.656 +    } else
   1.657 +      this.close();
   1.658 +
   1.659 +    this._makeLastActiveGroupItemActive();
   1.660 +  },
   1.661 +  
   1.662 +  // ----------
   1.663 +  // Function: _makeClosestTabActive
   1.664 +  // Make the closest tab external to this group active.
   1.665 +  // Used when closing the group.
   1.666 +  _makeClosestTabActive: function GroupItem__makeClosestTabActive() {
   1.667 +    let closeCenter = this.getBounds().center();
   1.668 +    // Find closest tab to make active
   1.669 +    let closestTabItem = UI.getClosestTab(closeCenter);
   1.670 +    if (closestTabItem)
   1.671 +      UI.setActive(closestTabItem);
   1.672 +  },
   1.673 +
   1.674 +  // ----------
   1.675 +  // Function: _makeLastActiveGroupItemActive
   1.676 +  // Makes the last active group item active.
   1.677 +  _makeLastActiveGroupItemActive: function GroupItem__makeLastActiveGroupItemActive() {
   1.678 +    let groupItem = GroupItems.getLastActiveGroupItem();
   1.679 +    if (groupItem)
   1.680 +      UI.setActive(groupItem);
   1.681 +    else
   1.682 +      this._makeClosestTabActive();
   1.683 +  },
   1.684 +
   1.685 +  // ----------
   1.686 +  // Function: closeIfEmpty
   1.687 +  // Closes the group if it's empty, is closable, and autoclose is enabled
   1.688 +  // (see pauseAutoclose()). Returns true if the close occurred and false
   1.689 +  // otherwise.
   1.690 +  closeIfEmpty: function GroupItem_closeIfEmpty() {
   1.691 +    if (this.isEmpty() && !UI._closedLastVisibleTab &&
   1.692 +        !GroupItems.getUnclosableGroupItemId() && !GroupItems._autoclosePaused) {
   1.693 +      this.close();
   1.694 +      return true;
   1.695 +    }
   1.696 +    return false;
   1.697 +  },
   1.698 +
   1.699 +  // ----------
   1.700 +  // Function: _unhide
   1.701 +  // Shows the hidden group.
   1.702 +  //
   1.703 +  // Parameters:
   1.704 +  //   options - various options (see below)
   1.705 +  //
   1.706 +  // Possible options:
   1.707 +  //   immediately - true when no animations should be used
   1.708 +  _unhide: function GroupItem__unhide(options) {
   1.709 +    this._cancelFadeAwayUndoButtonTimer();
   1.710 +    this.hidden = false;
   1.711 +    this.$undoContainer.remove();
   1.712 +    this.$undoContainer = null;
   1.713 +    this.droppable(true);
   1.714 +    this.setTrenches(this.bounds);
   1.715 +
   1.716 +    let self = this;
   1.717 +
   1.718 +    let finalize = function () {
   1.719 +      self._children.forEach(function(child) {
   1.720 +        iQ(child.container).show();
   1.721 +      });
   1.722 +
   1.723 +      UI.setActive(self);
   1.724 +      self._sendToSubscribers("groupShown");
   1.725 +    };
   1.726 +
   1.727 +    let $container = iQ(this.container).show();
   1.728 +
   1.729 +    if (!options || !options.immediately) {
   1.730 +      $container.animate({
   1.731 +        "transform": "scale(1)",
   1.732 +        "opacity": 1
   1.733 +      }, {
   1.734 +        duration: 170,
   1.735 +        complete: finalize
   1.736 +      });
   1.737 +    } else {
   1.738 +      $container.css({"transform": "none", opacity: 1});
   1.739 +      finalize();
   1.740 +    }
   1.741 +
   1.742 +    GroupItems.updateGroupCloseButtons();
   1.743 +  },
   1.744 +
   1.745 +  // ----------
   1.746 +  // Function: closeHidden
   1.747 +  // Removes the group item, its children and its container.
   1.748 +  closeHidden: function GroupItem_closeHidden() {
   1.749 +    let self = this;
   1.750 +
   1.751 +    this._cancelFadeAwayUndoButtonTimer();
   1.752 +
   1.753 +    // When the last non-empty groupItem is closed and there are no
   1.754 +    // pinned tabs then create a new group with a blank tab.
   1.755 +    let remainingGroups = GroupItems.groupItems.filter(function (groupItem) {
   1.756 +      return (groupItem != self && groupItem.getChildren().length);
   1.757 +    });
   1.758 +
   1.759 +    let tab = null;
   1.760 +
   1.761 +    if (!gBrowser._numPinnedTabs && !remainingGroups.length) {
   1.762 +      let emptyGroups = GroupItems.groupItems.filter(function (groupItem) {
   1.763 +        return (groupItem != self && !groupItem.getChildren().length);
   1.764 +      });
   1.765 +      let group = (emptyGroups.length ? emptyGroups[0] : GroupItems.newGroup());
   1.766 +      tab = group.newTab(null, {dontZoomIn: true});
   1.767 +    }
   1.768 +
   1.769 +    let closed = this.destroy();
   1.770 +
   1.771 +    if (!tab)
   1.772 +      return;
   1.773 +
   1.774 +    if (closed) {
   1.775 +      // Let's make the new tab the selected tab.
   1.776 +      UI.goToTab(tab);
   1.777 +    } else {
   1.778 +      // Remove the new tab and group, if this group is no longer closed.
   1.779 +      tab._tabViewTabItem.parent.destroy({immediately: true});
   1.780 +    }
   1.781 +  },
   1.782 +
   1.783 +  // ----------
   1.784 +  // Function: destroy
   1.785 +  // Close all tabs linked to children (tabItems), removes all children and 
   1.786 +  // close the groupItem.
   1.787 +  //
   1.788 +  // Parameters:
   1.789 +  //   options - An object with optional settings for this call.
   1.790 +  //
   1.791 +  // Options:
   1.792 +  //   immediately - (bool) if true, no animation will be used
   1.793 +  //
   1.794 +  // Returns true if the groupItem has been closed, or false otherwise. A group
   1.795 +  // could not have been closed due to a tab with an onUnload handler (that
   1.796 +  // waits for user interaction).
   1.797 +  destroy: function GroupItem_destroy(options) {
   1.798 +    let self = this;
   1.799 +
   1.800 +    // when "TabClose" event is fired, the browser tab is about to close and our 
   1.801 +    // item "close" event is fired.  And then, the browser tab gets closed. 
   1.802 +    // In other words, the group "close" event is fired before all browser
   1.803 +    // tabs in the group are closed.  The below code would fire the group "close"
   1.804 +    // event only after all browser tabs in that group are closed.
   1.805 +    this._children.concat().forEach(function(child) {
   1.806 +      child.removeSubscriber("close", self._onChildClose);
   1.807 +
   1.808 +      if (child.close(true)) {
   1.809 +        self.remove(child, { dontArrange: true });
   1.810 +      } else {
   1.811 +        // child.removeSubscriber() must be called before child.close(), 
   1.812 +        // therefore we call child.addSubscriber() if the tab is not removed.
   1.813 +        child.addSubscriber("close", self._onChildClose);
   1.814 +      }
   1.815 +    });
   1.816 +
   1.817 +    if (this._children.length) {
   1.818 +      if (this.hidden)
   1.819 +        this.$undoContainer.fadeOut(function() { self._unhide() });
   1.820 +
   1.821 +      return false;
   1.822 +    } else {
   1.823 +      this.close(options);
   1.824 +      return true;
   1.825 +    }
   1.826 +  },
   1.827 +
   1.828 +  // ----------
   1.829 +  // Function: _fadeAwayUndoButton
   1.830 +  // Fades away the undo button
   1.831 +  _fadeAwayUndoButton: function GroupItem__fadeAwayUndoButton() {
   1.832 +    let self = this;
   1.833 +
   1.834 +    if (this.$undoContainer) {
   1.835 +      // if there is more than one group and other groups are not empty,
   1.836 +      // fade away the undo button.
   1.837 +      let shouldFadeAway = false;
   1.838 +
   1.839 +      if (GroupItems.groupItems.length > 1) {
   1.840 +        shouldFadeAway = 
   1.841 +          GroupItems.groupItems.some(function(groupItem) {
   1.842 +            return (groupItem != self && groupItem.getChildren().length > 0);
   1.843 +          });
   1.844 +      }
   1.845 +
   1.846 +      if (shouldFadeAway) {
   1.847 +        self.$undoContainer.animate({
   1.848 +          color: "transparent",
   1.849 +          opacity: 0
   1.850 +        }, {
   1.851 +          duration: this._fadeAwayUndoButtonDuration,
   1.852 +          complete: function() { self.closeHidden(); }
   1.853 +        });
   1.854 +      }
   1.855 +    }
   1.856 +  },
   1.857 +
   1.858 +  // ----------
   1.859 +  // Function: _createUndoButton
   1.860 +  // Makes the affordance for undo a close group action
   1.861 +  _createUndoButton: function GroupItem__createUndoButton() {
   1.862 +    let self = this;
   1.863 +    this.$undoContainer = iQ("<div/>")
   1.864 +      .addClass("undo")
   1.865 +      .attr("type", "button")
   1.866 +      .attr("data-group-id", this.id)
   1.867 +      .appendTo("body");
   1.868 +    iQ("<span/>")
   1.869 +      .text(tabviewString("groupItem.undoCloseGroup"))
   1.870 +      .appendTo(this.$undoContainer);
   1.871 +    let undoClose = iQ("<span/>")
   1.872 +      .addClass("close")
   1.873 +      .attr("title", tabviewString("groupItem.discardClosedGroup"))
   1.874 +      .appendTo(this.$undoContainer);
   1.875 +
   1.876 +    this.$undoContainer.css({
   1.877 +      left: this.bounds.left + this.bounds.width/2 - iQ(self.$undoContainer).width()/2,
   1.878 +      top:  this.bounds.top + this.bounds.height/2 - iQ(self.$undoContainer).height()/2,
   1.879 +      "transform": "scale(.1)",
   1.880 +      opacity: 0
   1.881 +    });
   1.882 +    this.hidden = true;
   1.883 +
   1.884 +    // hide group item and show undo container.
   1.885 +    setTimeout(function() {
   1.886 +      self.$undoContainer.animate({
   1.887 +        "transform": "scale(1)",
   1.888 +        "opacity": 1
   1.889 +      }, {
   1.890 +        easing: "tabviewBounce",
   1.891 +        duration: 170,
   1.892 +        complete: function() {
   1.893 +          self._sendToSubscribers("groupHidden");
   1.894 +        }
   1.895 +      });
   1.896 +    }, 50);
   1.897 +
   1.898 +    // add click handlers
   1.899 +    this.$undoContainer.click(function(e) {
   1.900 +      // don't do anything if the close button is clicked.
   1.901 +      if (e.target == undoClose[0])
   1.902 +        return;
   1.903 +
   1.904 +      self.$undoContainer.fadeOut(function() { self._unhide(); });
   1.905 +    });
   1.906 +
   1.907 +    undoClose.click(function() {
   1.908 +      self.$undoContainer.fadeOut(function() { self.closeHidden(); });
   1.909 +    });
   1.910 +
   1.911 +    this.setupFadeAwayUndoButtonTimer();
   1.912 +    // Cancel the fadeaway if you move the mouse over the undo
   1.913 +    // button, and restart the countdown once you move out of it.
   1.914 +    this.$undoContainer.mouseover(function() { 
   1.915 +      self._cancelFadeAwayUndoButtonTimer();
   1.916 +    });
   1.917 +    this.$undoContainer.mouseout(function() {
   1.918 +      self.setupFadeAwayUndoButtonTimer();
   1.919 +    });
   1.920 +
   1.921 +    GroupItems.updateGroupCloseButtons();
   1.922 +  },
   1.923 +
   1.924 +  // ----------
   1.925 +  // Sets up fade away undo button timeout. 
   1.926 +  setupFadeAwayUndoButtonTimer: function GroupItem_setupFadeAwayUndoButtonTimer() {
   1.927 +    let self = this;
   1.928 +
   1.929 +    if (!this._undoButtonTimeoutId) {
   1.930 +      this._undoButtonTimeoutId = setTimeout(function() { 
   1.931 +        self._fadeAwayUndoButton(); 
   1.932 +      }, this.fadeAwayUndoButtonDelay);
   1.933 +    }
   1.934 +  },
   1.935 +  
   1.936 +  // ----------
   1.937 +  // Cancels the fade away undo button timeout. 
   1.938 +  _cancelFadeAwayUndoButtonTimer: function GroupItem__cancelFadeAwayUndoButtonTimer() {
   1.939 +    clearTimeout(this._undoButtonTimeoutId);
   1.940 +    this._undoButtonTimeoutId = null;
   1.941 +  }, 
   1.942 +
   1.943 +  // ----------
   1.944 +  // Function: add
   1.945 +  // Adds an item to the groupItem.
   1.946 +  // Parameters:
   1.947 +  //
   1.948 +  //   a - The item to add. Can be an <Item>, a DOM element or an iQ object.
   1.949 +  //       The latter two must refer to the container of an <Item>.
   1.950 +  //   options - An object with optional settings for this call.
   1.951 +  //
   1.952 +  // Options:
   1.953 +  //
   1.954 +  //   index - (int) if set, add this tab at this index
   1.955 +  //   immediately - (bool) if true, no animation will be used
   1.956 +  //   dontArrange - (bool) if true, will not trigger an arrange on the group
   1.957 +  add: function GroupItem_add(a, options) {
   1.958 +    try {
   1.959 +      var item;
   1.960 +      var $el;
   1.961 +      if (a.isAnItem) {
   1.962 +        item = a;
   1.963 +        $el = iQ(a.container);
   1.964 +      } else {
   1.965 +        $el = iQ(a);
   1.966 +        item = Items.item($el);
   1.967 +      }
   1.968 +
   1.969 +      // safeguard to remove the item from its previous group
   1.970 +      if (item.parent && item.parent !== this)
   1.971 +        item.parent.remove(item);
   1.972 +
   1.973 +      item.removeTrenches();
   1.974 +
   1.975 +      if (!options)
   1.976 +        options = {};
   1.977 +
   1.978 +      var self = this;
   1.979 +
   1.980 +      var wasAlreadyInThisGroupItem = false;
   1.981 +      var oldIndex = this._children.indexOf(item);
   1.982 +      if (oldIndex != -1) {
   1.983 +        this._children.splice(oldIndex, 1);
   1.984 +        wasAlreadyInThisGroupItem = true;
   1.985 +      }
   1.986 +
   1.987 +      // Insert the tab into the right position.
   1.988 +      var index = ("index" in options) ? options.index : this._children.length;
   1.989 +      this._children.splice(index, 0, item);
   1.990 +
   1.991 +      item.setZ(this.getZ() + 1);
   1.992 +
   1.993 +      if (!wasAlreadyInThisGroupItem) {
   1.994 +        item.droppable(false);
   1.995 +        item.groupItemData = {};
   1.996 +
   1.997 +        item.addSubscriber("close", this._onChildClose);
   1.998 +        item.setParent(this);
   1.999 +        $el.attr("data-group-id", this.id);
  1.1000 +
  1.1001 +        if (typeof item.setResizable == 'function')
  1.1002 +          item.setResizable(false, options.immediately);
  1.1003 +
  1.1004 +        if (item == UI.getActiveTab() || !this._activeTab)
  1.1005 +          this.setActiveTab(item);
  1.1006 +
  1.1007 +        // if it matches the selected tab or no active tab and the browser
  1.1008 +        // tab is hidden, the active group item would be set.
  1.1009 +        if (item.tab.selected ||
  1.1010 +            (!GroupItems.getActiveGroupItem() && !item.tab.hidden))
  1.1011 +          UI.setActive(this);
  1.1012 +      }
  1.1013 +
  1.1014 +      if (!options.dontArrange)
  1.1015 +        this.arrange({animate: !options.immediately});
  1.1016 +
  1.1017 +      this._unfreezeItemSize({dontArrange: true});
  1.1018 +      this._sendToSubscribers("childAdded", { item: item });
  1.1019 +
  1.1020 +      UI.setReorderTabsOnHide(this);
  1.1021 +    } catch(e) {
  1.1022 +      Utils.log('GroupItem.add error', e);
  1.1023 +    }
  1.1024 +  },
  1.1025 +
  1.1026 +  // ----------
  1.1027 +  // Function: _onChildClose
  1.1028 +  // Handles "close" events from the group's children.
  1.1029 +  //
  1.1030 +  // Parameters:
  1.1031 +  //   tabItem - The tabItem that is closed.
  1.1032 +  _onChildClose: function GroupItem__onChildClose(tabItem) {
  1.1033 +    let count = this._children.length;
  1.1034 +    let dontArrange = tabItem.closedManually &&
  1.1035 +                      (this.expanded || !this.shouldStack(count));
  1.1036 +    let dontClose = !tabItem.closedManually && gBrowser._numPinnedTabs > 0;
  1.1037 +    this.remove(tabItem, {dontArrange: dontArrange, dontClose: dontClose});
  1.1038 +
  1.1039 +    if (dontArrange)
  1.1040 +      this._freezeItemSize(count);
  1.1041 +
  1.1042 +    if (this._children.length > 0 && this._activeTab && tabItem.closedManually)
  1.1043 +      UI.setActive(this);
  1.1044 +  },
  1.1045 +
  1.1046 +  // ----------
  1.1047 +  // Function: remove
  1.1048 +  // Removes an item from the groupItem.
  1.1049 +  // Parameters:
  1.1050 +  //
  1.1051 +  //   a - The item to remove. Can be an <Item>, a DOM element or an iQ object.
  1.1052 +  //       The latter two must refer to the container of an <Item>.
  1.1053 +  //   options - An optional object with settings for this call. See below.
  1.1054 +  //
  1.1055 +  // Possible options: 
  1.1056 +  //   dontArrange - don't rearrange the remaining items
  1.1057 +  //   dontClose - don't close the group even if it normally would
  1.1058 +  //   immediately - don't animate
  1.1059 +  remove: function GroupItem_remove(a, options) {
  1.1060 +    try {
  1.1061 +      let $el;
  1.1062 +      let item;
  1.1063 +
  1.1064 +      if (a.isAnItem) {
  1.1065 +        item = a;
  1.1066 +        $el = iQ(item.container);
  1.1067 +      } else {
  1.1068 +        $el = iQ(a);
  1.1069 +        item = Items.item($el);
  1.1070 +      }
  1.1071 +
  1.1072 +      if (!options)
  1.1073 +        options = {};
  1.1074 +
  1.1075 +      let index = this._children.indexOf(item);
  1.1076 +      if (index != -1)
  1.1077 +        this._children.splice(index, 1);
  1.1078 +
  1.1079 +      if (item == this._activeTab || !this._activeTab) {
  1.1080 +        if (this._children.length > 0)
  1.1081 +          this._activeTab = this._children[0];
  1.1082 +        else
  1.1083 +          this._activeTab = null;
  1.1084 +      }
  1.1085 +
  1.1086 +      $el[0].removeAttribute("data-group-id");
  1.1087 +      item.setParent(null);
  1.1088 +      item.removeClass("stacked");
  1.1089 +      item.isStacked = false;
  1.1090 +      item.setHidden(false);
  1.1091 +      item.removeClass("stack-trayed");
  1.1092 +      item.setRotation(0);
  1.1093 +
  1.1094 +      // Force tabItem resize if it's dragged out of a stacked groupItem.
  1.1095 +      // The tabItems's title will be visible and that's why we need to
  1.1096 +      // recalculate its height.
  1.1097 +      if (item.isDragging && this.isStacked())
  1.1098 +        item.setBounds(item.getBounds(), true, {force: true});
  1.1099 +
  1.1100 +      item.droppable(true);
  1.1101 +      item.removeSubscriber("close", this._onChildClose);
  1.1102 +
  1.1103 +      if (typeof item.setResizable == 'function')
  1.1104 +        item.setResizable(true, options.immediately);
  1.1105 +
  1.1106 +      // if a blank tab is selected while restoring a tab the blank tab gets
  1.1107 +      // removed. we need to keep the group alive for the restored tab.
  1.1108 +      if (item.isRemovedAfterRestore)
  1.1109 +        options.dontClose = true;
  1.1110 +
  1.1111 +      let closed = options.dontClose ? false : this.closeIfEmpty();
  1.1112 +      if (closed ||
  1.1113 +          (this._children.length == 0 && !gBrowser._numPinnedTabs &&
  1.1114 +           !item.isDragging)) {
  1.1115 +        this._makeLastActiveGroupItemActive();
  1.1116 +      } else if (!options.dontArrange) {
  1.1117 +        this.arrange({animate: !options.immediately});
  1.1118 +        this._unfreezeItemSize({dontArrange: true});
  1.1119 +      }
  1.1120 +
  1.1121 +      this._sendToSubscribers("childRemoved", { item: item });
  1.1122 +    } catch(e) {
  1.1123 +      Utils.log(e);
  1.1124 +    }
  1.1125 +  },
  1.1126 +
  1.1127 +  // ----------
  1.1128 +  // Function: removeAll
  1.1129 +  // Removes all of the groupItem's children.
  1.1130 +  // The optional "options" param is passed to each remove call. 
  1.1131 +  removeAll: function GroupItem_removeAll(options) {
  1.1132 +    let self = this;
  1.1133 +    let newOptions = {dontArrange: true};
  1.1134 +    if (options)
  1.1135 +      Utils.extend(newOptions, options);
  1.1136 +      
  1.1137 +    let toRemove = this._children.concat();
  1.1138 +    toRemove.forEach(function(child) {
  1.1139 +      self.remove(child, newOptions);
  1.1140 +    });
  1.1141 +  },
  1.1142 +
  1.1143 +  // ----------
  1.1144 +  // Adds the given xul:tab as an app tab in this group's apptab tray
  1.1145 +  //
  1.1146 +  // Parameters:
  1.1147 +  //   xulTab - the xul:tab.
  1.1148 +  //   options - change how the app tab is added.
  1.1149 +  //
  1.1150 +  // Options:
  1.1151 +  //   position - the position of the app tab should be added to.
  1.1152 +  //   dontAdjustTray - (boolean) if true, do not adjust the tray.
  1.1153 +  addAppTab: function GroupItem_addAppTab(xulTab, options) {
  1.1154 +    GroupItems.getAppTabFavIconUrl(xulTab, function(iconUrl) {
  1.1155 +      // The tab might have been removed or unpinned while waiting.
  1.1156 +      if (!Utils.isValidXULTab(xulTab) || !xulTab.pinned)
  1.1157 +        return;
  1.1158 +
  1.1159 +      let self = this;
  1.1160 +      let $appTab = iQ("<img>")
  1.1161 +        .addClass("appTabIcon")
  1.1162 +        .attr("src", iconUrl)
  1.1163 +        .data("xulTab", xulTab)
  1.1164 +        .mousedown(function GroupItem_addAppTab_onAppTabMousedown(event) {
  1.1165 +          // stop mousedown propagation to disable group dragging on app tabs
  1.1166 +          event.stopPropagation();
  1.1167 +        })
  1.1168 +        .click(function GroupItem_addAppTab_onAppTabClick(event) {
  1.1169 +          if (!Utils.isLeftClick(event))
  1.1170 +            return;
  1.1171 +
  1.1172 +          UI.setActive(self, { dontSetActiveTabInGroup: true });
  1.1173 +          UI.goToTab(iQ(this).data("xulTab"));
  1.1174 +        });
  1.1175 +
  1.1176 +      if (options && "position" in options) {
  1.1177 +        let children = this.$appTabTray[0].childNodes;
  1.1178 +
  1.1179 +        if (options.position >= children.length)
  1.1180 +          $appTab.appendTo(this.$appTabTray);
  1.1181 +        else
  1.1182 +          this.$appTabTray[0].insertBefore($appTab[0], children[options.position]);
  1.1183 +      } else {
  1.1184 +        $appTab.appendTo(this.$appTabTray);
  1.1185 +      }
  1.1186 +      if (!options || !options.dontAdjustTray)
  1.1187 +        this.adjustAppTabTray(true);
  1.1188 +
  1.1189 +      this._sendToSubscribers("appTabIconAdded", { item: $appTab });
  1.1190 +    }.bind(this));
  1.1191 +  },
  1.1192 +
  1.1193 +  // ----------
  1.1194 +  // Removes the given xul:tab as an app tab in this group's apptab tray
  1.1195 +  removeAppTab: function GroupItem_removeAppTab(xulTab) {
  1.1196 +    // remove the icon
  1.1197 +    iQ(".appTabIcon", this.$appTabTray).each(function(icon) {
  1.1198 +      let $icon = iQ(icon);
  1.1199 +      if ($icon.data("xulTab") != xulTab)
  1.1200 +        return true;
  1.1201 +        
  1.1202 +      $icon.remove();
  1.1203 +      return false;
  1.1204 +    });
  1.1205 +    
  1.1206 +    // adjust the tray
  1.1207 +    this.adjustAppTabTray(true);
  1.1208 +  },
  1.1209 +
  1.1210 +  // ----------
  1.1211 +  // Arranges the given xul:tab as an app tab in the group's apptab tray
  1.1212 +  arrangeAppTab: function GroupItem_arrangeAppTab(xulTab) {
  1.1213 +    let self = this;
  1.1214 +
  1.1215 +    let elements = iQ(".appTabIcon", this.$appTabTray);
  1.1216 +    let length = elements.length;
  1.1217 +
  1.1218 +    elements.each(function(icon) {
  1.1219 +      let $icon = iQ(icon);
  1.1220 +      if ($icon.data("xulTab") != xulTab)
  1.1221 +        return true;
  1.1222 +
  1.1223 +      let targetIndex = xulTab._tPos;
  1.1224 +
  1.1225 +      $icon.remove({ preserveEventHandlers: true });
  1.1226 +      if (targetIndex < (length - 1))
  1.1227 +        self.$appTabTray[0].insertBefore(
  1.1228 +          icon,
  1.1229 +          iQ(".appTabIcon:nth-child(" + (targetIndex + 1) + ")", self.$appTabTray)[0]);
  1.1230 +      else
  1.1231 +        $icon.appendTo(self.$appTabTray);
  1.1232 +      return false;
  1.1233 +    });
  1.1234 +  },
  1.1235 +
  1.1236 +  // ----------
  1.1237 +  // Function: hideExpandControl
  1.1238 +  // Hide the control which expands a stacked groupItem into a quick-look view.
  1.1239 +  hideExpandControl: function GroupItem_hideExpandControl() {
  1.1240 +    this.$expander.hide();
  1.1241 +  },
  1.1242 +
  1.1243 +  // ----------
  1.1244 +  // Function: showExpandControl
  1.1245 +  // Show the control which expands a stacked groupItem into a quick-look view.
  1.1246 +  showExpandControl: function GroupItem_showExpandControl() {
  1.1247 +    let parentBB = this.getBounds();
  1.1248 +    let childBB = this.getChild(0).getBounds();
  1.1249 +    this.$expander
  1.1250 +        .show()
  1.1251 +        .css({
  1.1252 +          left: parentBB.width/2 - this.$expander.width()/2
  1.1253 +        });
  1.1254 +  },
  1.1255 +
  1.1256 +  // ----------
  1.1257 +  // Function: shouldStack
  1.1258 +  // Returns true if the groupItem, given "count", should stack (instead of 
  1.1259 +  // grid).
  1.1260 +  shouldStack: function GroupItem_shouldStack(count) {
  1.1261 +    let bb = this.getContentBounds();
  1.1262 +    let options = {
  1.1263 +      return: 'widthAndColumns',
  1.1264 +      count: count || this._children.length,
  1.1265 +      hideTitle: false
  1.1266 +    };
  1.1267 +    let arrObj = Items.arrange(this._children, bb, options);
  1.1268 +
  1.1269 +    let shouldStack = arrObj.childWidth < TabItems.minTabWidth * 1.35;
  1.1270 +    this._columns = shouldStack ? null : arrObj.columns;
  1.1271 +
  1.1272 +    return shouldStack;
  1.1273 +  },
  1.1274 +
  1.1275 +  // ----------
  1.1276 +  // Function: _freezeItemSize
  1.1277 +  // Freezes current item size (when removing a child).
  1.1278 +  //
  1.1279 +  // Parameters:
  1.1280 +  //   itemCount - the number of children before the last one was removed
  1.1281 +  _freezeItemSize: function GroupItem__freezeItemSize(itemCount) {
  1.1282 +    let data = this._frozenItemSizeData;
  1.1283 +
  1.1284 +    if (!data.lastItemCount) {
  1.1285 +      let self = this;
  1.1286 +      data.lastItemCount = itemCount;
  1.1287 +
  1.1288 +      // unfreeze item size when tabview is hidden
  1.1289 +      data.onTabViewHidden = function () self._unfreezeItemSize();
  1.1290 +      window.addEventListener('tabviewhidden', data.onTabViewHidden, false);
  1.1291 +
  1.1292 +      // we don't need to observe mouse movement when expanded because the
  1.1293 +      // tray is closed when we leave it and collapse causes unfreezing
  1.1294 +      if (!self.expanded) {
  1.1295 +        // unfreeze item size when cursor is moved out of group bounds
  1.1296 +        data.onMouseMove = function (e) {
  1.1297 +          let cursor = new Point(e.pageX, e.pageY);
  1.1298 +          if (!self.bounds.contains(cursor))
  1.1299 +            self._unfreezeItemSize();
  1.1300 +        }
  1.1301 +        iQ(window).mousemove(data.onMouseMove);
  1.1302 +      }
  1.1303 +    }
  1.1304 +
  1.1305 +    this.arrange({animate: true, count: data.lastItemCount});
  1.1306 +  },
  1.1307 +
  1.1308 +  // ----------
  1.1309 +  // Function: _unfreezeItemSize
  1.1310 +  // Unfreezes and updates item size.
  1.1311 +  //
  1.1312 +  // Parameters:
  1.1313 +  //   options - various options (see below)
  1.1314 +  //
  1.1315 +  // Possible options:
  1.1316 +  //   dontArrange - do not arrange items when unfreezing
  1.1317 +  _unfreezeItemSize: function GroupItem__unfreezeItemSize(options) {
  1.1318 +    let data = this._frozenItemSizeData;
  1.1319 +    if (!data.lastItemCount)
  1.1320 +      return;
  1.1321 +
  1.1322 +    if (!options || !options.dontArrange)
  1.1323 +      this.arrange({animate: true});
  1.1324 +
  1.1325 +    // unbind event listeners
  1.1326 +    window.removeEventListener('tabviewhidden', data.onTabViewHidden, false);
  1.1327 +    if (data.onMouseMove)
  1.1328 +      iQ(window).unbind('mousemove', data.onMouseMove);
  1.1329 +
  1.1330 +    // reset freeze status
  1.1331 +    this._frozenItemSizeData = {};
  1.1332 +  },
  1.1333 +
  1.1334 +  // ----------
  1.1335 +  // Function: arrange
  1.1336 +  // Lays out all of the children.
  1.1337 +  //
  1.1338 +  // Parameters:
  1.1339 +  //   options - passed to <Items.arrange> or <_stackArrange>, except those below
  1.1340 +  //
  1.1341 +  // Options:
  1.1342 +  //   addTab - (boolean) if true, we add one to the child count
  1.1343 +  //   oldDropIndex - if set, we will only set any bounds if the dropIndex has
  1.1344 +  //                  changed
  1.1345 +  //   dropPos - (<Point>) a position where a tab is currently positioned, above
  1.1346 +  //             this group.
  1.1347 +  //   animate - (boolean) if true, movement of children will be animated.
  1.1348 +  //
  1.1349 +  // Returns:
  1.1350 +  //   dropIndex - an index value for where an item would be dropped, if 
  1.1351 +  //               options.dropPos is given.
  1.1352 +  arrange: function GroupItem_arrange(options) {
  1.1353 +    if (!options)
  1.1354 +      options = {};
  1.1355 +
  1.1356 +    let childrenToArrange = [];
  1.1357 +    this._children.forEach(function(child) {
  1.1358 +      if (child.isDragging)
  1.1359 +        options.addTab = true;
  1.1360 +      else
  1.1361 +        childrenToArrange.push(child);
  1.1362 +    });
  1.1363 +
  1.1364 +    if (GroupItems._arrangePaused) {
  1.1365 +      GroupItems.pushArrange(this, options);
  1.1366 +      return false;
  1.1367 +    }
  1.1368 +
  1.1369 +    let shouldStack = this.shouldStack(childrenToArrange.length + (options.addTab ? 1 : 0));
  1.1370 +    let shouldStackArrange = (shouldStack && !this.expanded);
  1.1371 +    let box;
  1.1372 +
  1.1373 +    // if we should stack and we're not expanded
  1.1374 +    if (shouldStackArrange) {
  1.1375 +      this.showExpandControl();
  1.1376 +      box = this.getContentBounds({stacked: true});
  1.1377 +      this._stackArrange(childrenToArrange, box, options);
  1.1378 +      return false;
  1.1379 +    } else {
  1.1380 +      this.hideExpandControl();
  1.1381 +      box = this.getContentBounds();
  1.1382 +      // a dropIndex is returned
  1.1383 +      return this._gridArrange(childrenToArrange, box, options);
  1.1384 +    }
  1.1385 +  },
  1.1386 +
  1.1387 +  // ----------
  1.1388 +  // Function: _stackArrange
  1.1389 +  // Arranges the children in a stack.
  1.1390 +  //
  1.1391 +  // Parameters:
  1.1392 +  //   childrenToArrange - array of <TabItem> children
  1.1393 +  //   bb - <Rect> to arrange within
  1.1394 +  //   options - see below
  1.1395 +  //
  1.1396 +  // Possible "options" properties:
  1.1397 +  //   animate - whether to animate; default: true.
  1.1398 +  _stackArrange: function GroupItem__stackArrange(childrenToArrange, bb, options) {
  1.1399 +    if (!options)
  1.1400 +      options = {};
  1.1401 +    var animate = "animate" in options ? options.animate : true;
  1.1402 +
  1.1403 +    var count = childrenToArrange.length;
  1.1404 +    if (!count)
  1.1405 +      return;
  1.1406 +
  1.1407 +    let itemAspect = TabItems.tabHeight / TabItems.tabWidth;
  1.1408 +    let zIndex = this.getZ() + count + 1;
  1.1409 +    let maxRotation = 35; // degress
  1.1410 +    let scale = 0.7;
  1.1411 +    let newTabsPad = 10;
  1.1412 +    let bbAspect = bb.height / bb.width;
  1.1413 +    let numInPile = 6;
  1.1414 +    let angleDelta = 3.5; // degrees
  1.1415 +
  1.1416 +    // compute size of the entire stack, modulo rotation.
  1.1417 +    let size;
  1.1418 +    if (bbAspect > itemAspect) { // Tall, thin groupItem
  1.1419 +      size = TabItems.calcValidSize(new Point(bb.width * scale, -1),
  1.1420 +        {hideTitle:true});
  1.1421 +     } else { // Short, wide groupItem
  1.1422 +      size = TabItems.calcValidSize(new Point(-1, bb.height * scale),
  1.1423 +        {hideTitle:true});
  1.1424 +     }
  1.1425 +
  1.1426 +    // x is the left margin that the stack will have, within the content area (bb)
  1.1427 +    // y is the vertical margin
  1.1428 +    var x = (bb.width - size.x) / 2;
  1.1429 +    var y = Math.min(size.x, (bb.height - size.y) / 2);
  1.1430 +    var box = new Rect(bb.left + x, bb.top + y, size.x, size.y);
  1.1431 +
  1.1432 +    var self = this;
  1.1433 +    var children = [];
  1.1434 +
  1.1435 +    // ensure topChild is the first item in childrenToArrange
  1.1436 +    let topChild = this.getTopChild();
  1.1437 +    let topChildPos = childrenToArrange.indexOf(topChild);
  1.1438 +    if (topChildPos > 0) {
  1.1439 +      childrenToArrange.splice(topChildPos, 1);
  1.1440 +      childrenToArrange.unshift(topChild);
  1.1441 +    }
  1.1442 +
  1.1443 +    childrenToArrange.forEach(function GroupItem__stackArrange_order(child) {
  1.1444 +      // Children are still considered stacked even if they're hidden later.
  1.1445 +      child.addClass("stacked");
  1.1446 +      child.isStacked = true;
  1.1447 +      if (numInPile-- > 0) {
  1.1448 +        children.push(child);
  1.1449 +      } else {
  1.1450 +        child.setHidden(true);
  1.1451 +      }
  1.1452 +    });
  1.1453 +
  1.1454 +    self._isStacked = true;
  1.1455 +
  1.1456 +    let angleAccum = 0;
  1.1457 +    children.forEach(function GroupItem__stackArrange_apply(child, index) {
  1.1458 +      child.setZ(zIndex);
  1.1459 +      zIndex--;
  1.1460 +
  1.1461 +      // Force a recalculation of height because we've changed how the title
  1.1462 +      // is shown.
  1.1463 +      child.setBounds(box, !animate || child.getHidden(), {force:true});
  1.1464 +      child.setRotation((UI.rtl ? -1 : 1) * angleAccum);
  1.1465 +      child.setHidden(false);
  1.1466 +      angleAccum += angleDelta;
  1.1467 +    });
  1.1468 +  },
  1.1469 +  
  1.1470 +  // ----------
  1.1471 +  // Function: _gridArrange
  1.1472 +  // Arranges the children into a grid.
  1.1473 +  //
  1.1474 +  // Parameters:
  1.1475 +  //   childrenToArrange - array of <TabItem> children
  1.1476 +  //   box - <Rect> to arrange within
  1.1477 +  //   options - see below
  1.1478 +  //
  1.1479 +  // Possible "options" properties:
  1.1480 +  //   animate - whether to animate; default: true.
  1.1481 +  //   z - (int) a z-index to assign the children
  1.1482 +  //   columns - the number of columns to use in the layout, if known in advance
  1.1483 +  //
  1.1484 +  // Returns:
  1.1485 +  //   dropIndex - (int) the index at which a dragged item (if there is one) should be added
  1.1486 +  //               if it is dropped. Otherwise (boolean) false.
  1.1487 +  _gridArrange: function GroupItem__gridArrange(childrenToArrange, box, options) {
  1.1488 +    let arrangeOptions;
  1.1489 +    if (this.expanded) {
  1.1490 +      // if we're expanded, we actually want to use the expanded tray's bounds.
  1.1491 +      box = new Rect(this.expanded.bounds);
  1.1492 +      box.inset(8, 8);
  1.1493 +      arrangeOptions = Utils.extend({}, options, {z: 99999});
  1.1494 +    } else {
  1.1495 +      this._isStacked = false;
  1.1496 +      arrangeOptions = Utils.extend({}, options, {
  1.1497 +        columns: this._columns
  1.1498 +      });
  1.1499 +
  1.1500 +      childrenToArrange.forEach(function(child) {
  1.1501 +        child.removeClass("stacked");
  1.1502 +        child.isStacked = false;
  1.1503 +        child.setHidden(false);
  1.1504 +      });
  1.1505 +    }
  1.1506 +  
  1.1507 +    if (!childrenToArrange.length)
  1.1508 +      return false;
  1.1509 +
  1.1510 +    // Items.arrange will determine where/how the child items should be
  1.1511 +    // placed, but will *not* actually move them for us. This is our job.
  1.1512 +    let result = Items.arrange(childrenToArrange, box, arrangeOptions);
  1.1513 +    let {dropIndex, rects, columns} = result;
  1.1514 +    if ("oldDropIndex" in options && options.oldDropIndex === dropIndex)
  1.1515 +      return dropIndex;
  1.1516 +
  1.1517 +    this._columns = columns;
  1.1518 +    let index = 0;
  1.1519 +    let self = this;
  1.1520 +    childrenToArrange.forEach(function GroupItem_arrange_children_each(child, i) {
  1.1521 +      // If dropIndex spacing is active and this is a child after index,
  1.1522 +      // bump it up one so we actually use the correct rect
  1.1523 +      // (and skip one for the dropPos)
  1.1524 +      if (self._dropSpaceActive && index === dropIndex)
  1.1525 +        index++;
  1.1526 +      child.setBounds(rects[index], !options.animate);
  1.1527 +      child.setRotation(0);
  1.1528 +      if (arrangeOptions.z)
  1.1529 +        child.setZ(arrangeOptions.z);
  1.1530 +      index++;
  1.1531 +    });
  1.1532 +
  1.1533 +    return dropIndex;
  1.1534 +  },
  1.1535 +
  1.1536 +  expand: function GroupItem_expand() {
  1.1537 +    var self = this;
  1.1538 +    // ___ we're stacked, and command is held down so expand
  1.1539 +    UI.setActive(this.getTopChild());
  1.1540 +    
  1.1541 +    var startBounds = this.getChild(0).getBounds();
  1.1542 +    var $tray = iQ("<div>").css({
  1.1543 +      top: startBounds.top,
  1.1544 +      left: startBounds.left,
  1.1545 +      width: startBounds.width,
  1.1546 +      height: startBounds.height,
  1.1547 +      position: "absolute",
  1.1548 +      zIndex: 99998
  1.1549 +    }).appendTo("body");
  1.1550 +    $tray[0].id = "expandedTray";
  1.1551 +
  1.1552 +    var w = 180;
  1.1553 +    var h = w * (TabItems.tabHeight / TabItems.tabWidth) * 1.1;
  1.1554 +    var padding = 20;
  1.1555 +    var col = Math.ceil(Math.sqrt(this._children.length));
  1.1556 +    var row = Math.ceil(this._children.length/col);
  1.1557 +
  1.1558 +    var overlayWidth = Math.min(window.innerWidth - (padding * 2), w*col + padding*(col+1));
  1.1559 +    var overlayHeight = Math.min(window.innerHeight - (padding * 2), h*row + padding*(row+1));
  1.1560 +
  1.1561 +    var pos = {left: startBounds.left, top: startBounds.top};
  1.1562 +    pos.left -= overlayWidth / 3;
  1.1563 +    pos.top  -= overlayHeight / 3;
  1.1564 +
  1.1565 +    if (pos.top < 0)
  1.1566 +      pos.top = 20;
  1.1567 +    if (pos.left < 0)
  1.1568 +      pos.left = 20;
  1.1569 +    if (pos.top + overlayHeight > window.innerHeight)
  1.1570 +      pos.top = window.innerHeight - overlayHeight - 20;
  1.1571 +    if (pos.left + overlayWidth > window.innerWidth)
  1.1572 +      pos.left = window.innerWidth - overlayWidth - 20;
  1.1573 +
  1.1574 +    $tray
  1.1575 +      .animate({
  1.1576 +        width:  overlayWidth,
  1.1577 +        height: overlayHeight,
  1.1578 +        top: pos.top,
  1.1579 +        left: pos.left
  1.1580 +      }, {
  1.1581 +        duration: 200,
  1.1582 +        easing: "tabviewBounce",
  1.1583 +        complete: function GroupItem_expand_animate_complete() {
  1.1584 +          self._sendToSubscribers("expanded");
  1.1585 +        }
  1.1586 +      })
  1.1587 +      .addClass("overlay");
  1.1588 +
  1.1589 +    this._children.forEach(function(child) {
  1.1590 +      child.addClass("stack-trayed");
  1.1591 +      child.setHidden(false);
  1.1592 +    });
  1.1593 +
  1.1594 +    var $shield = iQ('<div>')
  1.1595 +      .addClass('shield')
  1.1596 +      .css({
  1.1597 +        zIndex: 99997
  1.1598 +      })
  1.1599 +      .appendTo('body')
  1.1600 +      .click(function() { // just in case
  1.1601 +        self.collapse();
  1.1602 +      });
  1.1603 +
  1.1604 +    // There is a race-condition here. If there is
  1.1605 +    // a mouse-move while the shield is coming up
  1.1606 +    // it will collapse, which we don't want. Thus,
  1.1607 +    // we wait a little bit before adding this event
  1.1608 +    // handler.
  1.1609 +    setTimeout(function() {
  1.1610 +      $shield.mouseover(function() {
  1.1611 +        self.collapse();
  1.1612 +      });
  1.1613 +    }, 200);
  1.1614 +
  1.1615 +    this.expanded = {
  1.1616 +      $tray: $tray,
  1.1617 +      $shield: $shield,
  1.1618 +      bounds: new Rect(pos.left, pos.top, overlayWidth, overlayHeight)
  1.1619 +    };
  1.1620 +
  1.1621 +    this.arrange();
  1.1622 +  },
  1.1623 +
  1.1624 +  // ----------
  1.1625 +  // Function: collapse
  1.1626 +  // Collapses the groupItem from the expanded "tray" mode.
  1.1627 +  collapse: function GroupItem_collapse() {
  1.1628 +    if (this.expanded) {
  1.1629 +      var z = this.getZ();
  1.1630 +      var box = this.getBounds();
  1.1631 +      let self = this;
  1.1632 +      this.expanded.$tray
  1.1633 +        .css({
  1.1634 +          zIndex: z + 1
  1.1635 +        })
  1.1636 +        .animate({
  1.1637 +          width:  box.width,
  1.1638 +          height: box.height,
  1.1639 +          top: box.top,
  1.1640 +          left: box.left,
  1.1641 +          opacity: 0
  1.1642 +        }, {
  1.1643 +          duration: 350,
  1.1644 +          easing: "tabviewBounce",
  1.1645 +          complete: function GroupItem_collapse_animate_complete() {
  1.1646 +            iQ(this).remove();
  1.1647 +            self._sendToSubscribers("collapsed");
  1.1648 +          }
  1.1649 +        });
  1.1650 +
  1.1651 +      this.expanded.$shield.remove();
  1.1652 +      this.expanded = null;
  1.1653 +
  1.1654 +      this._children.forEach(function(child) {
  1.1655 +        child.removeClass("stack-trayed");
  1.1656 +      });
  1.1657 +
  1.1658 +      this.arrange({z: z + 2});
  1.1659 +      this._unfreezeItemSize({dontArrange: true});
  1.1660 +    }
  1.1661 +  },
  1.1662 +
  1.1663 +  // ----------
  1.1664 +  // Function: _addHandlers
  1.1665 +  // Helper routine for the constructor; adds various event handlers to the container.
  1.1666 +  _addHandlers: function GroupItem__addHandlers(container) {
  1.1667 +    let self = this;
  1.1668 +    let lastMouseDownTarget;
  1.1669 +
  1.1670 +    container.mousedown(function(e) {
  1.1671 +      let target = e.target;
  1.1672 +      // only set the last mouse down target if it is a left click, not on the
  1.1673 +      // close button, not on the expand button, not on the title bar and its
  1.1674 +      // elements
  1.1675 +      if (Utils.isLeftClick(e) &&
  1.1676 +          self.$closeButton[0] != target &&
  1.1677 +          self.$titlebar[0] != target &&
  1.1678 +          self.$expander[0] != target &&
  1.1679 +          !self.$titlebar.contains(target) &&
  1.1680 +          !self.$appTabTray.contains(target)) {
  1.1681 +        lastMouseDownTarget = target;
  1.1682 +      } else {
  1.1683 +        lastMouseDownTarget = null;
  1.1684 +      }
  1.1685 +    });
  1.1686 +    container.mouseup(function(e) {
  1.1687 +      let same = (e.target == lastMouseDownTarget);
  1.1688 +      lastMouseDownTarget = null;
  1.1689 +
  1.1690 +      if (same && !self.isDragging) {
  1.1691 +        if (gBrowser.selectedTab.pinned &&
  1.1692 +            UI.getActiveTab() != self.getActiveTab() &&
  1.1693 +            self.getChildren().length > 0) {
  1.1694 +          UI.setActive(self, { dontSetActiveTabInGroup: true });
  1.1695 +          UI.goToTab(gBrowser.selectedTab);
  1.1696 +        } else {
  1.1697 +          let tabItem = self.getTopChild();
  1.1698 +          if (tabItem)
  1.1699 +            tabItem.zoomIn();
  1.1700 +          else
  1.1701 +            self.newTab();
  1.1702 +        }
  1.1703 +      }
  1.1704 +    });
  1.1705 +
  1.1706 +    let dropIndex = false;
  1.1707 +    let dropSpaceTimer = null;
  1.1708 +
  1.1709 +    // When the _dropSpaceActive flag is turned on on a group, and a tab is
  1.1710 +    // dragged on top, a space will open up.
  1.1711 +    this._dropSpaceActive = false;
  1.1712 +
  1.1713 +    this.dropOptions.over = function GroupItem_dropOptions_over(event) {
  1.1714 +      iQ(this.container).addClass("acceptsDrop");
  1.1715 +    };
  1.1716 +    this.dropOptions.move = function GroupItem_dropOptions_move(event) {
  1.1717 +      let oldDropIndex = dropIndex;
  1.1718 +      let dropPos = drag.info.item.getBounds().center();
  1.1719 +      let options = {dropPos: dropPos,
  1.1720 +                     addTab: self._dropSpaceActive && drag.info.item.parent != self,
  1.1721 +                     oldDropIndex: oldDropIndex};
  1.1722 +      let newDropIndex = self.arrange(options);
  1.1723 +      // If this is a new drop index, start a timer!
  1.1724 +      if (newDropIndex !== oldDropIndex) {
  1.1725 +        dropIndex = newDropIndex;
  1.1726 +        if (this._dropSpaceActive)
  1.1727 +          return;
  1.1728 +          
  1.1729 +        if (dropSpaceTimer) {
  1.1730 +          clearTimeout(dropSpaceTimer);
  1.1731 +          dropSpaceTimer = null;
  1.1732 +        }
  1.1733 +
  1.1734 +        dropSpaceTimer = setTimeout(function GroupItem_arrange_evaluateDropSpace() {
  1.1735 +          // Note that dropIndex's scope is GroupItem__addHandlers, but
  1.1736 +          // newDropIndex's scope is GroupItem_dropOptions_move. Thus,
  1.1737 +          // dropIndex may change with other movement events before we come
  1.1738 +          // back and check this. If it's still the same dropIndex, activate
  1.1739 +          // drop space display!
  1.1740 +          if (dropIndex === newDropIndex) {
  1.1741 +            self._dropSpaceActive = true;
  1.1742 +            dropIndex = self.arrange({dropPos: dropPos,
  1.1743 +                                      addTab: drag.info.item.parent != self,
  1.1744 +                                      animate: true});
  1.1745 +          }
  1.1746 +          dropSpaceTimer = null;
  1.1747 +        }, 250);
  1.1748 +      }
  1.1749 +    };
  1.1750 +    this.dropOptions.drop = function GroupItem_dropOptions_drop(event) {
  1.1751 +      iQ(this.container).removeClass("acceptsDrop");
  1.1752 +      let options = {};
  1.1753 +      if (this._dropSpaceActive)
  1.1754 +        this._dropSpaceActive = false;
  1.1755 +
  1.1756 +      if (dropSpaceTimer) {
  1.1757 +        clearTimeout(dropSpaceTimer);
  1.1758 +        dropSpaceTimer = null;
  1.1759 +        // If we drop this item before the timed rearrange was executed,
  1.1760 +        // we won't have an accurate dropIndex value. Get that now.
  1.1761 +        let dropPos = drag.info.item.getBounds().center();
  1.1762 +        dropIndex = self.arrange({dropPos: dropPos,
  1.1763 +                                  addTab: drag.info.item.parent != self,
  1.1764 +                                  animate: true});
  1.1765 +      }
  1.1766 +
  1.1767 +      if (dropIndex !== false)
  1.1768 +        options = {index: dropIndex};
  1.1769 +      this.add(drag.info.$el, options);
  1.1770 +      UI.setActive(this);
  1.1771 +      dropIndex = false;
  1.1772 +    };
  1.1773 +    this.dropOptions.out = function GroupItem_dropOptions_out(event) {
  1.1774 +      dropIndex = false;
  1.1775 +      if (this._dropSpaceActive)
  1.1776 +        this._dropSpaceActive = false;
  1.1777 +
  1.1778 +      if (dropSpaceTimer) {
  1.1779 +        clearTimeout(dropSpaceTimer);
  1.1780 +        dropSpaceTimer = null;
  1.1781 +      }
  1.1782 +      self.arrange();
  1.1783 +      var groupItem = drag.info.item.parent;
  1.1784 +      if (groupItem)
  1.1785 +        groupItem.remove(drag.info.$el, {dontClose: true});
  1.1786 +      iQ(this.container).removeClass("acceptsDrop");
  1.1787 +    }
  1.1788 +
  1.1789 +    this.draggable();
  1.1790 +    this.droppable(true);
  1.1791 +
  1.1792 +    this.$expander.click(function() {
  1.1793 +      self.expand();
  1.1794 +    });
  1.1795 +  },
  1.1796 +
  1.1797 +  // ----------
  1.1798 +  // Function: setResizable
  1.1799 +  // Sets whether the groupItem is resizable and updates the UI accordingly.
  1.1800 +  setResizable: function GroupItem_setResizable(value, immediately) {
  1.1801 +    var self = this;
  1.1802 +
  1.1803 +    this.resizeOptions.minWidth = GroupItems.minGroupWidth;
  1.1804 +    this.resizeOptions.minHeight = GroupItems.minGroupHeight;
  1.1805 +
  1.1806 +    let start = this.resizeOptions.start;
  1.1807 +    this.resizeOptions.start = function (event) {
  1.1808 +      start.call(self, event);
  1.1809 +      self._unfreezeItemSize();
  1.1810 +    }
  1.1811 +
  1.1812 +    if (value) {
  1.1813 +      immediately ? this.$resizer.show() : this.$resizer.fadeIn();
  1.1814 +      this.resizable(true);
  1.1815 +    } else {
  1.1816 +      immediately ? this.$resizer.hide() : this.$resizer.fadeOut();
  1.1817 +      this.resizable(false);
  1.1818 +    }
  1.1819 +  },
  1.1820 +
  1.1821 +  // ----------
  1.1822 +  // Function: newTab
  1.1823 +  // Creates a new tab within this groupItem.
  1.1824 +  // Parameters:
  1.1825 +  //  url - the new tab should open this url as well
  1.1826 +  //  options - the options object
  1.1827 +  //    dontZoomIn - set to true to not zoom into the newly created tab
  1.1828 +  //    closedLastTab - boolean indicates the last tab has just been closed
  1.1829 +  newTab: function GroupItem_newTab(url, options) {
  1.1830 +    if (options && options.closedLastTab)
  1.1831 +      UI.closedLastTabInTabView = true;
  1.1832 +
  1.1833 +    UI.setActive(this, { dontSetActiveTabInGroup: true });
  1.1834 +
  1.1835 +    let dontZoomIn = !!(options && options.dontZoomIn);
  1.1836 +    return gBrowser.loadOneTab(url || gWindow.BROWSER_NEW_TAB_URL, { inBackground: dontZoomIn });
  1.1837 +  },
  1.1838 +
  1.1839 +  // ----------
  1.1840 +  // Function: reorderTabItemsBasedOnTabOrder
  1.1841 +  // Reorders the tabs in a groupItem based on the arrangment of the tabs
  1.1842 +  // shown in the tab bar. It does it by sorting the children
  1.1843 +  // of the groupItem by the positions of their respective tabs in the
  1.1844 +  // tab bar.
  1.1845 +  reorderTabItemsBasedOnTabOrder: function GroupItem_reorderTabItemsBasedOnTabOrder() {
  1.1846 +    this._children.sort(function(a,b) a.tab._tPos - b.tab._tPos);
  1.1847 +
  1.1848 +    this.arrange({animate: false});
  1.1849 +    // this.arrange calls this.save for us
  1.1850 +  },
  1.1851 +
  1.1852 +  // Function: reorderTabsBasedOnTabItemOrder
  1.1853 +  // Reorders the tabs in the tab bar based on the arrangment of the tabs
  1.1854 +  // shown in the groupItem.
  1.1855 +  reorderTabsBasedOnTabItemOrder: function GroupItem_reorderTabsBasedOnTabItemOrder() {
  1.1856 +    let indices;
  1.1857 +    let tabs = this._children.map(function (tabItem) tabItem.tab);
  1.1858 +
  1.1859 +    tabs.forEach(function (tab, index) {
  1.1860 +      if (!indices)
  1.1861 +        indices = tabs.map(function (tab) tab._tPos);
  1.1862 +
  1.1863 +      let start = index ? indices[index - 1] + 1 : 0;
  1.1864 +      let end = index + 1 < indices.length ? indices[index + 1] - 1 : Infinity;
  1.1865 +      let targetRange = new Range(start, end);
  1.1866 +
  1.1867 +      if (!targetRange.contains(tab._tPos)) {
  1.1868 +        gBrowser.moveTabTo(tab, start);
  1.1869 +        indices = null;
  1.1870 +      }
  1.1871 +    });
  1.1872 +  },
  1.1873 +
  1.1874 +  // ----------
  1.1875 +  // Function: getTopChild
  1.1876 +  // Gets the <Item> that should be displayed on top when in stack mode.
  1.1877 +  getTopChild: function GroupItem_getTopChild() {
  1.1878 +    if (!this.getChildren().length) {
  1.1879 +      return null;
  1.1880 +    }
  1.1881 +
  1.1882 +    return this.getActiveTab() || this.getChild(0);
  1.1883 +  },
  1.1884 +
  1.1885 +  // ----------
  1.1886 +  // Function: getChild
  1.1887 +  // Returns the nth child tab or null if index is out of range.
  1.1888 +  //
  1.1889 +  // Parameters:
  1.1890 +  //  index - the index of the child tab to return, use negative
  1.1891 +  //          numbers to index from the end (-1 is the last child)
  1.1892 +  getChild: function GroupItem_getChild(index) {
  1.1893 +    if (index < 0)
  1.1894 +      index = this._children.length + index;
  1.1895 +    if (index >= this._children.length || index < 0)
  1.1896 +      return null;
  1.1897 +    return this._children[index];
  1.1898 +  },
  1.1899 +
  1.1900 +  // ----------
  1.1901 +  // Function: getChildren
  1.1902 +  // Returns all children.
  1.1903 +  getChildren: function GroupItem_getChildren() {
  1.1904 +    return this._children;
  1.1905 +  }
  1.1906 +});
  1.1907 +
  1.1908 +// ##########
  1.1909 +// Class: GroupItems
  1.1910 +// Singleton for managing all <GroupItem>s.
  1.1911 +let GroupItems = {
  1.1912 +  groupItems: [],
  1.1913 +  nextID: 1,
  1.1914 +  _inited: false,
  1.1915 +  _activeGroupItem: null,
  1.1916 +  _cleanupFunctions: [],
  1.1917 +  _arrangePaused: false,
  1.1918 +  _arrangesPending: [],
  1.1919 +  _removingHiddenGroups: false,
  1.1920 +  _delayedModUpdates: [],
  1.1921 +  _autoclosePaused: false,
  1.1922 +  minGroupHeight: 110,
  1.1923 +  minGroupWidth: 125,
  1.1924 +  _lastActiveList: null,
  1.1925 +
  1.1926 +  // ----------
  1.1927 +  // Function: toString
  1.1928 +  // Prints [GroupItems] for debug use
  1.1929 +  toString: function GroupItems_toString() {
  1.1930 +    return "[GroupItems count=" + this.groupItems.length + "]";
  1.1931 +  },
  1.1932 +
  1.1933 +  // ----------
  1.1934 +  // Function: init
  1.1935 +  init: function GroupItems_init() {
  1.1936 +    let self = this;
  1.1937 +
  1.1938 +    // setup attr modified handler, and prepare for its uninit
  1.1939 +    function handleAttrModified(event) {
  1.1940 +      self._handleAttrModified(event.target);
  1.1941 +    }
  1.1942 +
  1.1943 +    // make sure any closed tabs are removed from the delay update list
  1.1944 +    function handleClose(event) {
  1.1945 +      let idx = self._delayedModUpdates.indexOf(event.target);
  1.1946 +      if (idx != -1)
  1.1947 +        self._delayedModUpdates.splice(idx, 1);
  1.1948 +    }
  1.1949 +
  1.1950 +    this._lastActiveList = new MRUList();
  1.1951 +
  1.1952 +    AllTabs.register("attrModified", handleAttrModified);
  1.1953 +    AllTabs.register("close", handleClose);
  1.1954 +    this._cleanupFunctions.push(function() {
  1.1955 +      AllTabs.unregister("attrModified", handleAttrModified);
  1.1956 +      AllTabs.unregister("close", handleClose);
  1.1957 +    });
  1.1958 +  },
  1.1959 +
  1.1960 +  // ----------
  1.1961 +  // Function: uninit
  1.1962 +  uninit: function GroupItems_uninit() {
  1.1963 +    // call our cleanup functions
  1.1964 +    this._cleanupFunctions.forEach(function(func) {
  1.1965 +      func();
  1.1966 +    });
  1.1967 +
  1.1968 +    this._cleanupFunctions = [];
  1.1969 +
  1.1970 +    // additional clean up
  1.1971 +    this.groupItems = null;
  1.1972 +  },
  1.1973 +
  1.1974 +  // ----------
  1.1975 +  // Function: newGroup
  1.1976 +  // Creates a new empty group.
  1.1977 +  newGroup: function GroupItems_newGroup() {
  1.1978 +    let bounds = new Rect(20, 20, 250, 200);
  1.1979 +    return new GroupItem([], {bounds: bounds, immediately: true});
  1.1980 +  },
  1.1981 +
  1.1982 +  // ----------
  1.1983 +  // Function: pauseArrange
  1.1984 +  // Bypass arrange() calls and collect for resolution in
  1.1985 +  // resumeArrange()
  1.1986 +  pauseArrange: function GroupItems_pauseArrange() {
  1.1987 +    Utils.assert(this._arrangePaused == false, 
  1.1988 +      "pauseArrange has been called while already paused");
  1.1989 +    Utils.assert(this._arrangesPending.length == 0, 
  1.1990 +      "There are bypassed arrange() calls that haven't been resolved");
  1.1991 +    this._arrangePaused = true;
  1.1992 +  },
  1.1993 +
  1.1994 +  // ----------
  1.1995 +  // Function: pushArrange
  1.1996 +  // Push an arrange() call and its arguments onto an array
  1.1997 +  // to be resolved in resumeArrange()
  1.1998 +  pushArrange: function GroupItems_pushArrange(groupItem, options) {
  1.1999 +    Utils.assert(this._arrangePaused, 
  1.2000 +      "Ensure pushArrange() called while arrange()s aren't paused"); 
  1.2001 +    let i;
  1.2002 +    for (i = 0; i < this._arrangesPending.length; i++)
  1.2003 +      if (this._arrangesPending[i].groupItem === groupItem)
  1.2004 +        break;
  1.2005 +    let arrangeInfo = {
  1.2006 +      groupItem: groupItem,
  1.2007 +      options: options
  1.2008 +    };
  1.2009 +    if (i < this._arrangesPending.length)
  1.2010 +      this._arrangesPending[i] = arrangeInfo;
  1.2011 +    else
  1.2012 +      this._arrangesPending.push(arrangeInfo);
  1.2013 +  },
  1.2014 +
  1.2015 +  // ----------
  1.2016 +  // Function: resumeArrange
  1.2017 +  // Resolve bypassed and collected arrange() calls
  1.2018 +  resumeArrange: function GroupItems_resumeArrange() {
  1.2019 +    this._arrangePaused = false;
  1.2020 +    for (let i = 0; i < this._arrangesPending.length; i++) {
  1.2021 +      let g = this._arrangesPending[i];
  1.2022 +      g.groupItem.arrange(g.options);
  1.2023 +    }
  1.2024 +    this._arrangesPending = [];
  1.2025 +  },
  1.2026 +
  1.2027 +  // ----------
  1.2028 +  // Function: _handleAttrModified
  1.2029 +  // watch for icon changes on app tabs
  1.2030 +  _handleAttrModified: function GroupItems__handleAttrModified(xulTab) {
  1.2031 +    if (!UI.isTabViewVisible()) {
  1.2032 +      if (this._delayedModUpdates.indexOf(xulTab) == -1) {
  1.2033 +        this._delayedModUpdates.push(xulTab);
  1.2034 +      }
  1.2035 +    } else
  1.2036 +      this._updateAppTabIcons(xulTab); 
  1.2037 +  },
  1.2038 +
  1.2039 +  // ----------
  1.2040 +  // Function: flushTabUpdates
  1.2041 +  // Update apptab icons based on xulTabs which have been updated
  1.2042 +  // while the TabView hasn't been visible 
  1.2043 +  flushAppTabUpdates: function GroupItems_flushAppTabUpdates() {
  1.2044 +    let self = this;
  1.2045 +    this._delayedModUpdates.forEach(function(xulTab) {
  1.2046 +      self._updateAppTabIcons(xulTab);
  1.2047 +    });
  1.2048 +    this._delayedModUpdates = [];
  1.2049 +  },
  1.2050 +
  1.2051 +  // ----------
  1.2052 +  // Function: _updateAppTabIcons
  1.2053 +  // Update images of any apptab icons that point to passed in xultab 
  1.2054 +  _updateAppTabIcons: function GroupItems__updateAppTabIcons(xulTab) {
  1.2055 +    if (!xulTab.pinned)
  1.2056 +      return;
  1.2057 +
  1.2058 +    this.getAppTabFavIconUrl(xulTab, function(iconUrl) {
  1.2059 +      iQ(".appTabIcon").each(function GroupItems__updateAppTabIcons_forEach(icon) {
  1.2060 +         let $icon = iQ(icon);
  1.2061 +         if ($icon.data("xulTab") == xulTab && iconUrl != $icon.attr("src"))
  1.2062 +           $icon.attr("src", iconUrl);
  1.2063 +      });
  1.2064 +    });
  1.2065 +  },
  1.2066 +
  1.2067 +  // ----------
  1.2068 +  // Function: getAppTabFavIconUrl
  1.2069 +  // Gets the fav icon url for app tab.
  1.2070 +  getAppTabFavIconUrl: function GroupItems_getAppTabFavIconUrl(xulTab, callback) {
  1.2071 +    FavIcons.getFavIconUrlForTab(xulTab, function GroupItems_getAppTabFavIconUrl_getFavIconUrlForTab(iconUrl) {
  1.2072 +      callback(iconUrl || FavIcons.defaultFavicon);
  1.2073 +    });
  1.2074 +  },
  1.2075 +
  1.2076 +  // ----------
  1.2077 +  // Function: addAppTab
  1.2078 +  // Adds the given xul:tab to the app tab tray in all groups
  1.2079 +  addAppTab: function GroupItems_addAppTab(xulTab) {
  1.2080 +    this.groupItems.forEach(function(groupItem) {
  1.2081 +      groupItem.addAppTab(xulTab);
  1.2082 +    });
  1.2083 +    this.updateGroupCloseButtons();
  1.2084 +  },
  1.2085 +
  1.2086 +  // ----------
  1.2087 +  // Function: removeAppTab
  1.2088 +  // Removes the given xul:tab from the app tab tray in all groups
  1.2089 +  removeAppTab: function GroupItems_removeAppTab(xulTab) {
  1.2090 +    this.groupItems.forEach(function(groupItem) {
  1.2091 +      groupItem.removeAppTab(xulTab);
  1.2092 +    });
  1.2093 +    this.updateGroupCloseButtons();
  1.2094 +  },
  1.2095 +
  1.2096 +  // ----------
  1.2097 +  // Function: arrangeAppTab
  1.2098 +  // Arranges the given xul:tab as an app tab from app tab tray in all groups
  1.2099 +  arrangeAppTab: function GroupItems_arrangeAppTab(xulTab) {
  1.2100 +    this.groupItems.forEach(function(groupItem) {
  1.2101 +      groupItem.arrangeAppTab(xulTab);
  1.2102 +    });
  1.2103 +  },
  1.2104 +
  1.2105 +  // ----------
  1.2106 +  // Function: getNextID
  1.2107 +  // Returns the next unused groupItem ID.
  1.2108 +  getNextID: function GroupItems_getNextID() {
  1.2109 +    var result = this.nextID;
  1.2110 +    this.nextID++;
  1.2111 +    this._save();
  1.2112 +    return result;
  1.2113 +  },
  1.2114 +
  1.2115 +  // ----------
  1.2116 +  // Function: saveAll
  1.2117 +  // Saves GroupItems state, as well as the state of all of the groupItems.
  1.2118 +  saveAll: function GroupItems_saveAll() {
  1.2119 +    this._save();
  1.2120 +    this.groupItems.forEach(function(groupItem) {
  1.2121 +      groupItem.save();
  1.2122 +    });
  1.2123 +  },
  1.2124 +
  1.2125 +  // ----------
  1.2126 +  // Function: _save
  1.2127 +  // Saves GroupItems state.
  1.2128 +  _save: function GroupItems__save() {
  1.2129 +    if (!this._inited) // too soon to save now
  1.2130 +      return;
  1.2131 +
  1.2132 +    let activeGroupId = this._activeGroupItem ? this._activeGroupItem.id : null;
  1.2133 +    Storage.saveGroupItemsData(
  1.2134 +      gWindow,
  1.2135 +      { nextID: this.nextID, activeGroupId: activeGroupId,
  1.2136 +        totalNumber: this.groupItems.length });
  1.2137 +  },
  1.2138 +
  1.2139 +  // ----------
  1.2140 +  // Function: getBoundingBox
  1.2141 +  // Given an array of DOM elements, returns a <Rect> with (roughly) the union of their locations.
  1.2142 +  getBoundingBox: function GroupItems_getBoundingBox(els) {
  1.2143 +    var bounds = [iQ(el).bounds() for each (el in els)];
  1.2144 +    var left   = Math.min.apply({},[ b.left   for each (b in bounds) ]);
  1.2145 +    var top    = Math.min.apply({},[ b.top    for each (b in bounds) ]);
  1.2146 +    var right  = Math.max.apply({},[ b.right  for each (b in bounds) ]);
  1.2147 +    var bottom = Math.max.apply({},[ b.bottom for each (b in bounds) ]);
  1.2148 +
  1.2149 +    return new Rect(left, top, right-left, bottom-top);
  1.2150 +  },
  1.2151 +
  1.2152 +  // ----------
  1.2153 +  // Function: reconstitute
  1.2154 +  // Restores to stored state, creating groupItems as needed.
  1.2155 +  reconstitute: function GroupItems_reconstitute(groupItemsData, groupItemData) {
  1.2156 +    try {
  1.2157 +      let activeGroupId;
  1.2158 +
  1.2159 +      if (groupItemsData) {
  1.2160 +        if (groupItemsData.nextID)
  1.2161 +          this.nextID = Math.max(this.nextID, groupItemsData.nextID);
  1.2162 +        if (groupItemsData.activeGroupId)
  1.2163 +          activeGroupId = groupItemsData.activeGroupId;
  1.2164 +      }
  1.2165 +
  1.2166 +      if (groupItemData) {
  1.2167 +        var toClose = this.groupItems.concat();
  1.2168 +        for (var id in groupItemData) {
  1.2169 +          let data = groupItemData[id];
  1.2170 +          if (this.groupItemStorageSanity(data)) {
  1.2171 +            let groupItem = this.groupItem(data.id); 
  1.2172 +            if (groupItem && !groupItem.hidden) {
  1.2173 +              groupItem.userSize = data.userSize;
  1.2174 +              groupItem.setTitle(data.title);
  1.2175 +              groupItem.setBounds(data.bounds, true);
  1.2176 +              
  1.2177 +              let index = toClose.indexOf(groupItem);
  1.2178 +              if (index != -1)
  1.2179 +                toClose.splice(index, 1);
  1.2180 +            } else {
  1.2181 +              var options = {
  1.2182 +                dontPush: true,
  1.2183 +                immediately: true
  1.2184 +              };
  1.2185 +  
  1.2186 +              new GroupItem([], Utils.extend({}, data, options));
  1.2187 +            }
  1.2188 +          }
  1.2189 +        }
  1.2190 +
  1.2191 +        toClose.forEach(function(groupItem) {
  1.2192 +          // all tabs still existing in closed groups will be moved to new
  1.2193 +          // groups. prepare them to be reconnected later.
  1.2194 +          groupItem.getChildren().forEach(function (tabItem) {
  1.2195 +            if (tabItem.parent.hidden)
  1.2196 +              iQ(tabItem.container).show();
  1.2197 +
  1.2198 +            tabItem._reconnected = false;
  1.2199 +
  1.2200 +            // sanity check the tab's groupID
  1.2201 +            let tabData = Storage.getTabData(tabItem.tab);
  1.2202 +
  1.2203 +            if (tabData) {
  1.2204 +              let parentGroup = GroupItems.groupItem(tabData.groupID);
  1.2205 +
  1.2206 +              // the tab's group id could be invalid or point to a non-existing
  1.2207 +              // group. correct it by assigning the active group id or the first
  1.2208 +              // group of the just restored session.
  1.2209 +              if (!parentGroup || -1 < toClose.indexOf(parentGroup)) {
  1.2210 +                tabData.groupID = activeGroupId || Object.keys(groupItemData)[0];
  1.2211 +                Storage.saveTab(tabItem.tab, tabData);
  1.2212 +              }
  1.2213 +            }
  1.2214 +          });
  1.2215 +
  1.2216 +          // this closes the group but not its children
  1.2217 +          groupItem.close({immediately: true});
  1.2218 +        });
  1.2219 +      }
  1.2220 +
  1.2221 +      // set active group item
  1.2222 +      if (activeGroupId) {
  1.2223 +        let activeGroupItem = this.groupItem(activeGroupId);
  1.2224 +        if (activeGroupItem)
  1.2225 +          UI.setActive(activeGroupItem);
  1.2226 +      }
  1.2227 +
  1.2228 +      this._inited = true;
  1.2229 +      this._save(); // for nextID
  1.2230 +    } catch(e) {
  1.2231 +      Utils.log("error in recons: "+e);
  1.2232 +    }
  1.2233 +  },
  1.2234 +
  1.2235 +  // ----------
  1.2236 +  // Function: load
  1.2237 +  // Loads the storage data for groups. 
  1.2238 +  // Returns true if there was global group data.
  1.2239 +  load: function GroupItems_load() {
  1.2240 +    let groupItemsData = Storage.readGroupItemsData(gWindow);
  1.2241 +    let groupItemData = Storage.readGroupItemData(gWindow);
  1.2242 +    this.reconstitute(groupItemsData, groupItemData);
  1.2243 +    
  1.2244 +    return (groupItemsData && !Utils.isEmptyObject(groupItemsData));
  1.2245 +  },
  1.2246 +
  1.2247 +  // ----------
  1.2248 +  // Function: groupItemStorageSanity
  1.2249 +  // Given persistent storage data for a groupItem, returns true if it appears to not be damaged.
  1.2250 +  groupItemStorageSanity: function GroupItems_groupItemStorageSanity(groupItemData) {
  1.2251 +    let sane = true;
  1.2252 +    if (!groupItemData.bounds || !Utils.isRect(groupItemData.bounds)) {
  1.2253 +      Utils.log('GroupItems.groupItemStorageSanity: bad bounds', groupItemData.bounds);
  1.2254 +      sane = false;
  1.2255 +    } else if ((groupItemData.userSize && 
  1.2256 +               !Utils.isPoint(groupItemData.userSize)) ||
  1.2257 +               !groupItemData.id) {
  1.2258 +      sane = false;
  1.2259 +    }
  1.2260 +
  1.2261 +    return sane;
  1.2262 +  },
  1.2263 +
  1.2264 +  // ----------
  1.2265 +  // Function: register
  1.2266 +  // Adds the given <GroupItem> to the list of groupItems we're tracking.
  1.2267 +  register: function GroupItems_register(groupItem) {
  1.2268 +    Utils.assert(groupItem, 'groupItem');
  1.2269 +    Utils.assert(this.groupItems.indexOf(groupItem) == -1, 'only register once per groupItem');
  1.2270 +    this.groupItems.push(groupItem);
  1.2271 +    UI.updateTabButton();
  1.2272 +  },
  1.2273 +
  1.2274 +  // ----------
  1.2275 +  // Function: unregister
  1.2276 +  // Removes the given <GroupItem> from the list of groupItems we're tracking.
  1.2277 +  unregister: function GroupItems_unregister(groupItem) {
  1.2278 +    var index = this.groupItems.indexOf(groupItem);
  1.2279 +    if (index != -1)
  1.2280 +      this.groupItems.splice(index, 1);
  1.2281 +
  1.2282 +    if (groupItem == this._activeGroupItem)
  1.2283 +      this._activeGroupItem = null;
  1.2284 +
  1.2285 +    this._arrangesPending = this._arrangesPending.filter(function (pending) {
  1.2286 +      return groupItem != pending.groupItem;
  1.2287 +    });
  1.2288 +
  1.2289 +    this._lastActiveList.remove(groupItem);
  1.2290 +    UI.updateTabButton();
  1.2291 +  },
  1.2292 +
  1.2293 +  // ----------
  1.2294 +  // Function: groupItem
  1.2295 +  // Given some sort of identifier, returns the appropriate groupItem.
  1.2296 +  // Currently only supports groupItem ids.
  1.2297 +  groupItem: function GroupItems_groupItem(a) {
  1.2298 +    if (!this.groupItems) {
  1.2299 +      // uninit has been called
  1.2300 +      return null;
  1.2301 +    }
  1.2302 +    var result = null;
  1.2303 +    this.groupItems.forEach(function(candidate) {
  1.2304 +      if (candidate.id == a)
  1.2305 +        result = candidate;
  1.2306 +    });
  1.2307 +
  1.2308 +    return result;
  1.2309 +  },
  1.2310 +
  1.2311 +  // ----------
  1.2312 +  // Function: removeAll
  1.2313 +  // Removes all tabs from all groupItems (which automatically closes all unnamed groupItems).
  1.2314 +  removeAll: function GroupItems_removeAll() {
  1.2315 +    var toRemove = this.groupItems.concat();
  1.2316 +    toRemove.forEach(function(groupItem) {
  1.2317 +      groupItem.removeAll();
  1.2318 +    });
  1.2319 +  },
  1.2320 +
  1.2321 +  // ----------
  1.2322 +  // Function: newTab
  1.2323 +  // Given a <TabItem>, files it in the appropriate groupItem.
  1.2324 +  newTab: function GroupItems_newTab(tabItem, options) {
  1.2325 +    let activeGroupItem = this.getActiveGroupItem();
  1.2326 +
  1.2327 +    // 1. Active group
  1.2328 +    // 2. First visible non-app tab (that's not the tab in question)
  1.2329 +    // 3. First group
  1.2330 +    // 4. At this point there should be no groups or tabs (except for app tabs and the
  1.2331 +    // tab in question): make a new group
  1.2332 +
  1.2333 +    if (activeGroupItem && !activeGroupItem.hidden) {
  1.2334 +      activeGroupItem.add(tabItem, options);
  1.2335 +      return;
  1.2336 +    }
  1.2337 +
  1.2338 +    let targetGroupItem;
  1.2339 +    // find first non-app visible tab belongs a group, and add the new tabItem
  1.2340 +    // to that group
  1.2341 +    gBrowser.visibleTabs.some(function(tab) {
  1.2342 +      if (!tab.pinned && tab != tabItem.tab) {
  1.2343 +        if (tab._tabViewTabItem && tab._tabViewTabItem.parent &&
  1.2344 +            !tab._tabViewTabItem.parent.hidden) {
  1.2345 +          targetGroupItem = tab._tabViewTabItem.parent;
  1.2346 +        }
  1.2347 +        return true;
  1.2348 +      }
  1.2349 +      return false;
  1.2350 +    });
  1.2351 +
  1.2352 +    let visibleGroupItems;
  1.2353 +    if (targetGroupItem) {
  1.2354 +      // add the new tabItem to the first group item
  1.2355 +      targetGroupItem.add(tabItem);
  1.2356 +      UI.setActive(targetGroupItem);
  1.2357 +      return;
  1.2358 +    } else {
  1.2359 +      // find the first visible group item
  1.2360 +      visibleGroupItems = this.groupItems.filter(function(groupItem) {
  1.2361 +        return (!groupItem.hidden);
  1.2362 +      });
  1.2363 +      if (visibleGroupItems.length > 0) {
  1.2364 +        visibleGroupItems[0].add(tabItem);
  1.2365 +        UI.setActive(visibleGroupItems[0]);
  1.2366 +        return;
  1.2367 +      }
  1.2368 +    }
  1.2369 +
  1.2370 +    // create new group for the new tabItem
  1.2371 +    tabItem.setPosition(60, 60, true);
  1.2372 +    let newGroupItemBounds = tabItem.getBounds();
  1.2373 +
  1.2374 +    newGroupItemBounds.inset(-40,-40);
  1.2375 +    let newGroupItem = new GroupItem([tabItem], { bounds: newGroupItemBounds });
  1.2376 +    newGroupItem.snap();
  1.2377 +    UI.setActive(newGroupItem);
  1.2378 +  },
  1.2379 +
  1.2380 +  // ----------
  1.2381 +  // Function: getActiveGroupItem
  1.2382 +  // Returns the active groupItem. Active means its tabs are
  1.2383 +  // shown in the tab bar when not in the TabView interface.
  1.2384 +  getActiveGroupItem: function GroupItems_getActiveGroupItem() {
  1.2385 +    return this._activeGroupItem;
  1.2386 +  },
  1.2387 +
  1.2388 +  // ----------
  1.2389 +  // Function: setActiveGroupItem
  1.2390 +  // Sets the active groupItem, thereby showing only the relevant tabs and
  1.2391 +  // setting the groupItem which will receive new tabs.
  1.2392 +  //
  1.2393 +  // Paramaters:
  1.2394 +  //  groupItem - the active <GroupItem>
  1.2395 +  setActiveGroupItem: function GroupItems_setActiveGroupItem(groupItem) {
  1.2396 +    Utils.assert(groupItem, "groupItem must be given");
  1.2397 +
  1.2398 +    if (this._activeGroupItem)
  1.2399 +      iQ(this._activeGroupItem.container).removeClass('activeGroupItem');
  1.2400 +
  1.2401 +    iQ(groupItem.container).addClass('activeGroupItem');
  1.2402 +
  1.2403 +    this._lastActiveList.update(groupItem);
  1.2404 +    this._activeGroupItem = groupItem;
  1.2405 +    this._save();
  1.2406 +  },
  1.2407 +
  1.2408 +  // ----------
  1.2409 +  // Function: getLastActiveGroupItem
  1.2410 +  // Gets last active group item.
  1.2411 +  // Returns the <groupItem>. If nothing is found, return null.
  1.2412 +  getLastActiveGroupItem: function GroupItem_getLastActiveGroupItem() {
  1.2413 +    return this._lastActiveList.peek(function(groupItem) {
  1.2414 +      return (groupItem && !groupItem.hidden && groupItem.getChildren().length > 0)
  1.2415 +    });
  1.2416 +  },
  1.2417 +
  1.2418 +  // ----------
  1.2419 +  // Function: _updateTabBar
  1.2420 +  // Hides and shows tabs in the tab bar based on the active groupItem
  1.2421 +  _updateTabBar: function GroupItems__updateTabBar() {
  1.2422 +    if (!window.UI)
  1.2423 +      return; // called too soon
  1.2424 +
  1.2425 +    Utils.assert(this._activeGroupItem, "There must be something to show in the tab bar!");
  1.2426 +
  1.2427 +    let tabItems = this._activeGroupItem._children;
  1.2428 +    gBrowser.showOnlyTheseTabs(tabItems.map(function(item) item.tab));
  1.2429 +  },
  1.2430 +
  1.2431 +  // ----------
  1.2432 +  // Function: updateActiveGroupItemAndTabBar
  1.2433 +  // Sets active TabItem and GroupItem, and updates tab bar appropriately.
  1.2434 +  // Parameters:
  1.2435 +  // tabItem - the tab item
  1.2436 +  // options - is passed to UI.setActive() directly
  1.2437 +  updateActiveGroupItemAndTabBar: 
  1.2438 +    function GroupItems_updateActiveGroupItemAndTabBar(tabItem, options) {
  1.2439 +    Utils.assertThrow(tabItem && tabItem.isATabItem, "tabItem must be a TabItem");
  1.2440 +
  1.2441 +    UI.setActive(tabItem, options);
  1.2442 +    this._updateTabBar();
  1.2443 +  },
  1.2444 +
  1.2445 +  // ----------
  1.2446 +  // Function: getNextGroupItemTab
  1.2447 +  // Paramaters:
  1.2448 +  //  reverse - the boolean indicates the direction to look for the next groupItem.
  1.2449 +  // Returns the <tabItem>. If nothing is found, return null.
  1.2450 +  getNextGroupItemTab: function GroupItems_getNextGroupItemTab(reverse) {
  1.2451 +    var groupItems = Utils.copy(GroupItems.groupItems);
  1.2452 +    var activeGroupItem = GroupItems.getActiveGroupItem();
  1.2453 +    var tabItem = null;
  1.2454 +
  1.2455 +    if (reverse)
  1.2456 +      groupItems = groupItems.reverse();
  1.2457 +
  1.2458 +    if (!activeGroupItem) {
  1.2459 +      if (groupItems.length > 0) {
  1.2460 +        groupItems.some(function(groupItem) {
  1.2461 +          if (!groupItem.hidden) {
  1.2462 +            // restore the last active tab in the group
  1.2463 +            let activeTab = groupItem.getActiveTab();
  1.2464 +            if (activeTab) {
  1.2465 +              tabItem = activeTab;
  1.2466 +              return true;
  1.2467 +            }
  1.2468 +            // if no tab is active, use the first one
  1.2469 +            var child = groupItem.getChild(0);
  1.2470 +            if (child) {
  1.2471 +              tabItem = child;
  1.2472 +              return true;
  1.2473 +            }
  1.2474 +          }
  1.2475 +          return false;
  1.2476 +        });
  1.2477 +      }
  1.2478 +    } else {
  1.2479 +      var currentIndex;
  1.2480 +      groupItems.some(function(groupItem, index) {
  1.2481 +        if (!groupItem.hidden && groupItem == activeGroupItem) {
  1.2482 +          currentIndex = index;
  1.2483 +          return true;
  1.2484 +        }
  1.2485 +        return false;
  1.2486 +      });
  1.2487 +      var firstGroupItems = groupItems.slice(currentIndex + 1);
  1.2488 +      firstGroupItems.some(function(groupItem) {
  1.2489 +        if (!groupItem.hidden) {
  1.2490 +          // restore the last active tab in the group
  1.2491 +          let activeTab = groupItem.getActiveTab();
  1.2492 +          if (activeTab) {
  1.2493 +            tabItem = activeTab;
  1.2494 +            return true;
  1.2495 +          }
  1.2496 +          // if no tab is active, use the first one
  1.2497 +          var child = groupItem.getChild(0);
  1.2498 +          if (child) {
  1.2499 +            tabItem = child;
  1.2500 +            return true;
  1.2501 +          }
  1.2502 +        }
  1.2503 +        return false;
  1.2504 +      });
  1.2505 +      if (!tabItem) {
  1.2506 +        var secondGroupItems = groupItems.slice(0, currentIndex);
  1.2507 +        secondGroupItems.some(function(groupItem) {
  1.2508 +          if (!groupItem.hidden) {
  1.2509 +            // restore the last active tab in the group
  1.2510 +            let activeTab = groupItem.getActiveTab();
  1.2511 +            if (activeTab) {
  1.2512 +              tabItem = activeTab;
  1.2513 +              return true;
  1.2514 +            }
  1.2515 +            // if no tab is active, use the first one
  1.2516 +            var child = groupItem.getChild(0);
  1.2517 +            if (child) {
  1.2518 +              tabItem = child;
  1.2519 +              return true;
  1.2520 +            }
  1.2521 +          }
  1.2522 +          return false;
  1.2523 +        });
  1.2524 +      }
  1.2525 +    }
  1.2526 +    return tabItem;
  1.2527 +  },
  1.2528 +
  1.2529 +  // ----------
  1.2530 +  // Function: moveTabToGroupItem
  1.2531 +  // Used for the right click menu in the tab strip; moves the given tab
  1.2532 +  // into the given group. Does nothing if the tab is an app tab.
  1.2533 +  // Paramaters:
  1.2534 +  //  tab - the <xul:tab>.
  1.2535 +  //  groupItemId - the <groupItem>'s id.  If nothing, create a new <groupItem>.
  1.2536 +  moveTabToGroupItem : function GroupItems_moveTabToGroupItem(tab, groupItemId) {
  1.2537 +    if (tab.pinned)
  1.2538 +      return;
  1.2539 +
  1.2540 +    Utils.assertThrow(tab._tabViewTabItem, "tab must be linked to a TabItem");
  1.2541 +
  1.2542 +    // given tab is already contained in target group
  1.2543 +    if (tab._tabViewTabItem.parent && tab._tabViewTabItem.parent.id == groupItemId)
  1.2544 +      return;
  1.2545 +
  1.2546 +    let shouldUpdateTabBar = false;
  1.2547 +    let shouldShowTabView = false;
  1.2548 +    let groupItem;
  1.2549 +
  1.2550 +    // switch to the appropriate tab first.
  1.2551 +    if (tab.selected) {
  1.2552 +      if (gBrowser.visibleTabs.length > 1) {
  1.2553 +        gBrowser._blurTab(tab);
  1.2554 +        shouldUpdateTabBar = true;
  1.2555 +      } else {
  1.2556 +        shouldShowTabView = true;
  1.2557 +      }
  1.2558 +    } else {
  1.2559 +      shouldUpdateTabBar = true
  1.2560 +    }
  1.2561 +
  1.2562 +    // remove tab item from a groupItem
  1.2563 +    if (tab._tabViewTabItem.parent)
  1.2564 +      tab._tabViewTabItem.parent.remove(tab._tabViewTabItem);
  1.2565 +
  1.2566 +    // add tab item to a groupItem
  1.2567 +    if (groupItemId) {
  1.2568 +      groupItem = GroupItems.groupItem(groupItemId);
  1.2569 +      groupItem.add(tab._tabViewTabItem);
  1.2570 +      groupItem.reorderTabsBasedOnTabItemOrder()
  1.2571 +    } else {
  1.2572 +      let pageBounds = Items.getPageBounds();
  1.2573 +      pageBounds.inset(20, 20);
  1.2574 +
  1.2575 +      let box = new Rect(pageBounds);
  1.2576 +      box.width = 250;
  1.2577 +      box.height = 200;
  1.2578 +
  1.2579 +      new GroupItem([ tab._tabViewTabItem ], { bounds: box, immediately: true });
  1.2580 +    }
  1.2581 +
  1.2582 +    if (shouldUpdateTabBar)
  1.2583 +      this._updateTabBar();
  1.2584 +    else if (shouldShowTabView)
  1.2585 +      UI.showTabView();
  1.2586 +  },
  1.2587 +
  1.2588 +  // ----------
  1.2589 +  // Function: removeHiddenGroups
  1.2590 +  // Removes all hidden groups' data and its browser tabs.
  1.2591 +  removeHiddenGroups: function GroupItems_removeHiddenGroups() {
  1.2592 +    if (this._removingHiddenGroups)
  1.2593 +      return;
  1.2594 +    this._removingHiddenGroups = true;
  1.2595 +
  1.2596 +    let groupItems = this.groupItems.concat();
  1.2597 +    groupItems.forEach(function(groupItem) {
  1.2598 +      if (groupItem.hidden)
  1.2599 +        groupItem.closeHidden();
  1.2600 +     });
  1.2601 +
  1.2602 +    this._removingHiddenGroups = false;
  1.2603 +  },
  1.2604 +
  1.2605 +  // ----------
  1.2606 +  // Function: getUnclosableGroupItemId
  1.2607 +  // If there's only one (non-hidden) group, and there are app tabs present, 
  1.2608 +  // returns that group.
  1.2609 +  // Return the <GroupItem>'s Id
  1.2610 +  getUnclosableGroupItemId: function GroupItems_getUnclosableGroupItemId() {
  1.2611 +    let unclosableGroupItemId = null;
  1.2612 +
  1.2613 +    if (gBrowser._numPinnedTabs > 0) {
  1.2614 +      let hiddenGroupItems = 
  1.2615 +        this.groupItems.concat().filter(function(groupItem) {
  1.2616 +          return !groupItem.hidden;
  1.2617 +        });
  1.2618 +      if (hiddenGroupItems.length == 1)
  1.2619 +        unclosableGroupItemId = hiddenGroupItems[0].id;
  1.2620 +    }
  1.2621 +
  1.2622 +    return unclosableGroupItemId;
  1.2623 +  },
  1.2624 +
  1.2625 +  // ----------
  1.2626 +  // Function: updateGroupCloseButtons
  1.2627 +  // Updates group close buttons.
  1.2628 +  updateGroupCloseButtons: function GroupItems_updateGroupCloseButtons() {
  1.2629 +    let unclosableGroupItemId = this.getUnclosableGroupItemId();
  1.2630 +
  1.2631 +    if (unclosableGroupItemId) {
  1.2632 +      let groupItem = this.groupItem(unclosableGroupItemId);
  1.2633 +
  1.2634 +      if (groupItem) {
  1.2635 +        groupItem.$closeButton.hide();
  1.2636 +      }
  1.2637 +    } else {
  1.2638 +      this.groupItems.forEach(function(groupItem) {
  1.2639 +        groupItem.$closeButton.show();
  1.2640 +      });
  1.2641 +    }
  1.2642 +  },
  1.2643 +  
  1.2644 +  // ----------
  1.2645 +  // Function: calcValidSize
  1.2646 +  // Basic measure rules. Assures that item is a minimum size.
  1.2647 +  calcValidSize: function GroupItems_calcValidSize(size, options) {
  1.2648 +    Utils.assert(Utils.isPoint(size), 'input is a Point');
  1.2649 +    Utils.assert((size.x>0 || size.y>0) && (size.x!=0 && size.y!=0), 
  1.2650 +      "dimensions are valid:"+size.x+","+size.y);
  1.2651 +    return new Point(
  1.2652 +      Math.max(size.x, GroupItems.minGroupWidth),
  1.2653 +      Math.max(size.y, GroupItems.minGroupHeight));
  1.2654 +  },
  1.2655 +
  1.2656 +  // ----------
  1.2657 +  // Function: pauseAutoclose()
  1.2658 +  // Temporarily disable the behavior that closes groups when they become
  1.2659 +  // empty. This is used when entering private browsing, to avoid trashing the
  1.2660 +  // user's groups while private browsing is shuffling things around.
  1.2661 +  pauseAutoclose: function GroupItems_pauseAutoclose() {
  1.2662 +    this._autoclosePaused = true;
  1.2663 +  },
  1.2664 +
  1.2665 +  // ----------
  1.2666 +  // Function: unpauseAutoclose()
  1.2667 +  // Re-enables the auto-close behavior.
  1.2668 +  resumeAutoclose: function GroupItems_resumeAutoclose() {
  1.2669 +    this._autoclosePaused = false;
  1.2670 +  }
  1.2671 +};

mercurial