Bug 1308776 - Add choosing of default addressbook from preferences dialog and a context menu. ui-r=Paenglab, r=mkmelin,jorgk,frgrahl a=jorgk
authoraceman <acelists@atlas.sk>
Wed, 23 Nov 2016 22:30:39 +0200
changeset 27243 68809f0c899a4bdd1dfeca59367b562759263ab2
parent 27242 0b370e34182fc18c65d098a50b35b76803d08203
child 27244 be757587e6f0e5b23f54e8a3582aa64a393148dd
push id1878
push userclokep@gmail.com
push dateTue, 07 Mar 2017 14:18:40 +0000
treeherdercomm-beta@ab59ffd05575 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersPaenglab, mkmelin, jorgk, frgrahl, jorgk
bugs1308776
Bug 1308776 - Add choosing of default addressbook from preferences dialog and a context menu. ui-r=Paenglab, r=mkmelin,jorgk,frgrahl a=jorgk
mail/components/addrbook/content/abCommon.js
mail/components/addrbook/content/addressbook.js
mail/components/addrbook/content/addressbook.xul
mail/components/preferences/compose.js
mail/components/preferences/compose.xul
mail/locales/en-US/chrome/messenger/addressbook/abMainWindow.dtd
mail/locales/en-US/chrome/messenger/preferences/compose.dtd
mail/themes/shared/mail/messenger.css
mailnews/addrbook/content/addrbookWidgets.xml
--- a/mail/components/addrbook/content/abCommon.js
+++ b/mail/components/addrbook/content/abCommon.js
@@ -40,16 +40,17 @@ var DirPaneController =
 {
   supportsCommand: function(command)
   {
     switch (command) {
       case "cmd_selectAll":
       case "cmd_delete":
       case "button_delete":
       case "cmd_properties":
+      case "cmd_abToggleStartupDir":
       case "cmd_printcard":
       case "cmd_printcardpreview":
       case "cmd_newlist":
       case "cmd_newCard":
         return true;
       default:
         return false;
     }
@@ -131,16 +132,18 @@ var DirPaneController =
         goSetLabelAccesskeyTooltiptext("cmd_properties-button", null, null,
           tooltipTextAttr);
         goSetLabelAccesskeyTooltiptext("cmd_properties-contextMenu",
           labelAttr, accKeyAttr);
         goSetLabelAccesskeyTooltiptext("cmd_properties-menu",
           labelAttr, accKeyAttr);
         return (selectedDir != null);
       }
+      case "cmd_abToggleStartupDir":
+        return !!getSelectedDirectoryURI();
       case "cmd_newlist":
       case "cmd_newCard":
         return true;
       default:
         return false;
     }
   },
 
@@ -155,16 +158,19 @@ var DirPaneController =
       case "cmd_delete":
       case "button_delete":
         if (gDirTree)
           AbDeleteSelectedDirectory();
         break;
       case "cmd_properties":
         AbEditSelectedDirectory();
         break;
+      case "cmd_abToggleStartupDir":
+        abToggleSelectedDirStartup();
+        break;
       case "cmd_newlist":
         AbNewList();
         break;
       case "cmd_newCard":
         AbNewCard();
         break;
     }
   },
@@ -213,16 +219,51 @@ function AbEditSelectedDirectory()
   } else {
     window.openDialog(selectedDir.propertiesChromeURI,
                       "",
                       "chrome,modal,resizable=no,centerscreen",
                       {selectedDirectory: selectedDir});
   }
 }
 
