Bug 761852 - Port |Bug 664726 - Add hooks to make address book more extend-able| and follow-ups to SeaMonkey r/moa=Mnyromyr
authorIan Neal <iann_cvs@blueyonder.co.uk>
Thu, 12 Jul 2012 22:15:41 +0100
changeset 10641 175f81ed0caa3e1aee2d69e632f8621adfa1f5e2
parent 10640 f01683f0ef7e344ab10519a251bd9ee27a5ffac5
child 10642 cf074c127ab9bfe21769ee8448a88df6b4a10e43
push id8032
push useriann_cvs@blueyonder.co.uk
push dateThu, 12 Jul 2012 21:15:56 +0000
treeherdercomm-central@47a3e0556156 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs761852, 664726
Bug 761852 - Port |Bug 664726 - Add hooks to make address book more extend-able| and follow-ups to SeaMonkey r/moa=Mnyromyr
suite/mailnews/addrbook/abCardOverlay.js
suite/mailnews/addrbook/abCardOverlay.xul
suite/mailnews/addrbook/abCardViewOverlay.js
suite/mailnews/addrbook/abCommon.js
suite/mailnews/addrbook/abListOverlay.xul
--- a/suite/mailnews/addrbook/abCardOverlay.js
+++ b/suite/mailnews/addrbook/abCardOverlay.js
@@ -1,15 +1,15 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const kNonVcardFields =
-        ["nickNameContainer", "secondaryEmailContainer", "screenNameContainer",
+        ["NickNameContainer", "SecondaryEmailContainer", "ScreenNameContainer",
          "customFields", "allowRemoteContent", "preferDisplayName"];
 
 const kPhoneticFields =
         ["PhoneticLastName", "PhoneticLabel1", "PhoneticSpacer1",
          "PhoneticFirstName", "PhoneticLabel2", "PhoneticSpacer2"];
 
 // Item is |[dialogField, cardProperty]|.
 const kVcardFields =
@@ -52,20 +52,21 @@ const kVcardFields =
          ["Custom2", "Custom2"],
          ["Custom3", "Custom3"],
          ["Custom4", "Custom4"],
           // Other > Notes
          ["Notes", "Notes"]];
 
 const kDefaultYear = 2000;
 var gEditCard;
-var gOnSaveListeners = new Array();
+var gOnSaveListeners = [];
+var gOnLoadListeners = [];
 var gOkCallback = null;
 var gHideABPicker = false;
-var gOriginalPhotoURI = "";
+var gPhotoHandlers = {};
 
 function OnLoadNewCard()
 {
   InitEditCard();
 
   gEditCard.card =
     (("arguments" in window) && (window.arguments.length > 0) &&
      (window.arguments[0] instanceof Components.interfaces.nsIAbCard))
@@ -169,38 +170,48 @@ function EditCardOKButton()
   // if the directory is a mailing list we need to search all the mailing lists
   // in the parent directory if the card exists.
   if (directory.isMailList) {
     var parentURI = GetParentDirectoryFromMailingListURI(gEditCard.abURI);
     directory = GetDirectoryFromURI(parentURI);
   }
 
   var listDirectoriesCount = directory.addressLists.length;
-  var foundDirectories = new Array();
-  var foundDirectoriesCount = 0;
-  var i;
+  var foundDirectories = [];
+
   // create a list of mailing lists and the index where the card is at.
-  for ( i=0;  i < listDirectoriesCount; i++ ) {
+  for (let i = 0; i < listDirectoriesCount; i++)
+  {
     var subdirectory = directory.addressLists.queryElementAt(i, Components.interfaces.nsIAbDirectory);
-    try {
-      var index = subdirectory.indexOf(gEditCard);
-      foundDirectories[foundDirectoriesCount] = {directory:subdirectory, index:index};
-      foundDirectoriesCount++;
-    } catch (ex) {}
+    if (subdirectory.isMailList)
+    {
+      // See if any card in this list is the one we edited.
+      // Must compare card contents using .equals() instead of .indexOf()
+      // because gEditCard is not really a member of the .addressLists array.
+      let listCardsCount = subdir.addressLists.length;
+      for (let index = 0; index < listCardsCount; index++)
+      {
+        let card = subdirectory.addressLists.queryElementAt(index, Components.interfaces.nsIAbCard);
+        if (card.equals(gEditCard.card))
+          foundDirectories.push({directory:subdir, cardIndex:index});
+      }
+    }
   }
-  
+ 
   CheckAndSetCardValues(gEditCard.card, document, false);
 
   directory.modifyCard(gEditCard.card);
-  
-  for (i=0; i<foundDirectoriesCount; i++) {
-      // Update the addressLists item for this card
-      foundDirectories[i].directory.addressLists
-                         .replaceElementAt(gEditCard.card, foundDirectories[i].index, false);
+ 
+  while (foundDirectories.length)
+  {
+    // Update the addressLists item for this card
+    let foundItem = foundDirectories.pop();
+    foundItem.directory.addressLists.replaceElementAt(gEditCard.card, foundItem.cardIndex, false);
   }
+
   NotifySaveListeners(directory);
 
   // callback to allow caller to update
   if (gOkCallback)
     gOkCallback();
 
   return true;  // close the window
 }
@@ -279,32 +290,68 @@ function OnLoadEditCard()
 
       // hide remote content in HTML field for remote directories
       if (directory.isRemote)
         document.getElementById('allowRemoteContent').hidden = true;
     }
   }
 }
 
