+ We're now storing data in a file named after your candy, in a folder named 'tabcandy' inside your profile folder (e.g. tabcandy/revision-a.json). Currently the only data that's round-tripped is whether you have hidden the tab bar, but we are saving group data out (just not reading it yet).
authorIan Gilman <iangilman@gmail.com>
Thu, 22 Apr 2010 17:19:42 -0700
changeset 49823 17c59ee826a8244bf2f047c0c97d151baccc1acc
parent 49822 7713c51e408403a693269c57e4295f2ae9992aaa
child 49824 b6e3818e5c7951accf36f1f972fd9e83f3e4a6fa
push idunknown
push userunknown
push dateunknown
+ We're now storing data in a file named after your candy, in a folder named 'tabcandy' inside your profile folder (e.g. tabcandy/revision-a.json). Currently the only data that's round-tripped is whether you have hidden the tab bar, but we are saving group data out (just not reading it yet). + The nav bar wasn't successfully hiding when first starting the browser; fixed.
browser/base/content/tabcandy/app/groups.js
browser/base/content/tabcandy/app/storage.js
browser/base/content/tabcandy/app/switch.js
browser/base/content/tabcandy/app/ui.js
--- a/browser/base/content/tabcandy/app/groups.js
+++ b/browser/base/content/tabcandy/app/groups.js
@@ -1,8 +1,9 @@
+// Title: groups.js (revision-a)
 (function(){
 
 var numCmp = function(a,b){ return a-b; }
 
 function min(list){ return list.slice().sort(numCmp)[0]; }
 function max(list){ return list.slice().sort(numCmp).reverse()[0]; }
 
 function isEventOverElement(event, el){
@@ -21,710 +22,694 @@ function isEventOverElement(event, el){
   }
   
   var hidden;
   [$(hidden).show() for([,hidden] in Iterator(hiddenEls))];
   return isOver;
 }
 
 // ##########
-function Group(){}
-Group.prototype = {
-  _children: [],
-  _container: null,
-  _padding: 30,
-  _isStacked: false,
-  _stackAngles: ["rotate(0deg)"],
-  _blockResize: false,
-  _arrangeSpeed: 170,
-  _shield: null,
+// Class: Group
+// A single group in the tab candy window. Descended from <Item>.
+// Note that it implements the <Subscribable> interface.
+window.Group = function(listOfEls, options) {
+  if(typeof(options) == 'undefined')
+    options = {};
+
+  this._children = []; // an array of Items
+  this.defaultSize = new Point(TabItems.tabWidth * 1.5, TabItems.tabHeight * 1.5);
+  this.locked = options.locked || false;
+  this.isAGroup = true;
+
+  var self = this;
+
+  var rectToBe = options.bounds;
+  if(!rectToBe) {
+    var boundingBox = this._getBoundingBox(listOfEls);
+    var padding = 30;
+    rectToBe = new Rect(
+      boundingBox.left-padding,
+      boundingBox.top-padding,
+      boundingBox.width+padding*2,
+      boundingBox.height+padding*2
+    );
+  }
+
+  var $container = options.container; 
+  if(!$container) {
+    $container = $('<div class="group" />')
+      .css({position: 'absolute'})
+      .css(rectToBe);
+  }
+  
+  $container
+    .css({zIndex: -100})
+    .appendTo("body")
+    .dequeue();
+    
+  // ___ Resizer
+  this.$resizer = $("<div class='resizer'/>")
+    .css({
+      position: "absolute",
+      width: 16, height: 16,
+      bottom: 0, right: 0,
+    })
+    .appendTo($container)
+    .hide();
+
+  // ___ Titlebar
+  var html = "<div class='titlebar'><input class='name' value='"
+      + (options.title || "")
+      + "'/><div class='close'></div></div>";
+       
+  this.$titlebar = $(html)        
+    .appendTo($container);
+    
+  this.$titlebar.css({
+      position: "absolute",
+    });
+    
+  var $close = $('.close', this.$titlebar).click(function() {
+    self.close();
+  });
+  
+  // ___ Title 
+  var titleUnfocus = function() {
+    if(!self.getTitle()) {
+      self.$title
+        .addClass("defaultName")
+        .val(self.defaultName);
+    } else {
+      self.$title.css({"background":"none"})
+        .animate({"paddingLeft":1, "easing":"linear"}, 340);
+    }
+  };
+  
+  var handleKeyPress = function(e){
+    if( e.which == 13 ) { // return
+      self.$title.blur()
+        .addClass("transparentBorder")
+        .one("mouseout", function(){
+          self.$title.removeClass("transparentBorder");
+        });
+    }
+  }
+  
+  this.$title = $('.name', this.$titlebar)
+    .css({backgroundRepeat: 'no-repeat'})
+    .blur(titleUnfocus)
+    .focus(function() {
+      if(self.locked) {
+        self.$title.blur();
+        return;
+      }  
+      self.$title.select();
+      if(!self.getTitle()) {
+        self.$title
+          .removeClass("defaultName")
+          .val('');
+      }
+    })
+    .keydown(handleKeyPress);
+  
+  titleUnfocus();
+  
+  if(this.locked)
+    this.$title.addClass('name-locked');
+
+  // ___ Content
+  this.$content = $('<div class="group-content"/>')
+    .css({
+      left: 0,
+      top: this.$titlebar.height(),
+      position: 'absolute'
+    })
+    .appendTo($container);
+  
+  // ___ locking
+  if(this.locked) {
+    $container.css({cursor: 'default'});    
+    $close.hide();
+  }
+    
+  // ___ Superclass initialization
+  this._init($container.get(0));
+
+  if(this.$debug) 
+    this.$debug.css({zIndex: -1000});
+  
+  // ___ Children
+  $.each(listOfEls, function(index, el) {  
+    self.add(el, null, options);
+  });
+
+  // ___ Finish Up
+  this._addHandlers($container);
+  this.setResizable(true);
+  
+  Groups.register(this);
+  
+  this.setBounds(rectToBe);
+  
+  // ___ Push other objects away
+  if(!options.dontPush)
+    this.pushAway();   
+};
+
+// ----------
+window.Group.prototype = $.extend(new Item(), new Subscribable(), {
+  // ----------
+  defaultName: "name this group...",
+  
+  // ----------  
+  getStorageData: function() {
+    var data = {
+      bounds: this.getBounds(), 
+      locked: this.locked, 
+      title: this.getTitle()
+    };
+    
+    return data;
+  },
   
   // ----------
-  _randRotate: function(spread, index){
-    if( index >= this._stackAngles.length ){
-      var randAngle = parseInt( ((Math.random()+.6)/1.3)*spread-(spread/2) );
-      var retVal = "rotate(%deg)".replace(/%/, randAngle);
-      this._stackAngles.push(retVal);
-      Utils.log( this._stackAngles );
-      return retVal;          
-    }
-    else return this._stackAngles[index];
+  getTitle: function() {
+    var value = (this.$title ? this.$title.val() : '');
+    return (value == this.defaultName ? '' : value);
   },
-  
+
   // ----------  
-  _getBoundingBox: function(){
-    var els = this._children;
+  setValue: function(value) {
+    this.$title.val(value); 
+  },
+
+  // ----------  
+  _getBoundingBox: function(els) {
     var el;
     var boundingBox = {
       top:    min( [$(el).position().top  for([,el] in Iterator(els))] ),
       left:   min( [$(el).position().left for([,el] in Iterator(els))] ),
       bottom: max( [$(el).position().top  for([,el] in Iterator(els))] )  + $(els[0]).height(),
       right:  max( [$(el).position().left for([,el] in Iterator(els))] ) + $(els[0]).width(),
     };
     boundingBox.height = boundingBox.bottom - boundingBox.top;
     boundingBox.width  = boundingBox.right - boundingBox.left;
     return boundingBox;
   },
   
   // ----------  
-  _getContainerBox: function(){
-    var pos = $(this._container).position();
-    var w = $(this._container).width();
-    var h = $(this._container).height();
-    return {
-      top: pos.top,
-      left: pos.left,
-      bottom: pos.top + h,
-      right: pos.left + w,
-      height: h,
-      width: w
-    }
+  getContentBounds: function() {
+    var box = this.getBounds();
+    var titleHeight = this.$titlebar.height();
+    box.top += titleHeight;
+    box.height -= titleHeight;
+    return box;
+  },
+  
+  // ----------  
+  reloadBounds: function() {
+    var bb = Utils.getBounds(this.container);
+    
+    if(!this.bounds)
+      this.bounds = new Rect(0, 0, 0, 0);
+    
+    this.setBounds(bb, true);
   },
   
   // ----------  
-  create: function(listOfEls, options) {
-    var self = this;
-    this._children = $(listOfEls).toArray();
-
-    var boundingBox = this._getBoundingBox();
-    var padding = 30;
-    var container = $("<div class='group'/>")
-      .css({
-        position: "absolute",
-        top: boundingBox.top-padding,
-        left: boundingBox.left-padding,
-        width: boundingBox.width+padding*2,
-        height: boundingBox.height+padding*2,
-        zIndex: -100,
-        opacity: 0,
-      })
-      .data("group", this)
-      .appendTo("body")
-      .animate({opacity:1.0}).dequeue();
+  setBounds: function(rect, immediately) {
+    var titleHeight = this.$titlebar.height();
     
-/*
-    var contentEl = $('<div class="group-content"/>')
-      .appendTo(container);
-*/
-    
-    var resizer = $("<div class='resizer'/>")
-      .css({
-        position: "absolute",
-        width: 16, height: 16,
-        bottom: 0, right: 0,
-      }).appendTo(container);
+    // ___ Determine what has changed
+    var css = {};
+    var titlebarCSS = {};
+    var contentCSS = {};
 
+    if(rect.left != this.bounds.left)
+      css.left = rect.left;
+      
+    if(rect.top != this.bounds.top) 
+      css.top = rect.top;
+      
+    if(rect.width != this.bounds.width) {
+      css.width = rect.width;
+      titlebarCSS.width = rect.width;
+      contentCSS.width = rect.width;
+    }
+
+    if(rect.height != this.bounds.height) {
+      css.height = rect.height; 
+      contentCSS.height = rect.height - titleHeight; 
+    }
+      
+    if($.isEmptyObject(css))
+      return;
+      
+    var offset = new Point(rect.left - this.bounds.left, rect.top - this.bounds.top);
+    this.bounds = new Rect(rect);
 
-    this._container = container;
-    
-    this._addHandlers(container);
-    this._updateGroup();
+    // ___ Deal with children
+    if(this._children.length) {
+      if(css.width || css.height) {
+        this.arrange({animate: !immediately}); //(immediately ? 'sometimes' : true)});
+      } else if(css.left || css.top) {
+        $.each(this._children, function(index, child) {
+          var box = child.getBounds();
+          child.setPosition(box.left + offset.x, box.top + offset.y, immediately);
+        });
+      }
+    }
+          
+    // ___ Update our representation
+    if(immediately) {
+      $(this.container).css(css);
+      this.$titlebar.css(titlebarCSS);
+      this.$content.css(contentCSS);
+    } else {
+      TabMirror.pausePainting();
+      $(this.container).animate(css, {complete: function() {
+        TabMirror.resumePainting();
+      }}).dequeue();
+      
+      this.$titlebar.animate(titlebarCSS).dequeue();        
+      this.$content.animate(contentCSS).dequeue();        
+    }
 
-    var els = this._children;
-    this._children = [];
-    for(var i in els){
-      this.add( els[i] );
-    }
-    
-    // ___ Push other objects away
-    if(!options || !options.suppressPush)
-      this.pushAway(); 
+    this._updateDebugBounds();
   },
   
-  // ----------  
-  pushAway: function() {
-  	return; //doesn't work in this version of groups.js
-    var buffer = 10;
-    
-    var items = Items.getTopLevelItems();
-    $.each(items, function(index, item) {
-      var data = {};
-      data.bounds = item.getBounds();
-      data.startBounds = new Rect(data.bounds);
-      item.pushAwayData = data;
+  // ----------
+  setZ: function(value) {
+    $(this.container).css({zIndex: value});
+
+    if(this.$debug) 
+      this.$debug.css({zIndex: value + 1});
+
+    $.each(this._children, function(index, child) {
+      child.setZ(value + 1);
     });
-    
-    var itemsToPush = [this];
-    this.pushAwayData.anchored = true;
-
-    var pushOne = function(baseItem) {
-      var bb = new Rect(baseItem.pushAwayData.bounds);
-      bb.inset(-buffer, -buffer);
-      var bbc = bb.center();
+  },
     
-      $.each(items, function(index, item) {
-        if(item == baseItem)
-          return;
-          
-        var data = item.pushAwayData;
-        if(data.anchored)
-          return;
-          
-        var bounds = data.bounds;
-        var box = new Rect(bounds);
-        box.inset(-buffer, -buffer);
-        if(box.intersects(bb)) {
-          var offset = new Point();
-          var center = box.center(); 
-          if(Math.abs(center.x - bbc.x) < Math.abs(center.y - bbc.y)) {
-            if(center.y > bbc.y)
-              offset.y = bb.bottom - box.top; 
-            else
-              offset.y = bb.top - box.bottom;
-          } else {
-            if(center.x > bbc.x)
-              offset.x = bb.right - box.left; 
-            else
-              offset.x = bb.left - box.right;
-          }
-          
-          bounds.offset(offset); 
-          itemsToPush.push(item);
-        }
-      });
-    };   
-    
-    var a;
-    for(a = 0; a < 500 && itemsToPush.length; a++)
-      pushOne(itemsToPush.shift());         
-
-    $.each(items, function(index, item) {
-      var data = item.pushAwayData;
-      if(!data.bounds.equals(data.startBounds))
-        item.setPosition(data.bounds.left, data.bounds.top);
+  // ----------  
+  close: function() {
+    var toClose = $.merge([], this._children);
+    $.each(toClose, function(index, child) {
+      child.close();
     });
   },
   
   // ----------  
-  getBounds: function() {
-    var bb = Utils.getBounds(this._container);
-    return bb;
-  },
-  
-  // ----------  
-  setPosition: function(left, top) {
-    var box = this.getBounds();
-    var offset = new Point(left - box.left, top - box.top);
+  add: function($el, dropPos, options) {
+    Utils.assert('add expects jQuery objects', Utils.isJQuery($el));
     
-    $.each(this._children, function(index, value) {
-      var $el = $(this);
-      box = Utils.getBounds(this);
-      $el.animate({left: box.left + offset.x, top: box.top + offset.y});
-    });
-        
-    var bb = Utils.getBounds(this._container);
-    $(this._container).animate({left: bb.left + offset.x, top: bb.top + offset.y});
-  },
-
-  // ----------  
-  add: function($el, dropPos){
-    Utils.assert('add expects jQuery objects', Utils.isJQuery($el));
-    var el = $el.get(0);
-    
-    if( typeof(dropPos) == "undefined" ) dropPos = {top:window.innerWidth, left:window.innerHeight};
+    if(!dropPos) 
+      dropPos = {top:window.innerWidth, left:window.innerHeight};
+      
+    if(typeof(options) == 'undefined')
+      options = {};
+      
     var self = this;
     
     // TODO: You should be allowed to drop in the white space at the bottom and have it go to the end
     // (right now it can match the thumbnail above it and go there)
     function findInsertionPoint(dropPos){
-      // When stacked, the new page always appears on top.
-      // Also, we use the self._isStacked = false to force an animation. Hacky!
-      if(self._isStacked){ self._isStacked = false; return 0; }
-      
-      var best = {dist: Infinity, el: null};
+      var best = {dist: Infinity, item: null};
       var index = 0;
+      var box;
       for each(var child in self._children){
-        var pos = $(child).position();
-        var [w, h] = [$(child).width(), $(child).height()];
-        var dist = Math.sqrt( Math.pow((pos.top+h/2)-dropPos.top,2) + Math.pow((pos.left+w/2)-dropPos.left,2) );
+        box = child.getBounds();
+        var dist = Math.sqrt( Math.pow((box.top+box.height/2)-dropPos.top,2) + Math.pow((box.left+box.width/2)-dropPos.left,2) );
         if( dist <= best.dist ){
-          best.el = child;
+          best.item = child;
           best.dist = dist;
           best.index = index;
         }
         index += 1;
       }
 
       if( self._children.length > 0 ){
-        var insertLeft = dropPos.left <= $(best.el).position().left + $(best.el).width()/2;
-        if( !insertLeft ) return best.index+1
-        else return best.index
+        box = best.item.getBounds();
+        var insertLeft = dropPos.left <= box.left + box.width/2;
+        if( !insertLeft ) 
+          return best.index+1;
+        else 
+          return best.index;
       }
-      return 0;
       
+      return 0;      
     }
     
-    var oldIndex = $.inArray(el, this._children);
+    var item = Items.item($el);
+    var oldIndex = $.inArray(item, this._children);
     if(oldIndex != -1)
       this._children.splice(oldIndex, 1); 
 
+    // Insert the tab into the right position.
     var index = findInsertionPoint(dropPos);
-    this._children.splice( index, 0, el );
-
-    $(el).droppable("disable");
+    this._children.splice( index, 0, item );
 
-    if( typeof(Tabs) != "undefined" ){
-      var tab = Tabs.tab(el);
-      tab.mirror.addOnClose(el, function() {
-        self.remove($el);
-      });      
-    }
-    
-    this._updateGroup();
-    var self = this;
-    setTimeout(function(){
-      self.arrange()
-    }, 0)
-  },
-  
-  // ----------  
-  remove: function(el){
-    var self = this;
-    $(el).data("toRemove", true);
-    this._children = this._children.filter(function(child){
-      if( $(child).data("toRemove") == true ){
-        $(child).data("group", null);
-        scaleTab( $(child), 160/$(child).width());
-        $(child).droppable("enable");    
-        if( typeof(Tabs) != "undefined" ){
-          var tab = Tabs.tab(child);
-          tab.mirror.removeOnClose(el);              
-        }
-        return false;
-      }
-      else return true;
+    $el.droppable("disable");
+    item.setZ(this.getZ() + 1);
+    item.groupData = {};
+
+    item.addOnClose(this, function() {
+      self.remove($el);
     });
     
-    $(el).data("toRemove", false);
-    
-    if( this._children.length == 0 ){
-      this._container.fadeOut(function() $(this).remove());
-    } else {
-      this.arrange();
-    }
+    $el.data("group", this)
+       .addClass("tabInGroup");
     
-  },
-  
-  // ----------
-  // TODO: could be a lot more efficient by unwrapping the remove routine
-  removeAll: function() {
-    var self = this;
-    $.each(this._children, function(index, child) {
-      self.remove(child);
-    });
-  },
-  
-  // ----------  
-  _updateGroup: function(){
-    var self = this;
-    this._children.forEach(function(el){
-      $(el).data("group", self);
-    });    
+    if(typeof(item.setResizable) == 'function')
+      item.setResizable(false);
+    
+    if(!options.dontArrange)
+      this.arrange();
   },
   
   // ----------  
-  arrange: function(options){
-    if( options && options.animate == false ) animate = false;
-    else animate = true;
-    
-    var bb = this._getContainerBox();      
-    if( bb.width > 200 ) this._stackGrid(animate);
-    else this._stackArrange();
+  // The argument a can be an Item, a DOM element, or a jQuery object
+  remove: function(a, options) {
+    var $el;  
+    var item;
+     
+    if(a.isAnItem) {
+      item = a;
+      $el = $(item.container);
+    } else {
+      $el = $(a);  
+      item = Items.item($el);
+    }
     
-  },
-  
-  _stackGrid: function (animate){    
-    var self = this;
-
-    if( self._isStacked ){
-      $(self._shield).remove();      
-      animate = true;
-      self._blockResize = true;
-      setTimeout(function(){
-        self._blockResize = false;
-      }, self._arrangeSpeed+25);
-    }
-            
-    var bb = self._getContainerBox();
-    var aTab = $(self._children[0]);
-
-    var count = self._children.length;
-    var bbAspect = bb.width/bb.height;
-    var tabAspect = 4/3; 
-
-    function howManyColumns( numRows, count ){ return Math.ceil(count/numRows) }
-
-    var count = self._children.length;
-    var best = {cols: 0, rows:0, area:0};
-    for(var numRows=1; numRows<=count; numRows++){
-      numCols = howManyColumns( numRows, count);
-      var w = numCols*tabAspect;
-      var h = numRows;
+    if(typeof(options) == 'undefined')
+      options = {};
+    
+    var index = $.inArray(item, this._children);
+    if(index != -1)
+      this._children.splice(index, 1); 
 
-      // We are width constrained
-      if( w/bb.width >= h/bb.height ) var scale = bb.width/w;
-      // We are height constrained
-      else var scale = bb.height/h;
-      var w = w*scale;
-      var h = h*scale;
-
-      if( w*h >= best.area ){
-        best.numRows = numRows;
-        best.numCols = numCols;
-        best.area = w*h;
-        best.w = w;
-        best.h = h;
-      }
-    }
+    $el.data("group", null).removeClass("tabInGroup");
+    item.setSize(item.defaultSize.x, item.defaultSize.y);
+    $el.droppable("enable");    
+    item.removeOnClose(this);
+    
+    if(typeof(item.setResizable) == 'function')
+      item.setResizable(true);
 
-    var padAmount = .1;
-    var pad = padAmount * (best.w/best.numCols);
-    var tabW = (best.w-pad)/best.numCols - pad;
-    var tabH = (best.h-pad)/best.numRows - pad;
-
-    var x = pad; var y=pad; var numInCol = 0;
-    for each(var tab in self._children){
-      var sizeOptions = {width:tabW, height:tabH, top:y+bb.top, left:x+bb.left};
-      $(tab).css({"-moz-transform":"rotate(0deg)"});
-
-      if( !self._blockResize ){
-        if( animate ) $(tab).animate(sizeOptions).dequeue();
-        else $(tab).css(sizeOptions);       
-      } else {
-        $(tab).animate(sizeOptions, self._arrangeSpeed).dequeue();
-      }
-
-      x += tabW + pad;
-      numInCol += 1;
-      if( numInCol >= best.numCols ) [x, numInCol, y] = [pad, 0, y+tabH+pad];
+    if(this._children.length == 0 && !this.locked){  
+      this._sendOnClose();
+      Groups.unregister(this);
+      $(this.container).fadeOut(function() {
+        $(this).remove();
+      });
+    } else if(!options.dontArrange) {
+      this.arrange();
     }
-    self._isStacked = false;       
   },
   
-  _stackArrange: function (){
+  // ----------
+  removeAll: function() {
     var self = this;
-    var aTab = $(self._children[0]);
-    zIndex = aTab.css("zIndex");
-    if( zIndex > 99999 ){ zIndex -= 99999; }
-    var bb = self._getContainerBox();  
-    
-    var w = bb.width*.75;
-    var h = w*.75;
-    
-    var i = 0;
-    self._children.forEach(function(){
-      var tab = self._children[i];
-     
-      // GRRRRRRRR! HATE. Why is it that this uses the same group
-      // for every group. Man do I hate scopping problems in JS.
-      // TODO: FIX THIS
-      $(tab).css({
-        "-moz-transform": self._randRotate(25, i),
-        zIndex: --zIndex
-      });
-   
-      var options = {
-        top: 1.2*bb.height/2-w/2 + bb.top,
-        left: .8*bb.width/2-h/2 + bb.left,
-        width: w,
-        height: h,
-      }
-      
-      if( !self._isStacked ){
-        self._blockResize = true;
-        $(tab).animate(options, self._arrangeSpeed).dequeue();
-        setTimeout(function(){ self._blockResize = false;}, self._arrangeSpeed+25);
-      }
-      else if(!self._blockResize) $(tab).css(options)
-      i++;
+    var toRemove = $.merge([], this._children);
+    $.each(toRemove, function(index, child) {
+      self.remove(child, {dontArrange: true});
     });
-    
-    self._positionStackHandler()
-        
-    self._isStacked = true;
   },
-  
-  _positionStackHandler: function(){
-    var self = this;
-    $(self._shield).remove();
-    var aTab = $(self._children[0]);
     
-    self._shield = $("<div id='shield'>").css({
-      "backgroundColor": "rgba(0,0,0,0)",
-      top: aTab.position().top,
-      left: aTab.position().left,
-      width: aTab.width(),
-      height: aTab.height(),
-      position: "absolute",
-      zIndex: 99999
-    }).appendTo("body").click(function(){
-      var [w,h] = [240,160];
-      var padding = 20;
-      var col = Math.ceil(Math.sqrt(self._children.length));
-      var row = Math.ceil(self._children.length/col);
-
-      var [overlayWidth, overlayHeight] = [w*col + padding*(col+1), h*row + padding*(row+1)];
-      var pos = $(this).position();
-      pos.left -= overlayWidth/3;
-      pos.top  -= overlayHeight/3;      
-            
-      if( pos.top < 0 )  pos.top = 20;
-      if( pos.left < 0 ) pos.left = 20;      
-      if( pos.top+overlayHeight > window.innerHeight ) pos.top = window.innerHeight-overlayHeight-20;
-      if( pos.left+overlayWidth > window.innerWidth )  pos.left = window.innerWidth-overlayWidth-20;
+  // ----------  
+  arrange: function(options) {
+    var bb = this.getContentBounds();
+    bb.inset(6, 6);
 
-      
-      $(this).animate({
-        width:  overlayWidth,
-        height: overlayHeight,
-        top: pos.top,
-        left: pos.left
-      },170).addClass("overlay");
-      
-      var [x,y] = [pos.left + padding, pos.top+padding];
-      var count = 1;
-      for each( var tab in self._children){
-        $(tab).css({"-moz-transform":"rotate(0deg)", zIndex:99999+1});
-        $(tab).animate({
-          top: y,
-          left: x,
-          width: w,
-          height: h
-        },170).dequeue();
-        
-        x += w+padding;
-        if( count >= col ){
-          count = 0;
-          x = pos.left+padding;
-          y += h + padding;
-        }
-        
-        count++;
-      }
-      
-      setTimeout(function(){
-        $(self._shield).mouseout(function(e){
-          zIndex = $(e.relatedTarget).css("zIndex");
-          if( zIndex == "auto" || parseInt(zIndex) <= 9999 ){
-            self._isStacked = false;
-            // TODO: reset the z-index
-            // This does one more arrange after all the animations are done.
-            // A hack, but it works, to get the shield position to be right.
-            self._stackArrange();
-            setTimeout(function(){self._stackArrange();},self._arrangeSpeed+10)
-          }
-        });
-      }, 100);
-      
-    })    
+    Items.arrange(this._children, bb, options);
   },
   
   // ----------  
   _addHandlers: function(container){
     var self = this;
     
-    $(container).draggable({
-      start: function(){
-        $(container).data("origPosition", $(container).position());
-        self._children.forEach(function(el){
-          $(el).data("origPosition", $(el).position());
-        });
-      },
-      drag: function(e, ui){
-        var origPos = $(container).data("origPosition");
-        dX = ui.offset.left - origPos.left;
-        dY = ui.offset.top - origPos.top;
-        $(self._children).each(function(){
-          $(this).css({
-            left: $(this).data("origPosition").left + dX,
-            top:  $(this).data("origPosition").top + dY
-          })
-        })
-        self._positionStackHandler();
-      }
-    });
-    
+    if(!this.locked) {
+      $(container).draggable({
+        scroll: false,
+        start: function(){
+          drag.info = new DragInfo(this);
+        },
+        drag: function(e, ui){
+          drag.info.drag(e, ui);
+        }, 
+        stop: function() {
+          drag.info.stop();
+          drag.info = null;
+        }
+      });
+    }
     
     $(container).droppable({
       over: function(){
-        $dragged.addClass("willGroup");
+        drag.info.$el.addClass("willGroup");
       },
       out: function(){
-        var $group = $dragged.data("group");
+        var $group = drag.info.$el.data("group");
         if($group)
-          $group.remove($dragged);
-        $dragged.removeClass("willGroup");
+          $group.remove(drag.info.$el);
+        drag.info.$el.removeClass("willGroup");
       },
       drop: function(event){
-        $dragged.removeClass("willGroup");
-        self.add( $dragged, {left:event.pageX, top:event.pageY} )
+        drag.info.$el.removeClass("willGroup");
+        self.add( drag.info.$el, {left:event.pageX, top:event.pageY} );
       },
-      accept: ".tab",
+      accept: ".tab", //".tab, .group",
     });
-        
-    $(container).resizable({
-      handles: "se",
-      aspectRatio: false,
-      resize: function(){
-        if( !self._blockResize )
-          self.arrange({animate: false});
-      },
-      stop: function(){
-        self.arrange();
-      } 
-    })
+  },
+
+  // ----------  
+  setResizable: function(value){
+    var self = this;
     
+    if(value) {
+      this.$resizer.fadeIn();
+      $(this.container).resizable({
+        handles: "se",
+        aspectRatio: false,
+        minWidth: 60,
+        minHeight: 90,
+        resize: function(){
+          self.reloadBounds();
+        },
+        stop: function(){
+          self.reloadBounds();
+          self.pushAway();
+        } 
+      });
+    } else {
+      this.$resizer.fadeOut();
+      $(this.container).resizable('disable');
     }
-}
+  }
+});
 
 // ##########
-var zIndex = 100;
-var $dragged = null;
-var timeout = null;
+var DragInfo = function(element) {
+  this.el = element;
+  this.$el = $(this.el);
+  this.item = Items.item(this.el);
+  this.parent = this.$el.data('group');
+  
+  this.$el.data('isDragging', true);
+  this.item.setZ(9999);
+};
+
+DragInfo.prototype = {
+  // ----------  
+  drag: function(e, ui) {
+    if(this.item.isAGroup) {
+      var bb = this.item.getBounds();
+      bb.left = ui.position.left;
+      bb.top = ui.position.top;
+      this.item.setBounds(bb, true);
+    } else
+      this.item.reloadBounds();
+  },
 
+  // ----------  
+  stop: function() {
+    this.$el.data('isDragging', false);    
+    
+    if(this.parent && !this.parent.locked && this.parent != this.$el.data('group') 
+        && this.parent._children.length <= 1) 
+      this.parent.remove(this.parent._children[0]);
+      
+    if(this.item && !this.$el.hasClass('willGroup') && !this.$el.data('group')) {
+      this.item.setZ(drag.zIndex);
+      drag.zIndex++;
+      
+      this.item.reloadBounds();
+      this.item.pushAway();
+    }
+  }
+};
+
+var drag = {
+  info: null,
+  zIndex: 100
+};
+
+// ##########
 window.Groups = {
-  Group: Group, 
+  groups: [],
   
   // ----------  
   dragOptions: {
-    start:function(){
-      $dragged = $(this);
-      $dragged.data('isDragging', true);
+    scroll: false,
+    start: function(e, ui) {
+      drag.info = new DragInfo(this);
     },
-    stop: function(){
-      $dragged.data('isDragging', false);
-      $(this).css({zIndex: zIndex});
-      $dragged = null;          
-      zIndex += 1;
+    drag: function(e, ui) {
+      drag.info.drag(e, ui);
     },
-    zIndex: 999,
+    stop: function() {
+      drag.info.stop();
+      drag.info = null;
+    }
   },
   
   // ----------  
   dropOptions: {
     accept: ".tab",
     tolerance: "pointer",
     greedy: true,
     drop: function(e){
-      $target = $(e.target);
-  
-      // Only drop onto the top z-index
-      if( $target.css("zIndex") < $dragged.data("topDropZIndex") ) return;
-      $dragged.data("topDropZIndex", $target.css("zIndex") );
-      $dragged.data("topDrop", $target);
+      $target = $(e.target);  
+      drag.info.$el.removeClass("willGroup")   
+      var phantom = $target.data("phantomGroup")
       
-      // This strange timeout thing solves the problem of when
-      // something is dropped onto multiple potential drop targets.
-      // We wait a little bit to see get all drops, and then we have saved
-      // the top-most one and drop onto that.
-      clearTimeout( timeout );
-      var dragged = $dragged;
-      var target = $target;
-      timeout = setTimeout( function(){
-        dragged.removeClass("willGroup")   
-  
-        dragged.animate({
-          top: target.position().top+15,
-          left: target.position().left+15,      
-        }, 100);
-        
-        setTimeout( function(){
-          var group = $(target).data("group");
-          if( group == null ){
-            var group = new Group();
-            group.create([target, dragged]);            
-          } else {
-            group.add( dragged );
-          }
-          
-        }, 100);
-        
-        
-      }, 10 );
-      
-      
+      var group = $target.data("group");
+      if( group == null ){
+        phantom.removeClass("phantom");
+        phantom.removeClass("group-content");
+        var group = new Group([$target, drag.info.$el], {container:phantom});
+      } else 
+        group.add( drag.info.$el );      
     },
     over: function(e){
-      $dragged.addClass("willGroup");
-      $dragged.data("topDropZIndex", 0);    
+      var $target = $(e.target);
+
+      function elToRect($el){
+       return new Rect( $el.position().left, $el.position().top, $el.width(), $el.height() );
+      }
+
+      var height = elToRect($target).height * 1.5 + 20;
+      var width = elToRect($target).width * 1.5 + 20;
+      var unionRect = elToRect($target).union( elToRect(drag.info.$el) );
+
+      var newLeft = unionRect.left + unionRect.width/2 - width/2;
+      var newTop = unionRect.top + unionRect.height/2 - height/2;
+
+      $(".phantom").remove();
+      var phantom = $("<div class='group phantom group-content'/>").css({
+        width: width,
+        height: height,
+        position:"absolute",
+        top: newTop,
+        left: newLeft,
+        zIndex: -99
+      }).appendTo("body").hide().fadeIn();
+      $target.data("phantomGroup", phantom);      
     },
-    out: function(){      
-      $dragged.removeClass("willGroup");
+    out: function(e){      
+      $(e.target).data("phantomGroup").fadeOut(function(){
+        $(this).remove();
+      });
     }
   }, 
   
+  // ----------
+  init: function() {
+    var self = this;
+    setTimeout(function() {
+      // we do this in a timeout, as window.innerHeight hasn't adjusted for Firebug initially
+      var pad = 5;
+      var sw = window.innerWidth;
+      var sh = window.innerHeight;
+      var w = sw - (pad * 2);
+      var h = TabItems.tabHeight;
+      var box = new Rect(pad, sh - (h + pad), w, h);
+      self.newTabGroup = new Group([], {bounds: box, title: 'New Tabs', locked: true}); 
+    }, 1000);
+  },
+
+  // ----------
+  getStorageData: function() {
+    var data = {groups: []};
+    $.each(this.groups, function(index, group) {
+      data.groups.push(group.getStorageData());
+    });
+    
+    return data;
+  },
+  
+  // ----------  
+  register: function(group) {
+    Utils.assert('only register once per group', $.inArray(group, this.groups) == -1);
+    this.groups.push(group);
+  },
+  
+  // ----------  
+  unregister: function(group) {
+    var index = $.inArray(group, this.groups);
+    if(index != -1)
+      this.groups.splice(index, 1);     
+  },
+  
   // ----------  
   arrange: function() {
-    var $groups = $('.group');
-    var count = $groups.length;
+    var count = this.groups.length;
     var columns = Math.ceil(Math.sqrt(count));
     var rows = ((columns * columns) - count >= columns ? columns - 1 : columns); 
     var padding = 12;
     var startX = padding;
-    var startY = 100;
+    var startY = Page.startY;
     var totalWidth = window.innerWidth - startX;
     var totalHeight = window.innerHeight - startY;
-    var w = (totalWidth / columns) - padding;
-    var h = (totalHeight / rows) - padding;
-    var x = startX;
-    var y = startY;
+    var box = new Rect(startX, startY, 
+        (totalWidth / columns) - padding,
+        (totalHeight / rows) - padding);
     
-    $groups.each(function(i) {
-      $(this).css({left: x, top: y, width: w, height: h});
-      
-      $(this).data('group').arrange();
+    $.each(this.groups, function(index, group) {
+      group.setBounds(box, true);
       
-      x += w + padding;
-      if(i % columns == columns - 1) {
-        x = startX;
-        y += h + padding;
+      box.left += box.width + padding;
+      if(index % columns == columns - 1) {
+        box.left = startX;
+        box.top += box.height + padding;
       }
     });
   },
   
   // ----------
   removeAll: function() {
-    var $groups = $('.group');
-    $groups.each(function() {
-      var group = $(this).data('group');
+    var toRemove = $.merge([], this.groups);
+    $.each(toRemove, function(index, group) {
       group.removeAll();
     });
+  },
+  
+  // ----------
+  newTab: function(tabItem) {
+    var groupTitle = 'New Tabs';
+    var array = jQuery.grep(this.groups, function(group) {
+      return group.getTitle() == groupTitle;
+    });
+    
+    var $el = $(tabItem.container);
+    if(array.length) 
+      array[0].add($el);
   }
 };
 
-// ##########
-window.Items = {
-  // ----------  
-  getTopLevelItems: function() {
-    var items = [];
-    
-    $('.tab').each(function() {
-      $this = $(this);
-      if(!$this.data('group'))
-        items.push($this.data('tabItem'));
-    });
-    
-    $('.group').each(function() {
-      items.push($(this).data('group'));
-    });
-    
-    return items;
-  }
-};
+// ----------
+Groups.init();
 
 // ##########
-function scaleTab( el, factor ){  
-  var $el = $(el);
-
-  $el.animate({
-    width: $el.width()*factor,
-    height: $el.height()*factor,
-    fontSize: parseInt($el.css("fontSize"))*factor,
-  },250).dequeue();
-}
-
-
 $(".tab").data('isDragging', false)
   .draggable(window.Groups.dragOptions)
   .droppable(window.Groups.dropOptions);
 
-
 })();
\ No newline at end of file
--- a/browser/base/content/tabcandy/app/storage.js
+++ b/browser/base/content/tabcandy/app/storage.js
@@ -1,14 +1,57 @@
 // ##########
 Storage = {
+  // ----------
   init: function() {
+  },
+  
+  // ----------
+  read: function() {
+    var data = {};
+    var file = this.getFile();
+    if(file.exists()) {
+      var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].  
+                              createInstance(Components.interfaces.nsIFileInputStream);  
+      var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].  
+                              createInstance(Components.interfaces.nsIConverterInputStream);  
+      fstream.init(file, -1, 0, 0);  
+      cstream.init(fstream, "UTF-8", 0, 0); // you can use another encoding here if you wish  
+      
+      let (str = {}) {  
+        cstream.readString(-1, str); // read the whole file and put it in str.value  
+        if(str.value)
+          data = JSON.parse(str.value);  
+      }  
+      cstream.close(); // this closes fstream  
+    }
+   
+    return data;
+  },
+  
+  // ----------
+  write: function(data) {
+    var file = this.getFile();
+    var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].  
+                             createInstance(Components.interfaces.nsIFileOutputStream);  
+    foStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);   
+    var str = JSON.stringify(data);
+    foStream.write(str, str.length);
+    foStream.close();
+  },
+  
+  // ----------  
+  getFile: function() {
     var file = Components.classes["@mozilla.org/file/directory_service;1"].  
       getService(Components.interfaces.nsIProperties).  
       get("ProfD", Components.interfaces.nsIFile);  
-
-/*     var dir = Utils.getInstallDirectory('tabcandy@aza.raskin');   */
-    Utils.log(file); 
+      
+    file.append('tabcandy');
+    if(!file.exists())
+      file.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0777);        
+      
+    file.append(Switch.name + '.json');
+    return file;
   }
 };
 
 Storage.init();
 
--- a/browser/base/content/tabcandy/app/switch.js
+++ b/browser/base/content/tabcandy/app/switch.js
@@ -1,40 +1,48 @@
 (function(){
 
-  window.Switch = {
-    insert: function(selector, style) {
-      var chunks = window.location.toString().split('/');
-      var myName = chunks[chunks.length - 2];
-      var html = '<div style="font-size: 11px; margin: 5px 0px 0px 5px;'
-        + style
-        + '">Candy: <select id="visualization-select">';
-        
-      var names = Utils.getVisualizationNames();
-      var count = names.length;
-      var a;
-      for(a = 0; a < count; a++) {
-        var name = names[a];
-        html += '<option value="'
-          + name
-          + '"'
-          + (name == myName ? ' selected="true"' : '')
-          + '>'
-          + name
-          + '</option>';
-      }
+// ##########
+window.Switch = {
+  // ----------
+  get name() {
+    var chunks = window.location.toString().split('/');
+    var myName = chunks[chunks.length - 2];
+    return myName;
+  },
+      
+  // ----------
+  insert: function(selector, style) {
+    var myName = this.name;
+    var html = '<div style="font-size: 11px; margin: 5px 0px 0px 5px;'
+      + style
+      + '">Candy: <select id="visualization-select">';
       
-      html += '<option disabled="disabled">----------</option>';
-      html += '<option value="">Home</option>';
+    var names = Utils.getVisualizationNames();
+    var count = names.length;
+    var a;
+    for(a = 0; a < count; a++) {
+      var name = names[a];
+      html += '<option value="'
+        + name
+        + '"'
+        + (name == myName ? ' selected="true"' : '')
+        + '>'
+        + name
+        + '</option>';
+    }
+    
+    html += '<option disabled="disabled">----------</option>';
+    html += '<option value="">Home</option>';
 
-      html += '</select>';
-      $(selector).prepend(html);
-      $('#visualization-select').change(function () {
-        var name = $(this).val();
-        if(name)
-          location.href = '../' + name + '/index.html';
-        else
-          location.href = '../../index.html';
-      });
-    }
-  };
+    html += '</select>';
+    $(selector).prepend(html);
+    $('#visualization-select').change(function () {
+      var name = $(this).val();
+      if(name)
+        location.href = '../' + name + '/index.html';
+      else
+        location.href = '../../index.html';
+    });
+  }
+};
 
 })();
--- a/browser/base/content/tabcandy/app/ui.js
+++ b/browser/base/content/tabcandy/app/ui.js
@@ -1,36 +1,73 @@
 (function(){
 
 // ##########
 Navbar = {
+  // ----------
   get el(){
     var win = Utils.activeWindow;
-    var navbar = win.gBrowser.ownerDocument.getElementById("navigator-toolbox");
-    return navbar;      
+    if(win) {
+      var navbar = win.gBrowser.ownerDocument.getElementById("navigator-toolbox");
+      return navbar;      
+    }
+
+    return null;
   },
-  show: function(){ this.el.collapsed = false; },
-  hide: function(){ this.el.collapsed = true;}
+
+  // ----------
+  show: function() {
+    var el = this.el;
+    if(el)
+      el.collapsed = false; 
+    else { // needs a little longer to get going
+      var self = this;
+      setTimeout(function() {
+        self.show();
+      }, 300); 
+    }
+  },
+
+  // ----------
+  hide: function() {
+    var el = this.el;
+    if(el)
+      el.collapsed = true; 
+    else { // needs a little longer to get going
+      var self = this;
+      setTimeout(function() {
+        self.hide();
+      }, 300); 
+    }
+  },
 }
 
 // ##########
 var Tabbar = {
+  // ----------
+  // Variable: _hidden
+  // We keep track of whether the tabs are hidden in this (internal) variable
+  // so we still have access to that information during the window's unload event,
+  // when window.Tabs no longer exists.
+  _hidden: false, 
   get el(){ return window.Tabs[0].raw.parentNode; },
   height: window.Tabs[0].raw.parentNode.getBoundingClientRect().height,
-  hide: function(){
+  hide: function() {
+    this._hidden = true;
     var self = this;
     $(self.el).animate({"marginTop":-self.height}, 150, function(){
       self.el.collapsed = true;
     });
   },
-  show: function(){
+  show: function() {
+    this._hidden = false;
     this.el.collapsed = false;
     $(this.el).animate({"marginTop":0}, 150);
   },
-  get isHidden(){ return this.el.collapsed; }
+  get isHidden(){ return this._hidden; }
 }
 
 // ##########
 window.Page = {
   startX: 30, 
   startY: 70,
     
   // ----------  
@@ -78,27 +115,16 @@ window.Page = {
         },250, '', function() {
           $tab.css("zIndex",z);
           $("body").css("overflow", overflow);
           TabMirror.resumePainting();
         });
       }
       lastTab = this;
     });
-    
-    $("#tabbar-toggle").click(function(){
-      if( Tabbar.isHidden ){
-        Tabbar.show();
-        $(this).removeClass("tabbar-off");        
-      }
-      else {
-        Tabbar.hide();
-        $(this).addClass("tabbar-off");        
-      }
-    });
   },
   
   // ----------  
   findOpenSpaceFor: function($div) {
     var w = window.innerWidth;
     var h = 0;
     var bufferX = 30;
     var bufferY = 30;
@@ -228,22 +254,59 @@ function UIClass(){
     else
       self.navBar.show();
   });
 
   Tabs.onOpen(function(a, b) {
     self.navBar.show();
   });
 
+  // ___ tab bar
+  this.$tabBarToggle = $("#tabbar-toggle")
+    .click(function() {
+      if(self.tabBar.isHidden)
+        self.showTabBar();
+      else
+        self.hideTabBar();
+    });
+
   // ___ Finish up
   Page.init();
+  
+  // ___ Storage
+  var data = Storage.read();
+  if(data.hideTabBar)
+    this.hideTabBar();
+  
+  $(window).unload(function() {
+    var data = {
+      dataVersion: 1,
+      hideTabBar: self.tabBar._hidden,
+      groups: Groups.getStorageData()
+    };
+    
+    Storage.write(data);
+  });
 };
 
 // ----------
 UIClass.prototype = {
+  // ----------
+  showTabBar: function() {
+    this.tabBar.show();
+    this.$tabBarToggle.removeClass("tabbar-off");        
+  },
+
+  // ----------
+  hideTabBar: function() {
+    this.tabBar.hide();
+    this.$tabBarToggle.addClass("tabbar-off");        
+  },
+
+  // ----------
   _addArrangements: function() {
     this.grid = new ArrangeClass("Grid", function(value) {
       if(typeof(Groups) != 'undefined')
         Groups.removeAll();
     
       var immediately = false;
       if(typeof(value) == 'boolean')
         immediately = value;