+function updateDirTreeContext() {
+  let startupItem = document.getElementById("dirTreeContext-startupDir");
+  if (Services.prefs.getBoolPref("mail.addr_book.view.startupURIisDefault")) {
+    let startupURI = Services.prefs.getCharPref("mail.addr_book.view.startupURI");
+    let selectedDirURI = getSelectedDirectoryURI();
+    startupItem.setAttribute("checked", (startupURI == selectedDirURI));
+  } else {
+    startupItem.setAttribute("checked", "false");
+  }
+}
+
+function abToggleSelectedDirStartup()
+{
+  let selectedDirURI = getSelectedDirectoryURI();
+  if (!selectedDirURI)
+    return;
+
+  let isDefault = Services.prefs.getBoolPref("mail.addr_book.view.startupURIisDefault");
+  let startupURI = Services.prefs.getCharPref("mail.addr_book.view.startupURI");
+
+  if (isDefault && (startupURI == selectedDirURI)) {
+    // The current directory has been the default startup view directory;
+    // toggle that off now. So there's no default startup view directory any more.
+    Services.prefs.setBoolPref("mail.addr_book.view.startupURIisDefault", false);
+  } else {
+    // The current directory will now be the default view
+    // when starting up the main AB window.
+    Services.prefs.setCharPref("mail.addr_book.view.startupURI", selectedDirURI);
+    Services.prefs.setBoolPref("mail.addr_book.view.startupURIisDefault", true);
+  }
+
+  // Update the checkbox in the menuitem.
+  goUpdateCommand("cmd_abToggleStartupDir");
+}
+
 function AbDeleteSelectedDirectory()
 {
   let selectedDirURI = getSelectedDirectoryURI();
   if (!selectedDirURI)
     return;
 
   AbDeleteDirectory(selectedDirURI);
 }