-// this is used by people who extend the ab card dialog
-// like Netscape does for screenname
-function RegisterSaveListener(func)
+/* Registers functions that are called when loading the card
+ * values into the contact editor dialog.  This is useful if
+ * extensions have added extra fields to the nsIAbCard, and
+ * need to display them in the contact editor.
+ */
+function RegisterLoadListener(aFunc)
 {
-  gOnSaveListeners[gOnSaveListeners.length] = func;
+  gOnLoadListeners.push(aFunc);
+}
+
+function UnregisterLoadListener(aFunc)
+{
+  var fIndex = gOnLoadListeners.indexOf(aFunc);
+  if (fIndex != -1)
+    gOnLoadListeners.splice(fIndex, 1);
 }
 
-// this is used by people who extend the ab card dialog
-// like Netscape does for screenname
+// Notifies load listeners that an nsIAbCard is being loaded.
+function NotifyLoadListeners(aCard, aDoc)
+{
+  if (!gOnLoadListeners.length)
+    return;
+
+  for (let listener of gOnLoadListeners)
+    listener(aCard, aDoc);
+}
+
+/* Registers functions that are called when saving the card
+ * values.  This is useful if extensions have added extra
+ * fields to the user interface, and need to set those values
+ * in their nsIAbCard.
+ */
+function RegisterSaveListener(aFunc)
+{
+  gOnSaveListeners.push(aFunc);
+}
+
+function UnregisterSaveListener(aFunc)
+{
+  var fIndex = gOnSaveListeners.indexOf(aFunc);
+  if (fIndex != -1)
+    gOnSaveListeners.splice(fIndex, 1);
+}
+
+// Notifies save listeners that an nsIAbCard is being saved.
 function NotifySaveListeners(directory)
 {
   if (!gOnSaveListeners.length)
     return;
 
-  for ( var i = 0; i < gOnSaveListeners.length; i++ )
-    gOnSaveListeners[i]();
+  for (let listener of gOnSaveListeners)
+    listener(gEditCard.card, document);
 
   // the save listeners might have tweaked the card
   // in which case we need to commit it.
   directory.modifyCard(gEditCard.card);
 }
 
 function InitPhoneticFields()
 {
@@ -370,18 +417,19 @@ function NewCardOKButton()
     if (gEditCard.card)
     {
       if (!CheckAndSetCardValues(gEditCard.card, document, true))
         return false;  // don't close window
 
       // replace gEditCard.card with the card we added
       // so that save listeners can get / set attributes on
       // the card that got created.
-      gEditCard.card = GetDirectoryFromURI(uri).addCard(gEditCard.card);
-      NotifySaveListeners();
+      var directory = GetDirectoryFromURI(uri);
+      gEditCard.card = directory.addCard(gEditCard.card);
+      NotifySaveListeners(directory);
       if ("arguments" in window && window.arguments[0] &&
           "allowRemoteContent" in window.arguments[0])
         // getProperty may return a "1" or "0" string, we want a boolean
         window.arguments[0].allowRemoteContent =
           gEditCard.card.getProperty("AllowRemoteContent", false) != false;
     }
   }
 
