From e3213adec4f57904a20831fd735a4630364d0e0d Mon Sep 17 00:00:00 2001 From: Joey Minta Date: Fri, 6 May 2011 12:03:00 +0100 Subject: [PATCH] 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 | 35 +- mail/components/addrbook/content/abTrees.js | 321 ++++++++++++++++++ .../addrbook/content/addressbook.js | 59 ++-- .../addrbook/content/addressbook.xul | 52 +-- mail/components/addrbook/jar.mn | 1 + .../mozmill/addrbook/test-address-book.js | 156 +++++++++ mail/test/mozmill/mozmilltests.list | 1 + .../test-address-book-helpers.js | 256 +++++++++++++- mailnews/addrbook/content/abDragDrop.js | 17 +- mailnews/base/content/jsTreeView.js | 265 +++++++++++++++ mailnews/jar.mn | 1 + 11 files changed, 1049 insertions(+), 115 deletions(-) create mode 100644 mail/components/addrbook/content/abTrees.js create mode 100644 mail/test/mozmill/addrbook/test-address-book.js create mode 100644 mailnews/base/content/jsTreeView.js diff --git a/mail/components/addrbook/content/abCommon.js b/mail/components/addrbook/content/abCommon.js index 2970370281..46d5f2d0f1 100644 --- a/mail/components/addrbook/content/abCommon.js +++ b/mail/components/addrbook/content/abCommon.js @@ -25,6 +25,7 @@ # Seth Spitzer # Mark Banner # Simon Wilkinson +# Mike Conley # # 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 @@ -42,7 +43,7 @@ Components.utils.import("resource:///modules/mailServices.js"); -var dirTree = 0; +var gDirTree = 0; var abList = 0; var gAbResultsTree = null; var gAbView = null; @@ -85,7 +86,7 @@ var DirPaneController = 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 @@ -156,7 +157,7 @@ var DirPaneController = break; case "cmd_delete": case "button_delete": - if (dirTree) + if (gDirTree) AbDeleteSelectedDirectory(); break; case "button_edit": @@ -203,7 +204,7 @@ function AbNewAddressBook() function AbEditSelectedDirectory() { - if (dirTree.view.selection.count == 1) { + if (gDirTree.view.selection.count == 1) { var selecteduri = GetSelectedDirectory(); var directory = GetDirectoryFromURI(selecteduri); if (directory.isMailList) { @@ -288,7 +289,7 @@ function GetParentRow(aTree, aRow) function InitCommonJS() { - dirTree = document.getElementById("dirTree"); + gDirTree = document.getElementById("dirTree"); abList = document.getElementById("addressbookList"); gAddressBookBundle = document.getElementById("bundle_addressBook"); } @@ -396,9 +397,8 @@ 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); @@ -435,7 +435,7 @@ function GetAddressesForCards(cards) function SelectFirstAddressBook() { - dirTree.view.selection.select(0); + gDirTree.view.selection.select(0); ChangeDirectoryByURI(GetSelectedDirectory()); gAbResultsTree.focus(); @@ -460,13 +460,13 @@ function DirPaneDoubleClick(event) 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(); } @@ -474,8 +474,8 @@ 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'); @@ -607,7 +607,7 @@ function GetParentDirectoryFromMailingListURI(abURI) 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() @@ -615,10 +615,9 @@ 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; } } diff --git a/mail/components/addrbook/content/abTrees.js b/mail/components/addrbook/content/abTrees.js new file mode 100644 index 0000000000..10bd75b33f --- /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 + * 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(); diff --git a/mail/components/addrbook/content/addressbook.js b/mail/components/addrbook/content/addressbook.js index 436d06f842..1779fcf20a 100644 --- a/mail/components/addrbook/content/addressbook.js +++ b/mail/components/addrbook/content/addressbook.js @@ -25,6 +25,7 @@ # Contributor(s): # Seth Spitzer # Mark Banner +# Joey Minta # # 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 @@ -42,16 +43,17 @@ // 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; @@ -86,7 +88,7 @@ var gAddressBookAbListener = { // 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 @@ -99,7 +101,7 @@ var gAddressBookAbListener = { --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. @@ -107,11 +109,11 @@ var gAddressBookAbListener = { 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); } } } @@ -125,9 +127,12 @@ var gAddressBookAbListener = { 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) @@ -188,8 +193,11 @@ function delayedOnLoadAddressBook() // 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")) @@ -200,15 +208,13 @@ function delayedOnLoadAddressBook() // 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"); @@ -228,12 +234,6 @@ function delayedOnLoadAddressBook() .AddMsgWindow(msgWindow); } -function OnLoadDirTree() { - var treeBuilder = dirTree.builder.QueryInterface(Components.interfaces.nsIXULTreeBuilder); - treeBuilder.addObserver(abDirTreeObserver); - - SelectFirstAddressBook(); -} function GetCurrentPrefs() { @@ -435,9 +435,7 @@ function AbExport() 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); @@ -740,13 +738,10 @@ function AbOSXAddressBookExists() 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); } diff --git a/mail/components/addrbook/content/addressbook.xul b/mail/components/addrbook/content/addressbook.xul index 5e5ed26f86..c5606f13a2 100644 --- a/mail/components/addrbook/content/addressbook.xul +++ b/mail/components/addrbook/content/addressbook.xul @@ -71,6 +71,8 @@ +