--- a/mail/components/addrbook/content/addressbook.js
+++ b/mail/components/addrbook/content/addressbook.js
@@ -287,16 +287,17 @@ function onOSXFileMenuInit()
           .setAttribute("checked", AbOSXAddressBookExists());
 }
 
 function CommandUpdate_AddressBook()
 {
   goUpdateCommand('cmd_delete');
   goUpdateCommand('button_delete');
   goUpdateCommand('cmd_properties');
+  goUpdateCommand("cmd_abToggleStartupDir");
   goUpdateCommand('cmd_newlist');
   goUpdateCommand('cmd_newCard');
   goUpdateCommand('cmd_chatWithCard');
 }
 
 function ResultsPaneSelectionChanged()
 {
   UpdateCardView();
--- a/mail/components/addrbook/content/addressbook.xul
+++ b/mail/components/addrbook/content/addressbook.xul
@@ -195,26 +195,32 @@
 #ifdef XP_MACOSX
   <!-- Mac Window keys -->
   <key id="key_minimizeWindow" command="minimizeWindow" key="&minimizeWindow.key;" modifiers="accel"/>
 #endif
 </keyset>
 
 <keyset id="baseMenuKeyset"/>
 
-<menupopup id="dirTreeContext">
+<menupopup id="dirTreeContext" onpopupshowing="updateDirTreeContext();">
   <menuitem id="dirTreeContext-properties"
             command="cmd_properties-contextMenu"/>
   <menuseparator/>
   <menuitem id="dirTreeContext-newcard" label="&newContactButton.label;"
             accesskey="&newContactButton.accesskey;" command="cmd_newCard"/>
   <menuitem id="dirTreeContext-newlist"
             label="&newlistButton.label;"
             accesskey="&newlistButton.accesskey;"
             command="cmd_newlist"/>
+  <menuitem id="dirTreeContext-startupDir"
+            label="&showAsDefault.label;"
+            accesskey="&showAsDefault.accesskey;"
+            type="checkbox"
+            checked="false"
+            oncommand="goDoCommand('cmd_abToggleStartupDir');"/>
   <menuseparator/>
   <menuitem id="dirTreeContext-delete"
             label="&deleteButton2.label;"
             accesskey="&deleteButton2.accesskey;"
             observes="button_delete"
             oncommand="goDoCommand('button_delete');"/>
 </menupopup>
 
--- a/mail/components/preferences/compose.js
+++ b/mail/components/preferences/compose.js
@@ -21,16 +21,18 @@ var gComposePane = {
     this.populateFonts();
 
     this.updateAutosave();
 
     this.updateAttachmentCheck();
 
     this.updateEmailCollection();
 
+    this.initAbDefaultStartupDir();
+
     if (!(("arguments" in window) && window.arguments[1])) {
       // If no tab was specified, select the last used tab.
       let preference = document.getElementById("mail.preferences.compose.selectedTabIndex");
       if (preference.value)
         document.getElementById("composePrefs").selectedIndex = preference.value;
     }
 
     if (this._loadInContent) {
@@ -111,16 +113,50 @@ var gComposePane = {
     if (this._loadInContent) {
       gSubDialog.open("chrome://messenger/content/addressbook/pref-editdirectories.xul");
     } else {
       window.openDialog("chrome://messenger/content/addressbook/pref-editdirectories.xul",
                         "editDirectories", "chrome,modal=yes,resizable=no", null);
     }
   },
 
+  initAbDefaultStartupDir: function() {
+    if (!this.startupDirListener.inited)
+      this.startupDirListener.load();
+
+    let dirList = document.getElementById("defaultStartupDirList");
+    if (Services.prefs.getBoolPref("mail.addr_book.view.startupURIisDefault")) {
+      // Some directory is the default.
+      let startupURI = Services.prefs.getCharPref("mail.addr_book.view.startupURI");
+      let dirItem = dirList.menupopup.querySelector('[value="' + startupURI + '"]');
+      // It may happen that the stored URI is not in the list.
+      // In that case select the "none" value and let the AB code clear out
+      // the invalid value, unless the user selects something here.
+      if (dirItem)
+        dirList.selectedItem = dirItem;
+      else
+        dirList.value = "";
+    } else {
+      // Choose item meaning there is no default startup directory any more.
+      dirList.value = "";
+    }
+  },
+
+  setDefaultStartupDir: function(aDirURI) {
+    if (aDirURI) {
+      // Some AB directory was selected. Set prefs to make this directory
+      // the default view when starting up the main AB.
+      Services.prefs.setCharPref("mail.addr_book.view.startupURI", aDirURI);
+      Services.prefs.setBoolPref("mail.addr_book.view.startupURIisDefault", true);
+    } else {
+      // Set pref that there's no default startup view directory any more.
+      Services.prefs.setBoolPref("mail.addr_book.view.startupURIisDefault", false);
+    }
+  },
+
   initLanguageMenu: function ()
   {
     var languageMenuList = document.getElementById("languageMenuList");
     this.mSpellChecker = Components.classes['@mozilla.org/spellchecker/engine;1'].getService(Components.interfaces.mozISpellCheckingEngine);
     var o1 = {};
     var o2 = {};
 
     // Get the list of dictionaries from
@@ -170,17 +206,17 @@ var gComposePane = {
             localFonts[i] != "sans-serif" && localFonts[i] != "monospace")
           fontsList.appendItem(localFonts[i], localFonts[i]);
       }
     }
     catch(e) { }
     // Choose the item after the list is completely generated.
     var preference = document.getElementById(fontsList.getAttribute("preference"));
     fontsList.value = preference.value;
-   },
+  },
 
    restoreHTMLDefaults: function()
    {
      // reset throws an exception if the pref value is already the default so
      // work around that with some try/catch exception handling
      try {
        document.getElementById('msgcompose.font_face').reset();
      } catch (ex) {}
@@ -191,10 +227,35 @@ var gComposePane = {
 
      try {
        document.getElementById('msgcompose.text_color').reset();
      } catch (ex) {}
 
      try {
        document.getElementById('msgcompose.background_color').reset();
      } catch (ex) {}
-   }
+  },
+
+  startupDirListener: {
+    inited: false,
+    domain: "mail.addr_book.view.startupURI",
+    observe: function(subject, topic, prefName) {
+      if (topic != "nsPref:changed")
+        return;
+
+      // If the default startup directory prefs have changed,
+      // reinitialize the default startup dir picker to show the new value.
+      gComposePane.initAbDefaultStartupDir();
+    },
+    load: function() {
+      // Observe changes of our prefs.
+      Services.prefs.addObserver(this.domain, this, false);
+      // Unload the pref observer when preferences window is closed.
+      window.addEventListener("unload", this.unload, true);
+      this.inited = true;
+    },
+
+    unload: function(event) {
+      Services.prefs.removeObserver(gComposePane.startupDirListener.domain,
+                                    gComposePane.startupDirListener);
+    }
+  }
 };