@@ -389,16 +437,20 @@ function NewCardOKButton()
 }
 
 // Move the data from the cardproperty to the dialog
 function GetCardValues(cardproperty, doc)
 {
   if (!cardproperty)
     return;
 
+  // Pass the nsIAbCard and the Document through the listeners
+  // to give extensions a chance to populate custom fields.
+  NotifyLoadListeners(cardproperty, doc);
+
   for (var i = kVcardFields.length; i-- > 0; ) {
     doc.getElementById(kVcardFields[i][0]).value =
       cardproperty.getProperty(kVcardFields[i][1], "");
   }
 
   var birthday = doc.getElementById("Birthday");
   modifyDatepicker(birthday);
 
@@ -452,42 +504,20 @@ function GetCardValues(cardproperty, doc
     doc.getElementById("PhoneticFirstName").value = cardproperty.getProperty("PhoneticFirstName", "");
     doc.getElementById("PhoneticLastName").value = cardproperty.getProperty("PhoneticLastName", "");
   }
   catch (ex) {}
 
   // Store the original photo URI and update the photo
   // Select the type if there is a valid value stored for that type, otherwise
   // select the generic photo
-  gOriginalPhotoURI = cardproperty.getProperty("PhotoURI", "");
-  switch (cardproperty.getProperty("PhotoType", "")) {
-    case "file":
-      try {
-        var file = Components.classes["@mozilla.org/network/io-service;1"]
-                             .getService(Components.interfaces.nsIIOService)
-                             .newURI(gOriginalPhotoURI, null, null)
-                             .QueryInterface(Components.interfaces.nsIFileURL)
-                             .file;
-      } catch (e) {}
-      if (file) {
-        document.getElementById("PhotoFile").file = file;
-        updatePhoto("file");
-      }
-      else
-        updatePhoto("generic");
-      break;
-    case "web":
-      document.getElementById("PhotoURI").value = gOriginalPhotoURI;
-      updatePhoto("web");
-      break;
-    default:
-      if (gOriginalPhotoURI)
-        document.getElementById("GenericPhotoList").value = gOriginalPhotoURI;
-      updatePhoto("generic");
-  }
+  var photoType = cardproperty.getProperty("PhotoType", "");
+  document.getElementById("PhotoType").value = photoType;
+  loadPhoto(cardproperty);
+  setCardEditorPhoto(photoType, cardproperty);
 }
 
 // when the ab card dialog is being loaded to show a vCard,
 // hide the fields which aren't supported
 // by vCard so the user does not try to edit them.
 function HideNonVcardFields()
 {
   document.getElementById("homeTabButton").hidden = true;
@@ -540,47 +570,17 @@ function CheckAndSetCardValues(cardprope
 
   // set phonetic fields if exist
   try {
     cardproperty.setProperty("PhoneticFirstName", doc.getElementById("PhoneticFirstName").value);
     cardproperty.setProperty("PhoneticLastName", doc.getElementById("PhoneticLastName").value);
   }
   catch (ex) {}
 
-  var type = document.getElementById("PhotoType").value;
-  var photoURI = gOriginalPhotoURI;
-  if (type == "file" && document.getElementById("PhotoFile").file)
-    photoURI = Components.classes["@mozilla.org/network/io-service;1"]
-                         .getService(Components.interfaces.nsIIOService)
-                         .newFileURI(document.getElementById("PhotoFile").file)
-                         .spec;
-  else if (type == "web" && document.getElementById("PhotoURI").value)
-    photoURI = document.getElementById("PhotoURI").value;
-  else {
-    type = "generic";
-    photoURI = document.getElementById("GenericPhotoList").value;
-  }
-  cardproperty.setProperty("PhotoType", type);
-  if (photoURI != gOriginalPhotoURI) {
-    // Store the original URI
-    cardproperty.setProperty("PhotoURI", photoURI);
-    // Save the photo if it isn't one of the generic photos
-    if (type == "generic") {
-      // Remove the original, if any
-      removePhoto(cardproperty.getProperty("PhotoName", null));
-    } else {
-      // Save the new file and store its URI as PhotoName 
-      var file = savePhoto(photoURI);
-      if (file) {
-        // Remove the original, if any
-        removePhoto(cardproperty.getProperty("PhotoName", null));
-        cardproperty.setProperty("PhotoName", file.leafName);
-      }
-    }
-  }
+  savePhoto(cardproperty);
 
   return true;
 }
 
 function CleanUpWebPage(webPage)
 {
   // no :// yet so we should add something
   if ( webPage.length && webPage.search("://") == -1 )
@@ -871,46 +871,79 @@ function modifyDatepicker(aDatepicker) {
     }
     // make the field's value null if aValue is null and the field's value isn't
     if (aValue == null && aField.value != null)
       aField.value = null;
   }
 }
 
 /**
- * Updates the photo by setting the src attribute of the photo element.
+ * Updates the photo displayed in the contact editor based on the
+ * type of photo selected.  If the type is not recognized, the
+ * photo will automatically switch to the generic photo.
+ *
+ * @param aType The type of photo (web, file, or generic available
+ *              by default).
+ * @param aCard The nsIAbCard being edited
+ *
+ */
+function setCardEditorPhoto(aType, aCard)
+{
+  if (!gPhotoHandlers[aType] ||
+      !gPhotoHandlers[aType].onShow(aCard, document, "photo"))
+    gPhotoHandlers["generic"].onShow(aCard, document, "photo");
+}
+
+/**
+ * Extract the photo information from an nsIAbCard, and populate
+ * the appropriate input fields in the contact editor.  If the
+ * nsIAbCard returns an unrecognized PhotoType, the generic
+ * display photo is switched to.
+ *
+ * @param aCard The nsIAbCard to extract the information from.
  *
- * @param aType Optional. The type of photo (web, file, or generic).
- *              If supplied the corresponding radio button will be selected.
- *              If not supplied the type will be determined by the currently
- *              selected type.
+ */
+function loadPhoto(aCard)
+{
+  var type = aCard.getProperty("PhotoType", "")
+  if (!gPhotoHandlers[type] ||
+      !gPhotoHandlers[type].onLoad(aCard, document))
+    gPhotoHandlers["generic"].onLoad(aCard, document);
+}
+
+/**
+ * Given the fields in the current contact editor, commit
+ * the photo to an nsIAbCard.  If the photo cannot be saved,
+ * the generic contact photo is saved instead.
+ *
+ * @param aType
+ *
  */
