Bug 422845 - Replace rdf-driven addressbook directory tree with js one; Patch originally by Joey Minta, updated and completed by Mike Conley. r=Standard8
authorJoey Minta <jminta@gmail.com>
Fri, 06 May 2011 12:03:00 +0100
changeset 7710 d5ac0eb3914110891276c969d3a3d72bb7795c50
parent 7709 29fd2b00d1adb293280c31b691628d06d392de0b
child 7711 6e2ec369d646e8f700f6b3ab4059b91e82ef97ea
push idunknown
push userunknown
push dateunknown
reviewersStandard8
bugs422845
Bug 422845 - Replace rdf-driven addressbook directory tree with js one; Patch originally by Joey Minta, updated and completed by Mike Conley. r=Standard8
mail/components/addrbook/content/abCommon.js
mail/components/addrbook/content/abTrees.js
mail/components/addrbook/content/addressbook.js
mail/components/addrbook/content/addressbook.xul
mail/components/addrbook/jar.mn
mail/test/mozmill/addrbook/test-address-book.js
mail/test/mozmill/mozmilltests.list
mail/test/mozmill/shared-modules/test-address-book-helpers.js
mailnews/addrbook/content/abDragDrop.js
mailnews/base/content/jsTreeView.js
mailnews/jar.mn
--- a/mail/components/addrbook/content/abCommon.js
+++ b/mail/components/addrbook/content/abCommon.js
@@ -20,16 +20,17 @@
 #
 # Original Author:
 #   Paul Hangas <hangas@netscape.com>
 #
 # Contributor(s):
 #   Seth Spitzer <sspitzer@netscape.com>
 #   Mark Banner <mark@standard8.demon.co.uk>
 #   Simon Wilkinson <simon@sxw.org.uk>
+#   Mike Conley <mconley@mozilla.com>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either the GNU General Public License Version 2 or later (the "GPL"), or
 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -37,17 +38,17 @@
 # and other provisions required by the GPL or the LGPL. If you do not delete
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
 Components.utils.import("resource:///modules/mailServices.js");
 
-var dirTree = 0;
+var gDirTree = 0;
 var abList = 0;
 var gAbResultsTree = null;
 var gAbView = null;
 var gAddressBookBundle;
 
 var gPrefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
 var gHeaderParser = Components.classes["@mozilla.org/messenger/headerparser;1"].getService(Components.interfaces.nsIMsgHeaderParser);
 