--- a/mail/components/preferences/compose.xul
+++ b/mail/components/preferences/compose.xul
@@ -214,16 +214,27 @@
                         oncommand="gComposePane.updateEmailCollection();"/>
               <menulist id="localDirectoriesList" flex="1"
                         preference="mail.collect_addressbook"
                         aria-labelledby="emailCollectionOutgoing">
                 <menupopup id="abPopup-menupopup" class="addrbooksPopup"
                            localonly="true" writable="true"/>
               </menulist>
             </hbox>
+
+           <hbox align="center" pack="start">
+             <label value="&showAsDefault.label;" accesskey="&showAsDefault.accesskey;"/>
+             <menulist id="defaultStartupDirList" flex="1"
+                       oncommand="gComposePane.setDefaultStartupDir(this.value);">
+               <menupopup class="addrbooksPopup"
+                          none="&showAsDefaultLast.label;"
+                          alladdressbooks="true"
+                          mailinglists="true"/>
+             </menulist>
+           </hbox>
          </tabpanel>
 
          <tabpanel orient="vertical">
             <hbox>
               <checkbox id="spellCheckBeforeSend"
                         label="&spellCheck.label;"
                         preference="mail.SpellCheckBeforeSend"
                         accesskey="&spellCheck.accesskey;"/>
--- a/mail/locales/en-US/chrome/messenger/addressbook/abMainWindow.dtd
+++ b/mail/locales/en-US/chrome/messenger/addressbook/abMainWindow.dtd
@@ -135,16 +135,18 @@ because displayed names don't have the c
 <!ENTITY preferencesCmdUnix.label                       "Preferences">
 <!ENTITY preferencesCmdUnix.accesskey                   "n">
 
 <!-- Address Book Toolbar and Context Menus -->
 <!ENTITY newContactButton.label                         "New Contact">
 <!ENTITY newContactButton.accesskey                     "C">
 <!ENTITY newlistButton.label                            "New List">
 <!ENTITY newlistButton.accesskey                        "L">
+<!ENTITY showAsDefault.label                            "Default startup directory">
+<!ENTITY showAsDefault.accesskey                        "S">
 <!ENTITY editPropertiesButton.label                     "Edit">
 <!ENTITY propertiesContext.label                        "Properties">
 <!ENTITY propertiesContext.accesskey                    "i">
 <!ENTITY abPropertiesContext.label                      "Properties">
 <!ENTITY abPropertiesContext.accesskey                  "i">
 <!ENTITY editContactContext.label                       "Edit Contact">
 <!ENTITY editContactContext.accesskey                   "E">
 <!ENTITY editMailingListContext.label                   "Edit List">
--- a/mail/locales/en-US/chrome/messenger/preferences/compose.dtd
+++ b/mail/locales/en-US/chrome/messenger/preferences/compose.dtd
@@ -49,16 +49,19 @@
 <!ENTITY autocompleteText.label                "When addressing messages, look for matching entries in:">
 <!ENTITY addressingEnable.label                "Local Address Books">
 <!ENTITY addressingEnable.accesskey            "L">
 <!ENTITY directories.label                     "Directory Server:">
 <!ENTITY directories.accesskey                 "D">
 <!ENTITY directoriesNone.label                 "None">
 <!ENTITY editDirectories.label                 "Edit Directories…">
 <!ENTITY editDirectories.accesskey             "E">
+<!ENTITY showAsDefault.label                   "Default startup directory in the address book window:">
+<!ENTITY showAsDefault.accesskey               "S">
+<!ENTITY showAsDefaultLast.label               "Last used directory">
 
 <!ENTITY sendOptionsDescription.label          "Configure text format behavior">
 <!ENTITY sendOptions.label                     "Send Options…">
 <!ENTITY sendOptions.accesskey                 "S">
 
 <!ENTITY attachmentReminder.label              "Check for missing attachments">
 <!ENTITY attachmentReminder.accesskey          "m">
 <!ENTITY attachmentReminderOptions.label       "Keywords…">
--- a/mail/themes/shared/mail/messenger.css
+++ b/mail/themes/shared/mail/messenger.css
@@ -77,8 +77,26 @@ notification[value="addon-install-failed
 @keyframes highlight {
   from { background-color: Highlight; }
   to { background-color: transparent; }
 }
 
 #findbar-beforeReplaceSeparator {
   height: 16px;
 }