-function updatePhoto(aType) {
-  if (aType)
-    // Select the type's radio button
+function savePhoto(aCard)
+{
+  var type = document.getElementById("PhotoType").value;
+  if (!gPhotoHandlers[type] ||
+      !gPhotoHandlers[type].onSave(aCard, document))
+    gPhotoHandlers["generic"].onSave(aCard, document);
+}
+
+/**
+ * Event handler for when the user switches the type of
+ * photo for the nsIAbCard being edited.  Called from
+ * abCardOverlay.xul.
+ */
+function onSwitchPhotoType(aType)
+{
+  if (!gEditCard)
+    return;
+
+  if (aType != document.getElementById("PhotoType").value)
     document.getElementById("PhotoType").value = aType;
   else
-    aType = document.getElementById("PhotoType").value;
-
-  var value;
-  switch (aType) {
-    case "file":
-      var file = document.getElementById("PhotoFile").file;
-      value = file ? Components.classes["@mozilla.org/network/io-service;1"]
-                               .getService(Components.interfaces.nsIIOService)
-                               .newFileURI(file)
-                               .spec : "";
-      break;
-    case "web":
-      value = document.getElementById("PhotoURI").value;
-      break;
-    default:
-      value = document.getElementById("GenericPhotoList").value;
-  }
-  document.getElementById("photo").setAttribute("src", value || defaultPhotoURI);
+    setCardEditorPhoto(aType, gEditCard.card);
 }
 
 /**
  * Removes the photo file at the given path, if present.
  *
  * @param aName The name of the photo to remove from the Photos directory.
  *
  * @return true if the file was deleted.
@@ -944,13 +977,192 @@ function browsePhoto() {
   fp.init(window, gAddressBookBundle.getString("browsePhoto"), nsIFilePicker.modeOpen);
   
   // Add All Files & Image Files filters and select the latter
   fp.appendFilters(nsIFilePicker.filterImages);
   fp.appendFilters(nsIFilePicker.filterAll);
 
   if (fp.show() == nsIFilePicker.returnOK) {
     document.getElementById("PhotoFile").file = fp.file;
-    updatePhoto("file");
+    onSwitchPhotoType(document.getElementById("FilePhotoType").value);
     return true;
   }
   return false;
 }
+
+/* A photo handler defines the behaviour of the contact editor
+ * for a particular photo type. Each photo handler must implement
+ * the following interface:
+ *
+ * onLoad: function(aCard, aDocument):
+ *   Called when the editor wants to populate the contact editor
+ *   input fields with information about aCard's photo.  Note that
+ *   this does NOT make aCard's photo appear in the contact editor -
+ *   this is left to the onShow function.  Returns true on success.
+ *   If the function returns false, the generic photo handler onLoad
+ *   function will be called.
+ *
+ * onShow: function(aCard, aDocument, aTargetID):
+ *   Called when the editor wants to show this photo type.
+ *   The onShow method should take the input fields in the document,
+ *   and render the requested photo in the IMG tag with id
+ *   aTargetID.  Note that onShow does NOT save the photo for aCard -
+ *   this job is left to the onSave function.  Returns true on success.
+ *   If the function returns false, the generic photo handler onShow
+ *   function will be called.
+ *
+ * onSave: function(aCard, aDocument)
+ *   Called when the editor wants to save this photo type.  The
+ *   onSave method is responsible for analyzing the photo of this
+ *   type requested by the user, and storing it, as well as the
+ *   other fields required by onLoad/onShow to retrieve and display
+ *   the photo again.  Returns true on success.  If the function
+ *   returns false, the generic photo handler onSave function will
+ *   be called.
+ */
+
+var gGenericPhotoHandler =
+{
+
+  onLoad: function(aCard, aDocument)
+  {
+    return true;
+  },
+
+  onShow: function(aCard, aDocument, aTargetID)
+  {
+    aDocument.getElementById(aTargetID)
+             .setAttribute("src", defaultPhotoURI);
+    return true;
+  },
+
+  onSave: function(aCard, aDocument)
+  {
+    // If we had the photo saved locally, clear it.
+    removePhoto(aCard.getProperty("PhotoName", null));
+    aCard.setProperty("PhotoName", "");
+    aCard.setProperty("PhotoURI", "");
+    aCard.setProperty("PhotoType", "generic");
+    return true;
+  }
+};
+
+var gFilePhotoHandler =
+{
+
+  onLoad: function(aCard, aDocument)
+  {
+    var photoURI = aCard.getProperty("PhotoURI", "");
+    try
+    {
+      var file = Components.classes["@mozilla.org/network/io-service;1"]
+                           .getService(Components.interfaces.nsIIOService)
+                           .newURI(photoURI, null, null)
+                           .QueryInterface(Components.interfaces.nsIFileURL)
+                           .file;
+    } catch (e) {}
+
+    if (!file)
+      return false;
+
+    aDocument.getElementById("PhotoFile").file = file;
+    return true;
+  },
+
+  onShow: function(aCard, aDocument, aTargetID)
+  {
+    var file = aDocument.getElementById("PhotoFile").file;
+    try
+    {
+      var value = Components.classes["@mozilla.org/network/io-service;1"]
+                            .getService(Components.interfaces.nsIIOService)
+                            .newFileURI(file)
+                            .spec;
+    } catch (e) {}
+
+    if (!value)
+      return false;
+
+    aDocument.getElementById(aTargetID).setAttribute("src", value);
+    return true;
+  },
+
+  onSave: function(aCard, aDocument)
+  {
+    var file = aDocument.getElementById("PhotoFile").file;
+    if (!file)
+      return false;
+
+    var photoURI = Components.classes["@mozilla.org/network/io-service;1"]
+                             .getService(Components.interfaces.nsIIOService)
+                             .newFileURI(file)
+                             .spec;
+
+    var file = storePhoto(photoURI);
+
+    if (!file)
+      return false;
+
+    // Remove the original, if any
+    removePhoto(aCard.getProperty("PhotoName", null));
+    aCard.setProperty("PhotoName", file.leafName);
+    aCard.setProperty("PhotoType", "file");
+    aCard.setProperty("PhotoURI", photoURI);
+    return true;
+  }
+};
+
+var gWebPhotoHandler =
+{
+
+  onLoad: function(aCard, aDocument)
+  {
+    var photoURI = aCard.getProperty("PhotoURI", null);
+
+    if (!photoURI)
+      return false;
+
+    aDocument.getElementById("PhotoURI").value = photoURI;
+    return true;
+  },
+
+  onShow: function(aCard, aDocument, aTargetID)
+  {
+    var photoURI = aDocument.getElementById("PhotoURI").value;
+
+    if (!photoURI)
+      return false;
+
+    aDocument.getElementById(aTargetID).setAttribute("src", photoURI);
+    return true;
+  },
+
+  onSave: function(aCard, aDocument)
+  {
+    var photoURI = aDocument.getElementById("PhotoURI").value;
+
+    var file = storePhoto(photoURI);
+    if (!file)
+      return false;
+
+    // Remove the original, if any
+    removePhoto(aCard.getProperty("PhotoName", null));
+    aCard.setProperty("PhotoName", file.leafName);
+    aCard.setProperty("PhotoURI", photoURI);
+    aCard.setProperty("PhotoType", "web");
+    return true;
+  }
+};
+
+/* In order for other photo handlers to be recognized for
+ * a particular type, they must be registered through this
+ * function.
+ * @param aType the type of photo to handle
+ * @param aPhotoHandler the photo handler to register
+ */
+function registerPhotoHandler(aType, aPhotoHandler)
+{
+  gPhotoHandlers[aType] = aPhotoHandler;
+}
+
+registerPhotoHandler("generic", gGenericPhotoHandler);
+registerPhotoHandler("web", gWebPhotoHandler);
+registerPhotoHandler("file", gFilePhotoHandler);
--- a/suite/mailnews/addrbook/abCardOverlay.xul
+++ b/suite/mailnews/addrbook/abCardOverlay.xul
@@ -32,25 +32,26 @@
     <tabpanels id="abTabPanels" flex="1">
       <!-- ** Name Tab ** -->
       <!-- The following vbox contains two hboxes
            top: Name/Email/Phonenumber bottom: Email prefs. -->
       <vbox id="abNameTab" >
         <!-- This hbox contains two vboxes
              left: Name/Email, right: Phonenumbers -->
         <hbox>
-          <vbox> <!-- This box contains the Names and Emailnames -->
+          <vbox id="namesAndEmailAddresses">
+            <!-- This box contains the Names and Emailnames -->
 
             <!-- LOCALIZATION NOTE:
                  NameField1, NameField2, PhoneticField1, PhoneticField2
                  those fields are either LN or FN depends on the target country.
                  They are configurable in the .dtd file.
               -->
 
-            <hbox align="center">
+            <hbox id="NameField1Container" align="center">
               <spacer flex="1"/>
               <label control="&NameField1.id;" value="&NameField1.label;"
                      accesskey="&NameField1.accesskey;"/>
               <hbox class="CardEditWidth" align="center">
                 <textbox id="&NameField1.id;" flex="1"
                          oninput="GenerateDisplayName()"/>
 
                 <!-- LOCALIZATION NOTE:
@@ -59,17 +60,17 @@
                   -->
 
                 <spacer id="PhoneticSpacer1" flex="1" hidden="true"/>
                 <label id="PhoneticLabel1" control="&PhoneticField1.id;"
                        value="&PhoneticField1.label;" hidden="true"/>
                 <textbox id="&PhoneticField1.id;" flex="1" hidden="true"/>
               </hbox>
             </hbox>
-            <hbox align="center">
+            <hbox id="NameField2Container" align="center">
               <spacer flex="1"/>
               <label control="&NameField2.id;" value="&NameField2.label;"
                      accesskey="&NameField2.accesskey;"/>
               <hbox class="CardEditWidth" align="center">
                 <textbox id="&NameField2.id;" flex="1"
                          oninput="GenerateDisplayName()"/>
 
                 <!-- LOCALIZATION NOTE:
@@ -78,94 +79,94 @@
                   -->
 
                 <spacer id="PhoneticSpacer2" flex="1" hidden="true"/>
                 <label id="PhoneticLabel2" control="&PhoneticField2.id;"
                        value="&PhoneticField2.label;" hidden="true"/>
                 <textbox id="&PhoneticField2.id;" flex="1" hidden="true"/>
               </hbox>
             </hbox>
-            <hbox align="center">
+            <hbox id="DisplayNameContainer" align="center">
               <spacer flex="1"/>
               <label control="DisplayName" value="&DisplayName.label;"
                      accesskey="&DisplayName.accesskey;" />
               <hbox class="CardEditWidth">
                 <textbox id="DisplayName" flex="1"
                          oninput="DisplayNameChanged()"/>
               </hbox>
             </hbox>
-            <hbox align="center">
+            <hbox id="PreferDisplayNameContainer" align="center">
               <spacer flex="1"/>
               <hbox class="CardEditWidth">
                 <checkbox id="preferDisplayName"
                           label="&preferDisplayName.label;"
                           accesskey="&preferDisplayName.accesskey;"/>
               </hbox>
             </hbox>
 
-            <hbox id="nickNameContainer" align="center">
+            <hbox id="NickNameContainer" align="center">
               <spacer flex="1"/>
               <label control="NickName" value="&NickName.label;"
                      accesskey="&NickName.accesskey;"/>
               <hbox class="CardEditWidth">
                 <textbox id="NickName" flex="1"/>
               </hbox>
             </hbox>
-            <hbox align="center">
+            <hbox id="PrimaryEmailContainer" align="center">
               <spacer flex="1"/>
               <label control="PrimaryEmail" value="&PrimaryEmail.label;"
                      accesskey="&PrimaryEmail.accesskey;"/>
               <hbox class="CardEditWidth">
                 <textbox id="PrimaryEmail" flex="1" class="uri-element"/>
               </hbox>
             </hbox>
-            <hbox id="secondaryEmailContainer" align="center">
+            <hbox id="SecondaryEmailContainer" align="center">
               <spacer flex="1"/>
               <label control="SecondEmail" value="&SecondEmail.label;"
                      accesskey="&SecondEmail.accesskey;"/>
               <hbox class="CardEditWidth">
                 <textbox id="SecondEmail" flex="1" class="uri-element"/>
               </hbox>
             </hbox>
-            <hbox id="screenNameContainer" align="center">
+            <hbox id="ScreenNameContainer" align="center">
               <spacer flex="1"/>
               <label control="ScreenName" value="&ScreenName.label;"
                      accesskey="&ScreenName.accesskey;"/>
               <hbox class="CardEditWidth">
                 <textbox id="ScreenName" flex="1"/>
               </hbox>
             </hbox>
           </vbox> <!-- End of Name/Email -->
           <!-- Phone Number section -->
-          <vbox>
-            <hbox align="center">
+          <vbox id="PhoneNumbers">
+            <hbox id="WorkPhoneContainer" align="center">
               <spacer flex="1"/>
               <label control="WorkPhone" value="&WorkPhone.label;"
                      accesskey="&WorkPhone.accesskey;" />
               <textbox id="WorkPhone" class="PhoneEditWidth"/>
             </hbox>
-            <hbox align="center">
+            <hbox id="HomePhoneContainer" align="center">
               <spacer flex="1"/>
               <label control="HomePhone" value="&HomePhone.label;"
                      accesskey="&HomePhone.accesskey;"/>
               <textbox id="HomePhone" class="PhoneEditWidth"/>
             </hbox>
-            <hbox align="center">
+            <hbox id="FaxNumberContainer" align="center">
               <spacer flex="1"/>
               <label control="FaxNumber" value="&FaxNumber.label;"
                      accesskey="&FaxNumber.accesskey;"/>
               <textbox id="FaxNumber" class="PhoneEditWidth"/>
             </hbox>
-            <hbox align="center">
+            <hbox id="PagerNumberContainer" align="center">
               <spacer flex="1"/>
               <label control="PagerNumber" value="&PagerNumber.label;"
                      accesskey="&PagerNumber.accesskey;"/>
               <textbox id="PagerNumber" class="PhoneEditWidth"/>
             </hbox>
-            <hbox align="center">
+            <hbox id="CellularNumberContainer" align="center">
               <spacer flex="1"/>
               <label control="CellularNumber" value="&CellularNumber.label;"
                      accesskey="&CellularNumber.accesskey;"/>
               <textbox id="CellularNumber" class="PhoneEditWidth"/>
             </hbox>
           </vbox> <!-- End of Phonenumbers -->
         </hbox> <!-- End of Name/Email/Phonenumbers -->
         <!-- Email Preferences -->
@@ -202,21 +203,23 @@
         <hbox align="center">
           <spacer flex="1"/>
           <label control="HomeAddress2" value="&HomeAddress2.label;"
                  accesskey="&HomeAddress2.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="HomeAddress2" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="HomeCityContainer" align="center">
           <spacer flex="1"/>
           <label control="HomeCity" value="&HomeCity.label;"
                  accesskey="&HomeCity.accesskey;"/>
-          <hbox class="AddressCardEditWidth">
+          <hbox id="HomeCityFieldContainer"
+                class="AddressCardEditWidth"
+                align="center">
             <textbox id="HomeCity" flex="1"/>
           </hbox>
         </hbox>
         <hbox align="center">
           <spacer flex="1"/>
           <label control="HomeState" value="&HomeState.label;"
                  accesskey="&HomeState.accesskey;"/>
           <hbox align="center" class="AddressCardEditWidth">
@@ -230,17 +233,17 @@
         <hbox align="center">
           <spacer flex="1"/>
           <label control="HomeCountry" value="&HomeCountry.label;"
                  accesskey="&HomeCountry.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="HomeCountry" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WebPage2Container" align="center">
           <spacer flex="1"/>
           <label control="WebPage2" value="&HomeWebPage.label;"
                  accesskey="&HomeWebPage.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="WebPage2" flex="1" class="uri-element"/>
           </hbox>
         </hbox>
         <hbox id="birthdayField" align="center">
@@ -260,80 +263,82 @@
             <label value="&YearsOld.label;"/>
             <spacer flex="1"/>
           </hbox>
         </hbox>
       </vbox>
 
       <!-- ** Business Address Tab ** -->
       <vbox id="abBusinessTab" >
-        <hbox align="center">
+        <hbox id="JobTitleDepartmentContainer" align="center">
           <spacer flex="1"/>
           <label control="JobTitle" value="&JobTitle.label;"
                  accesskey="&JobTitle.accesskey;"/>
           <hbox class="AddressCardEditWidth" align="center">
             <textbox id="JobTitle" flex="1"/>
             <label control="Department" value="&Department.label;"
                    accesskey="&Department.accesskey;"/>
             <textbox id="Department" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="CompanyContainer" align="center">
           <spacer flex="1"/>
           <label control="Company" value="&Company.label;"
                  accesskey="&Company.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="Company" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WorkAddressContainer" align="center">
           <spacer flex="1"/>
           <label control="WorkAddress" value="&WorkAddress.label;"
                  accesskey="&WorkAddress.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="WorkAddress" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WorkAddress2Container" align="center">
           <spacer flex="1"/>
           <label control="WorkAddress2" value="&WorkAddress2.label;"
                  accesskey="&WorkAddress2.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="WorkAddress2" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WorkCityContainer" align="center">
           <spacer flex="1"/>
           <label control="WorkCity" value="&WorkCity.label;"
                  accesskey="&WorkCity.accesskey;"/>
-          <hbox class="AddressCardEditWidth">
+          <hbox id="WorkCityFieldContainer"
+                class="AddressCardEditWidth"
+                align="center">
             <textbox id="WorkCity" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WorkStateZipContainer" align="center">
           <spacer flex="1"/>
           <label control="WorkState" value="&WorkState.label;"
                  accesskey="&WorkState.accesskey;"/>
           <hbox class="AddressCardEditWidth" align="center">
             <textbox id="WorkState" flex="1"/>
             <spacer class="stateZipSpacer"/>
             <label control="WorkZipCode" value="&WorkZipCode.label;"
                    accesskey="&WorkZipCode.accesskey;"/>
             <textbox id="WorkZipCode" class="ZipWidth"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WorkCountryContainer" align="center">
           <spacer flex="1"/>
           <label control="WorkCountry" value="&WorkCountry.label;"
                  accesskey="&WorkCountry.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="WorkCountry" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WebPage1Container" align="center">
           <spacer flex="1"/>
           <label control="WebPage1" value="&WorkWebPage.label;"
                  accesskey="&WorkWebPage.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="WebPage1" flex="1" class="uri-element"/>
           </hbox>
         </hbox>
       </vbox>
@@ -369,43 +374,64 @@
 
       <!-- ** Photo Tab ** -->
       <hbox id="abPhotoTab" align="center">
         <description style="min-width: 25ch; max-width: 25ch; text-align: center">
           <html:img id="photo" style="max-width: 25ch; max-height: 25ch;"/>
         </description>
         <groupbox flex="1">
           <caption label="&PhotoDesc.label;"/>
-          <radiogroup id="PhotoType" onselect="updatePhoto();">
-            <radio value="generic" label="&GenericPhoto.label;"
-                   accesskey="&GenericPhoto.accesskey;"/>
-            <menulist id="GenericPhotoList" class="indent" flex="1"
-                      oncommand="updatePhoto('generic');">
-              <menupopup>
-                <menuitem label="&DefaultPhoto.label;" selected="true"
-                          value="chrome://messenger/skin/addressbook/icons/contact-generic.png"
-                          image="chrome://messenger/skin/addressbook/icons/contact-generic-tiny.png"/>
-              </menupopup>
-            </menulist>
-            <radio value="file" label="&PhotoFile.label;"
-                   accesskey="&PhotoFile.accesskey;"/>
-            <hbox class="indent">
-              <filefield id="PhotoFile" maxlength="255" flex="1"/>
-              <button oncommand="browsePhoto();" id="BrowsePhoto"
-                      label="&BrowsePhoto.label;"
-                      accesskey="&BrowsePhoto.accesskey;"/>
-            </hbox>
-            <radio value="web" label="&PhotoURL.label;"
-                   accesskey="&PhotoURL.accesskey;"/>
-            <hbox class="indent">
-              <textbox id="PhotoURI" maxlength="255" flex="1"
-                       placeholder="&PhotoURL.placeholder;"/>
-              <button oncommand="updatePhoto('web');" id="UpdatePhoto"
-                      label="&UpdatePhoto.label;"
-                      accesskey="&UpdatePhoto.accesskey;"/>
-            </hbox>
+          <radiogroup id="PhotoType" onselect="onSwitchPhotoType(this.value);">
+            <vbox id="GenericPhotoContainer">
+              <radio id="GenericPhotoType"
+                     value="generic"
+                     label="&GenericPhoto.label;"
+                     accesskey="&GenericPhoto.accesskey;"/>
+              <menulist id="GenericPhotoList"
+                        class="indent"
+                        flex="1"
+                        oncommand="onSwitchPhotoType('generic');">
+                <menupopup>
+                  <menuitem label="&DefaultPhoto.label;"
+                            selected="true"
+                            value="chrome://messenger/skin/addressbook/icons/contact-generic.png"
+                            image="chrome://messenger/skin/addressbook/icons/contact-generic-tiny.png"/>
+                </menupopup>
+              </menulist>
+            </vbox>
+
+            <vbox id="FilePhotoContainer">
+              <radio id="FilePhotoType"
+                     value="file"
+                     label="&PhotoFile.label;"
+                     accesskey="&PhotoFile.accesskey;"/>
+              <hbox class="indent">
+                <filefield id="PhotoFile" maxlength="255" flex="1"/>
+                <button id="BrowsePhoto"
+                        label="&BrowsePhoto.label;"
+                        accesskey="&BrowsePhoto.accesskey;"
+                        oncommand="browsePhoto();"/>
+              </hbox>
+            </vbox>
+
+            <vbox id="WebPhotoContainer">
+              <radio id="WebPhotoType"
+                     value="web"
+                     label="&PhotoURL.label;"
+                     accesskey="&PhotoURL.accesskey;"/>
+              <hbox class="indent">
+                <textbox id="PhotoURI"
+                         maxlength="255"
+                         flex="1"
+                         placeholder="&PhotoURL.placeholder;"/>
+                <button id="UpdatePhoto"
+                        label="&UpdatePhoto.label;"
+                        accesskey="&UpdatePhoto.accesskey;"
+                        oncommand="onSwitchPhotoType('web');"/>
+              </hbox>
+            </vbox>
           </radiogroup>
         </groupbox>
       </hbox>
     </tabpanels>
   </tabbox>
 </vbox>
 </overlay>
--- a/suite/mailnews/addrbook/abCardViewOverlay.js
+++ b/suite/mailnews/addrbook/abCardViewOverlay.js
@@ -9,16 +9,17 @@ var gPrefs = Components.classes["@mozill
 gPrefs = gPrefs.getService();
 gPrefs = gPrefs.QueryInterface(Components.interfaces.nsIPrefBranch);
 
 var gProfileDirURL;
 
 var gMapItURLFormat = GetLocalizedStringPref("mail.addr_book.mapit_url.format");
 
 var gFileHandler = Services.io.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
+var gPhotoDisplayHandlers = {};
 
 var zListName;
 var zPrimaryEmail;
 var zSecondaryEmail;
 var zScreenName;
 var zNickname;
 var zDisplayName;
 var zWork;
@@ -164,17 +165,17 @@ function DisplayCardViewPane(realCard)
                },
                primaryEmail : realCard.primaryEmail,
                displayName : realCard.displayName,
                isMailList : realCard.isMailList,
                mailListURI : realCard.mailListURI
   };
 
   // Contact photo