@@ -80,17 +81,17 @@ var DirPaneController =
   },
 
   isCommandEnabled: function(command)
   {
     var selectedDir;
 
     switch (command) {
       case "cmd_selectAll":
-        // the dirTree pane
+        // the gDirTree pane
         // only handles single selection
         // so we forward select all to the results pane
         // but if there is no gAbView
         // don't bother sending to the results pane
         return (gAbView != null);
       case "cmd_delete":
       case "button_delete":
         var selectedDir = GetSelectedDirectory();
@@ -151,17 +152,17 @@ var DirPaneController =
     switch (command) {
       case "cmd_printcard":
       case "cmd_printcardpreview":
       case "cmd_selectAll":
         SendCommandToResultsPane(command);
         break;
       case "cmd_delete":
       case "button_delete":
-        if (dirTree)
+        if (gDirTree)
           AbDeleteSelectedDirectory();
         break;
       case "button_edit":
         AbEditSelectedDirectory();
         break;
       case "cmd_newlist":
         AbNewList();
         break;
@@ -198,17 +199,17 @@ function AbNewAddressBook()
   window.openDialog("chrome://messenger/content/addressbook/abAddressBookNameDialog.xul",
                     "",
                     "chrome,modal,resizable=no,centerscreen",
                     null);
 }
 
 function AbEditSelectedDirectory()
 {
-  if (dirTree.view.selection.count == 1) {
+  if (gDirTree.view.selection.count == 1) {
     var selecteduri = GetSelectedDirectory();
     var directory = GetDirectoryFromURI(selecteduri);
     if (directory.isMailList) {
       var dirUri = GetParentDirectoryFromMailingListURI(selecteduri);
       goEditListDialog(null, selecteduri);
     }
     else {
       window.openDialog(directory.propertiesChromeURI,
@@ -283,17 +284,17 @@ function GetParentRow(aTree, aRow)
       return row;
     parentLevel = aTree.view.getLevel(row);
   }
   return row;
 }
 
 function InitCommonJS()
 {
-  dirTree = document.getElementById("dirTree");
+  gDirTree = document.getElementById("dirTree");
   abList = document.getElementById("addressbookList");
   gAddressBookBundle = document.getElementById("bundle_addressBook");
 }
 
 function AbDelete()
 {
   var types = GetSelectedCardTypes();
   if (types == kNothingSelected)
@@ -391,19 +392,18 @@ function goToggleSplitter( id, elementID
 // Generate a list of cards from the selected mailing list
 // and get a comma separated list of card addresses. If the
 // item selected in the directory pane is not a mailing list,
 // an empty string is returned.
 function GetSelectedAddressesFromDirTree()
 {
   var addresses = "";
 
-  if (dirTree.currentIndex >= 0) {
-    var selectedResource = dirTree.builderView.getResourceAtIndex(dirTree.currentIndex);
-    var directory = GetDirectoryFromURI(selectedResource.Value);
+  if (gDirTree.currentIndex >= 0) {
+    var directory = gDirectoryTreeView.getDirectoryAtIndex(gDirTree.currentIndex);
     if (directory.isMailList) {
       var listCardsCount = directory.addressLists.length;
       var cards = new Array(listCardsCount);
       for (var i = 0; i < listCardsCount; ++i)
         cards[i] = directory.addressLists
                             .queryElementAt(i, Components.interfaces.nsIAbCard);
       addresses = GetAddressesForCards(cards);
     }
@@ -430,17 +430,17 @@ function GetAddressesForCards(cards)
     if (generatedAddress)
       addresses += "," + generatedAddress;
   }
   return addresses;
 }
 
 function SelectFirstAddressBook()
 {
-  dirTree.view.selection.select(0);
+  gDirTree.view.selection.select(0);
 
   ChangeDirectoryByURI(GetSelectedDirectory());
   gAbResultsTree.focus();
 }
 
 function DirPaneClick(event)
 {
   // we only care about left button events
@@ -455,32 +455,32 @@ function DirPaneClick(event)
 }
 
 function DirPaneDoubleClick(event)
 {
   // we only care about left button events
   if (event.button != 0)
     return;
 
-  var row = dirTree.treeBoxObject.getRowAt(event.clientX, event.clientY);
-  if (row == -1 || row > dirTree.view.rowCount-1) {
+  var row = gDirTree.treeBoxObject.getRowAt(event.clientX, event.clientY);
+  if (row == -1 || row > gDirTree.view.rowCount-1) {
     // double clicking on a non valid row should not open the dir properties dialog
     return;
   }
 
-  if (dirTree && dirTree.view.selection && dirTree.view.selection.count == 1)
+  if (gDirTree && gDirTree.view.selection && gDirTree.view.selection.count == 1)
     AbEditSelectedDirectory();
 }
 
 function DirPaneSelectionChange()
 {
   // clear out the search box when changing folders...
   onAbClearSearch();
-  if (dirTree && dirTree.view.selection && dirTree.view.selection.count == 1) {
-    gPreviousDirTreeIndex = dirTree.currentIndex;
+  if (gDirTree && gDirTree.view.selection && gDirTree.view.selection.count == 1) {
+    gPreviousDirTreeIndex = gDirTree.currentIndex;
     ChangeDirectoryByURI(GetSelectedDirectory());
   }
   goUpdateCommand('cmd_newlist');
 }
 
 function ChangeDirectoryByURI(uri)
 {
   if (!uri)
@@ -602,28 +602,27 @@ function GetParentDirectoryFromMailingLi
   }
 
   return null;
 }
 
 function DirPaneHasFocus()
 {
   // returns true if diectory pane has the focus. Returns false, otherwise.
-  return (top.document.commandDispatcher.focusedElement == dirTree)
+  return (top.document.commandDispatcher.focusedElement == gDirTree)
 }
 
 function GetSelectedDirectory()
 {
   if (abList)
     return abList.value;
   else {
-    if (dirTree.currentIndex < 0)
+    if (gDirTree.currentIndex < 0)
       return null;
-    var selected = dirTree.builderView.getResourceAtIndex(dirTree.currentIndex)
-    return selected.Value;
+    return gDirectoryTreeView.getDirectoryAtIndex(gDirTree.currentIndex).URI;
   }
 }
 
 function onAbClearSearch()
 {
   gSearchInput.value = "";
   onEnterInSearchBar();
 }
new file mode 100644
--- /dev/null
+++ b/mail/components/addrbook/content/abTrees.js
@@ -0,0 +1,321 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mail Addressbook code.
+ *
+ * The Initial Developer of the Original Code is
+ *   Joey Minta <jminta@gmail.com>
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This file contains our implementation for various addressbook trees.  It
+ * depends on jsTreeView.js being loaded before this script is loaded.
+ */
+
+Components.utils.import("resource:///modules/mailServices.js");
+
+/**
+ * Each abDirTreeItem corresponds to one row in the tree view.
+ */
+function abDirTreeItem(aDirectory) {
+  this._directory = aDirectory;
+}
+
+abDirTreeItem.prototype = {
+  getText: function atv_getText() {
+    return this._directory.dirName;
+  },
+
+  get id() {
+    return this._directory.URI;
+  },
+
+  _open: false,
+  get open() {
+    return this._open;
+  },
+
+  _level: 0,
+  get level() {
+    return this._level;
+  },
+
+  _children: null,
+  get children() {
+    if (!this._children) {
+      this._children = [];
+      const Ci = Components.interfaces;
+      var myEnum = this._directory.childNodes;
+      while (myEnum.hasMoreElements()) {
+        var abItem = new abDirTreeItem(myEnum.getNext()
+                                      .QueryInterface(Ci.nsIAbDirectory));
+        this._children.push(abItem);
+        this._children[this._children.length - 1]._level = this._level + 1;
+        this._children[this._children.length - 1]._parent = this;
+      }
+
+      // We sort children based on their names
+      function nameSort(a, b) {
+        return a._directory.dirName.localeCompare(b._directory.dirName);
+      }
+      this._children.sort(nameSort);
+    }
+    return this._children;
+  },
+
+  getProperties: function atv_getProps(aProps) {
+    var atomSvc = Components.classes["@mozilla.org/atom-service;1"]
+                            .getService(Components.interfaces.nsIAtomService);
+    if (this._directory.isMailList)
+      aProps.AppendElement(atomSvc.getAtom("IsMailList-true"));
+    if (this._directory.isRemote)
+      aProps.AppendElement(atomSvc.getAtom("IsRemote-true"));
+    if (this._directory.isSecure)
+      aProps.AppendElement(atomSvc.getAtom("IsSecure-true"));
+  }
+};
+
+/**
+ * Our actual implementation of nsITreeView.
+ */
+function directoryTreeView() {}
+directoryTreeView.prototype = {
+  __proto__: new PROTO_TREE_VIEW(),
+
+  init: function dtv_init(aTree, aJSONFile) {
+    const Cc = Components.classes;
+    const Ci = Components.interfaces;
+
+    if (aJSONFile) {
+      // Parse our persistent-open-state json file
+      let file = Cc["@mozilla.org/file/directory_service;1"]
+                    .getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
+      file.append(aJSONFile);
+
+      if (file.exists()) {
+        let data = "";
+        let fstream = Cc["@mozilla.org/network/file-input-stream;1"]
+                         .createInstance(Ci.nsIFileInputStream);
+        let sstream = Cc["@mozilla.org/scriptableinputstream;1"]
+                         .createInstance(Ci.nsIScriptableInputStream);
+        fstream.init(file, -1, 0, 0);
+        sstream.init(fstream);
+
+        while (sstream.available())
+          data += sstream.read(4096);
+
+        sstream.close();
+        fstream.close();
+        let JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
+        this._persistOpenMap = JSON.decode(data);
+      }
+    }
+
+    this._rebuild();
+    aTree.view = this;
+  },
+
+  shutdown: function dtv_shutdown(aJSONFile) {
+    const Cc = Components.classes;
+    const Ci = Components.interfaces;
+
+    // Write out the persistOpenMap to our JSON file
+    if (aJSONFile) {
+      // Write out our json file...
+      let JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
+      let data = JSON.encode(this._persistOpenMap);
+      let file = Cc["@mozilla.org/file/directory_service;1"]
+                 .getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
+      file.append(aJSONFile);
+      let foStream = Cc["@mozilla.org/network/file-output-stream;1"]
+                    .createInstance(Ci.nsIFileOutputStream);
+
+      foStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);
+      foStream.write(data, data.length);
+      foStream.close();
+    }
+  },
+
+  // Override the dnd methods for those functions in abDragDrop.js
+  canDrop: function dtv_canDrop(aIndex, aOrientation) {
+    return abDirTreeObserver.canDrop(aIndex, aOrientation);
+  },
+
+  drop: function dtv_drop(aRow, aOrientation) {
+    abDirTreeObserver.onDrop(aRow, aOrientation);
+  },
+
+  getDirectoryAtIndex: function dtv_getDirForIndex(aIndex) {
+    return this._rowMap[aIndex]._directory;
+  },
+
+  // Override jsTreeView's isContainer, since we want to be able
+  // to react to drag-drop events for all items in the directory
+  // tree.
+  isContainer: function dtv_isContainer(aIndex) {
+    return true;
+  },
+
+  /**
+   * NOTE: This function will result in indeterminate rows being selected.
+   *       Callers should take care to re-select a desired row after calling
+   *       this function.
+   */
+  _rebuild: function dtv__rebuild() {
+    var oldCount = this._rowMap.length;
+    this._rowMap = [];
+
+    const Cc = Components.classes;
+    const Ci = Components.interfaces;
+
+    var dirEnum = MailServices.ab.directories;
+
+    while (dirEnum.hasMoreElements()) {
+      this._rowMap.push(new abDirTreeItem(dirEnum.getNext().QueryInterface(Ci.nsIAbDirectory)));
+    }
+
+    // Sort our addressbooks now
+
+    const AB_ORDER = ["pab", "mork", "ldap", "mapi+other", "cab"];
+
+    function getDirectoryValue(aDir, aKey) {
+      if (aKey == "ab_type") {
+        if (aDir._directory.URI == kPersonalAddressbookURI)
+          return "pab";
+        if (aDir._directory.URI == kCollectedAddressbookURI)
+          return "cab";
+        if (aDir._directory instanceof Ci.nsIAbMDBDirectory)
+          return "mork";
+        if (aDir._directory instanceof Ci.nsIAbLDAPDirectory)
+          return "ldap";
+        return "mapi+other";
+      } else if (aKey == "ab_name") {
+        return aDir._directory.dirName;
+      }
+    }
+
+    function abNameCompare(a, b) {
+      return a.localeCompare(b);
+    }
+
+    function abTypeCompare(a, b) {
+      return (AB_ORDER.indexOf(a) - AB_ORDER.indexOf(b));
+    }
+
+    const SORT_PRIORITY = ["ab_type", "ab_name"];
+    const SORT_FUNCS = [abTypeCompare, abNameCompare];
+
+    function abSort(a, b) {
+      for (let i = 0; i < SORT_FUNCS.length; i++) {
+        let sortBy = SORT_PRIORITY[i];
+        let aValue = getDirectoryValue(a, sortBy);
+        let bValue = getDirectoryValue(b, sortBy);
+
+        if (!aValue && !bValue)
+          return 0;
+        if (!aValue)
+          return -1;
+        if (!bValue)
+          return 1;
+        if (aValue != bValue) {
+          let result = SORT_FUNCS[i](aValue, bValue);
+
+          if (result != 0)
+            return result;
+        }
+      }
+      return 0;
+    }
+
+    this._rowMap.sort(abSort);
+
+    if (this._tree)
+      this._tree.rowCountChanged(0, this._rowMap.length - oldCount);
+
+    this._restoreOpenStates();
+  },
+
+  // nsIAbListener interfaces
+  onItemAdded: function dtv_onItemAdded(aParent, aItem) {
+    if (!(aItem instanceof Components.interfaces.nsIAbDirectory))
+      return;
+    //xxx we can optimize this later
+    this._rebuild();
+
+    if (!this._tree)
+      return;
+
+    // Now select this new item
+    for (var [i, row] in Iterator(this._rowMap)) {
+      if (row.id == aItem.URI) {
+        this.selection.select(i);
+        break;
+      }
+    }
+  },
+
+  onItemRemoved: function dtv_onItemRemoved(aParent, aItem) {
+    if (!(aItem instanceof Components.interfaces.nsIAbDirectory))
+      return;
+    //xxx we can optimize this later
+    this._rebuild();
+
+    if (!this._tree)
+      return;
+
+    // If we're deleting a top-level address-book, just select the first book
+    if (aParent.URI == "moz-abdirectory://") {
+      this.selection.select(0);
+      return;
+    }
+
+    // Now select this parent item
+    for (var [i, row] in Iterator(this._rowMap)) {
+      if (row.id == aParent.URI) {
+        this.selection.select(i);
+        break;
+      }
+    }
+  },
+
+  onItemPropertyChanged: function dtv_onItemProp(aItem, aProp, aOld, aNew) {
+    if (!(aItem instanceof Components.interfaces.nsIAbDirectory))
+      return;
+
+    for (var i in this._rowMap)  {
+      if (this._rowMap[i]._directory == aItem) {
+        this._tree.invalidateRow(i);
+        break;
+      }
+    }
+  }
+};
+
+var gDirectoryTreeView = new directoryTreeView();
--- a/mail/components/addrbook/content/addressbook.js
+++ b/mail/components/addrbook/content/addressbook.js
@@ -20,16 +20,17 @@
 # the Initial Developer. All Rights Reserved.
 #
 # Original Author:
 #   Paul Hangas <hangas@netscape.com>
 #
 # Contributor(s):
 #   Seth Spitzer <sspitzer@netscape.com>
 #   Mark Banner <mark@standard8.demon.co.uk>
+#   Joey Minta <jminta@gmail.com>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either the GNU General Public License Version 2 or later (the "GPL"), or
 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -37,26 +38,27 @@
 # and other provisions required by the GPL or the LGPL. If you do not delete
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
 // Ensure the activity modules are loaded for this window.
 Components.utils.import("resource:///modules/activity/activityModules.js");
+Components.utils.import("resource:///modules/mailServices.js");
 
 const nsIAbListener = Components.interfaces.nsIAbListener;
 const kPrefMailAddrBookLastNameFirst = "mail.addr_book.lastnamefirst";
+const kPersistCollapseMapStorage = "directoryTree.json";
 
 var cvPrefs = 0;
 var gSearchTimer = null;
 var gStatusText = null;
 var gQueryURIFormat = null;
 var gSearchInput;
-var gDirTree;
 var gCardViewBox;
 var gCardViewBoxEmail1;
 var gPreviousDirTreeIndex = -1;
 
 var msgWindow = Components.classes["@mozilla.org/messenger/msgwindow;1"]
                           .createInstance(Components.interfaces.nsIMsgWindow);
 
 // Constants that correspond to choices
@@ -81,58 +83,61 @@ var gAddressBookAbListener = {
       // option is to select the first.
       if (gPreviousDirTreeIndex == -1) {
         SelectFirstAddressBook();
       }
       else {
         // Don't reselect if we already have a valid selection, this may be
         // the case if items are being removed via other methods, e.g. sidebar,
         // LDAP preference pane etc.
-        if (dirTree.currentIndex == -1) {
+        if (gDirTree.currentIndex == -1) {
           var directory = item.QueryInterface(Components.interfaces.nsIAbDirectory);
 
           // If we are a mail list, move the selection up the list before
           // trying to find the parent. This way we'll end up selecting the
           // parent address book when we remove a mailing list.
           //
           // For simple address books we don't need to move up the list, as
           // we want to select the next one upon removal.
           if (directory.isMailList && gPreviousDirTreeIndex > 0)
             --gPreviousDirTreeIndex;
 
           // Now get the parent of the row.
-          var newRow = dirTree.view.getParentIndex(gPreviousDirTreeIndex);
+          var newRow = gDirTree.view.getParentIndex(gPreviousDirTreeIndex);
 
           // if we have no parent (i.e. we are an address book), use the
           // previous index.
           if (newRow == -1)
             newRow = gPreviousDirTreeIndex;
 
           // Fall back to the first adddress book if we're not in a valid range
-          if (newRow >= dirTree.view.rowCount)
+          if (newRow >= gDirTree.view.rowCount)
             newRow = 0;
 
           // Now select the new item.
-          dirTree.view.selection.select(newRow);
+          gDirTree.view.selection.select(newRow);
         }
       }
     }
     catch (ex) {
     }
   },
   onItemPropertyChanged: function(item, property, oldValue, newValue) {
     // will not be called
   }
 };
 
 function OnUnloadAddressBook()
 {
-  Components.classes["@mozilla.org/abmanager;1"]
-            .getService(Components.interfaces.nsIAbManager)
-            .removeAddressBookListener(gAddressBookAbListener);
+  MailServices.ab.removeAddressBookListener(gAddressBookAbListener);
+  MailServices.ab.removeAddressBookListener(gDirectoryTreeView);
+
+  // Shutdown the tree view - this will also save the open/collapsed
+  // state of the tree view to a JSON file.
+  gDirectoryTreeView.shutdown(kPersistCollapseMapStorage);
 
   Components.classes["@mozilla.org/messenger/services/session;1"]
             .getService(Components.interfaces.nsIMsgMailSession)
             .RemoveMsgWindow(msgWindow);
 
   CloseAbView();
 }
 
@@ -183,37 +188,38 @@ function delayedOnLoadAddressBook()
 
   InitCommonJS();
 
   GetCurrentPrefs();
 
   // FIX ME - later we will be able to use onload from the overlay
   OnLoadCardView();
 
-  //workaround - add setTimeout to make sure dynamic overlays get loaded first
-  setTimeout('OnLoadDirTree()', 0);
+  // Initialize the Address Book tree view
+  gDirectoryTreeView.init(gDirTree,
+                          kPersistCollapseMapStorage);
+
+  SelectFirstAddressBook();
 
   // if the pref is locked disable the menuitem New->LDAP directory
   if (gPrefs.prefIsLocked("ldap_2.disable_button_add"))
     document.getElementById("addLDAP").setAttribute("disabled", "true");
 
   // Add a listener, so we can switch directories if the current directory is
   // deleted. This listener cares when a directory (= address book), or a
   // directory item is/are removed. In the case of directory items, we are
   // only really interested in mailing list changes and not cards but we have
   // to have both.
-  Components.classes["@mozilla.org/abmanager;1"]
-            .getService(Components.interfaces.nsIAbManager)
-            .addAddressBookListener(gAddressBookAbListener,
-                                    nsIAbListener.directoryRemoved |
-                                    nsIAbListener.directoryItemRemoved);
+  MailServices.ab.addAddressBookListener(gAddressBookAbListener,
+                                   nsIAbListener.directoryRemoved |
+                                   nsIAbListener.directoryItemRemoved);
+  MailServices.ab.addAddressBookListener(gDirectoryTreeView, nsIAbListener.all);
 
-  var dirTree = GetDirTree();
-  dirTree.addEventListener("click",DirPaneClick,true);
-  dirTree.controllers.appendController(DirPaneController);
+
+  gDirTree.controllers.appendController(DirPaneController);
 
   // initialize the customizeDone method on the customizeable toolbar
   var toolbox = document.getElementById("ab-toolbox");
   toolbox.customizeDone = function(aEvent) { MailToolboxCustomizeDone(aEvent, "CustomizeABToolbar"); };
 
   var toolbarset = document.getElementById('customToolbars');
   toolbox.toolbarset = toolbarset;
 
@@ -223,22 +229,16 @@ function delayedOnLoadAddressBook()
         .QueryInterface(Components.interfaces.nsIDocShell)
         .useErrorPages = false;
 
   Components.classes["@mozilla.org/messenger/services/session;1"]
             .getService(Components.interfaces.nsIMsgMailSession)
             .AddMsgWindow(msgWindow);
 }
 
-function OnLoadDirTree() {
-  var treeBuilder = dirTree.builder.QueryInterface(Components.interfaces.nsIXULTreeBuilder);
-  treeBuilder.addObserver(abDirTreeObserver);
-
-  SelectFirstAddressBook();
-}
 
 function GetCurrentPrefs()
 {
   // prefs
   if ( cvPrefs == 0 )
     cvPrefs = new Object;
 
   cvPrefs.prefs = gPrefs;
@@ -430,19 +430,17 @@ function AbPrintPreviewAddressBook()
 
 function AbExport()
 {
   try {
     var selectedABURI = GetSelectedDirectory();
     if (!selectedABURI) return;
 
     var directory = GetDirectoryFromURI(selectedABURI);
-    Components.classes["@mozilla.org/abmanager;1"]
-              .getService(Components.interfaces.nsIAbManager)
-              .exportAddressBook(window, directory);
+    MailServices.ab.exportAddressBook(window, directory);
   }
   catch (ex) {
     var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService);
 
     if (promptService) {
       var message;
       switch (ex.result) {
         case Components.results.NS_ERROR_FILE_ACCESS_DENIED:
@@ -735,19 +733,16 @@ function AbOSXAddressBookExists()
   catch (e) { }
 
   // Address book exists if the uri is correct and the position is not zero.
   return uriPresent && position != 0;
 }
 
 function AbShowHideOSXAddressBook()
 {
-  var abMgr = Components.classes["@mozilla.org/abmanager;1"] 
-                        .getService(Components.interfaces.nsIAbManager);
-
   if (AbOSXAddressBookExists())
-    abMgr.deleteAddressBook(kOSXDirectoryURI);
+    MailServices.ab.deleteAddressBook(kOSXDirectoryURI);
   else {
-    abMgr.newAddressBook(
+    MailServices.ab.newAddressBook(
       gAddressBookBundle.getString(kOSXPrefBase + ".description"),
       kOSXDirectoryURI, 3, kOSXPrefBase);
   }
 }
--- a/mail/components/addrbook/content/addressbook.xul
+++ b/mail/components/addrbook/content/addressbook.xul
@@ -66,16 +66,18 @@
     onunload="OnUnloadAddressBook()">
 
   <stringbundleset id="stringbundleset">
     <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/>
     <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
     <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
   </stringbundleset>
 
+<script type="application/javascript" src="chrome://messenger/content/jsTreeView.js"/>
+<script type="application/javascript" src="chrome://messenger/content/addressbook/abTrees.js"/>
 <script type="application/javascript" src="chrome://messenger/content/accountUtils.js"/>
 <script type="application/javascript" src="chrome://messenger/content/widgetglue.js"/>
 <script type="application/javascript" src="chrome://messenger/content/mailCore.js"/>
 <script type="application/javascript" src="chrome://messenger/content/addressbook/addressbook.js"/>
 <script type="application/javascript" src="chrome://messenger/content/addressbook/abCommon.js"/>
 <script type="application/javascript" src="chrome://communicator/content/contentAreaClick.js"/>
 <script type="application/javascript" src="chrome://global/content/printUtils.js"/>
 <script type="application/javascript" src="chrome://messenger/content/msgPrintEngine.js"/>
@@ -605,66 +607,28 @@
   <hbox id="abContent" flex="1">
     <vbox id="dirTreeBox" persist="width collapsed">
       <sidebarheader id="abDirTreeHeader" align="center">
         <label id="abDirTreeHeader-title" value="&dirTreeHeader.label;" control="dirTree"/>
       </sidebarheader>
 
       <!-- FIX ME - remove document.commandDispatcher.updateCommands() when tree selection calls this automatically -->
       <tree id="dirTree" class="abDirectory plain" seltype="single" minwidth="150" flex="1" persist="width"
-            datasources="rdf:addressdirectory" ref="moz-abdirectory://"
-            flags="dont-build-content"
             hidecolumnpicker="true"
             context="dirTreeContext"
             onselect="DirPaneSelectionChange(); document.commandDispatcher.updateCommands('addrbook-select');"
             ondblclick="DirPaneDoubleClick(event);"
+            onclick="DirPaneClick(event);"
             onblur="goOnEvent(this,'blur')">
- 
+
         <treecols>
-          <treecol id="DirCol" flex="1" primary="true" 
-                   crop="center" persist="width" ignoreincolumnpicker="true" hideheader="true"
-                   sort="?DirTreeNameSort" sortActive="true" sortDirection="ascending"/>
+          <treecol id="DirCol" flex="1" primary="true" hideheader="true"
+                   crop="center" persist="width" ignoreincolumnpicker="true"/>
         </treecols>
-     
-        <template>
-          <rule>
-            <conditions>
-              <content uri="?container"/> 
-              <member container="?container" child="?member"/>
-            </conditions>
- 
-            <bindings>
-              <binding subject="?member"
-                       predicate="http://home.netscape.com/NC-rdf#DirName"
-                       object="?DirName"/>
-              <binding subject="?member"
-                       predicate="http://home.netscape.com/NC-rdf#DirTreeNameSort"
-                       object="?DirTreeNameSort"/>
-              <binding subject="?member"
-                       predicate="http://home.netscape.com/NC-rdf#IsMailList"
-                       object="?IsMailList"/>
-              <binding subject="?member"
-                       predicate="http://home.netscape.com/NC-rdf#IsRemote"
-                       object="?IsRemote"/>
-              <binding subject="?member"
-                       predicate="http://home.netscape.com/NC-rdf#IsSecure"
-                       object="?IsSecure"/>
-            </bindings>
-
-            <action>
-              <treechildren>
-                <treeitem uri="?member" persist="sortDirection sortColumn open">
-                  <treerow>
-                    <treecell label="?DirName" properties="IsMailList-?IsMailList IsRemote-?IsRemote IsSecure-?IsSecure"/>
-                  </treerow>
-                </treeitem>
-              </treechildren>
-            </action>
-          </rule>
-        </template>
+        <treechildren/>
       </tree>
     </vbox>
 
     <splitter id="dirTree-splitter" collapse="before" persist="state"/> 
     
     <vbox flex="1" minwidth="100">
 
       <!-- results pane -->
--- a/mail/components/addrbook/jar.mn
+++ b/mail/components/addrbook/jar.mn
@@ -4,8 +4,9 @@ messenger.jar:
 *   content/messenger/addressbook/addressbook.xul               (content/addressbook.xul)
 *   content/messenger/addressbook/abCommon.js                   (content/abCommon.js)
 *   content/messenger/addressbook/abCardOverlay.js              (content/abCardOverlay.js)
     content/messenger/addressbook/abCardOverlay.xul             (content/abCardOverlay.xul)
 *   content/messenger/addressbook/abEditListDialog.xul          (content/abEditListDialog.xul)
 *   content/messenger/addressbook/abMailListDialog.xul          (content/abMailListDialog.xul)
 *   content/messenger/addressbook/abContactsPanel.xul           (content/abContactsPanel.xul)
 *   content/messenger/addressbook/abContactsPanel.js            (content/abContactsPanel.js)
+    content/messenger/addressbook/abTrees.js                    (content/abTrees.js)
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/addrbook/test-address-book.js
@@ -0,0 +1,156 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Mike Conley <mconley@mozillamessaging.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Tests for the address book.
+ */
+
+var MODULE_NAME = 'test-address-book';
+
+var RELATIVE_ROOT = '../shared-modules';
+var MODULE_REQUIRES = ['address-book-helpers', 'folder-display-helpers'];
+
+let abController = null;
+
+var addrBook1, addrBook2, addrBook3, addrBook4;
+var mListA, mListB, mListC, mListD, mListE;
+
+function setupModule(module)
+{
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+
+  let abh = collector.getModule('address-book-helpers');
+  abh.installInto(module);
+
+  // Open the address book main window
+  abController = open_address_book_window();
+
+  // Let's add some new address books.  I'll add them
+  // out of order to properly test the alphabetical
+  // ordering of the address books.
+  ldapBook = create_ldap_address_book("LDAP Book");
+  addrBook3 = create_mork_address_book("AB 3");
+  addrBook1 = create_mork_address_book("AB 1");
+  addrBook4 = create_mork_address_book("AB 4");
+  addrBook2 = create_mork_address_book("AB 2");
+
+  mListA = create_mailing_list("ML A");
+  addrBook1.addMailList(mListA);
+
+  mListB = create_mailing_list("ML B");
+  addrBook2.addMailList(mListB);
+
+  mListC = create_mailing_list("ML C");
+  addrBook3.addMailList(mListC);
+
+  mListD = create_mailing_list("ML D");
+  addrBook3.addMailList(mListD);
+}
+
+/* Test that the address book manager automatically sorts
+ * address books.
+ *
+ * Currently, we sort address books as follows:
+ * 1. Personal Address Book
+ * 2. Mork Address Books
+ * 3. LDAP / Other Address Books
+ * 4. Collected Address Book
+ *
+ * With the Personal and Collapsed address books existing
+ * automatically, our address books *should* be in this order:
+ *
+ * Personal Address Book
+ * AB 1
+ *    ML A
+ * AB 2
+ *    ML B
+ * AB 3
+ *    ML C
+ *    ML D
+ * AB 4
+ * LDAP Book
+ * Collected Address Book
+ **/
+function test_order_of_address_books()
+{
+  const EXPECTED_AB_ORDER = ["Personal Address Book", "AB 1", "AB 2",
+                             "AB 3", "AB 4", "LDAP Book",
+                             "Collected Addresses"];
+
+  for (let i = 0; i < EXPECTED_AB_ORDER.length; i++)
+  {
+    let abName = get_name_of_address_book_element_at(i);
+    assert_equals(abName, EXPECTED_AB_ORDER[i],
+                  "The address books are out of order.");
+  }
+}
+
+/* Test that the expanded and collapsed states of address books
+ * in the tree persist state when closing and re-opening the
+ * address book manager
+ */
+function test_persist_collapsed_and_expanded_states()
+{
+  // Set the state of address books 1 and 3 to expanded
+  set_address_books_expanded([addrBook1, addrBook3]);
+
+  // Set address book 2 to be collapsed
+  set_address_book_collapsed(addrBook2);
+
+  // Now close and re-open the address book
+  abController.window.close();
+  abController = open_address_book_window();
+
+  assert_true(is_address_book_collapsed(addrBook2));
+  assert_true(!is_address_book_collapsed(addrBook1));
+  assert_true(!is_address_book_collapsed(addrBook3));
+
+  // Now set the state of address books 1 and 3 to collapsed
+  // and make sure 2 is expanded
+  set_address_books_collapsed([addrBook1, addrBook3]);
+  set_address_book_expanded(addrBook2);
+
+  // Now close and re-open the address book
+  abController.window.close();
+  abController = open_address_book_window();
+
+  assert_true(!is_address_book_collapsed(addrBook2));
+  assert_true(is_address_book_collapsed(addrBook1));
+  assert_true(is_address_book_collapsed(addrBook3));
+}
+
--- a/mail/test/mozmill/mozmilltests.list
+++ b/mail/test/mozmill/mozmilltests.list
@@ -1,9 +1,10 @@
 account
+addrbook
 composition
 content-policy
 content-tabs
 cookies
 folder-display
 folder-pane
 folder-tree-modes
 folder-widget
--- a/mail/test/mozmill/shared-modules/test-address-book-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-address-book-helpers.js
@@ -36,68 +36,300 @@
  * ***** END LICENSE BLOCK ***** */
 
 var Ci = Components.interfaces;
 var Cc = Components.classes;
 var Cu = Components.utils;
 
 const MODULE_NAME = "address-book-helpers";
 const RELATIVE_ROOT = "../shared-modules";
+const MODULE_REQUIRES = ['window-helpers'];
+
+const ABMDB_PREFIX = "moz-abmdbdirectory://";
+const ABLDAP_PREFIX = "moz-abldapdirectory://";
+
+Cu.import("resource:///modules/mailServices.js");
+Cu.import("resource:///modules/Services.jsm");
 
 var collectedAddresses;
 
+var abController;
+
 function setupModule() {
-  let abManager = Cc["@mozilla.org/abmanager;1"].getService(Ci.nsIAbManager);
   // Ensure all the directories are initialised.
-  abManager.directories;
-  collectedAddresses = abManager.getDirectory("moz-abmdbdirectory://history.mab");
+  MailServices.ab.directories;
+  collectedAddresses = MailServices.ab
+                       .getDirectory("moz-abmdbdirectory://history.mab");
 }
 
 function installInto(module) {
   setupModule();
 
   // Now copy helper functions
   module.ensure_card_exists = ensure_card_exists;
   module.ensure_no_card_exists = ensure_no_card_exists;
+  module.open_address_book_window = open_address_book_window;
+  module.create_mork_address_book = create_mork_address_book;
+  module.create_ldap_address_book = create_ldap_address_book;
+  module.create_contact = create_contact;
+  module.create_mailing_list = create_mailing_list;
+  module.load_contacts_into_address_book = load_contacts_into_address_book;
+  module.load_contacts_into_mailing_list = load_contacts_into_mailing_list;
+  module.get_address_book_tree_view_index = get_address_book_tree_view_index;
+  module.set_address_books_collapsed = set_address_books_collapsed;
+  module.set_address_books_expanded = set_address_books_expanded;
+  // set_address_book_collapsed and set_address_book_expanded use
+  // the same code as set_address_books_expanded/collapsed, so I just
+  // alias them here.
+  module.set_address_book_collapsed = set_address_books_collapsed;
+  module.set_address_book_expanded = set_address_books_expanded;
+
+  module.is_address_book_collapsed = is_address_book_collapsed;
+  module.is_address_book_collapsible = is_address_book_collapsible;
+  module.get_name_of_address_book_element_at = get_name_of_address_book_element_at;
+  module.select_address_book = select_address_book;
 }
 
 /**
  * Make sure that there is a card for this email address
  * @param emailAddress the address that should have a card
  * @param displayName the display name the card should have
  * @param preferDisplayName |true| if the card display name should override the
  *                          header display name
  */
-function ensure_card_exists(emailAddress, displayName, preferDisplayName) {
+function ensure_card_exists(emailAddress, displayName, preferDisplayName)
+{
   ensure_no_card_exists(emailAddress);
-  let card = Cc["@mozilla.org/addressbook/cardproperty;1"]
-               .createInstance(Ci.nsIAbCard);
-
-  card.primaryEmail = emailAddress;
-  card.displayName = displayName;
-  card.setProperty("PreferDisplayName", preferDisplayName ? true : false);
+  let card = create_card(emailAddress, displayName, preferDisplayName);
   collectedAddresses.addCard(card);
 }
 
 /**
  * Make sure that there is no card for this email address
  * @param emailAddress the address that should have no cards
  */
 function ensure_no_card_exists(emailAddress)
 {
-  var books = Cc["@mozilla.org/abmanager;1"].getService(Ci.nsIAbManager)
-                .directories;
+  var books = MailServices.ab.directories;
 
   while (books.hasMoreElements()) {
     var ab = books.getNext().QueryInterface(Ci.nsIAbDirectory);
     try {
       var card = ab.cardForEmailAddress(emailAddress);
       if (card) {
         let cardArray = Cc["@mozilla.org/array;1"]
                           .createInstance(Ci.nsIMutableArray);
         cardArray.appendElement(card, false);
         ab.deleteCards(cardArray);
       }
     }
     catch (ex) { }
   }
 }
 
+/**
+ * Opens the address book interface
+ * @returns a controller for the address book
+ */
+function open_address_book_window()
+{
+  abController = mozmill.getAddrbkController();
+  return abController;
+}
+
+/**
+ * Creates and returns a Mork-backed address book.
+ * @param aName the name for the address book
+ * @returns the nsIAbDirectory address book
+ */
+function create_mork_address_book(aName)
+{
+  let abPrefString = MailServices.ab.newAddressBook(aName, "", 2);
+  let abURI = Services.prefs.getCharPref(abPrefString + ".filename");
+  return MailServices.ab.getDirectory(ABMDB_PREFIX + abURI);
+}
+
+/**
+ * Creates and returns an LDAP-backed address book.
+ * This function will automatically fill in a dummy
+ * LDAP URI if no URI is supplied.
+ * @param aName the name for the address book
+ * @param aURI an optional URI for the address book
+ * @returns the nsIAbDirectory address book
+ */
+function create_ldap_address_book(aName, aURI)
+{
+  if (!aURI)
+    aURI = "ldap://dummyldap/??sub?(objectclass=*)";
+  let abPrefString = MailServices.ab.newAddressBook(aName, aURI, 0);
+  return MailServices.ab.getDirectory(ABLDAP_PREFIX + abPrefString);
+}
+
+/**
+ * Creates and returns an address book contact
+ * @param aEmailAddress the e-mail address for this contact
+ * @param aDisplayName the display name for the contact
+ * @param aPreferDisplayName set to true if the card display name should
+ *                           override the header display name
+ */
+function create_contact(aEmailAddress, aDisplayName, aPreferDisplayName)
+{
+  let card = Cc["@mozilla.org/addressbook/cardproperty;1"]
+               .createInstance(Ci.nsIAbCard);
+  card.primaryEmail = aEmailAddress;
+  card.displayName = aDisplayName;
+  card.setProperty("PreferDisplayName", aPreferDisplayName ? true : false);
+  return card;
+}
+
+/* Creates and returns a mailing list
+ * @param aMailingListName the display name for the new mailing list
+ */
+function create_mailing_list(aMailingListName)
+{
+  var mailList = Cc["@mozilla.org/addressbook/directoryproperty;1"]
+                   .createInstance(Ci.nsIAbDirectory);
+  mailList.isMailList = true;
+  mailList.dirName = aMailingListName;
+  return mailList;
+}
+
+/* Given some address book, adds a collection of contacts to that
+ * address book.
+ * @param aAddressBook an address book to add the contacts to
+ * @param aContacts a collection of contacts, where each contact has
+ *                  members "email" and "displayName"
+ *
+ *                  Example:
+ *                  [{email: 'test@test.com', displayName: 'Sammy Jenkis'}]
+ */
+function load_contacts_into_address_book(aAddressBook, aContacts)
+{
+  for each (contact_info in aContacts) {
+    let contact = create_contact(contact_info.email,
+                                 contact_info.displayName, true);
+    aAddressBook.addCard(contact);
+  }
+}
+
+/* Given some mailing list, adds a collection of contacts to that
+ * mailing list.
+ * @param aMailingList a mailing list to add the contacts to
+ * @param aContacts a collection of contacts, where each contact has
+ *                  members "email" and "displayName"
+ *
+ *                  Example:
+ *                  [{email: 'test@test.com', displayName: 'Sammy Jenkis'}]
+ */
+function load_contacts_into_mailing_list(aMailingList, aContacts)
+{
+  for each (contact_info in aContacts) {
+    let contact = create_contact(contact_info.email,
+                                 contact_info.displayName, true);
+    aMailingList.addressLists.appendElement(contact, false);
+  }
+}
+
+/* Given some address book, return the row index for that address book
+ * in the tree view.  Throws an error if it cannot find the address book.
+ * @param aAddrBook an address book to search for
+ * @return the row index for that address book
+ */
+function get_address_book_tree_view_index(aAddrBook)
+{
+  let addrBooks = abController.window.gDirectoryTreeView._rowMap;
+  for (let i = 0; i < addrBooks.length; i++) {
+    if (addrBooks[i]._directory == aAddrBook) {
+      return i;
+    }
+  }
+  throw Error("Could not find the index for the address book named "
+              + aAddrbook.dirName);
+}
+
+/* Determines whether or not an address book is collapsed in
+ * the tree view.
+ * @param aAddrBook the address book to check
+ * @return true if the address book is collapsed, otherwise false
+ */
+function is_address_book_collapsed(aAddrbook)
+{
+  let aIndex = get_address_book_tree_view_index(aAddrbook);
+  return !abController.window.gDirectoryTreeView.isContainerOpen(aIndex);
+}
+
+/* Determines whether or not an address book is collapsible in
+ * the tree view.
+ * @param aAddrBook the address book to check
+ * @return true if the address book is collapsible, otherwise false
+ */
+function is_address_book_collapsible(aAddrbook)
+{
+  let aIndex = get_address_book_tree_view_index(aAddrbook);
+  return !abController.window.gDirectoryTreeView.isContainerEmpty(aIndex);
+}
+
+/* Sets one or more address books to the expanded state in the
+ * tree view.  If any of the address books cannot be expanded,
+ * an error is thrown.
+ * @param aAddrBooks either a lone address book, or an array of
+ *        address books
+ */
+function set_address_books_expanded(aAddrBooks)
+{
+  if (!Array.isArray(aAddrBooks))
+    aAddrBooks = [aAddrBooks];
+
+  for (let i = 0; i < aAddrBooks.length; i++)
+  {
+    let addrBook = aAddrBooks[i];
+    if (!is_address_book_collapsible(addrBook))
+      throw Error("Address book called " + addrBook.dirName
+                  + " cannot be expanded.");
+    if (is_address_book_collapsed(addrBook)) {
+      let aIndex = get_address_book_tree_view_index(addrBook);
+      abController.window.gDirectoryTreeView.toggleOpenState(aIndex);
+    }
+  }
+}
+
+/* Sets one or more address books to the collapsed state in the
+ * tree view.  If any of the address books cannot be collapsed,
+ * an error is thrown.
+ * @param aAddrBooks either a lone address book, or an array of
+ *        address books
+ */
+function set_address_books_collapsed(aAddrBooks)
+{
+  if (!Array.isArray(aAddrBooks))
+    aAddrBooks = [aAddrBooks];
+
+  for (let i = 0; i < aAddrBooks.length; i++)
+  {
+    let addrBook = aAddrBooks[i]
+    if (!is_address_book_collapsible(addrBook))
+      throw Error("Address book called " + addrBook.dirName
+                  + " cannot be collapsed.");
+    if (!is_address_book_collapsed(addrBook)) {
+      let aIndex = get_address_book_tree_view_index(addrBook);
+      abController.window.gDirectoryTreeView.toggleOpenState(aIndex);
+    }
+  }
+}
+
+/* Returns the displayed name of an address book in the tree view
+ * at a particular row index.
+ * @param aIndex the row index of the target address book
+ * @return the displayed name of the address book
+ */
+function get_name_of_address_book_element_at(aIndex)
+{
+  return abController.window.gDirectoryTreeView.getCellText(aIndex, 0);
+}
+
+/* Selects a given address book in the tree view.
+ * @param aAddrBook an address book to select
+ */
+function select_address_book(aAddrBook)
+{
+  let aIndex = get_address_book_tree_view_index(aAddrBook);
+  abController.window.gDirectoryTreeView.selection.select(aIndex);
+}
--- a/mailnews/addrbook/content/abDragDrop.js
+++ b/mailnews/addrbook/content/abDragDrop.js
@@ -17,16 +17,17 @@
  * The Initial Developer of the Original Code is
  * Netscape Communications Corporation.
  * Portions created by the Initial Developer are Copyright (C) 1998
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Seth Spitzer <sspitzer@netscape.com>
  *   Mark Banner <mark@standard8.demon.co.uk>
+ *   Mike Conley <mconley@mozillamessaging.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either of the GNU General Public License Version 2 or later (the "GPL"),
  * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -83,17 +84,18 @@ var abResultsPaneObserver = {
  
   getSupportedFlavours: function ()
 	{
      return null;
   }
 };
 
 
-var dragService = Components.classes["@mozilla.org/widget/dragservice;1"].getService().QueryInterface(Components.interfaces.nsIDragService);
+var dragService = Components.classes["@mozilla.org/widget/dragservice;1"]
+                            .getService().QueryInterface(Components.interfaces.nsIDragService);
 
 var abDirTreeObserver = {
   /**
    * canDrop - determine if the tree will accept the dropping of a item
    * onto it.
    *
    * Note 1: We don't allow duplicate mailing list names, therefore copy
    * is not allowed for mailing lists.
@@ -116,18 +118,17 @@ var abDirTreeObserver = {
    *   card in mailing list -> other address book  = MOVE or COPY
    *   read only directory item -> anywhere        = COPY only
    */
   canDrop: function(index, orientation)
   {
     if (orientation != Components.interfaces.nsITreeView.DROP_ON)
       return false;
 
-    var targetResource = dirTree.builderView.getResourceAtIndex(index);
-    var targetURI = targetResource.Value;
+    var targetURI = gDirectoryTreeView.getDirectoryAtIndex(index).URI;
 
     var srcURI = GetSelectedDirectory();
 
     // The same place case
     if (targetURI == srcURI)
       return false;
 
     // determine if we dragging from a mailing list on a directory x to the parent (directory x).
@@ -218,38 +219,36 @@ var abDirTreeObserver = {
     return true;
   },
 
   /**
    * onDrop - we don't need to check again for correctness as the
    * tree view calls canDrop just before calling onDrop.
    *
    */
-  onDrop: function(row, orientation)
+  onDrop: function(index, orientation)
   {
     var dragSession = dragService.getCurrentSession();
     if (!dragSession)
       return;
-      
+
     var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
     trans.addDataFlavor("moz/abcard");
 
-    var targetResource = dirTree.builderView.getResourceAtIndex(row);
-
-    var targetURI = targetResource.Value;
+    var targetURI = gDirectoryTreeView.getDirectoryAtIndex(index).URI;
     var srcURI = GetSelectedDirectory();
 
     for (var i = 0; i < dragSession.numDropItems; i++) {
       dragSession.getData(trans, i);
       var dataObj = new Object();
       var flavor = new Object();
       var len = new Object();
       try {
         trans.getAnyTransferData(flavor, dataObj, len);
-        dataObj = 
+        dataObj =
           dataObj.value.QueryInterface(Components.interfaces.nsISupportsString);
       }
       catch (ex) {
         continue;
       }
 
       var transData = dataObj.data.split("\n");
       var rows = transData[0].split(",");
new file mode 100644
--- /dev/null
+++ b/mailnews/base/content/jsTreeView.js
@@ -0,0 +1,265 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mail tree code.
+ *
+ * The Initial Developer of the Original Code is
+ *   Joey Minta <jminta@gmail.com>
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Mike Conley <mconley@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This file contains a prototype object designed to make the implementation of
+ * nsITreeViews in javascript simpler.  This object requires that consumers
+ * override the _rebuild function.  This function must set the _rowMap object to
+ * an array of objects fitting the following interface:
+ *
+ * readonly attribute string id - a unique identifier for the row/object
+ * readonly attribute integer level - the hierarchy level of the row
+ * attribute boolean open - whether or not this item's children are exposed
+ * string getText(aColName) - return the text to display for this row in the
+ *                            specified column
+ * void getProperties(aProps) - set the css-selectors on aProps when this is
+ *                              called
+ * attribute array children - return an array of child-objects also meeting this
+ *                            interface
+ */
+
+function PROTO_TREE_VIEW() {
+  this._tree = null;
+  this._rowMap = [];
+  this._persistOpenMap = [];
+}
+
+PROTO_TREE_VIEW.prototype = {
+  get rowCount() {
+    return this._rowMap.length;
+  },
+
+  /**
+   * CSS files will cue off of these.  Note that we reach into the rowMap's
+   * items so that custom data-displays can define their own properties
+   */
+  getCellProperties: function jstv_getCellProperties(aRow, aCol, aProps) {
+    this._rowMap[aRow].getProperties(aProps, aCol);
+  },
+
+  /**
+   * The actual text to display in the tree
+   */
+  getCellText: function jstv_getCellText(aRow, aCol) {
+    return this._rowMap[aRow].getText(aCol.id);
+  },
+
+  /**
+   * The jstv items take care of assigning this when building children lists
+   */
+  getLevel: function jstv_getLevel(aIndex) {
+    return this._rowMap[aIndex].level;
+  },
+
+  /**
+   * This is easy since the jstv items assigned the _parent property when making
+   * the child lists
+   */
+  getParentIndex: function jstv_getParentIndex(aIndex) {
+    for (let i = 0; i < this._rowMap.length; i++) {
+      if (this._rowMap[i] == this._rowMap[aIndex]._parent)
+        return i;
+    }
+    return -1;
+  },
+
+  /**
+   * This is duplicative for our normal jstv views, but custom data-displays may
+   * want to do something special here
+   */
+  getRowProperties: function jstv_getRowProperties(aIndex, aProps) {
+    this._rowMap[aIndex].getProperties(aProps);
+  },
+
+  /**
+   * If an item in our list has the same level and parent as us, it's a sibling
+   */
+  hasNextSibling: function jstv_hasNextSibling(aIndex, aNextIndex) {
+    let targetLevel = this._rowMap[aIndex].level;
+    for (let i = aNextIndex + 1; i < this._rowMap.length; i++) {
+      if (this._rowMap[i].level == targetLevel)
+        return true;
+      if (this._rowMap[i].level < targetLevel)
+        return false;
+    }
+    return false;
+  },
+
+  /**
+   * If we have a child-list with at least one element, we are a container.
+   */
+  isContainer: function jstv_isContainer(aIndex) {
+    return this._rowMap[aIndex].children.length > 0;
+  },
+
+  isContainerEmpty: function jstv_isContainerEmpty(aIndex) {
+    // If the container has no children, the container is empty.
+    return !this._rowMap[aIndex].children.length;
+  },
+
+  /**
+   * Just look at the jstv item here
+   */
+  isContainerOpen: function jstv_isContainerOpen(aIndex) {
+    return this._rowMap[aIndex].open;
+  },
+
+  isEditable: function jstv_isEditable(aRow, aCol) {
+    // We don't support editing rows in the tree yet.
+    return false;
+  },
+
+  isSeparator: function jstv_isSeparator(aIndex) {
+    // There are no separators in our trees
+    return false;
+  },
+
+  isSorted: function jstv_isSorted() {
+    // We do our own customized sorting
+    return false;
+  },
+
+  setTree: function jstv_setTree(aTree) {
+    this._tree = aTree;
+  },
+
+  /**
+   * Opens or closes a container with children.  The logic here is a bit hairy, so
+   * be very careful about changing anything.
+   */
+  toggleOpenState: function jstv_toggleOpenState(aIndex) {
+
+    // Ok, this is a bit tricky.
+    this._rowMap[aIndex]._open = !this._rowMap[aIndex].open;
+
+    if (!this._rowMap[aIndex].open) {
+      // We're closing the current container.  Remove the children
+
+      // Note that we can't simply splice out children.length, because some of
+      // them might have children too.  Find out how many items we're actually
+      // going to splice
+      let level = this._rowMap[aIndex].level;
+      let row = aIndex + 1;
+      while (row < this._rowMap.length && this._rowMap[row].level > level) {
+        row++;
+      }
+      let count = row - aIndex - 1;
+      this._rowMap.splice(aIndex + 1, count);
+
+      // Remove us from the persist map
+      let index = this._persistOpenMap.indexOf(this._rowMap[aIndex].id);
+      if (index != -1)
+        this._persistOpenMap.splice(index, 1);
+
+      // Notify the tree of changes
+      if (this._tree) {
+        this._tree.rowCountChanged(aIndex + 1, -count);
+      }
+    } else {
+      // We're opening the container.  Add the children to our map
+
+      // Note that these children may have been open when we were last closed,
+      // and if they are, we also have to add those grandchildren to the map
+      let tree = this;
+      let oldCount = this._rowMap.length;
+      function recursivelyAddToMap(aChild, aNewIndex) {
+        // When we add sub-children, we're going to need to increase our index
+        // for the next add item at our own level
+        let currentCount = tree._rowMap.length;
+        if (aChild.children.length && aChild.open) {
+          for (let [i, child] in Iterator(tree._rowMap[aNewIndex].children)) {
+            let index = aNewIndex + i + 1;
+            tree._rowMap.splice(index, 0, child);
+            aNewIndex += recursivelyAddToMap(child, index);
+          }
+        }
+        return tree._rowMap.length - currentCount;
+      }
+      recursivelyAddToMap(this._rowMap[aIndex], aIndex);
+
+      // Add this container to the persist map
+      let id = this._rowMap[aIndex].id;
+      if (this._persistOpenMap.indexOf(id) == -1)
+        this._persistOpenMap.push(id);
+
+      // Notify the tree of changes
+      if (this._tree)
+        this._tree.rowCountChanged(aIndex + 1, this._rowMap.length - oldCount);
+    }
+
+    // Invalidate the toggled row, so that the open/closed marker changes
+    if (this._tree)
+      this._tree.invalidateRow(aIndex);
+  },
+
+  // We don't implement any of these at the moment
+  canDrop: function jstv_canDrop(aIndex, aOrientation) {},
+  drop: function jstv_drop(aRow, aOrientation) {},
+  performAction: function jstv_performAction(aAction) {},
+  performActionOnCell: function jstv_performActionOnCell(aAction, aRow, aCol) {},
+  performActionOnRow: function jstv_performActionOnRow(aAction, aRow) {},
+  selectionChanged: function jstv_selectionChanged() {},
+  setCellText: function jstv_setCellText(aRow, aCol, aValue) {},
+  setCellValue: function jstv_setCellValue(aRow, aCol, aValue) {},
+  getCellValue: function jstv_getCellValue(aRow, aCol) {},
+  getColumnProperties: function jstv_getColumnProperties(aCol, aProps) {},
+  getImageSrc: function jstv_getImageSrc(aRow, aCol) {},
+  getProgressMode: function jstv_getProgressMode(aRow, aCol) {},
+  cycleCell: function jstv_cycleCell(aRow, aCol) {},
+  cycleHeader: function jstv_cycleHeader(aCol) {},
+
+  _tree: null,
+
+  /**
+   * An array of jstv items, where each item corresponds to a row in the tree
+   */
+  _rowMap: null,
+
+  /**
+   * This is a javascript map of which containers we had open, so that we can
+   * persist their state over-time.  It is designed to be used as a JSON object.
+   */
+  _persistOpenMap: null,
+
+  _restoreOpenStates: function jstv__restoreOpenStates() {
+    // Note that as we iterate through here, .length may grow
+    for (let i = 0; i < this._rowMap.length; i++) {
+      if (this._persistOpenMap.indexOf(this._rowMap[i].id) != -1)
+        this.toggleOpenState(i);
+    }
+  }
+};
--- a/mailnews/jar.mn
+++ b/mailnews/jar.mn
@@ -92,16 +92,17 @@ messenger.jar:
     content/messenger/virtualFolderProperties.js                               (base/content/virtualFolderProperties.js)
     content/messenger/virtualFolderListDialog.xul                              (base/content/virtualFolderListDialog.xul)
     content/messenger/virtualFolderListDialog.js                               (base/content/virtualFolderListDialog.js)
     content/messenger/msgPrintEngine.js                                        (base/content/msgPrintEngine.js)
 *   content/messenger/junkMailInfo.xul                                         (base/content/junkMailInfo.xul)
     content/messenger/junkCommands.js                                          (base/content/junkCommands.js)
     content/messenger/junkLog.xul                                              (base/content/junkLog.xul)
     content/messenger/junkLog.js                                               (base/content/junkLog.js)
+    content/messenger/jsTreeView.js                                            (base/content/jsTreeView.js)
     content/messenger/searchTermOverlay.js                                     (base/search/content/searchTermOverlay.js)
     content/messenger/searchTermOverlay.xul                                    (base/search/content/searchTermOverlay.xul)
     content/messenger/CustomHeaders.xul                                        (base/search/content/CustomHeaders.xul)
     content/messenger/CustomHeaders.js                                         (base/search/content/CustomHeaders.js)
     content/messenger/FilterEditor.xul                                         (base/search/content/FilterEditor.xul)
     content/messenger/FilterEditor.js                                          (base/search/content/FilterEditor.js)
 *   content/messenger/searchWidgets.xml                                        (base/search/content/searchWidgets.xml)
     content/messenger/viewLog.xul                                              (base/search/content/viewLog.xul)