+
+/* ::::: Address Book menuitem icons ::::: */
+
+.abMenuItem[AddrBook="true"] {
+  list-style-image: url("chrome://messenger/skin/addressbook/icons/addrbook.png");
+}
+
+.abMenuItem[MailList="true"] {
+  list-style-image: url("chrome://messenger/skin/addressbook/icons/ablist.png");
+}
+
+.abMenuItem[AddrBook="true"][IsRemote="true"] {
+  list-style-image: url("chrome://messenger/skin/addressbook/icons/remote-addrbook.png");
+}
+
+.abMenuItem[AddrBook="true"][IsRemote="true"][IsSecure="true"] {
+  list-style-image: url("chrome://messenger/skin/addressbook/icons/secure-remote-addrbook.png");
+}
--- a/mailnews/addrbook/content/addrbookWidgets.xml
+++ b/mailnews/addrbook/content/addrbookWidgets.xml
@@ -13,134 +13,181 @@
     <implementation implements="nsIAbListener">
       <!-- A cache of nsIAbDirectory objects. -->
       <field name="_directories">[]</field>
 
       <!-- Represents the nsIAbDirectory attribute used as the value of the
            parent menulist. Defaults to URI but can be e.g. dirPrefId -->
       <field name="_value">this.getAttribute("value") || "URI"</field>
 