-  cvData.cvPhoto.setAttribute("src", getPhotoURI(card.getProperty("PhotoName")));
+  displayPhoto(card, cvData.cvPhoto);
 
   var titleString;
   if (generatedName == "")
     titleString = card.primaryEmail;  // if no generatedName, use email
   else
     titleString = generatedName;
 
   // set fields in card view pane
@@ -495,8 +496,64 @@ function CreateMapItURL(address1, addres
 
 function openLink(aEvent)
 {
   openAsExternal(aEvent.target.getAttribute("href"));
   // return false, so we don't load the href in the addressbook window
   return false;
 }
 
+/* Display the contact photo from the nsIAbCard in the IMG element.
+ * If the photo cannot be displayed, show the generic contact
+ * photo.
+ */
+function displayPhoto(aCard, aImg)
+{
+  var type = aCard.getProperty("PhotoType", "");
+  if (!gPhotoDisplayHandlers[type] ||
+      !gPhotoDisplayHandlers[type](aCard, aImg))
+    gPhotoDisplayHandlers["generic"](aCard, aImg);
+}
+
+/* In order to display the contact photos in the card view, there
+ * must be a registered photo display handler for the card photo
+ * type.  The generic, file, and web photo types are handled
+ * by default.
+ *
+ * A photo display handler is a function that behaves as follows:
+ *
+ * function(aCard, aImg):
+ *    The function is responsible for determining how to retrieve
+ *    the photo from nsIAbCard aCard, and for displaying it in img
+ *    img element aImg.  Returns true if successful.  If it returns
+ *    false, the generic photo display handler will be called.
+ *
+ * The following display handlers are for the generic, file and
+ * web photo types.
+ */
+
+var gGenericPhotoDisplayHandler = function(aCard, aImg)
+{
+  aImg.setAttribute("src", defaultPhotoURI);
+  return true;
+};
+
+var gPhotoNameDisplayHandler = function(aCard, aImg)
+{
+  var photoSrc = getPhotoURI(aCard.getProperty("PhotoName"));
+  aImg.setAttribute("src", photoSrc);
+  return true;
+};
+
+/* In order for a photo display handler to be registered for
+ * a particular photo type, it must be registered here.
+ */
+function registerPhotoDisplayHandler(aType, aPhotoDisplayHandler)
+{
+  if (!gPhotoDisplayHandlers[aType])
+    gPhotoDisplayHandlers[aType] = aPhotoDisplayHandler;
+}
+
+registerPhotoDisplayHandler("generic", gGenericPhotoDisplayHandler);
+// File and Web are treated the same, and therefore use the
+// same handler.
+registerPhotoDisplayHandler("file", gPhotoNameDisplayHandler);
+registerPhotoDisplayHandler("web", gPhotoNameDisplayHandler);
--- a/suite/mailnews/addrbook/abCommon.js
+++ b/suite/mailnews/addrbook/abCommon.js
@@ -696,17 +696,17 @@ function saveStreamToFile(aIStream, aFil
  * The URI is used to obtain a channel which is then opened synchronously and
  * this stream is written to the new file to store an offline, local copy of the
  * photo.
  *
  * @param aUri The URI of the photo.
  *
  * @return An nsIFile representation of the photo.
  */
-function savePhoto(aUri) {
+function storePhoto(aUri) {
   if (!aUri)
     return false;
 
   // Get the photos directory and check that it exists
   var file = getPhotosDir();
 
   // Create a channel from the URI and open it as an input stream
   var ios = Components.classes["@mozilla.org/network/io-service;1"]
--- a/suite/mailnews/addrbook/abListOverlay.xul
+++ b/suite/mailnews/addrbook/abListOverlay.xul
@@ -22,33 +22,33 @@
 <vbox id="editlist">
   <grid>
     <columns>
       <column/>
       <column class="CardEditWidth" flex="1"/>
     </columns>
 
     <rows>
-      <row align="center">
+      <row id="ListNameContainer" align="center">
         <label control="ListName"
                class="CardEditLabel"
                value="&ListName.label;"
                accesskey="&ListName.accesskey;"/>
         <textbox id="ListName"/>
       </row>
 
-      <row align="center">
+      <row id="ListNickNameContainer" align="center">
         <label control="ListNickName"
                class="CardEditLabel"
                value="&ListNickName.label;"
                accesskey="&ListNickName.accesskey;"/>
         <textbox id="ListNickName"/>
       </row>
 
-      <row align="center">
+      <row id="ListDescriptionContainer" align="center">
         <label control="ListDescription"
                class="CardEditLabel"
                value="&ListDescription.label;"
                accesskey="&ListDescription.accesskey;"/>
         <textbox id="ListDescription"/>
       </row>
     </rows>
   </grid>