+      <field name="_stringBundle">null</field>
+
       <constructor>
         <![CDATA[
-          Components.utils.import("resource:///modules/mailServices.js");
-          // Init the address book cache.
-          const nsIAbDirectory = Components.interfaces.nsIAbDirectory;
-          let directories = MailServices.ab.directories;
-          while (directories && directories.hasMoreElements()) {
-            var ab = directories.getNext();
-            if (ab instanceof nsIAbDirectory && this._matches(ab))
-              this._directories.push(ab);
-          }
-
-          this._directories.sort(this._compare);
+          Components.utils.import("resource:///modules/mailServices.js", this);
+          Components.utils.import("resource:///modules/iteratorUtils.jsm", this);
+          Components.utils.import("resource:///modules/StringBundle.js", this);
+          this._stringBundle = new this
+              .StringBundle("chrome://messenger/locale/addressbook/addressBook.properties");
 
-          // Now create menuitems for all displayed directories.
-          var menulist = this.parentNode;
-          var value = this._value;
-          this._directories.forEach(function (ab) {
-            menulist.appendItem(ab.dirName, ab[value]);
-          });
-          if (this.hasAttribute("none")) {
-            // Create a dummy menuitem representing no selection.
-            this._directories.unshift(null);
-            menulist.insertItemAt(0, this.getAttribute("none"), "");
-          }
-
-          // Attempt to select the persisted or otherwise first directory.
-          menulist.value = menulist.value;
-          if (!menulist.selectedItem && this.hasChildNodes())
-            menulist.selectedIndex = 0;
-
+          this._rebuild();
           const nsIAbListener = Components.interfaces.nsIAbListener;
           // Add a listener so we can update correctly if the list should change
-          MailServices.ab.addAddressBookListener(this,
-                                                 nsIAbListener.itemAdded |
-                                                 nsIAbListener.directoryRemoved |
-                                                 nsIAbListener.itemChanged);
+          this.MailServices.ab
+              .addAddressBookListener(this,
+                                      nsIAbListener.itemAdded |
+                                      nsIAbListener.directoryItemRemoved |
+                                      nsIAbListener.directoryRemoved |
+                                      nsIAbListener.itemChanged);
         ]]>
       </constructor>
 
       <destructor>
         <![CDATA[
-          Components.utils.import("resource:///modules/mailServices.js");
-          MailServices.ab.removeAddressBookListener(this);
+          this.MailServices.ab.removeAddressBookListener(this);
+
+          this._teardown();
+        ]]>
+      </destructor>
+
+      <method name="_rebuild">
+        <parameter name="aSelectValue"/>
+        <body><![CDATA[
+          // Init the address book cache.
+          this._directories.length = 0;
+          const nsIAbDirectory = Components.interfaces.nsIAbDirectory;
+          let directories = this.MailServices.ab.directories;
+          while (directories && directories.hasMoreElements()) {
+            let ab = directories.getNext();
+            if ((ab instanceof nsIAbDirectory) && this._matches(ab)) {
+              this._directories.push(ab);
+              if (this.getAttribute("mailinglists") == "true") {
+                // Also append contained mailinglists.
+                for (let list of this.fixIterator(ab.childNodes, Components.interfaces.nsIAbDirectory)) {
+                  if (this._matches(list))
+                    this._directories.push(list);
+                }
+              }
+            }
+          }
+
+          this._sort();
+
+          this._teardown();
+
+          // Now create menuitems for all displayed directories.
+          let menulist = this.parentNode;
+          let value = this._value;
+          for (let ab of this._directories) {
+            let listItem = menulist.appendItem(ab.dirName, ab[value]);
+            listItem.setAttribute("class", "menuitem-iconic abMenuItem");
 
+            // Style the items by type.
+            if (ab.isMailList)
+              listItem.setAttribute("MailList", "true");
+            else
+              listItem.setAttribute("AddrBook", "true");
+
+            if (ab.isRemote)
+              listItem.setAttribute("IsRemote", "true");
+            if (ab.isSecure)
+              listItem.setAttribute("IsSecure", "true");
+          }
+
+          if (this.hasAttribute("alladdressbooks")) {
+            // Insert a menuitem representing All addressbooks.
+            let allABLabel = this.getAttribute("alladdressbooks");
+            if (allABLabel == "true")
+              allABLabel = this._stringBundle.getString("allAddressBooks");
+            const allABURI = "moz-abdirectory://?";
+            this._directories.unshift(null);
+            let listItem = menulist.insertItemAt(0, allABLabel, allABURI);
+            listItem.setAttribute("class", "menuitem-iconic abMenuItem");
+            listItem.setAttribute("AddrBook", "true");
+            listItem.setAttribute("IsAllAB", "true");
+          }
+
+          if (this.hasAttribute("none")) {
+            // Create a dummy menuitem representing no selection.
+            this._directories.unshift(null);
+            let listItem = menulist.insertItemAt(0, this.getAttribute("none"), "");
+            listItem.setAttribute("class", "menuitem-iconic abMenuItem");
+            listItem.setAttribute("IsNone", "true");
+          }
+
+          // Attempt to select the persisted or otherwise first directory.
+          menulist.value = aSelectValue || this.parentNode.value;
+          if (!menulist.selectedItem && this.hasChildNodes())
+            menulist.selectedIndex = 0;
+        ]]></body>
+      </method>
+
+      <method name="_teardown">
+        <body><![CDATA[
           // Empty out anything in the list.
+          // (Don't use menulist.removeAllItems() as it would remove
+          // the menupopup with our special attributes too.)
           while (this.hasChildNodes())
             this.lastChild.remove();
-        ]]>
-      </destructor>
+        ]]></body>
+      </method>
 
       <!-- nsIAbListener methods -->
       <method name="onItemAdded">
         <parameter name="aParentDir"/>
         <parameter name="aItem"/>
         <body><![CDATA[
           // Are we interested in this new directory?
           if (aItem instanceof Components.interfaces.nsIAbDirectory &&
-              !aItem.isMailList && this._matches(aItem)) {
-            this._directories.push(aItem);
-            this._directories.sort(this._compare);
-            // Insert the new menuitem at the position to which it was sorted.
-            this.parentNode.insertItemAt(this._directories.indexOf(aItem),
-                                         aItem.dirName, aItem[this._value]);
+              this._matches(aItem)) {
+            this._rebuild();
           }
         ]]></body>
       </method>
 
       <method name="onItemRemoved">
         <parameter name="aParentDir"/>
         <parameter name="aItem"/>
         <body><![CDATA[
-          if (aItem instanceof Components.interfaces.nsIAbDirectory &&
-              !aItem.isMailList) {
-            // Find the item in the list to remove
-            // We can't use indexOf here because we need loose equality
+          if (aItem instanceof Components.interfaces.nsIAbDirectory) {
+            // Find the item in the list to remove.
+            // We can't use indexOf here because we need loose equality.
             for (var index = this._directories.length; --index >= 0; )
               if (this._directories[index] == aItem)
                 break;
-            if (index != -1)
+            if (index != -1) {
+              this._directories.splice(index, 1);
               // Are we removing the selected directory?
               if (this.parentNode.selectedItem ==
-                  this.removeChild(this.childNodes[index]))
+                  this.removeChild(this.childNodes[index])) {
                 // If so, try to select the first directory, if available.
                 if (this.hasChildNodes())
                   this.firstChild.doCommand();
                 else
                   this.parentNode.selectedItem = null;
+              }
+            }
           }
         ]]></body>
       </method>
 
       <method name="onItemPropertyChanged">
         <parameter name="aItem"/>
         <parameter name="aProperty"/>
         <parameter name="aOldValue"/>
         <parameter name="aNewValue"/>
         <body><![CDATA[
-          if (aItem instanceof Components.interfaces.nsIAbDirectory &&
-              !aItem.isMailList) {
+          if (aItem instanceof Components.interfaces.nsIAbDirectory) {
             // Find the item in the list to rename.
-            // We can't use indexOf here because we need loose equality
+            // We can't use indexOf here because we need loose equality.
             for (var oldIndex = this._directories.length; --oldIndex >= 0; )
               if (this._directories[oldIndex] == aItem)
                 break;
-            if (oldIndex != -1) {
-              // Cache the matching item so that we can use indexOf next time.
-              aItem = this._directories[oldIndex];
-              var child = this.childNodes[oldIndex];
-              child.label = aItem.dirName;
-              this._directories.sort(this._compare);
-              // Reorder the menuitems if renaming changed the directory index.
-              var newIndex = this._directories.indexOf(aItem);
-              if (newIndex < oldIndex)
-                this.insertBefore(child, this.childNodes[newIndex]);
-              else if (newIndex > oldIndex)
-                this.insertBefore(child, this.childNodes[newIndex].nextSibling);
-            }
+            if (oldIndex != -1)
+              this._rebuild();
           }
         ]]></body>
       </method>
 
       <!-- Private methods -->
       <!-- Tests to see whether this directory should display in the list. -->
       <method name="_matches">
         <parameter name="ab"/>
@@ -153,16 +200,53 @@
           if (this.getAttribute("supportsmaillists") == "true" &&
               !ab.supportsMailingLists)
             return false;
 
           return this.getAttribute(ab.isRemote ? "localonly" : "remoteonly") != "true";
         ]]></body>
       </method>
 
+      <!-- Sort all our directories. -->
+      <method name="_sort">
+        <body><![CDATA[
+          let lists = {};
+          let lastAB;
+          // If there are any mailing lists, pull them out of the array temporarily.
+          for (let d = 0; d < this._directories.length; d++) {
+            if (this._directories[d].isMailList) {
+              let [list] = this._directories.splice(d, 1);
+              if (!(lastAB in lists))
+                lists[lastAB] = [];
+              lists[lastAB].push(list);
+              d--;
+            } else {
+              lastAB = this._directories[d].URI;
+            }
+          }
+
+          this._directories.sort(this._compare);
+
+          // Push mailing lists back appending them after their respective
+          // containing addressbook.
+          for (let d = this._directories.length - 1; d >= 0; d--) {
+            let abURI = this._directories[d].URI;
+            if (abURI in lists) {
+              lists[abURI].sort(function(a,b) { return a.dirName.localeCompare(b.dirName); });
+              let listIndex = d;
+              for (let list of lists[abURI]) {
+                listIndex++;
+                this._directories.splice(listIndex, 0, list);
+              }
+              delete lists[abURI];
+            }
+          }
+        ]]></body>
+      </method>
+
       <!-- Used to sort directories in order -->
       <method name="_compare">
         <parameter name="a"/>
         <parameter name="b"/>
         <body><![CDATA[
           // Null at the very top.
           if (!a)
             return -1;