merge the last green changeset on m-c to fx-team
authorTim Taubert <tim.taubert@gmx.de>
Tue, 06 Sep 2011 14:27:23 +0200
changeset 77966 2857288e9a3faa4660e1800a61fb887be2a85e95
parent 77965 19db4fd8a771eaf3065098336bcd7ef92b062fd0 (current diff)
parent 77880 1f5cd567c93a7ef6db27aa0539bdc2a4099935d0 (diff)
child 77967 ab1e3be27b4315cca736ec10486089786b2e3127
push id78
push userclegnitto@mozilla.com
push dateFri, 16 Dec 2011 17:32:24 +0000
treeherdermozilla-release@79d24e644fdd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone9.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge the last green changeset on m-c to fx-team
--- a/accessible/public/nsIAccessible.idl
+++ b/accessible/public/nsIAccessible.idl
@@ -51,17 +51,17 @@ interface nsIAccessibleRelation;
  * accessibility APIs like MSAA and ATK. Contains the sum of what's needed
  * to support IAccessible as well as ATK's generic accessibility objects.
  * Can also be used by in-process accessibility clients to get information
  * about objects in the accessible tree. The accessible tree is a subset of 
  * nodes in the DOM tree -- such as documents, focusable elements and text.
  * Mozilla creates the implementations of nsIAccessible on demand.
  * See http://www.mozilla.org/projects/ui/accessibility for more information.
  */
-[scriptable, uuid(c7ac764a-b4c5-4479-9fb7-06e3c9f3db34)]
+[scriptable, uuid(3126544c-826c-4694-a2ed-67bfe56a1f37)]
 interface nsIAccessible : nsISupports
 {
   /**
    * Parent node in accessible tree.
    */
   readonly attribute nsIAccessible parent;
 
   /**
@@ -217,36 +217,16 @@ interface nsIAccessible : nsISupports
   nsIAccessible getDeepestChildAtPoint(in long x, in long y);
 
   /**
    * Nth accessible child using zero-based index or last child if index less than zero
    */
   nsIAccessible getChildAt(in long aChildIndex);
 
   /**
-   * Accessible node geometrically to the right of this one
-   */
-  nsIAccessible getAccessibleToRight();
-
-  /**
-   * Accessible node geometrically to the left of this one
-   */
-  nsIAccessible getAccessibleToLeft();
-
-  /**
-   * Accessible node geometrically above this one
-   */
-  nsIAccessible getAccessibleAbove();
-
-  /**
-   * Accessible node geometrically below this one
-   */
-  nsIAccessible getAccessibleBelow();
-
-  /**
    * Return accessible relation by the given relation type (see.
    * constants defined in nsIAccessibleRelation).
    */
   nsIAccessibleRelation getRelationByType(in unsigned long aRelationType);
 
   /**
    * Returns multiple accessible relations for this object.
    */
--- a/accessible/src/base/nsAccessible.cpp
+++ b/accessible/src/base/nsAccessible.cpp
@@ -1969,40 +1969,16 @@ nsAccessible::DoAction(PRUint8 aIndex)
 }
 
 /* DOMString getHelp (); */
 NS_IMETHODIMP nsAccessible::GetHelp(nsAString& _retval)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
-/* nsIAccessible getAccessibleToRight(); */
-NS_IMETHODIMP nsAccessible::GetAccessibleToRight(nsIAccessible **_retval)
-{
-  return NS_ERROR_NOT_IMPLEMENTED;
-}
-
-/* nsIAccessible getAccessibleToLeft(); */
-NS_IMETHODIMP nsAccessible::GetAccessibleToLeft(nsIAccessible **_retval)
-{
-  return NS_ERROR_NOT_IMPLEMENTED;
-}
-
-/* nsIAccessible getAccessibleAbove(); */
-NS_IMETHODIMP nsAccessible::GetAccessibleAbove(nsIAccessible **_retval)
-{
-  return NS_ERROR_NOT_IMPLEMENTED;
-}
-
-/* nsIAccessible getAccessibleBelow(); */
-NS_IMETHODIMP nsAccessible::GetAccessibleBelow(nsIAccessible **_retval)
-{
-  return NS_ERROR_NOT_IMPLEMENTED;
-}
-
 nsIContent*
 nsAccessible::GetAtomicRegion() const
 {
   nsIContent *loopContent = mContent;
   nsAutoString atomic;
   while (loopContent && !loopContent->GetAttr(kNameSpaceID_None, nsAccessibilityAtoms::aria_atomic, atomic))
     loopContent = loopContent->GetParent();
 
--- a/accessible/src/msaa/nsAccessibleWrap.cpp
+++ b/accessible/src/msaa/nsAccessibleWrap.cpp
@@ -805,52 +805,45 @@ STDMETHODIMP nsAccessibleWrap::accNaviga
       /* [in] */ long navDir,
       /* [optional][in] */ VARIANT varStart,
       /* [retval][out] */ VARIANT __RPC_FAR *pvarEndUpAt)
 {
 __try {
   if (!pvarEndUpAt)
     return E_INVALIDARG;
 
-  nsAccessible *xpAccessibleStart = GetXPAccessibleFor(varStart);
-  if (!xpAccessibleStart || IsDefunct())
+  nsAccessible* accessible = GetXPAccessibleFor(varStart);
+  if (!accessible || accessible->IsDefunct())
     return E_FAIL;
 
   VariantInit(pvarEndUpAt);
 
-  nsCOMPtr<nsIAccessible> xpAccessibleResult;
+  nsAccessible* navAccessible = nsnull;
   PRUint32 xpRelation = 0;
 
   switch(navDir) {
-    case NAVDIR_DOWN:
-      xpAccessibleStart->GetAccessibleBelow(getter_AddRefs(xpAccessibleResult));
-      break;
     case NAVDIR_FIRSTCHILD:
-      if (!nsAccUtils::MustPrune(xpAccessibleStart))
-        xpAccessibleStart->GetFirstChild(getter_AddRefs(xpAccessibleResult));
+      if (!nsAccUtils::MustPrune(accessible))
+        navAccessible = accessible->FirstChild();
       break;
     case NAVDIR_LASTCHILD:
-      if (!nsAccUtils::MustPrune(xpAccessibleStart))
-        xpAccessibleStart->GetLastChild(getter_AddRefs(xpAccessibleResult));
-      break;
-    case NAVDIR_LEFT:
-      xpAccessibleStart->GetAccessibleToLeft(getter_AddRefs(xpAccessibleResult));
+      if (!nsAccUtils::MustPrune(accessible))
+        navAccessible = accessible->LastChild();
       break;
     case NAVDIR_NEXT:
-      xpAccessibleStart->GetNextSibling(getter_AddRefs(xpAccessibleResult));
+      navAccessible = accessible->NextSibling();
       break;
     case NAVDIR_PREVIOUS:
-      xpAccessibleStart->GetPreviousSibling(getter_AddRefs(xpAccessibleResult));
+      navAccessible = accessible->PrevSibling();
       break;
+    case NAVDIR_DOWN:
+    case NAVDIR_LEFT:
     case NAVDIR_RIGHT:
-      xpAccessibleStart->GetAccessibleToRight(getter_AddRefs(xpAccessibleResult));
-      break;
     case NAVDIR_UP:
-      xpAccessibleStart->GetAccessibleAbove(getter_AddRefs(xpAccessibleResult));
-      break;
+      return E_NOTIMPL;
 
     // MSAA relationship extensions to accNavigate
     case NAVRELATION_CONTROLLED_BY:
       xpRelation = nsIAccessibleRelation::RELATION_CONTROLLED_BY;
       break;
     case NAVRELATION_CONTROLLER_FOR:
       xpRelation = nsIAccessibleRelation::RELATION_CONTROLLER_FOR;
       break;
@@ -891,27 +884,30 @@ STDMETHODIMP nsAccessibleWrap::accNaviga
       xpRelation = nsIAccessibleRelation::RELATION_DEFAULT_BUTTON;
       break;
     case NAVRELATION_DESCRIBED_BY:
       xpRelation = nsIAccessibleRelation::RELATION_DESCRIBED_BY;
       break;
     case NAVRELATION_DESCRIPTION_FOR:
       xpRelation = nsIAccessibleRelation::RELATION_DESCRIPTION_FOR;
       break;
+
+    default:
+      return E_INVALIDARG;
   }
 
   pvarEndUpAt->vt = VT_EMPTY;
 
   if (xpRelation) {
     Relation rel = RelationByType(xpRelation);
-    xpAccessibleResult = rel.Next();
+    navAccessible = rel.Next();
   }
 
-  if (xpAccessibleResult) {
-    pvarEndUpAt->pdispVal = NativeAccessible(xpAccessibleResult);
+  if (navAccessible) {
+    pvarEndUpAt->pdispVal = NativeAccessible(navAccessible);
     pvarEndUpAt->vt = VT_DISPATCH;
     return S_OK;
   }
 } __except(nsAccessNodeWrap::FilterA11yExceptions(::GetExceptionCode(), GetExceptionInformation())) { }
   return E_FAIL;
 }
 
 STDMETHODIMP nsAccessibleWrap::accHitTest(
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -316,20 +316,18 @@ nsContextMenu.prototype = {
                   !this.onTextInput && top.gBidiUI);
   },
 
   initSpellingItems: function() {
     var canSpell = InlineSpellCheckerUI.canSpellCheck;
     var onMisspelling = InlineSpellCheckerUI.overMisspelling;
     this.showItem("spell-check-enabled", canSpell);
     this.showItem("spell-separator", canSpell || this.onEditableArea);
-    if (canSpell) {
-      document.getElementById("spell-check-enabled")
-              .setAttribute("checked", InlineSpellCheckerUI.enabled);
-    }
+    document.getElementById("spell-check-enabled")
+            .setAttribute("checked", canSpell && InlineSpellCheckerUI.enabled);
 
     this.showItem("spell-add-to-dictionary", onMisspelling);
 
     // suggestion list
     this.showItem("spell-suggestions-separator", onMisspelling);
     if (onMisspelling) {
       var suggestionsSeparator =
         document.getElementById("spell-add-to-dictionary");
--- a/browser/components/sessionstore/src/nsSessionStore.js
+++ b/browser/components/sessionstore/src/nsSessionStore.js
@@ -481,16 +481,21 @@ SessionStoreService.prototype = {
     case "domwindowclosed": // catch closed windows
       this.onClose(aSubject);
       break;
     case "quit-application-requested":
       // get a current snapshot of all windows
       this._forEachBrowserWindow(function(aWindow) {
         this._collectWindowData(aWindow);
       });
+      // we must cache this because _getMostRecentBrowserWindow will always
+      // return null by the time quit-application occurs
+      var activeWindow = this._getMostRecentBrowserWindow();
+      if (activeWindow)
+        this.activeWindowSSiCache = activeWindow.__SSi || "";
       this._dirtyWindows = [];
       break;
     case "quit-application-granted":
       // freeze the data at what we've got (ignoring closing windows)
       this._loadState = STATE_QUITTING;
       break;
     case "browser-lastwindow-close-granted":
       // last browser window is quitting.
@@ -1507,16 +1512,23 @@ SessionStoreService.prototype = {
 
     // We want to re-use the last opened window instead of opening a new one in
     // the case where it's "empty" and not associated with a window in the session.
     // We will do more processing via _prepWindowToRestoreInto if we need to use
     // the lastWindow.
     let lastWindow = this._getMostRecentBrowserWindow();
     let canUseLastWindow = lastWindow &&
                            !lastWindow.__SS_lastSessionWindowID;
+    let lastSessionFocusedWindow = null;
+    this.windowToFocus = lastWindow;
+
+    // move the last focused window to the start of the array so that we
+    // minimize window movement (see bug 669272)
+    lastSessionState.windows.unshift(
+      lastSessionState.windows.splice(lastSessionState.selectedWindow - 1, 1)[0]);
 
     // Restore into windows or open new ones as needed.
     for (let i = 0; i < lastSessionState.windows.length; i++) {
       let winState = lastSessionState.windows[i];
       let lastSessionWindowID = winState.__lastSessionWindowID;
       // delete lastSessionWindowID so we don't add that to the window again
       delete winState.__lastSessionWindowID;
 
@@ -1544,19 +1556,28 @@ SessionStoreService.prototype = {
         // Restore into that window - pretend it's a followup since we'll already
         // have a focused window.
         //XXXzpao This is going to merge extData together (taking what was in
         //        winState over what is in the window already. The hack we have
         //        in _preWindowToRestoreInto will prevent most (all?) Panorama
         //        weirdness but we will still merge other extData.
         //        Bug 588217 should make this go away by merging the group data.
         this.restoreWindow(windowToUse, { windows: [winState] }, canOverwriteTabs, true);
+        if (i == 0)
+          lastSessionFocusedWindow = windowToUse;
+
+        // if we overwrote the tabs for our last focused window, we should
+        // give focus to the window that had it in the previous session
+        if (canOverwriteTabs && windowToUse == lastWindow)
+          this.windowToFocus = lastSessionFocusedWindow;
       }
       else {
-        this._openWindowWithState({ windows: [winState] });
+        let win = this._openWindowWithState({ windows: [winState] });
+        if (i == 0)
+          lastSessionFocusedWindow = win;
       }
     }
 
     // Merge closed windows from this session with ones from last session
     if (lastSessionState._closedWindows) {
       this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows);
       this._capClosedWindows();
     }
@@ -2539,28 +2560,29 @@ SessionStoreService.prototype = {
     // We're not returning from this before we end up calling restoreHistoryPrecursor
     // for this window, so make sure we send the SSWindowStateBusy event.
     this._setWindowStateBusy(aWindow);
 
     if (root._closedWindows)
       this._closedWindows = root._closedWindows;
 
     var winData;
-    if (!aState.selectedWindow) {
-      aState.selectedWindow = 0;
+    if (!root.selectedWindow) {
+      root.selectedWindow = 0;
+    } else {
+      // put the selected window at the beginning of the array to ensure that
+      // it gets restored first
+      root.windows.unshift(root.windows.splice(root.selectedWindow - 1, 1)[0]);
     }
     // open new windows for all further window entries of a multi-window session
     // (unless they don't contain any tab data)
     for (var w = 1; w < root.windows.length; w++) {
       winData = root.windows[w];
       if (winData && winData.tabs && winData.tabs[0]) {
         var window = this._openWindowWithState({ windows: [winData] });
-        if (w == aState.selectedWindow - 1) {
-          this.windowToFocus = window;
-        }
       }
     }
     winData = root.windows[0];
     if (!winData.tabs) {
       winData.tabs = [];
     }
     // don't restore a single blank tab when we've had an external
     // URL passed in for loading at startup (cf. bug 357419)
--- a/browser/devtools/styleinspector/StyleInspector.jsm
+++ b/browser/devtools/styleinspector/StyleInspector.jsm
@@ -50,17 +50,23 @@ var StyleInspector = {
    * Is the Style Inspector enabled?
    * @returns {Boolean} true or false
    */
   get isEnabled()
   {
     return Services.prefs.getBoolPref("devtools.styleinspector.enabled");
   },
 
-  createPanel: function SI_createPanel()
+  /**
+   * Factory method to create the actual style panel
+   * @param {Boolean} aPreserveOnHide Prevents destroy from being called
+   * onpopuphide. USE WITH CAUTION: When this value is set to true then you are
+   * responsible to manually call destroy from outside the style inspector.
+   */
+  createPanel: function SI_createPanel(aPreserveOnHide)
   {
     let win = Services.wm.getMostRecentWindow("navigator:browser");
     let popupSet = win.document.getElementById("mainPopupSet");
     let ns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
     let panel = win.document.createElementNS(ns, "panel");
 
     panel.setAttribute("orient", "vertical");
     panel.setAttribute("ignorekeys", "true");
@@ -93,31 +99,45 @@ var StyleInspector = {
     spacer.setAttribute("flex", "1");
     hbox.appendChild(spacer);
 
     let resizer = win.document.createElement("resizer");
     resizer.setAttribute("dir", "bottomend");
     hbox.appendChild(resizer);
     popupSet.appendChild(panel);
 
-    panel.addEventListener("popupshown", function SI_popup_shown() {
+    /**
+     * Initialize the popup when it is first shown
+     */
+    function SI_popupShown() {
       if (!this.cssHtmlTree) {
         this.cssLogic = new CssLogic();
         this.cssHtmlTree = new CssHtmlTree(iframe, this.cssLogic, this);
       }
 
       this.cssLogic.highlight(this.selectedNode);
       this.cssHtmlTree.highlight(this.selectedNode);
       Services.obs.notifyObservers(null, "StyleInspector-opened", null);
-    }, false);
+    }
 
-    panel.addEventListener("popuphidden", function SI_popup_hidden() {
-      Services.obs.notifyObservers(null, "StyleInspector-closed", null);
-    }, false);
-    
+    /**
+     * Hide the popup and conditionally destroy it
+     */
+    function SI_popupHidden() {
+      if (panel.preserveOnHide) {
+        Services.obs.notifyObservers(null, "StyleInspector-closed", null);
+      } else {
+        panel.destroy();
+      }
+    }
+
+    panel.addEventListener("popupshown", SI_popupShown);
+    panel.addEventListener("popuphidden", SI_popupHidden);
+    panel.preserveOnHide = !!aPreserveOnHide;
+
     /**
      * Check if the style inspector is open
      */
     panel.isOpen = function SI_isOpen()
     {
       return this.state && this.state == "open";
     };
 
@@ -134,16 +154,29 @@ var StyleInspector = {
         this.cssHtmlTree.highlight(aNode);
       } else {
         let win = Services.wm.getMostRecentWindow("navigator:browser");
         this.openPopup(win.gBrowser.selectedBrowser, "end_before", 0, 0, false, false);
       }
     };
 
     /**
+     * Destroy the style panel, remove listeners etc.
+     */
+    panel.destroy = function SI_destroy()
+    {
+      this.cssLogic = null;
+      this.cssHtmlTree = null;
+      this.removeEventListener("popupshown", SI_popupShown);
+      this.removeEventListener("popuphidden", SI_popupHidden);
+      this.parentNode.removeChild(this);
+      Services.obs.notifyObservers(null, "StyleInspector-closed", null);
+    };
+
+    /**
      * Is the Style Inspector initialized?
      * @returns {Boolean} true or false
      */
     function isInitialized()
     {
       return panel.cssLogic && panel.cssHtmlTree;
     }
 
--- a/browser/devtools/webconsole/HUDService.jsm
+++ b/browser/devtools/webconsole/HUDService.jsm
@@ -1781,17 +1781,16 @@ HUD_SERVICE.prototype =
     let popupset = hud.chromeDocument.getElementById("mainPopupSet");
     let panels = popupset.querySelectorAll("panel[hudId=" + aHUDId + "]");
     for (let i = 0; i < panels.length; i++) {
       panels[i].hidePopup();
     }
     panels = popupset.querySelectorAll("panel[hudToolId=" + aHUDId + "]");
     for (let i = 0; i < panels.length; i++) {
       panels[i].hidePopup();
-      popupset.removeChild(panels[i]);
     }
 
     let id = ConsoleUtils.supString(aHUDId);
     Services.obs.notifyObservers(id, "web-console-destroyed", null);
 
     if (Object.keys(this.hudReferences).length == 0) {
       let autocompletePopup = hud.chromeDocument.
                               getElementById("webConsole_autocompletePopup");
--- a/content/svg/content/test/Makefile.in
+++ b/content/svg/content/test/Makefile.in
@@ -55,16 +55,17 @@ include $(topsrcdir)/config/rules.mk
 		a_href_helper_01.svg \
 		a_href_helper_02_03.svg \
 		a_href_helper_04.svg \
 		test_animLengthObjectIdentity.xhtml \
 		test_animLengthReadonly.xhtml \
 		test_animLengthUnits.xhtml \
 		test_bbox.xhtml \
 		test_bbox-with-invalid-viewBox.xhtml \
+		test_bounds.html \
 		bbox-helper.svg \
 		bounds-helper.svg \
 		test_dataTypes.html \
 		dataTypes-helper.svg \
 		getCTM-helper.svg \
 		test_getCTM.html \
 		test_getSubStringLength.xhtml \
 		getSubStringLength-helper.svg \
--- a/content/svg/content/test/bounds-helper.svg
+++ b/content/svg/content/test/bounds-helper.svg
@@ -2,29 +2,28 @@
 <svg xmlns="http://www.w3.org/2000/svg" width="750"
      xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
   <style type="text/css">
 text { font: 20px monospace; }
   </style>
 
 <g id="g">
   <text id="text1" x="25" y="25">abc</text>
+  <text id="text1a" x="85" y="25" stroke="black" stroke-width="4">abc</text>
   <rect id="rect1" x="50" y="50" width="50" height="50" fill="green"/>
-  <rect id="rect1a" x="50" y="50" width="50" height="50" fill="none" stroke-width="2" stroke="yellow"/>
+  <rect id="rect1a" x="50" y="50" width="50" height="50" fill="none" stroke-width="4" stroke="yellow"/>
   <text id="text2" x="125" y="25">abc</text>
+  <text id="text2a" x="185" y="25" stroke="black" stroke-width="10">abc</text>
   <g transform="rotate(45 175 75)">
     <rect id="rect2" x="150" y="50" width="50" height="50" fill="yellow"/>
-    <rect id="rect2a" x="150" y="50" width="50" height="50" fill="none" stroke-width="2" stroke="blue"/>
+    <rect id="rect2a" x="150" y="50" width="50" height="50" fill="none" stroke-width="4" stroke="blue"/>
     <text id="text3" x="150" y="50" text-anchor="middle">abc</text>
   </g>
   <g transform="scale(2)">
     <rect id="rect3" x="25" y="80" width="50" height="50" fill="green"/>
-    <rect id="rect3a" x="25" y="80" width="50" height="50" fill="none" stroke-width="2" stroke="blue"/>
+    <rect id="rect3a" x="25" y="80" width="50" height="50" fill="none" stroke-width="4" stroke="blue"/>
   </g>
   <g transform="scale(2) rotate(45 175 75)">
     <rect id="rect4" x="150" y="50" width="50" height="50" fill="yellow"/>
-    <rect id="rect4a" x="150" y="50" width="50" height="50" fill="none" stroke-width="2" stroke="blue"/>
-    <text id="text4" x="125" y="125">abc</text>
+    <rect id="rect4a" x="150" y="50" width="50" height="50" fill="none" stroke-width="4" stroke="blue"/>
   </g>
-  <text id="text1a" x="85" y="25" stroke="black" stroke-width="1">M</text>
-  <text id="text2a" x="185" y="25" stroke="black" stroke-width="10">M</text>
 </g>
 </svg>
--- a/content/svg/content/test/test_bounds.html
+++ b/content/svg/content/test/test_bounds.html
@@ -14,111 +14,130 @@ https://bugzilla.mozilla.org/show_bug.cg
 <div id="content" style="display: none"></div>
 
 <iframe id="svg" src="bounds-helper.svg"></iframe>
 
 <pre id="test">
 <script class="testbody" type="application/javascript">
 SimpleTest.waitForExplicitFinish();
 
+function Rect(left, top, width, height)
+{
+  this.left = left;
+  this.top = top;
+  this.width = width;
+  this.height = height;
+}
+
+Rect.prototype.roundOut = function()
+{
+  this.width = Math.ceil(this.left + this.width) - Math.floor(this.left);
+  this.height = Math.ceil(this.top + this.height) - Math.floor(this.top);
+  this.left = Math.floor(this.left);
+  this.top = Math.floor(this.top);
+}
+
+var delta = 1;
+
+function isApproximately(a, b, message)
+{
+  ok(delta >= Math.abs(a - b), message + " - got " + a + ", expected " + b + " ± " + delta);
+}
+
 function runTest()
 {
-  function isRounded(a, b, message) {
-    is (Math.round(a), Math.round(b), message);
-  }
+  var doc = $("svg").contentWindow.document;
 
-  var doc = $("svg").contentWindow.document;
-  
   var text1 = doc.getElementById("text1");
-  
-  var len = text1.getComputedTextLength();
-  
+
   var text1Bounds = text1.getBoundingClientRect();
   var text2Bounds = doc.getElementById("text2").getBoundingClientRect();
   var text3Bounds = doc.getElementById("text3").getBoundingClientRect();
-  var text4Bounds = doc.getElementById("text4").getBoundingClientRect();
 
   var sin45 = Math.sin(Math.PI / 4);
 
-  isRounded(text1Bounds.left, 25, "text1.getBoundingClientRect().left");
-  isRounded(text1Bounds.width, len, "text1.getBoundingClientRect().width");
+  isApproximately(text1Bounds.left, 24, "text1.getBoundingClientRect().left");
 
-  isRounded(text2Bounds.left, text1Bounds.left + 100, "text2.getBoundingClientRect().left");
-  isRounded(text2Bounds.top, text1Bounds.top, "text2.getBoundingClientRect().top");
-  isRounded(text2Bounds.width, text1Bounds.width, "text2.getBoundingClientRect().width");
-  isRounded(text2Bounds.height, text1Bounds.height, "text2.getBoundingClientRect().height");
+  is(text2Bounds.left, text1Bounds.left + 100, "text2.getBoundingClientRect().left");
+  is(text2Bounds.top, text1Bounds.top, "text2.getBoundingClientRect().top");
+  is(text2Bounds.width, text1Bounds.width, "text2.getBoundingClientRect().width");
+  is(text2Bounds.height, text1Bounds.height, "text2.getBoundingClientRect().height");
 
-  isRounded(text3Bounds.width, (text1Bounds.width + text1Bounds.height) * sin45 + .5, "text3.getBoundingClientRect().width");
-  isRounded(text3Bounds.height, (text1Bounds.height  + text1Bounds.width) * sin45 + .5, "text3.getBoundingClientRect().height");
-
-  isRounded(text4Bounds.width, 2 * (text1Bounds.width + text1Bounds.height) * sin45, "text4.getBoundingClientRect().width");
-  isRounded(text4Bounds.height, 2 * ((text1Bounds.height  + text1Bounds.width) * sin45 - .5), "text4.getBoundingClientRect().height");
+  var r = (text1Bounds.width + text1Bounds.height) * sin45;
+  isApproximately(text3Bounds.width, Math.ceil(r), "text3.getBoundingClientRect().width");
+  isApproximately(text3Bounds.height, Math.ceil(r), "text3.getBoundingClientRect().height");
 
   var rect1Bounds = doc.getElementById("rect1").getBoundingClientRect();
   var rect2Bounds = doc.getElementById("rect2").getBoundingClientRect();
   var rect3Bounds = doc.getElementById("rect3").getBoundingClientRect();
   var rect4Bounds = doc.getElementById("rect4").getBoundingClientRect();
-  
-  isRounded(rect1Bounds.left, 50, "rect1.getBoundingClientRect().left");
-  isRounded(rect1Bounds.top, 50, "rect1.getBoundingClientRect().top");
-  isRounded(rect1Bounds.width, 50, "rect1.getBoundingClientRect().width");
-  isRounded(rect1Bounds.height, 50, "rect1.getBoundingClientRect().height");
+
+  is(rect1Bounds.left, 50, "rect1.getBoundingClientRect().left");
+  is(rect1Bounds.top, 50, "rect1.getBoundingClientRect().top");
+  is(rect1Bounds.width, 50, "rect1.getBoundingClientRect().width");
+  is(rect1Bounds.height, 50, "rect1.getBoundingClientRect().height");
 
-  isRounded(rect2Bounds.left, 175 - 50 * sin45 - .5, "rect2.getBoundingClientRect().left");
-  isRounded(rect2Bounds.top, 75 - 50 * sin45 - .5, "rect2.getBoundingClientRect().top");
-  isRounded(rect2Bounds.width, (50 * sin45 + .5) * 2, "rect2.getBoundingClientRect().width");
-  isRounded(rect2Bounds.height, (50 * sin45 + .5) * 2, "rect2.getBoundingClientRect().height");
+  rect = new Rect(175 - 50 * sin45, 75 - 50 * sin45, 50 * sin45 * 2, 50 * sin45 * 2);
+  rect.roundOut();
+  is(rect2Bounds.left, rect.left, "rect2.getBoundingClientRect().left");
+  is(rect2Bounds.top, rect.top, "rect2.getBoundingClientRect().top");
+  is(rect2Bounds.width, rect.width, "rect2.getBoundingClientRect().width");
+  is(rect2Bounds.height, rect.height, "rect2.getBoundingClientRect().height");
 
-  isRounded(rect3Bounds.left, 50, "rect3.getBoundingClientRect().left");
-  isRounded(rect3Bounds.top, 160, "rect3.getBoundingClientRect().top");
-  isRounded(rect3Bounds.width, 100, "rect3.getBoundingClientRect().width");
-  isRounded(rect3Bounds.height, 100, "rect3.getBoundingClientRect().height");
+  is(rect3Bounds.left, 50, "rect3.getBoundingClientRect().left");
+  is(rect3Bounds.top, 160, "rect3.getBoundingClientRect().top");
+  is(rect3Bounds.width, 100, "rect3.getBoundingClientRect().width");
+  is(rect3Bounds.height, 100, "rect3.getBoundingClientRect().height");
 
-  isRounded(rect4Bounds.left, 350 - 100 * sin45 - .5, "rect4.getBoundingClientRect().left");
-  isRounded(rect4Bounds.top, 150 - 100 * sin45 - .5, "rect4.getBoundingClientRect().top");
-  isRounded(rect4Bounds.width, (100 * sin45 + .5) * 2, "rect4.getBoundingClientRect().width");
-  isRounded(rect4Bounds.height, (100 * sin45 + .5) * 2, "rect4.getBoundingClientRect().height");
+  rect = new Rect(350 - 100 * sin45, 150 - 100 * sin45, 100 * sin45 * 2, 100 * sin45 * 2);
+  rect.roundOut();
+  is(rect4Bounds.left, rect.left, "rect4.getBoundingClientRect().left");
+  is(rect4Bounds.top, rect.top, "rect4.getBoundingClientRect().top");
+  is(rect4Bounds.width, rect.width, "rect4.getBoundingClientRect().width");
+  is(rect4Bounds.height, rect.height, "rect4.getBoundingClientRect().height");
 
   var rect1aBounds = doc.getElementById("rect1a").getBoundingClientRect();
   var rect2aBounds = doc.getElementById("rect2a").getBoundingClientRect();
   var rect3aBounds = doc.getElementById("rect3a").getBoundingClientRect();
   var rect4aBounds = doc.getElementById("rect4a").getBoundingClientRect();
-  
-  isRounded(rect1aBounds.left, 49, "rect1a.getBoundingClientRect().left");
-  isRounded(rect1aBounds.top, 49, "rect1a.getBoundingClientRect().top");
-  isRounded(rect1aBounds.width, 52, "rect1a.getBoundingClientRect().width");
-  isRounded(rect1aBounds.height, 52, "rect1a.getBoundingClientRect().height");
+
+  is(rect1aBounds.left, 48, "rect1a.getBoundingClientRect().left");
+  is(rect1aBounds.top, 48, "rect1a.getBoundingClientRect().top");
+  is(rect1aBounds.width, 54, "rect1a.getBoundingClientRect().width");
+  is(rect1aBounds.height, 54, "rect1a.getBoundingClientRect().height");
 
-  isRounded(rect2aBounds.left, 175 - 52 * sin45 - .5, "rect2a.getBoundingClientRect().left");
-  isRounded(rect2aBounds.top, 75 - 52 * sin45 - .5, "rect2a.getBoundingClientRect().top");
-  isRounded(rect2aBounds.width, 52 * sin45 * 2, "rect2a.getBoundingClientRect().width");
-  isRounded(rect2aBounds.height, 52 * sin45 * 2, "rect2a.getBoundingClientRect().height");
+  rect = new Rect(175 - 54 * sin45, 75 - 54 * sin45, 54 * sin45 * 2, 54 * sin45 * 2);
+  rect.roundOut();
+  is(rect2aBounds.left, rect.left, "rect2a.getBoundingClientRect().left");
+  is(rect2aBounds.top, rect.top, "rect2a.getBoundingClientRect().top");
+  is(rect2aBounds.width, rect.width, "rect2a.getBoundingClientRect().width");
+  is(rect2aBounds.height, rect.height, "rect2a.getBoundingClientRect().height");
 
-  isRounded(rect3aBounds.left, 48, "rect3a.getBoundingClientRect().left");
-  isRounded(rect3aBounds.top, 158, "rect3a.getBoundingClientRect().top");
-  isRounded(rect3aBounds.width, 104, "rect3a.getBoundingClientRect().width");
-  isRounded(rect3aBounds.height, 104, "rect3a.getBoundingClientRect().height");
+  is(rect3aBounds.left, 46, "rect3a.getBoundingClientRect().left");
+  is(rect3aBounds.top, 156, "rect3a.getBoundingClientRect().top");
+  is(rect3aBounds.width, 108, "rect3a.getBoundingClientRect().width");
+  is(rect3aBounds.height, 108, "rect3a.getBoundingClientRect().height");
 
-  isRounded(rect4aBounds.left, 350 - 104 * sin45 - .5, "rect4a.getBoundingClientRect().left");
-  isRounded(rect4aBounds.top, 150 - 104 * sin45 - .5, "rect4a.getBoundingClientRect().top");
-  isRounded(rect4aBounds.width, (104 * sin45 + .5) * 2, "rect4a.getBoundingClientRect().width");
-  isRounded(rect4aBounds.height, (104 * sin45 + .5) * 2, "rect4a.getBoundingClientRect().height");
+  rect = new Rect(350 - 108 * sin45, 150 - 108 * sin45, 108 * sin45 * 2, 108 * sin45 * 2);
+  rect.roundOut();
+  is(rect4aBounds.left, rect.left, "rect4a.getBoundingClientRect().left");
+  is(rect4aBounds.top, rect.top, "rect4a.getBoundingClientRect().top");
+  is(rect4aBounds.width, rect.width, "rect4a.getBoundingClientRect().width");
+  is(rect4aBounds.height, rect.height, "rect4a.getBoundingClientRect().height");
 
   var text1a = doc.getElementById("text1a");
-  
+
   var text1aBounds = text1a.getBoundingClientRect();
   var text2aBounds = doc.getElementById("text2a").getBoundingClientRect();
 
-  var len = text1a.getComputedTextLength();
+  isApproximately(text1aBounds.left, 82, "text1a.getBoundingClientRect().left");
+  is(text1aBounds.width, text1Bounds.width + 4, "text1a.getBoundingClientRect().width");
 
-  isRounded(text1aBounds.left, 85 - 1, "text1a.getBoundingClientRect().left");
-  isRounded(text1aBounds.width, len + 1, "text1a.getBoundingClientRect().width");
-  
-  isRounded(text2aBounds.left, text1aBounds.left + 100 - 4, "text2a.getBoundingClientRect().left");
-  isRounded(text2aBounds.width, text1aBounds.width + 9, "text2a.getBoundingClientRect().width");
+  is(text2aBounds.left, text1aBounds.left + 100 - 3, "text2a.getBoundingClientRect().left");
+  is(text2aBounds.width, text1aBounds.width + 6, "text2a.getBoundingClientRect().width");
 
   SimpleTest.finish();
 }
 
 window.addEventListener("load", runTest, false);
 </script>
 </pre>
 </body>
--- a/editor/composer/src/nsEditorSpellCheck.cpp
+++ b/editor/composer/src/nsEditorSpellCheck.cpp
@@ -645,16 +645,24 @@ nsEditorSpellCheck::SetCurrentDictionary
 
     // Also store it in as a preference. It will be used as a default value
     // when everything else fails.
     Preferences::SetString("spellchecker.dictionary", aDictionary);
   }
   return mSpellChecker->SetCurrentDictionary(aDictionary);
 }
 
+NS_IMETHODIMP
+nsEditorSpellCheck::GetSpellChecker(nsISpellChecker **aSpellChecker)
+{
+  *aSpellChecker = mSpellChecker;
+  NS_IF_ADDREF(*aSpellChecker);
+  return NS_OK;
+}
+
 NS_IMETHODIMP    
 nsEditorSpellCheck::UninitSpellChecker()
 {
   NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
 
   // Cleanup - kill the spell checker
   DeleteSuggestedWordList();
   mDictionaryList.Clear();
@@ -813,21 +821,17 @@ nsEditorSpellCheck::UpdateCurrentDiction
   // lang attribute, we try to get a dictionary. First try, en-US. If it does
   // not work, pick the first one.
   if (mPreferredLang.IsEmpty()) {
     nsAutoString currentDictionary;
     rv = GetCurrentDictionary(currentDictionary);
     if (NS_FAILED(rv) || currentDictionary.IsEmpty()) {
       rv = SetCurrentDictionary(NS_LITERAL_STRING("en-US"));
       if (NS_FAILED(rv)) {
-        nsTArray<nsString> dictList;
-        rv = mSpellChecker->GetDictionaryList(&dictList);
-        if (NS_SUCCEEDED(rv) && dictList.Length() > 0) {
-          SetCurrentDictionary(dictList[0]);
-        }
+        mSpellChecker->CheckCurrentDictionary();
       }
     }
   }
 
   // If an error was thrown while setting the dictionary, just
   // fail silently so that the spellchecker dialog is allowed to come
   // up. The user can manually reset the language to their choice on
   // the dialog if it is wrong.
--- a/editor/composer/test/test_bug434998.xul
+++ b/editor/composer/test/test_bug434998.xul
@@ -60,16 +60,17 @@ https://bugzilla.mozilla.org/show_bug.cg
           // Should not throw
           var threw = false;
           try {
             this.mEditor.contentDocument.execCommand("bold", false, null);
           } catch (e) {
             threw = true;
           }
           ok(!threw, "The execCommand API should work on <xul:editor>");
+          progress.removeProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
           SimpleTest.finish();
         }
       }
     },
 
 
     onProgressChange : function(aWebProgress, aRequest,
                                 aCurSelfProgress, aMaxSelfProgress,
--- a/editor/idl/nsIEditorSpellCheck.idl
+++ b/editor/idl/nsIEditorSpellCheck.idl
@@ -35,21 +35,30 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
  
 #include "nsISupports.idl"
 
 interface nsIEditor;
 interface nsITextServicesFilter;
+%{C++
+#include "nsISpellChecker.h"
+%}
+[ptr] native nsISpellChecker(nsISpellChecker);
 
-[scriptable, uuid(af84da62-588f-409f-847d-feecc991bd93)]
+[scriptable, uuid(334946c3-0e93-4aac-b662-e1b56f95d68b)]
 interface nsIEditorSpellCheck : nsISupports
 {
 
+  /**
+   * Get the spell checker used by this editor.
+   */
+  [noscript] readonly attribute nsISpellChecker spellChecker;
+
  /**
    * Returns true if we can enable spellchecking. If there are no available
    * dictionaries, this will return false.
    */
   boolean       canSpellCheck();
 
   /**
    * Turns on the spell checker for the given editor. enableSelectionChecking
--- a/editor/libeditor/base/Makefile.in
+++ b/editor/libeditor/base/Makefile.in
@@ -89,9 +89,10 @@ FORCE_STATIC_LIB = 1
 
 include $(topsrcdir)/config/rules.mk
 
 INCLUDES	+= \
 		-I$(topsrcdir)/editor/libeditor/text \
 		-I$(topsrcdir)/content/base/src \
 		-I$(topsrcdir)/content/events/src \
 		-I$(topsrcdir)/layout/style \
+		-I$(topsrcdir)/extensions/spellcheck/src \
 		$(NULL)
--- a/editor/libeditor/base/nsEditor.cpp
+++ b/editor/libeditor/base/nsEditor.cpp
@@ -19,16 +19,17 @@
  * Portions created by the Initial Developer are Copyright (C) 1998
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Pierre Phaneuf <pp@ludusdesign.com>
  *   Daniel Glazman <glazman@netscape.com>
  *   Masayuki Nakano <masayuki@d-toybox.com>
  *   Mats Palmgren <matspal@gmail.com>
+ *   Jesper Kristensen <mail@jesperkristensen.dk>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either of the GNU General Public License Version 2 or later (the "GPL"),
  * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -43,16 +44,21 @@
 #include "nsIDOMDocument.h"
 #include "nsIDOMHTMLElement.h"
 #include "nsIDOMNSHTMLElement.h"
 #include "nsIDOMNSEvent.h"
 #include "nsIMEStateManager.h"
 #include "nsFocusManager.h"
 #include "nsUnicharUtils.h"
 #include "nsReadableUtils.h"
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
+#include "mozISpellCheckingEngine.h"
+#include "nsIEditorSpellCheck.h"
+#include "mozInlineSpellChecker.h"
 
 #include "nsIDOMText.h"
 #include "nsIDOMElement.h"
 #include "nsIDOMAttr.h"
 #include "nsIDOMNode.h"
 #include "nsIDOMDocumentFragment.h"
 #include "nsIDOMNamedNodeMap.h"
 #include "nsIDOMNodeList.h"
@@ -202,16 +208,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mEventListener)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsEditor)
  NS_INTERFACE_MAP_ENTRY(nsIPhonetic)
  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
  NS_INTERFACE_MAP_ENTRY(nsIEditorIMESupport)
  NS_INTERFACE_MAP_ENTRY(nsIEditor)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditor)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsEditor)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsEditor)
 
 
 NS_IMETHODIMP
@@ -295,16 +302,23 @@ nsEditor::PostCreate()
 
     // nuke the modification count, so the doc appears unmodified
     // do this before we notify listeners
     ResetModificationCount();
 
     // update the UI with our state
     NotifyDocumentListeners(eDocumentCreated);
     NotifyDocumentListeners(eDocumentStateChanged);
+
+    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+    if (obs) {
+      obs->AddObserver(this,
+                       SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION,
+                       PR_FALSE);
+    }
   }
 
   // update nsTextStateManager and caret if we have focus
   nsCOMPtr<nsIContent> focusedContent = GetFocusedContent();
   if (focusedContent) {
     nsCOMPtr<nsIPresShell> ps = GetPresShell();
     NS_ASSERTION(ps, "no pres shell even though we have focus");
     NS_ENSURE_TRUE(ps, NS_ERROR_UNEXPECTED);
@@ -407,16 +421,22 @@ nsEditor::GetDesiredSpellCheckState()
 }
 
 NS_IMETHODIMP
 nsEditor::PreDestroy(PRBool aDestroyingFrames)
 {
   if (mDidPreDestroy)
     return NS_OK;
 
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    obs->RemoveObserver(this,
+                        SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION);
+  }
+
   // Let spellchecker clean up its observers etc. It is important not to
   // actually free the spellchecker here, since the spellchecker could have
   // caused flush notifications, which could have gotten here if a textbox
   // is being removed. Setting the spellchecker to NULL could free the
   // object that is still in use! It will be freed when the editor is
   // destroyed.
   if (mInlineSpellChecker)
     mInlineSpellChecker->Cleanup(aDestroyingFrames);
@@ -1278,16 +1298,23 @@ NS_IMETHODIMP nsEditor::GetInlineSpellCh
 
   if (mDidPreDestroy) {
     // Don't allow people to get or create the spell checker once the editor
     // is going away.
     *aInlineSpellChecker = nsnull;
     return autoCreate ? NS_ERROR_NOT_AVAILABLE : NS_OK;
   }
 
+  // We don't want to show the spell checking UI if there are no spell check dictionaries available.
+  PRBool canSpell = mozInlineSpellChecker::CanEnableInlineSpellChecking();
+  if (!canSpell) {
+    *aInlineSpellChecker = nsnull;
+    return NS_ERROR_FAILURE;
+  }
+
   nsresult rv;
   if (!mInlineSpellChecker && autoCreate) {
     mInlineSpellChecker = do_CreateInstance(MOZ_INLINESPELLCHECKER_CONTRACTID, &rv);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   if (mInlineSpellChecker) {
     rv = mInlineSpellChecker->Init(this);
@@ -1296,27 +1323,59 @@ NS_IMETHODIMP nsEditor::GetInlineSpellCh
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   NS_IF_ADDREF(*aInlineSpellChecker = mInlineSpellChecker);
 
   return NS_OK;
 }
 
+NS_IMETHODIMP nsEditor::Observe(nsISupports* aSubj, const char *aTopic,
+                                const PRUnichar *aData)
+{
+  NS_ASSERTION(!strcmp(aTopic,
+                       SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION),
+               "Unexpected observer topic");
+
+  // When mozInlineSpellChecker::CanEnableInlineSpellChecking changes
+  SyncRealTimeSpell();
+
+  // When nsIEditorSpellCheck::GetCurrentDictionary changes
+  if (mInlineSpellChecker) {
+    // if the current dictionary is no longer available, find another one
+    nsCOMPtr<nsIEditorSpellCheck> editorSpellCheck;
+    mInlineSpellChecker->GetSpellChecker(getter_AddRefs(editorSpellCheck));
+    if (editorSpellCheck) {
+      nsCOMPtr<nsISpellChecker> spellChecker;
+      editorSpellCheck->GetSpellChecker(getter_AddRefs(spellChecker));
+      spellChecker->CheckCurrentDictionary();
+    }
+
+    // update the inline spell checker to reflect the new current dictionary
+    mInlineSpellChecker->SpellCheckRange(nsnull); // causes recheck
+  }
+
+  return NS_OK;
+}
+
 NS_IMETHODIMP nsEditor::SyncRealTimeSpell()
 {
   NS_TIME_FUNCTION;
 
   PRBool enable = GetDesiredSpellCheckState();
 
+  // Initializes mInlineSpellChecker
   nsCOMPtr<nsIInlineSpellChecker> spellChecker;
   GetInlineSpellChecker(enable, getter_AddRefs(spellChecker));
 
-  if (spellChecker) {
-    spellChecker->SetEnableRealTimeSpell(enable);
+  if (mInlineSpellChecker) {
+    // We might have a mInlineSpellChecker even if there are no dictionaries
+    // available since we don't destroy the mInlineSpellChecker when the last
+    // dictionariy is removed, but in that case spellChecker is null
+    mInlineSpellChecker->SetEnableRealTimeSpell(enable && spellChecker);
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP nsEditor::SetSpellcheckUserOverride(PRBool enable)
 {
   mSpellcheckCheckboxState = enable ? eTriTrue : eTriFalse;
--- a/editor/libeditor/base/nsEditor.h
+++ b/editor/libeditor/base/nsEditor.h
@@ -61,16 +61,17 @@
 #include "nsIDOMElement.h"
 #include "nsSelectionState.h"
 #include "nsIEditorSpellCheck.h"
 #include "nsIInlineSpellChecker.h"
 #include "nsIDOMEventTarget.h"
 #include "nsStubMutationObserver.h"
 #include "nsIViewManager.h"
 #include "nsCycleCollectionParticipant.h"
+#include "nsIObserver.h"
 
 class nsIDOMCharacterData;
 class nsIDOMRange;
 class nsIPresShell;
 class ChangeAttributeTxn;
 class CreateElementTxn;
 class InsertElementTxn;
 class DeleteElementTxn;
@@ -95,16 +96,17 @@ class nsIDOMNSEvent;
 /** implementation of an editor object.  it will be the controller/focal point 
  *  for the main editor services. i.e. the GUIManager, publishing, transaction 
  *  manager, event interfaces. the idea for the event interfaces is to have them 
  *  delegate the actual commands to the editor independent of the XPFE implementation.
  */
 class nsEditor : public nsIEditor,
                  public nsIEditorIMESupport,
                  public nsSupportsWeakReference,
+                 public nsIObserver,
                  public nsIPhonetic
 {
 public:
 
   enum IterDirection
   {
     kIterForward,
     kIterBackward
@@ -148,16 +150,19 @@ public:
   already_AddRefed<nsIPresShell> GetPresShell();
   void NotifyEditorObservers();
 
   /* ------------ nsIEditor methods -------------- */
   NS_DECL_NSIEDITOR
   /* ------------ nsIEditorIMESupport methods -------------- */
   NS_DECL_NSIEDITORIMESUPPORT
   
+  /* ------------ nsIObserver methods -------------- */
+  NS_DECL_NSIOBSERVER
+
   // nsIPhonetic
   NS_DECL_NSIPHONETIC
 
 public:
 
   
   NS_IMETHOD InsertTextImpl(const nsAString& aStringToInsert, 
                                nsCOMPtr<nsIDOMNode> *aInOutNode, 
--- a/editor/txtsvc/public/nsISpellChecker.h
+++ b/editor/txtsvc/public/nsISpellChecker.h
@@ -39,19 +39,19 @@
 #define nsISpellChecker_h__
 
 #include "nsISupports.h"
 #include "nsTArray.h"
 
 #define NS_SPELLCHECKER_CONTRACTID "@mozilla.org/spellchecker;1"
 
 #define NS_ISPELLCHECKER_IID                    \
-{ /* E75AC48C-E948-452E-8DB3-30FEE29FE3D2 */    \
-0xe75ac48c, 0xe948, 0x452e, \
-  { 0x8d, 0xb3, 0x30, 0xfe, 0xe2, 0x9f, 0xe3, 0xd2 } }
+{ /* 27bff957-b486-40ae-9f5d-af0cdd211868 */    \
+0x27bff957, 0xb486, 0x40ae, \
+  { 0x9f, 0x5d, 0xaf, 0x0c, 0xdd, 0x21, 0x18, 0x68 } }
 
 class nsITextServicesDocument;
 class nsString;
 
 /**
  * A generic interface for a spelling checker.
  */
 class nsISpellChecker  : public nsISupports{
@@ -141,14 +141,20 @@ public:
 
   /**
    * Tells the spellchecker to use a specific dictionary.
    * @param aDictionary a string that is in the list returned
    * by GetDictionaryList() or an empty string. If aDictionary is 
    * empty string, spellchecker will be disabled.
    */
   NS_IMETHOD SetCurrentDictionary(const nsAString &aDictionary) = 0;
+
+  /**
+   * Call this on any change in installed dictionaries to ensure that the spell
+   * checker is not using a current dictionary which is no longer available.
+   */
+  NS_IMETHOD CheckCurrentDictionary() = 0;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsISpellChecker, NS_ISPELLCHECKER_IID)
 
 #endif // nsISpellChecker_h__
 
--- a/embedding/android/GeckoApp.java
+++ b/embedding/android/GeckoApp.java
@@ -462,19 +462,25 @@ abstract public class GeckoApp
         File applicationPackage = new File(getApplication().getPackageResourcePath());
         File componentsDir = new File(sGREDir, "components");
         if (componentsDir.lastModified() == applicationPackage.lastModified())
             return;
 
         componentsDir.mkdir();
         componentsDir.setLastModified(applicationPackage.lastModified());
 
+        surfaceView.mSplashStatusMsg =
+                    getResources().getString(R.string.splash_firstrun);
+        surfaceView.drawSplashScreen();
+
+        GeckoAppShell.killAnyZombies();
+
         ZipFile zip = new ZipFile(applicationPackage);
 
-        byte[] buf = new byte[8192];
+        byte[] buf = new byte[32768];
         try {
             if (unpackFile(zip, buf, null, "removed-files"))
                 removeFiles();
         } catch (Exception ex) {
             // This file may not be there, so just log any errors and move on
             Log.w(LOG_FILE_NAME, "error removing files", ex);
         }
         unpackFile(zip, buf, null, "application.ini");
@@ -515,45 +521,33 @@ abstract public class GeckoApp
                     removedFile.delete();
             }
         } finally {
             reader.close();
         }
         
     }
 
-    boolean haveKilledZombies = false;
-
     private boolean unpackFile(ZipFile zip, byte[] buf, ZipEntry fileEntry,
                             String name)
         throws IOException, FileNotFoundException
     {
         if (fileEntry == null)
             fileEntry = zip.getEntry(name);
         if (fileEntry == null)
             throw new FileNotFoundException("Can't find " + name + " in " +
                                             zip.getName());
 
         File outFile = new File(sGREDir, name);
-        if (outFile.exists() &&
-            outFile.lastModified() == fileEntry.getTime() &&
+        if (outFile.lastModified() == fileEntry.getTime() &&
             outFile.length() == fileEntry.getSize())
             return false;
 
-        surfaceView.mSplashStatusMsg =
-                    getResources().getString(R.string.splash_firstrun);
-        surfaceView.drawSplashScreen();
-
-        if (!haveKilledZombies) {
-            haveKilledZombies = true;
-            GeckoAppShell.killAnyZombies();
-        }
-
         File dir = outFile.getParentFile();
-        if (!outFile.exists())
+        if (!dir.exists())
             dir.mkdirs();
 
         InputStream fileStream;
         fileStream = zip.getInputStream(fileEntry);
 
         OutputStream outStream = new FileOutputStream(outFile);
 
         while (fileStream.available() > 0) {
--- a/extensions/spellcheck/Makefile.in
+++ b/extensions/spellcheck/Makefile.in
@@ -39,9 +39,13 @@ topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE		= spellchecker
 DIRS		= idl locales hunspell src
 
+ifdef ENABLE_TESTS
+DIRS		+= tests
+endif
+
 include $(topsrcdir)/config/rules.mk
--- a/extensions/spellcheck/hunspell/src/Makefile.in
+++ b/extensions/spellcheck/hunspell/src/Makefile.in
@@ -66,11 +66,13 @@ CPPSRCS         += affentry.cpp \
 
 # This variable is referenced in configure.in.  Make sure to change that file
 # too if you need to change this variable.
 DEFINES = -DHUNSPELL_STATIC
 endif
 
 include $(topsrcdir)/config/rules.mk
 
+INCLUDES        += -I$(topsrcdir)/extensions/spellcheck/src
+
 ifdef MOZ_NATIVE_HUNSPELL
 CXXFLAGS += $(MOZ_HUNSPELL_CFLAGS)
 endif
--- a/extensions/spellcheck/hunspell/src/mozHunspell.cpp
+++ b/extensions/spellcheck/hunspell/src/mozHunspell.cpp
@@ -36,16 +36,17 @@
  *                 Varga Daniel
  *                 Chris Halls
  *                 Rene Engelhard
  *                 Bram Moolenaar
  *                 Dafydd Jones
  *                 Harri Pitkanen
  *                 Andras Timar
  *                 Tor Lillqvist
+ *                 Jesper Kristensen (mail@jesperkristensen.dk)
  *
  * 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
@@ -65,16 +66,18 @@
 #include "nsIFile.h"
 #include "nsDirectoryServiceUtils.h"
 #include "nsDirectoryServiceDefs.h"
 #include "mozISpellI18NManager.h"
 #include "nsICharsetConverterManager.h"
 #include "nsUnicharUtilCIID.h"
 #include "nsUnicharUtils.h"
 #include "nsCRT.h"
+#include "mozInlineSpellChecker.h"
+#include "mozilla/Services.h"
 #include <stdlib.h>
 #include "nsIMemoryReporter.h"
 
 static NS_DEFINE_CID(kCharsetConverterManagerCID, NS_ICHARSETCONVERTERMANAGER_CID);
 static NS_DEFINE_CID(kUnicharUtilCID, NS_UNICHARUTIL_CID);
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(mozHunspell)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(mozHunspell)
@@ -117,18 +120,17 @@ NS_MEMORY_REPORTER_IMPLEMENT(Hunspell,
 nsresult
 mozHunspell::Init()
 {
   if (!mDictionaries.Init())
     return NS_ERROR_OUT_OF_MEMORY;
 
   LoadDictionaryList();
 
-  nsCOMPtr<nsIObserverService> obs =
-    do_GetService("@mozilla.org/observer-service;1");
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
   if (obs) {
     obs->AddObserver(this, "profile-do-change", PR_TRUE);
   }
 
   mHunspellReporter = new NS_MEMORY_REPORTER_NAME(Hunspell);
   NS_RegisterMemoryReporter(mHunspellReporter);
 
   return NS_OK;
@@ -142,60 +144,58 @@ mozHunspell::~mozHunspell()
   NS_UnregisterMemoryReporter(mHunspellReporter);
 }
 
 /* attribute wstring dictionary; */
 NS_IMETHODIMP mozHunspell::GetDictionary(PRUnichar **aDictionary)
 {
   NS_ENSURE_ARG_POINTER(aDictionary);
 
-  if (mDictionary.IsEmpty())
-    return NS_ERROR_NOT_INITIALIZED;
-
   *aDictionary = ToNewUnicode(mDictionary);
   return *aDictionary ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
 }
 
 /* set the Dictionary.
  * This also Loads the dictionary and initializes the converter using the dictionaries converter
  */
 NS_IMETHODIMP mozHunspell::SetDictionary(const PRUnichar *aDictionary)
 {
   NS_ENSURE_ARG_POINTER(aDictionary);
 
-  if (mDictionary.Equals(aDictionary))
-    return NS_OK;
-
   nsIFile* affFile = mDictionaries.GetWeak(nsDependentString(aDictionary));
   if (!affFile)
     return NS_ERROR_FILE_NOT_FOUND;
 
   nsCAutoString dictFileName, affFileName;
 
   // XXX This isn't really good. nsIFile->NativePath isn't safe for all
   // character sets on Windows.
   // A better way would be to QI to nsILocalFile, and get a filehandle
   // from there. Only problem is that hunspell wants a path
 
   nsresult rv = affFile->GetNativePath(affFileName);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  if (mAffixFileName.Equals(affFileName.get()))
+    return NS_OK;
+
   dictFileName = affFileName;
   PRInt32 dotPos = dictFileName.RFindChar('.');
   if (dotPos == -1)
     return NS_ERROR_FAILURE;
 
   dictFileName.SetLength(dotPos);
   dictFileName.AppendLiteral(".dic");
 
   // SetDictionary can be called multiple times, so we might have a
   // valid mHunspell instance which needs cleaned up.
   delete mHunspell;
 
   mDictionary = aDictionary;
+  mAffixFileName = affFileName;
 
   mHunspell = new Hunspell(affFileName.get(),
                          dictFileName.get());
   if (!mHunspell)
     return NS_ERROR_OUT_OF_MEMORY;
 
   nsCOMPtr<nsICharsetConverterManager> ccm =
     do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv);
@@ -217,16 +217,23 @@ NS_IMETHODIMP mozHunspell::SetDictionary
   if (pos == -1)
     pos = mDictionary.FindChar('_');
 
   if (pos == -1)
     mLanguage.Assign(mDictionary);
   else
     mLanguage = Substring(mDictionary, 0, pos);
 
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    obs->NotifyObservers(nsnull,
+                         SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION,
+                         nsnull);
+  }
+
   return NS_OK;
 }
 
 /* readonly attribute wstring language; */
 NS_IMETHODIMP mozHunspell::GetLanguage(PRUnichar **aLanguage)
 {
   NS_ENSURE_ARG_POINTER(aLanguage);
 
@@ -328,28 +335,37 @@ NS_IMETHODIMP mozHunspell::GetDictionary
   }
 
   *aDictionaries = ans.dics;
   *aCount = ans.count;
 
   return NS_OK;
 }
 
+static PLDHashOperator
+FindFirstString(const nsAString& aString, nsIFile* aFile, void* aClosure)
+{
+  nsAString *dic = (nsAString*) aClosure;
+  dic->Assign(aString);
+  return PL_DHASH_STOP;
+}
+
 void
 mozHunspell::LoadDictionaryList()
 {
   mDictionaries.Clear();
 
   nsresult rv;
 
   nsCOMPtr<nsIProperties> dirSvc =
     do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
   if (!dirSvc)
     return;
 
+  // find built in dictionaries
   nsCOMPtr<nsIFile> dictDir;
   rv = dirSvc->Get(DICTIONARY_SEARCH_DIRECTORY,
                    NS_GET_IID(nsIFile), getter_AddRefs(dictDir));
   if (NS_SUCCEEDED(rv)) {
     LoadDictionariesFromDir(dictDir);
   }
   else {
     // try to load gredir/dictionaries
@@ -367,31 +383,77 @@ mozHunspell::LoadDictionaryList()
                      NS_GET_IID(nsIFile), getter_AddRefs(appDir));
     PRBool equals;
     if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(appDir->Equals(greDir, &equals)) && !equals) {
       appDir->AppendNative(NS_LITERAL_CSTRING("dictionaries"));
       LoadDictionariesFromDir(appDir);
     }
   }
 
+  // find dictionaries from extensions requiring restart
   nsCOMPtr<nsISimpleEnumerator> dictDirs;
   rv = dirSvc->Get(DICTIONARY_SEARCH_DIRECTORY_LIST,
                    NS_GET_IID(nsISimpleEnumerator), getter_AddRefs(dictDirs));
   if (NS_FAILED(rv))
     return;
 
   PRBool hasMore;
   while (NS_SUCCEEDED(dictDirs->HasMoreElements(&hasMore)) && hasMore) {
     nsCOMPtr<nsISupports> elem;
     dictDirs->GetNext(getter_AddRefs(elem));
 
     dictDir = do_QueryInterface(elem);
     if (dictDir)
       LoadDictionariesFromDir(dictDir);
   }
+
+  // find dictionaries from restartless extensions
+  for (PRUint32 i = 0; i < mDynamicDirectories.Count(); i++) {
+    LoadDictionariesFromDir(mDynamicDirectories[i]);
+  }
+
+  // Now we have finished updating the list of dictionaries, update the current
+  // dictionary and any editors which may use it.
+  mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking();
+
+  // If the current dictionary has gone, try to replace it with another
+  // dictionary of the same language
+  if (!mDictionary.IsEmpty()) {
+    rv = SetDictionary(mDictionary.get());
+    if (NS_SUCCEEDED(rv))
+      return;
+  }
+
+  // If we didn't find a dictionary equal to the current dictionary or we had
+  // no current dictionary, just pick an arbitrary dictionary.
+  nsAutoString firstDictionary;
+  mDictionaries.EnumerateRead(FindFirstString, &firstDictionary);
+  if (!firstDictionary.IsEmpty()) {
+    rv = SetDictionary(firstDictionary.get());
+    if (NS_SUCCEEDED(rv))
+      return;
+  }
+
+  // If there are no dictionaries, set no current dictionary
+  if (!mDictionary.IsEmpty()) {
+    delete mHunspell;
+    mHunspell = nsnull;
+    mDictionary.AssignLiteral("");
+    mAffixFileName.AssignLiteral("");
+    mLanguage.AssignLiteral("");
+    mDecoder = nsnull;
+    mEncoder = nsnull;
+
+    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+    if (obs) {
+      obs->NotifyObservers(nsnull,
+                           SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION,
+                           nsnull);
+    }
+  }
 }
 
 NS_IMETHODIMP
 mozHunspell::LoadDictionariesFromDir(nsIFile* aDir)
 {
   nsresult rv;
 
   PRBool check = PR_FALSE;
@@ -537,8 +599,24 @@ mozHunspell::Observe(nsISupports* aSubj,
 {
   NS_ASSERTION(!strcmp(aTopic, "profile-do-change"),
                "Unexpected observer topic");
 
   LoadDictionaryList();
 
   return NS_OK;
 }
+
+/* void addDirectory(in nsIFile dir); */
+NS_IMETHODIMP mozHunspell::AddDirectory(nsIFile *aDir)
+{
+  mDynamicDirectories.AppendObject(aDir);
+  LoadDictionaryList();
+  return NS_OK;
+}
+
+/* void removeDirectory(in nsIFile dir); */
+NS_IMETHODIMP mozHunspell::RemoveDirectory(nsIFile *aDir)
+{
+  mDynamicDirectories.RemoveObject(aDir);
+  LoadDictionaryList();
+  return NS_OK;
+}
--- a/extensions/spellcheck/hunspell/src/mozHunspell.h
+++ b/extensions/spellcheck/hunspell/src/mozHunspell.h
@@ -36,16 +36,17 @@
  *                 Varga Daniel
  *                 Chris Halls
  *                 Rene Engelhard
  *                 Bram Moolenaar
  *                 Dafydd Jones
  *                 Harri Pitkanen
  *                 Andras Timar
  *                 Tor Lillqvist
+ *                 Jesper Kristensen (mail@jesperkristensen.dk)
  * 
  * 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
@@ -59,16 +60,17 @@
 #ifndef mozHunspell_h__
 #define mozHunspell_h__
 
 #include <hunspell.hxx>
 #include "mozISpellCheckingEngine.h"
 #include "mozIPersonalDictionary.h"
 #include "nsString.h"
 #include "nsCOMPtr.h"
+#include "nsCOMArray.h"
 #include "nsIObserver.h"
 #include "nsIUnicodeEncoder.h"
 #include "nsIUnicodeDecoder.h"
 #include "nsInterfaceHashtable.h"
 #include "nsWeakReference.h"
 #include "nsCycleCollectionParticipant.h"
 
 #define MOZ_HUNSPELL_CONTRACTID "@mozilla.org/spellchecker/engine;1"
@@ -104,15 +106,19 @@ protected:
   nsCOMPtr<mozIPersonalDictionary> mPersonalDictionary;
   nsCOMPtr<nsIUnicodeEncoder>      mEncoder; 
   nsCOMPtr<nsIUnicodeDecoder>      mDecoder; 
 
   // Hashtable matches dictionary name to .aff file
   nsInterfaceHashtable<nsStringHashKey, nsIFile> mDictionaries;
   nsString  mDictionary;
   nsString  mLanguage;
+  nsCString mAffixFileName;
+
+  // dynamic dirs used to search for dictionaries
+  nsCOMArray<nsIFile> mDynamicDirectories;
 
   Hunspell  *mHunspell;
 
   nsIMemoryReporter* mHunspellReporter;
 };
 
 #endif
--- a/extensions/spellcheck/idl/mozISpellCheckingEngine.idl
+++ b/extensions/spellcheck/idl/mozISpellCheckingEngine.idl
@@ -15,16 +15,17 @@
  * The Original Code is mozilla.org code.
  *
  * The Initial Developer of the Original Code is
  * David Einstein.
  * Portions created by the Initial Developer are Copyright (C) 2001
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
+ *   Jesper Kristensen <mail@jesperkristensen.dk>
  *
  * 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
@@ -35,30 +36,41 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsISupports.idl"
 
 interface nsIFile;
 interface mozIPersonalDictionary;
 
-[scriptable, uuid(6eb307d6-3567-481a-971a-feb666b8ae72)]
+[scriptable, uuid(8ba643a4-7ddc-4662-b976-7ec123843f10)]
 
 /**
  * This interface represents a SpellChecker.
  */
 
 interface mozISpellCheckingEngine : nsISupports {
   /**
    * The name of the current dictionary
+   *
+   * Whenever getDictionaryList is not empty, this attribute contains a value
+   * from that list. Whenever getDictionaryList is empty, this attribute
+   * contains the empty string. Setting this attribute to a value not in
+   * getDictionaryList will throw NS_ERROR_FILE_NOT_FOUND.
+   *
+   * The spellcheck engine will send a notification with
+   * "spellcheck-dictionary-update" as topic when this changes.
    */
   attribute wstring dictionary;
 
   /**
    * The language this spellchecker is using when checking
+   *
+   * The spellcheck engine will send a notification with
+   * "spellcheck-dictionary-update" as topic when this changes.
    */
   readonly attribute wstring language;
 
   /**
    * Does the engine provide its own personal dictionary?
    */
   readonly attribute boolean providesPersonalDictionary;
 
@@ -84,26 +96,45 @@ interface mozISpellCheckingEngine : nsIS
 
   /**
    * Get the list of dictionaries
    */
   void getDictionaryList([array, size_is(count)] out wstring dictionaries, out PRUint32 count);
 
   /**
    * check a word
+   *
+   * The spellcheck engine will send a notification with
+   * "spellcheck-dictionary-update" as topic when this changes.
    */
   boolean check(in wstring word);
 
   /**
    * get a list of suggestions for a misspelled word
+   *
+   * The spellcheck engine will send a notification with
+   * "spellcheck-dictionary-update" as topic when this changes.
    */
   void suggest(in wstring word,[array, size_is(count)] out wstring suggestions, out PRUint32 count);
 
   /**
    * Load dictionaries from the specified dir
    */
   void loadDictionariesFromDir(in nsIFile dir);
+
+  /**
+   * Add dictionaries from a directory to the spell checker
+   */
+  void addDirectory(in nsIFile dir);
+
+  /**
+   * Remove dictionaries from a directory from the spell checker
+   */
+  void removeDirectory(in nsIFile dir);
 };
 
 %{C++
 #define DICTIONARY_SEARCH_DIRECTORY "DictD"
 #define DICTIONARY_SEARCH_DIRECTORY_LIST "DictDL"
+
+#define SPELLCHECK_DICTIONARY_UPDATE_NOTIFICATION \
+  "spellcheck-dictionary-update"
 %}
--- a/extensions/spellcheck/src/mozInlineSpellChecker.cpp
+++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp
@@ -595,19 +595,19 @@ nsresult mozInlineSpellChecker::Cleanup(
 //    This function can be called to see if it seems likely that we can enable
 //    spellchecking before actually creating the InlineSpellChecking objects.
 //
 //    The problem is that we can't get the dictionary list without actually
 //    creating a whole bunch of spellchecking objects. This function tries to
 //    do that and caches the result so we don't have to keep allocating those
 //    objects if there are no dictionaries or spellchecking.
 //
-//    This caching will prevent adding dictionaries at runtime if we start out
-//    with no dictionaries! Installing dictionaries as extensions will require
-//    a restart anyway, so it shouldn't be a problem.
+//    Whenever dictionaries are added or removed at runtime, this value must be
+//    updated before an observer notification is sent out about the change, to
+//    avoid editors getting a wrong cached result.
 
 PRBool // static
 mozInlineSpellChecker::CanEnableInlineSpellChecking()
 {
   nsresult rv;
   if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
     gCanEnableSpellChecking = SpellCheck_NotAvailable;
 
@@ -620,16 +620,22 @@ mozInlineSpellChecker::CanEnableInlineSp
     NS_ENSURE_SUCCESS(rv, PR_FALSE);
 
     if (canSpellCheck)
       gCanEnableSpellChecking = SpellCheck_Available;
   }
   return (gCanEnableSpellChecking == SpellCheck_Available);
 }
 
+void // static
+mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking()
+{
+  gCanEnableSpellChecking = SpellCheck_Uninitialized;
+}
+
 // mozInlineSpellChecker::RegisterEventListeners
 //
 //    The inline spell checker listens to mouse events and keyboard navigation+ //    events.
 
 nsresult
 mozInlineSpellChecker::RegisterEventListeners()
 {
   nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
--- a/extensions/spellcheck/src/mozInlineSpellChecker.h
+++ b/extensions/spellcheck/src/mozInlineSpellChecker.h
@@ -224,18 +224,20 @@ private:
 public:
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_NSIEDITACTIONLISTENER
   NS_DECL_NSIINLINESPELLCHECKER
   NS_DECL_NSIDOMEVENTLISTENER
   NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(mozInlineSpellChecker, nsIDOMEventListener)
 
-  // returns true if it looks likely that we can enable real-time spell checking
+  // returns true if there are any spell checking dictionaries available
   static PRBool CanEnableInlineSpellChecking();
+  // update the cached value whenever the list of available dictionaries changes
+  static void UpdateCanEnableInlineSpellChecking();
 
   nsresult Blur(nsIDOMEvent* aEvent);
   nsresult MouseClick(nsIDOMEvent* aMouseEvent);
   nsresult KeyPress(nsIDOMEvent* aKeyEvent);
 
   mozInlineSpellChecker();
   virtual ~mozInlineSpellChecker();
 
--- a/extensions/spellcheck/src/mozSpellChecker.cpp
+++ b/extensions/spellcheck/src/mozSpellChecker.cpp
@@ -13,16 +13,17 @@
  *
  * The Original Code is Mozilla Spellchecker Component.
  *
  * The Initial Developer of the Original Code is David Einstein.
  * Portions created by the Initial Developer are Copyright (C) 2001
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s): David Einstein Deinst@world.std.com
+ *   Jesper Kristensen <mail@jesperkristensen.dk>
  *
  * 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
@@ -72,19 +73,16 @@ mozSpellChecker::~mozSpellChecker()
 }
 
 nsresult 
 mozSpellChecker::Init()
 {
   mPersonalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1");
   
   mSpellCheckingEngine = nsnull;
-  mCurrentEngineContractId = nsnull;
-  mDictionariesMap.Init();
-  InitSpellCheckDictionaryMap();
 
   return NS_OK;
 } 
 
 NS_IMETHODIMP 
 mozSpellChecker::SetDocument(nsITextServicesDocument *aDoc, PRBool aFromStartofDoc)
 {
   mTsDoc = aDoc;
@@ -302,45 +300,55 @@ mozSpellChecker::GetPersonalDictionary(n
   nsAutoString word;
   while (NS_SUCCEEDED(words->HasMore(&hasMore)) && hasMore) {
     words->GetNext(word);
     aWordList->AppendElement(word);
   }
   return NS_OK;
 }
 
-struct AppendNewStruct
-{
-  nsTArray<nsString> *dictionaryList;
-  PRBool failed;
-};
-
-static PLDHashOperator
-AppendNewString(const nsAString& aString, nsCString*, void* aClosure)
-{
-  AppendNewStruct *ans = (AppendNewStruct*) aClosure;
-
-  if (!ans->dictionaryList->AppendElement(aString))
-  {
-    ans->failed = PR_TRUE;
-    return PL_DHASH_STOP;
-  }
-
-  return PL_DHASH_NEXT;
-}
-
 NS_IMETHODIMP 
 mozSpellChecker::GetDictionaryList(nsTArray<nsString> *aDictionaryList)
 {
-  AppendNewStruct ans = {aDictionaryList, PR_FALSE};
+  nsresult rv;
+
+  // For catching duplicates
+  nsClassHashtable<nsStringHashKey, nsCString> dictionaries;
+  dictionaries.Init();
+
+  nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines;
+  rv = GetEngineList(&spellCheckingEngines);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  for (PRUint32 i = 0; i < spellCheckingEngines.Count(); i++) {
+    nsCOMPtr<mozISpellCheckingEngine> engine = spellCheckingEngines[i];
 
-  mDictionariesMap.EnumerateRead(AppendNewString, &ans);
+    PRUint32 count = 0;
+    PRUnichar **words = NULL;
+    engine->GetDictionaryList(&words, &count);
+    for (PRUint32 k = 0; k < count; k++) {
+      nsAutoString dictName;
+
+      dictName.Assign(words[k]);
 
-  if (ans.failed)
-    return NS_ERROR_OUT_OF_MEMORY;
+      // Skip duplicate dictionaries. Only take the first one
+      // for each name.
+      if (dictionaries.Get(dictName, NULL))
+        continue;
+
+      dictionaries.Put(dictName, NULL);
+
+      if (!aDictionaryList->AppendElement(dictName)) {
+        NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words);
+        return NS_ERROR_OUT_OF_MEMORY;
+      }
+    }
+
+    NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words);
+  }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP 
 mozSpellChecker::GetCurrentDictionary(nsAString &aDictionary)
 {
   nsXPIDLString dictname;
@@ -351,54 +359,97 @@ mozSpellChecker::GetCurrentDictionary(ns
   mSpellCheckingEngine->GetDictionary(getter_Copies(dictname));
   aDictionary = dictname;
   return NS_OK;
 }
 
 NS_IMETHODIMP 
 mozSpellChecker::SetCurrentDictionary(const nsAString &aDictionary)
 {
-  nsresult rv;
-  nsCString *contractId;
+  mSpellCheckingEngine = nsnull;
 
   if (aDictionary.IsEmpty()) {
-    mCurrentEngineContractId = nsnull;
-    mSpellCheckingEngine = nsnull;
     return NS_OK;
   }
 
-  if (!mDictionariesMap.Get(aDictionary, &contractId)){
-    NS_WARNING("Dictionary not found");
-    return NS_ERROR_NOT_AVAILABLE;
-  }
+  nsresult rv;
+  nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines;
+  rv = GetEngineList(&spellCheckingEngines);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  for (PRUint32 i = 0; i < spellCheckingEngines.Count(); i++) {
+    nsCOMPtr<mozISpellCheckingEngine> engine = spellCheckingEngines[i];
 
-  if (!mCurrentEngineContractId || !mCurrentEngineContractId->Equals(*contractId)){
-    mSpellCheckingEngine = do_GetService(contractId->get(), &rv);
-    if (NS_FAILED(rv))
-      return rv;
+    rv = engine->SetDictionary(PromiseFlatString(aDictionary).get());
+    if (NS_SUCCEEDED(rv)) {
+      mSpellCheckingEngine = engine;
 
-    mCurrentEngineContractId = contractId;
+      nsCOMPtr<mozIPersonalDictionary> personalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1");
+      mSpellCheckingEngine->SetPersonalDictionary(personalDictionary.get());
+
+      return NS_OK;
+    }
   }
 
-  nsresult res;
-  res = mSpellCheckingEngine->SetDictionary(PromiseFlatString(aDictionary).get());
-  if(NS_FAILED(res)){
-    NS_WARNING("Dictionary load failed");
-    return res;
+  // We could not find any engine with the requested dictionary
+  return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP 
+mozSpellChecker::CheckCurrentDictionary()
+{
+  // Check if the current engine has any dictionaries available. If not,
+  // the last dictionary has just been uninstalled, and we need to stop using
+  // the engine.
+  if (mSpellCheckingEngine) {
+    nsXPIDLString dictname;
+
+    mSpellCheckingEngine->GetDictionary(getter_Copies(dictname));
+
+    // We still have a dictionary, so keep using that.
+    if (!dictname.IsEmpty()) {
+      return NS_OK;
+    }
+
+    // Our current dictionary has gone, so we cannot use the engine anymore.
+    mSpellCheckingEngine = nsnull;
   }
 
-  mSpellCheckingEngine->SetPersonalDictionary(mPersonalDictionary);
+  // We have no current engine. Pick one.
+  nsresult rv;
+  nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines;
+  rv = GetEngineList(&spellCheckingEngines);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  for (PRUint32 i = 0; i < spellCheckingEngines.Count(); i++) {
+    nsCOMPtr<mozISpellCheckingEngine> engine = spellCheckingEngines[i];
+
+    nsXPIDLString dictname;
+
+    engine->GetDictionary(getter_Copies(dictname));
+
+    if (!dictname.IsEmpty()) {
+      mSpellCheckingEngine = engine;
 
-  nsXPIDLString language;
-  
-  nsCOMPtr<mozISpellI18NManager> serv(do_GetService("@mozilla.org/spellchecker/i18nmanager;1", &res));
-  if(serv && NS_SUCCEEDED(res)){
-    res = serv->GetUtil(language.get(),getter_AddRefs(mConverter));
+      nsCOMPtr<mozIPersonalDictionary> personalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1");
+      mSpellCheckingEngine->SetPersonalDictionary(personalDictionary.get());
+
+      nsXPIDLString language;
+      nsresult rv;
+      nsCOMPtr<mozISpellI18NManager> serv = do_GetService("@mozilla.org/spellchecker/i18nmanager;1", &rv);
+      if(serv && NS_SUCCEEDED(rv)) {
+        serv->GetUtil(language.get(), getter_AddRefs(mConverter));
+      }
+
+      return NS_OK;
+    }
   }
-  return res;
+
+  // There are no dictionaries available
+  return NS_OK;
 }
 
 nsresult
 mozSpellChecker::SetupDoc(PRInt32 *outBlockOffset)
 {
   nsresult  rv;
 
   nsITextServicesDocument::TSDBlockSelectionStatus blockStatus;
@@ -472,21 +523,20 @@ mozSpellChecker::GetCurrentBlockIndex(ns
   } while (NS_SUCCEEDED(result) && !isDone);
   
   *outBlockIndex = blockIndex;
 
   return result;
 }
 
 nsresult
-mozSpellChecker::InitSpellCheckDictionaryMap()
+mozSpellChecker::GetEngineList(nsCOMArray<mozISpellCheckingEngine>* aSpellCheckingEngines)
 {
   nsresult rv;
   PRBool hasMoreEngines;
-  nsTArray<nsCString> contractIds;
 
   nsCOMPtr<nsICategoryManager> catMgr = do_GetService(NS_CATEGORYMANAGER_CONTRACTID);
   if (!catMgr)
     return NS_ERROR_NULL_POINTER;
 
   nsCOMPtr<nsISimpleEnumerator> catEntries;
 
   // Get contract IDs of registrated external spell-check engines and
@@ -503,57 +553,29 @@ mozSpellChecker::InitSpellCheckDictionar
     if (NS_FAILED(rv))
       return rv;
 
     nsCString contractId;
     rv = entry->GetData(contractId);
     if (NS_FAILED(rv))
       return rv;
 
-    contractIds.AppendElement(contractId);
-  }
-
-  contractIds.AppendElement(NS_LITERAL_CSTRING(DEFAULT_SPELL_CHECKER));
-
-  // Retrieve dictionaries from all available spellcheckers and
-  // fill mDictionariesMap hash (only the first dictionary with the
-  // each name is used).
-  for (PRUint32 i=0;i < contractIds.Length();i++){
-    PRUint32 count,k;
-    PRUnichar **words;
-
-    const nsCString& contractId = contractIds[i];
-
     // Try to load spellchecker engine. Ignore errors silently
     // except for the last one (HunSpell).
     nsCOMPtr<mozISpellCheckingEngine> engine =
       do_GetService(contractId.get(), &rv);
-    if (NS_FAILED(rv)){
-      // Fail if not succeeded to load HunSpell. Ignore errors
-      // for external spellcheck engines.
-      if (i==contractIds.Length()-1){
-        return rv;
-      }
-
-      continue;
+    if (NS_SUCCEEDED(rv)) {
+      aSpellCheckingEngines->AppendObject(engine);
     }
-
-    engine->GetDictionaryList(&words,&count);
-    for(k=0;k<count;k++){
-      nsAutoString dictName;
+  }
 
-      dictName.Assign(words[k]);
-
-      nsCString dictCName = NS_ConvertUTF16toUTF8(dictName);
-
-      // Skip duplicate dictionaries. Only take the first one
-      // for each name.
-      if (mDictionariesMap.Get(dictName, NULL))
-        continue;
-
-      mDictionariesMap.Put(dictName, new nsCString(contractId));
-    }
-
-    NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words);
+  // Try to load HunSpell spellchecker engine.
+  nsCOMPtr<mozISpellCheckingEngine> engine =
+    do_GetService(DEFAULT_SPELL_CHECKER, &rv);
+  if (NS_FAILED(rv)) {
+    // Fail if not succeeded to load HunSpell. Ignore errors
+    // for external spellcheck engines.
+    return rv;
   }
+  aSpellCheckingEngines->AppendObject(engine);
 
   return NS_OK;
 }
--- a/extensions/spellcheck/src/mozSpellChecker.h
+++ b/extensions/spellcheck/src/mozSpellChecker.h
@@ -15,16 +15,17 @@
  * The Original Code is Mozilla Spellchecker Component.
  *
  * The Initial Developer of the Original Code is
  * David Einstein.
  * Portions created by the Initial Developer are Copyright (C) 2001
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s): David Einstein Deinst@world.std.com
+ *   Jesper Kristensen <mail@jesperkristensen.dk>
  *
  * 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
@@ -34,16 +35,17 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #ifndef mozSpellChecker_h__
 #define mozSpellChecker_h__
 
 #include "nsCOMPtr.h"
+#include "nsCOMArray.h"
 #include "nsISpellChecker.h"
 #include "nsString.h"
 #include "nsITextServicesDocument.h"
 #include "mozIPersonalDictionary.h"
 #include "mozISpellCheckingEngine.h"
 #include "nsClassHashtable.h"
 #include "nsVoidArray.h"
 #include "nsTArray.h"
@@ -70,29 +72,25 @@ public:
 
   NS_IMETHOD AddWordToPersonalDictionary(const nsAString &aWord);
   NS_IMETHOD RemoveWordFromPersonalDictionary(const nsAString &aWord);
   NS_IMETHOD GetPersonalDictionary(nsTArray<nsString> *aWordList);
 
   NS_IMETHOD GetDictionaryList(nsTArray<nsString> *aDictionaryList);
   NS_IMETHOD GetCurrentDictionary(nsAString &aDictionary);
   NS_IMETHOD SetCurrentDictionary(const nsAString &aDictionary);
+  NS_IMETHOD CheckCurrentDictionary();
 
 protected:
   nsCOMPtr<mozISpellI18NUtil> mConverter;
   nsCOMPtr<nsITextServicesDocument> mTsDoc;
   nsCOMPtr<mozIPersonalDictionary> mPersonalDictionary;
 
-  // Hastable maps directory name to the spellchecker contract ID
-  nsClassHashtable<nsStringHashKey, nsCString> mDictionariesMap;
-
-  nsString mDictionaryName;
-  nsCString *mCurrentEngineContractId;
   nsCOMPtr<mozISpellCheckingEngine>  mSpellCheckingEngine;
   PRBool mFromStart;
 
   nsresult SetupDoc(PRInt32 *outBlockOffset);
 
   nsresult GetCurrentBlockIndex(nsITextServicesDocument *aDoc, PRInt32 *outBlockIndex);
 
-  nsresult InitSpellCheckDictionaryMap();
+  nsresult GetEngineList(nsCOMArray<mozISpellCheckingEngine> *aDictionaryList);
 };
 #endif // mozSpellChecker_h__
--- a/extensions/spellcheck/src/mozSpellCheckerFactory.cpp
+++ b/extensions/spellcheck/src/mozSpellCheckerFactory.cpp
@@ -54,49 +54,17 @@ 0x8227F019, 0xAFC7, 0x461e,             
 0x9fe5d975, 0x9bd, 0x44aa,                      \
 { 0xa0, 0x1a, 0x66, 0x40, 0x2e, 0xa2, 0x86, 0x57} }
 
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(mozHunspell, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(mozHunspellDirProvider)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(mozSpellChecker, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(mozPersonalDictionary, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(mozSpellI18NManager)
-
-// This special constructor for the inline spell checker asks the inline
-// spell checker if we can create spell checking objects at all (ie, if there
-// are any dictionaries loaded) before trying to create one. The static
-// CanEnableInlineSpellChecking caches the value so this will be faster (we
-// have to run this code for every edit box we create, as well as for every
-// right click in those edit boxes).
-static nsresult
-mozInlineSpellCheckerConstructor(nsISupports *aOuter, REFNSIID aIID,
-                                 void **aResult)
-{
-  if (! mozInlineSpellChecker::CanEnableInlineSpellChecking())
-    return NS_ERROR_FAILURE;
-
-  nsresult rv;
-
-  *aResult = NULL;
-  if (NULL != aOuter) {
-    rv = NS_ERROR_NO_AGGREGATION;
-    return rv;
-  }
-
-  mozInlineSpellChecker* inst = new mozInlineSpellChecker();
-  if (NULL == inst) {
-    rv = NS_ERROR_OUT_OF_MEMORY;
-    return rv;
-  }
-  NS_ADDREF(inst);
-  rv = inst->QueryInterface(aIID, aResult);
-  NS_RELEASE(inst);
-
-  return rv;
-}
+NS_GENERIC_FACTORY_CONSTRUCTOR(mozInlineSpellChecker)
 
 NS_DEFINE_NAMED_CID(MOZ_HUNSPELL_CID);
 NS_DEFINE_NAMED_CID(HUNSPELLDIRPROVIDER_CID);
 NS_DEFINE_NAMED_CID(NS_SPELLCHECKER_CID);
 NS_DEFINE_NAMED_CID(MOZ_PERSONALDICTIONARY_CID);
 NS_DEFINE_NAMED_CID(MOZ_SPELLI18NMANAGER_CID);
 NS_DEFINE_NAMED_CID(MOZ_INLINESPELLCHECKER_CID);
 
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/Makefile.in
@@ -0,0 +1,48 @@
+#
+# ***** 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 mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# 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 *****
+
+DEPTH		= ../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir  = extensions/spellcheck/tests
+
+include $(DEPTH)/config/autoconf.mk
+
+DIRS		= chrome
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/Makefile.in
@@ -0,0 +1,54 @@
+#
+# ***** 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 mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# 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 *****
+
+DEPTH		= ../../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir  = extensions/spellcheck/tests/chrome
+
+include $(DEPTH)/config/autoconf.mk
+
+DIRS = base map
+
+include $(topsrcdir)/config/rules.mk
+
+_TEST_FILES = 	test_add_remove_dictionaries.xul \
+				$(NULL)
+
+libs:: $(_TEST_FILES)
+	$(INSTALL) $^ $(DEPTH)/_tests/testing/mochitest/chrome/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/base/Makefile.in
@@ -0,0 +1,52 @@
+#
+# ***** 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 mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# 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 *****
+
+DEPTH		= ../../../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir  = extensions/spellcheck/tests/chrome/base
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/rules.mk
+
+_TEST_FILES = 	base_utf.dic \
+				base_utf.aff \
+				$(NULL)
+
+libs:: $(_TEST_FILES)
+	$(INSTALL) $^ $(DEPTH)/_tests/testing/mochitest/chrome/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/base/base_utf.aff
@@ -0,0 +1,198 @@
+# OpenOffice.org’s en_US.aff file
+# with Unicode apostrophe: ’
+
+SET UTF-8
+TRY esianrtolcdugmphbyfvkwzESIANRTOLCDUGMPHBYFVKWZ'
+
+MAXNGRAMSUGS 1
+WORDCHARS .'’
+
+PFX A Y 1
+PFX A   0     re         .
+
+PFX I Y 1
+PFX I   0     in         .
+
+PFX U Y 1
+PFX U   0     un         .
+
+PFX C Y 1
+PFX C   0     de          .
+
+PFX E Y 1
+PFX E   0     dis         .
+
+PFX F Y 1
+PFX F   0     con         .
+
+PFX K Y 1
+PFX K   0     pro         .
+
+SFX V N 2
+SFX V   e     ive        e
+SFX V   0     ive        [^e]
+
+SFX N Y 3
+SFX N   e     ion        e
+SFX N   y     ication    y 
+SFX N   0     en         [^ey] 
+
+SFX X Y 3
+SFX X   e     ions       e
+SFX X   y     ications   y
+SFX X   0     ens        [^ey]
+
+SFX H N 2
+SFX H   y     ieth       y
+SFX H   0     th         [^y] 
+
+SFX Y Y 1
+SFX Y   0     ly         .
+
+SFX G Y 2
+SFX G   e     ing        e
+SFX G   0     ing        [^e] 
+
+SFX J Y 2
+SFX J   e     ings       e
+SFX J   0     ings       [^e]
+
+SFX D Y 4
+SFX D   0     d          e
+SFX D   y     ied        [^aeiou]y
+SFX D   0     ed         [^ey]
+SFX D   0     ed         [aeiou]y
+
+SFX T N 4
+SFX T   0     st         e
+SFX T   y     iest       [^aeiou]y
+SFX T   0     est        [aeiou]y
+SFX T   0     est        [^ey]
+
+SFX R Y 4
+SFX R   0     r          e
+SFX R   y     ier        [^aeiou]y
+SFX R   0     er         [aeiou]y
+SFX R   0     er         [^ey]
+
+SFX Z Y 4
+SFX Z   0     rs         e
+SFX Z   y     iers       [^aeiou]y
+SFX Z   0     ers        [aeiou]y
+SFX Z   0     ers        [^ey]
+
+SFX S Y 4
+SFX S   y     ies        [^aeiou]y
+SFX S   0     s          [aeiou]y
+SFX S   0     es         [sxzh]
+SFX S   0     s          [^sxzhy]
+
+SFX P Y 3
+SFX P   y     iness      [^aeiou]y
+SFX P   0     ness       [aeiou]y
+SFX P   0     ness       [^y]
+
+SFX M Y 1
+SFX M   0     's         .
+
+SFX B Y 3
+SFX B   0     able       [^aeiou]
+SFX B   0     able       ee
+SFX B   e     able       [^aeiou]e
+
+SFX L Y 1
+SFX L   0     ment       .
+
+REP 88
+REP a ei
+REP ei a
+REP a ey
+REP ey a
+REP ai ie
+REP ie ai
+REP are air
+REP are ear
+REP are eir
+REP air are
+REP air ere
+REP ere air
+REP ere ear
+REP ere eir
+REP ear are
+REP ear air
+REP ear ere
+REP eir are
+REP eir ere
+REP ch te
+REP te ch
+REP ch ti
+REP ti ch
+REP ch tu
+REP tu ch
+REP ch s
+REP s ch
+REP ch k
+REP k ch
+REP f ph
+REP ph f
+REP gh f
+REP f gh
+REP i igh
+REP igh i
+REP i uy
+REP uy i
+REP i ee
+REP ee i
+REP j di
+REP di j
+REP j gg
+REP gg j
+REP j ge
+REP ge j
+REP s ti
+REP ti s
+REP s ci
+REP ci s
+REP k cc
+REP cc k
+REP k qu
+REP qu k
+REP kw qu
+REP o eau
+REP eau o
+REP o ew
+REP ew o
+REP oo ew
+REP ew oo
+REP ew ui
+REP ui ew
+REP oo ui
+REP ui oo
+REP ew u
+REP u ew
+REP oo u
+REP u oo
+REP u oe
+REP oe u
+REP u ieu
+REP ieu u
+REP ue ew
+REP ew ue
+REP uff ough
+REP oo ieu
+REP ieu oo
+REP ier ear
+REP ear ier
+REP ear air
+REP air ear
+REP w qu
+REP qu w
+REP z ss
+REP ss z
+REP shun tion
+REP shun sion
+REP shun cion
+McDonalds’sá/w
+McDonald’sszá/g3)	st:McDonald’s	po:noun_prs	is:TRANS
+McDonald’sszal/g4)	st:McDonald’s	po:noun_prs	is:INSTR
+McDonald’ssal/w
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/base/base_utf.dic
@@ -0,0 +1,29 @@
+28
+created/U
+create/XKVNGADS
+imply/GNSDX
+natural/PUY
+like/USPBY
+convey/BDGS
+look/GZRDS
+text
+hello
+said
+sawyer
+NASA
+rotten
+day
+tomorrow
+seven
+FAQ/SM
+can’t
+doesn’t
+etc
+won’t
+lip
+text
+horrifying
+speech
+suggest
+uncreate/V
+Hunspell
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/map/Makefile.in
@@ -0,0 +1,52 @@
+#
+# ***** 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 mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# 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 *****
+
+DEPTH		= ../../../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir  = extensions/spellcheck/tests/chrome/map
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/rules.mk
+
+_TEST_FILES = 	maputf.dic \
+				maputf.aff \
+				$(NULL)
+
+libs:: $(_TEST_FILES)
+	$(INSTALL) $^ $(DEPTH)/_tests/testing/mochitest/chrome/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/map/maputf.aff
@@ -0,0 +1,11 @@
+# With MAP suggestion, Hunspell can add missing accents to a word.
+
+SET UTF-8
+
+# switch off ngram suggestion for testing
+MAXNGRAMSUGS 0
+
+MAP 3
+MAP uúü
+MAP öóo
+MAP ß(ss)
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/map/maputf.dic
@@ -0,0 +1,4 @@
+3
+Frühstück
+tükörfúró
+groß
new file mode 100644
--- /dev/null
+++ b/extensions/spellcheck/tests/chrome/test_add_remove_dictionaries.xul
@@ -0,0 +1,110 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window title="Add and remove dictionaries test"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        onload="RunTest();">
+  <title>Add and remove dictionaries test</title>
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+  <script type="application/javascript">
+  <![CDATA[
+SimpleTest.waitForExplicitFinish();
+
+function getMisspelledWords(editor) {
+  return editor.selectionController.getSelection(Components.interfaces.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+function getDictionaryList(editor) {
+  var spellchecker = editor.getInlineSpellChecker(true).spellChecker;
+  var o1 = {};
+  spellchecker.GetDictionaryList(o1, {});
+  return o1.value;
+}
+
+function getCurrentDictionary(editor) {
+  var spellchecker = editor.getInlineSpellChecker(true).spellChecker;
+  return spellchecker.GetCurrentDictionary();
+}
+
+function setCurrentDictionary(editor, dictionary) {
+  var spellchecker = editor.getInlineSpellChecker(true).spellChecker;
+  spellchecker.SetCurrentDictionary(dictionary);
+}
+
+function RunTest() {
+  var editor = document.getElementById('textbox').editor;
+
+  var dir = Components.classes["@mozilla.org/file/directory_service;1"].
+            getService(Components.interfaces.nsIProperties).
+            get("CurWorkD", Components.interfaces.nsIFile);
+  dir.append("chrome");
+  dir.append("extensions");
+  dir.append("spellcheck");
+  dir.append("tests");
+  dir.append("chrome");
+
+  var hunspell = Components
+    .classes["@mozilla.org/spellchecker/engine;1"]
+    .getService(Components.interfaces.mozISpellCheckingEngine);
+
+  // install base dictionary
+  var base = dir.clone();
+  base.append("base");
+  ok(base.exists());
+  hunspell.addDirectory(base);
+
+  // install map dictionary
+  var map = dir.clone();
+  map.append("map");
+  ok(map.exists());
+  hunspell.addDirectory(map);
+
+  // test that base and map dictionaries are available
+  var dicts = getDictionaryList(editor);
+  isnot(dicts.indexOf("base_utf"), -1, "base is available");
+  isnot(dicts.indexOf("maputf"), -1, "map is available");
+
+  // select base dictionary
+  setCurrentDictionary(editor, "base_utf");
+
+  SimpleTest.executeSoon(function() {
+    // test that base dictionary is in use
+    is(getMisspelledWords(editor), "Frühstück" + "qwertyu", "base misspellings");
+    is(getCurrentDictionary(editor), "base_utf", "current dictionary");
+
+    // select map dictionary
+    setCurrentDictionary(editor, "maputf");
+
+    SimpleTest.executeSoon(function() {
+      // test that map dictionary is in use
+      is(getMisspelledWords(editor), "created" + "imply" + "tomorrow" + "qwertyu", "map misspellings");
+      is(getCurrentDictionary(editor), "maputf", "current dictionary");
+
+      // uninstall map dictionary
+      hunspell.removeDirectory(map);
+
+      SimpleTest.executeSoon(function() {
+        // test that map dictionary is not in use
+        isnot(getMisspelledWords(editor), "created" + "imply" + "tomorrow" + "qwertyu", "map misspellings");
+        isnot(getCurrentDictionary(editor), "maputf", "current dictionary");
+
+        // test that base dictionary is available and map dictionary is unavailable
+        var dicts = getDictionaryList(editor);
+        isnot(dicts.indexOf("base_utf"), -1, "base is available");
+        is(dicts.indexOf("maputf"), -1, "map is unavailable");
+
+        // uninstall base dictionary
+        hunspell.removeDirectory(base);
+
+        SimpleTest.finish();
+      });
+    });
+  });
+}
+  ]]>
+  </script>
+  <textbox id="textbox" spellcheck="true" value="created imply Frühstück tomorrow qwertyu"/>
+</window>
--- a/js/src/jscrashreport.h
+++ b/js/src/jscrashreport.h
@@ -51,32 +51,32 @@ void
 SnapshotGCStack();
 
 void
 SnapshotErrorStack();
 
 void
 SaveCrashData(uint64 tag, void *ptr, size_t size);
 
-template<size_t size, char marker>
+template<size_t size, unsigned char marker>
 class StackBuffer {
   private:
     JS_DECL_USE_GUARD_OBJECT_NOTIFIER
-    volatile char buffer[size + 4];
+    volatile unsigned char buffer[size + 4];
 
   public:
     StackBuffer(void *data JS_GUARD_OBJECT_NOTIFIER_PARAM) {
         JS_GUARD_OBJECT_NOTIFIER_INIT;
 
         buffer[0] = marker;
         buffer[1] = '[';
 
         for (size_t i = 0; i < size; i++) {
             if (data)
-                buffer[i + 2] = ((char *)data)[i];
+                buffer[i + 2] = ((unsigned char *)data)[i];
             else
                 buffer[i + 2] = 0;
         }
 
         buffer[size - 2] = ']';
         buffer[size - 1] = marker;
     }
 };
--- a/js/src/methodjit/FastBuiltins.cpp
+++ b/js/src/methodjit/FastBuiltins.cpp
@@ -503,17 +503,17 @@ mjit::Compiler::compileArrayWithArgs(uin
 
     JS_ASSERT(templateObject->getDenseArrayCapacity() >= argc);
 
     RegisterID result = frame.allocReg();
     Jump emptyFreeList = masm.getNewObject(cx, result, templateObject);
     stubcc.linkExit(emptyFreeList, Uses(0));
 
     for (unsigned i = 0; i < argc; i++) {
-        FrameEntry *arg = frame.peek(-argc + i);
+        FrameEntry *arg = frame.peek(-(int)argc + i);
         frame.storeTo(arg, Address(result, JSObject::getFixedSlotOffset(i)), /* popped = */ true);
     }
 
     masm.storePtr(ImmPtr((void *) argc), Address(result, offsetof(JSObject, initializedLength)));
 
     stubcc.leave();
 
     stubcc.masm.move(Imm32(argc), Registers::ArgReg1);
--- a/js/src/methodjit/MethodJIT.h
+++ b/js/src/methodjit/MethodJIT.h
@@ -682,19 +682,19 @@ TryCompile(JSContext *cx, JSScript *scri
 
 void
 ReleaseScriptCode(JSContext *cx, JSScript *script, bool construct);
 
 inline void
 ReleaseScriptCode(JSContext *cx, JSScript *script)
 {
     if (script->jitCtor)
-        mjit::ReleaseScriptCode(cx, script, CONSTRUCT);
+        mjit::ReleaseScriptCode(cx, script, true);
     if (script->jitNormal)
-        mjit::ReleaseScriptCode(cx, script, NO_CONSTRUCT);
+        mjit::ReleaseScriptCode(cx, script, false);
 }
 
 // Expand all stack frames inlined by the JIT within a compartment.
 void
 ExpandInlineFrames(JSCompartment *compartment);
 
 // Return all VMFrames in a compartment to the interpreter. This must be
 // followed by destroying all JIT code in the compartment.
--- a/js/src/xpconnect/src/xpcconvert.cpp
+++ b/js/src/xpconnect/src/xpcconvert.cpp
@@ -2114,17 +2114,18 @@ XPCConvert::JSArray2Native(XPCCallContex
     }
 
     if(pErr)
         *pErr = NS_ERROR_XPC_BAD_CONVERT_JS;
 
 #define POPULATE(_mode, _t)                                                  \
     PR_BEGIN_MACRO                                                           \
         cleanupMode = _mode;                                                 \
-        if (capacity > PR_UINT32_MAX / sizeof(_t) ||                         \
+        size_t max = PR_UINT32_MAX / sizeof(_t);                             \
+        if (capacity > max ||                                                \
             nsnull == (array = nsMemory::Alloc(capacity * sizeof(_t))))      \
         {                                                                    \
             if(pErr)                                                         \
                 *pErr = NS_ERROR_OUT_OF_MEMORY;                              \
             goto failure;                                                    \
         }                                                                    \
         for(initedCount = 0; initedCount < count; initedCount++)             \
         {                                                                    \
--- a/js/src/xpconnect/src/xpcwrappedjs.cpp
+++ b/js/src/xpconnect/src/xpcwrappedjs.cpp
@@ -276,17 +276,17 @@ nsresult
 nsXPCWrappedJS::GetNewOrUsed(XPCCallContext& ccx,
                              JSObject* aJSObj,
                              REFNSIID aIID,
                              nsISupports* aOuter,
                              nsXPCWrappedJS** wrapperResult)
 {
     JSObject2WrappedJSMap* map;
     JSObject* rootJSObj;
-    nsXPCWrappedJS* root;
+    nsXPCWrappedJS* root = nsnull;
     nsXPCWrappedJS* wrapper = nsnull;
     nsXPCWrappedJSClass* clazz = nsnull;
     XPCJSRuntime* rt = ccx.GetRuntime();
     JSBool release_root = JS_FALSE;
 
     map = rt->GetWrappedJSMap();
     if(!map)
     {
--- a/layout/svg/base/src/nsSVGUtils.cpp
+++ b/layout/svg/base/src/nsSVGUtils.cpp
@@ -64,32 +64,32 @@
 #include "nsSVGClipPathFrame.h"
 #include "nsSVGMaskFrame.h"
 #include "nsSVGContainerFrame.h"
 #include "nsSVGTextContainerFrame.h"
 #include "nsSVGLength2.h"
 #include "nsGenericElement.h"
 #include "nsSVGGraphicElement.h"
 #include "nsAttrValue.h"
-#include "nsSVGGeometryFrame.h"
 #include "nsIScriptError.h"
 #include "gfxContext.h"
 #include "gfxMatrix.h"
 #include "gfxRect.h"
 #include "gfxImageSurface.h"
 #include "gfxPlatform.h"
 #include "nsSVGForeignObjectFrame.h"
 #include "nsIDOMSVGUnitTypes.h"
 #include "nsSVGEffects.h"
 #include "nsMathUtils.h"
 #include "nsSVGIntegrationUtils.h"
 #include "nsSVGFilterPaintCallback.h"
 #include "nsSVGGeometryFrame.h"
 #include "nsComputedDOMStyle.h"
 #include "nsSVGPathGeometryFrame.h"
+#include "nsSVGPathGeometryElement.h"
 #include "prdtoa.h"
 #include "mozilla/dom/Element.h"
 #include "gfxUtils.h"
 #include "mozilla/Preferences.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
@@ -1426,47 +1426,67 @@ nsSVGUtils::WritePPM(const char *fname, 
       fwrite(data + y * stride + 4 * x + GFX_ARGB32_OFFSET_G, 1, 1, f);
       fwrite(data + y * stride + 4 * x + GFX_ARGB32_OFFSET_B, 1, 1, f);
     }
   }
   fclose(f);
 }
 #endif
 
-/*static*/ gfxRect
-nsSVGUtils::PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents,
-                                          nsSVGGeometryFrame* aFrame)
+// The logic here comes from _cairo_stroke_style_max_distance_from_path
+static gfxRect
+PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents,
+                              nsSVGGeometryFrame* aFrame,
+                              double styleExpansionFactor)
 {
-  // The logic here comes from _cairo_stroke_style_max_distance_from_path
-
-  double style_expansion = 0.5;
-
-  const nsStyleSVG* style = aFrame->GetStyleSVG();
-
-  if (style->mStrokeLinecap == NS_STYLE_STROKE_LINECAP_SQUARE) {
-    style_expansion = M_SQRT1_2;
-  }
-
-  if (style->mStrokeLinejoin == NS_STYLE_STROKE_LINEJOIN_MITER &&
-      style_expansion < style->mStrokeMiterlimit) {
-    style_expansion = style->mStrokeMiterlimit;
-  }
-
-  style_expansion *= aFrame->GetStrokeWidth();
+  double style_expansion =
+    styleExpansionFactor * aFrame->GetStrokeWidth();
 
   gfxMatrix ctm = aFrame->GetCanvasTM();
 
   double dx = style_expansion * (fabs(ctm.xx) + fabs(ctm.xy));
   double dy = style_expansion * (fabs(ctm.yy) + fabs(ctm.yx));
 
   gfxRect strokeExtents = aPathExtents;
   strokeExtents.Inflate(dx, dy);
   return strokeExtents;
 }
 
+/*static*/ gfxRect
+nsSVGUtils::PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents,
+                                          nsSVGGeometryFrame* aFrame)
+{
+  return ::PathExtentsToMaxStrokeExtents(aPathExtents, aFrame, 0.5);
+}
+
+/*static*/ gfxRect
+nsSVGUtils::PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents,
+                                          nsSVGPathGeometryFrame* aFrame)
+{
+  double styleExpansionFactor = 0.5;
+
+  if (static_cast<nsSVGPathGeometryElement*>(aFrame->GetContent())->IsMarkable()) {
+    const nsStyleSVG* style = aFrame->GetStyleSVG();
+
+    if (style->mStrokeLinecap == NS_STYLE_STROKE_LINECAP_SQUARE) {
+      styleExpansionFactor = M_SQRT1_2;
+    }
+
+    if (style->mStrokeLinejoin == NS_STYLE_STROKE_LINEJOIN_MITER &&
+        styleExpansionFactor < style->mStrokeMiterlimit &&
+        aFrame->GetContent()->Tag() != nsGkAtoms::line) {
+      styleExpansionFactor = style->mStrokeMiterlimit;
+    }
+  }
+
+  return ::PathExtentsToMaxStrokeExtents(aPathExtents,
+                                         aFrame,
+                                         styleExpansionFactor);
+}
+
 // ----------------------------------------------------------------------
 
 nsSVGRenderState::nsSVGRenderState(nsRenderingContext *aContext) :
   mRenderMode(NORMAL), mRenderingContext(aContext), mPaintingToWindow(PR_FALSE)
 {
   mGfxContext = aContext->ThebesContext();
 }
 
--- a/layout/svg/base/src/nsSVGUtils.h
+++ b/layout/svg/base/src/nsSVGUtils.h
@@ -71,16 +71,17 @@ class gfxContext;
 class gfxASurface;
 class gfxPattern;
 class gfxImageSurface;
 struct gfxSize;
 struct nsStyleFont;
 class nsSVGEnum;
 class nsISVGChildFrame;
 class nsSVGGeometryFrame;
+class nsSVGPathGeometryFrame;
 class nsSVGDisplayContainerFrame;
 
 namespace mozilla {
 class SVGAnimatedPreserveAspectRatio;
 class SVGPreserveAspectRatio;
 namespace dom {
 class Element;
 } // namespace dom
@@ -559,16 +560,18 @@ public:
    * without doing the calculations for the actual tight extents. We exploit
    * the fact that cairo does have an API for getting the tight device space
    * fill/path extents.
    *
    * This should die once bug 478152 is fixed.
    */
   static gfxRect PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents,
                                                nsSVGGeometryFrame* aFrame);
+  static gfxRect PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents,
+                                               nsSVGPathGeometryFrame* aFrame);
 
   /**
    * Convert a floating-point value to a 32-bit integer value, clamping to
    * the range of valid integers.
    */
   static PRInt32 ClampToInt(double aVal)
   {
     return NS_lround(NS_MAX(double(PR_INT32_MIN),
--- a/mobile/chrome/content/bindings.xml
+++ b/mobile/chrome/content/bindings.xml
@@ -1847,9 +1847,45 @@
             }
           }
 
           ContextHelper.showPopup({ target: aTextbox, json: json });
         ]]></body>
       </method>
     </implementation>
   </binding>
+  
+  <binding id="setting-fulltoggle-bool" extends="chrome://mozapps/content/extensions/setting.xml#setting-bool">
+    <handlers>
+      <handler event="click" button="0" phase="capturing">
+        <![CDATA[
+        this.input.setChecked(!this.value);
+        this.inputChanged();
+        event.stopPropagation();
+        ]]>
+      </handler>
+    </handlers>
+  </binding>
+
+  <binding id="setting-fulltoggle-boolint" extends="chrome://mozapps/content/extensions/setting.xml#setting-boolint">
+    <handlers>
+      <handler event="click" button="0" phase="capturing">
+        <![CDATA[
+        this.input.setChecked(!this.value);
+        this.inputChanged();
+        event.stopPropagation();
+        ]]>
+      </handler>
+    </handlers>
+  </binding>
+
+  <binding id="setting-fulltoggle-localized-bool" extends="chrome://mozapps/content/extensions/setting.xml#setting-localized-bool">
+    <handlers>
+      <handler event="click" button="0" phase="capturing">
+        <![CDATA[
+        this.input.setChecked(!this.value);
+        this.inputChanged();
+        event.stopPropagation();
+        ]]>
+      </handler>
+    </handlers>
+  </binding>
 </bindings>
--- a/mobile/chrome/content/browser.css
+++ b/mobile/chrome/content/browser.css
@@ -22,25 +22,25 @@ documenttab {
   -moz-binding: url("chrome://browser/content/tabs.xml#documenttab");
 }
 
 settings {
   -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#settings");
 }
 
 setting[type="bool"] {
-  -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-bool");
+  -moz-binding: url("chrome://browser/content/bindings.xml#setting-fulltoggle-bool");
 }
 
 setting[type="bool"][localized="true"] {
-  -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-localized-bool");
+  -moz-binding: url("chrome://browser/content/bindings.xml#setting-fulltoggle-localized-bool");
 }
 
 setting[type="boolint"] {
-  -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-boolint");
+  -moz-binding: url("chrome://browser/content/bindings.xml#setting-fulltoggle-boolint");
 }
 
 setting[type="integer"] {
   -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-integer");
 }
 
 setting[type="control"] {
   -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-control");
--- a/mobile/chrome/tests/Makefile.in
+++ b/mobile/chrome/tests/Makefile.in
@@ -69,16 +69,17 @@ include $(topsrcdir)/config/rules.mk
   browser_focus.js \
   browser_forms.html \
   $(warning browser_forms.js disabled due to failures) \
   browser_formsZoom.html \
   $(warning browser_formsZoom.js disabled due to failures) \
   browser_history.js \
   browser_mainui.js \
   browser_preferences_text.js \
+  browser_preferences_fulltoggle.js \
   browser_rect.js \
   browser_rememberPassword.js \
   browser_scroll.js \
   browser_scroll.html \
   browser_scrollbar.js \
   browser_select.html \
   browser_select.js \
   browser_sessionstore.js \
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/tests/browser_preferences_fulltoggle.js
@@ -0,0 +1,58 @@
+// browser-chrome test for fennec preferences to toggle values while clicking on the preference name
+
+var gTests = [];
+var gCurrentTest = null;
+
+function test() {
+  // The "runNextTest" approach is async, so we need to call "waitForExplicitFinish()"
+  // We call "finish()" when the tests are finished
+  waitForExplicitFinish();
+
+  // Start the tests
+  runNextTest();
+}
+//------------------------------------------------------------------------------
+// Iterating tests by shifting test out one by one as runNextTest is called.
+function runNextTest() {
+  // Run the next test until all tests completed
+  if (gTests.length > 0) {
+    gCurrentTest = gTests.shift();
+    info(gCurrentTest.desc);
+    gCurrentTest.run();
+  }
+  else {
+    // Cleanup. All tests are completed at this point
+    finish();
+  }
+}
+
+// -----------------------------------------------------------------------------------------
+// Verify preferences and text
+gTests.push({
+  desc: "Verify full toggle on Preferences",
+
+  run: function(){
+    // 1.Click preferences to view prefs
+    document.getElementById("tool-panel-open").click();
+    is(document.getElementById("panel-container").hidden, false, "Preferences should be visible");
+
+    var contentRegion = document.getElementById("prefs-content");
+
+    // Check for *Show images*
+    var imageRegion = document.getAnonymousElementByAttribute(contentRegion, "pref", "permissions.default.image");
+    var imageValue  = imageRegion.value;
+    var imageTitle  = document.getAnonymousElementByAttribute(imageRegion, "class", "preferences-title");
+    var imageButton = document.getAnonymousElementByAttribute(imageRegion, "anonid", "input");
+    imageButton.click();
+    is(imageRegion.value, !imageValue, "Tapping on input control should change the value");
+    imageTitle.click();
+    is(imageRegion.value, imageValue, "Tapping on the title should change the value"); 
+    imageRegion.click();
+    is(imageRegion.value, !imageValue, "Tapping on the setting should change the value"); 
+
+    BrowserUI.hidePanel();
+    is(document.getElementById("panel-container").hidden, true, "Preferences panel should be closed");
+    runNextTest();
+  }
+});
+
--- a/storage/public/mozIStorageConnection.idl
+++ b/storage/public/mozIStorageConnection.idl
@@ -356,18 +356,22 @@ interface mozIStorageConnection : nsISup
    * Remove a progress handler.
    *
    * @return previous registered handler.
    */
   mozIStorageProgressHandler removeProgressHandler();
 
   /**
    * Controls SQLITE_FCNTL_CHUNK_SIZE setting in sqlite. This helps avoid fragmentation
-   * by growing/shrinking the database file in SQLITE_FCNTL_CHUNK_SIZE increments.
+   * by growing/shrinking the database file in SQLITE_FCNTL_CHUNK_SIZE increments. To
+   * conserve memory on systems short on storage space, this function will have no effect
+   * on mobile devices or if less than 500MiB of space is left available.
    *
    * @param aIncrement
    *        The database file will grow in multiples of chunkSize.
    * @param aDatabaseName
    *        Sqlite database name. "" means pass NULL for zDbName to sqlite3_file_control.
    *        See http://sqlite.org/c3ref/file_control.html for more details.
+   * @throws NS_ERROR_FILE_TOO_BIG
+   *         If the system is short on storage space.
    */
   void setGrowthIncrement(in PRInt32 aIncrement, in AUTF8String aDatabaseName);
 };
--- a/storage/src/mozStorageConnection.cpp
+++ b/storage/src/mozStorageConnection.cpp
@@ -42,19 +42,19 @@
  * ***** END LICENSE BLOCK ***** */
 
 #include <stdio.h>
 
 #include "nsError.h"
 #include "nsIMutableArray.h"
 #include "nsHashSets.h"
 #include "nsAutoPtr.h"
-#include "nsIFile.h"
 #include "nsIMemoryReporter.h"
 #include "nsThreadUtils.h"
+#include "nsILocalFile.h"
 
 #include "mozIStorageAggregateFunction.h"
 #include "mozIStorageCompletionCallback.h"
 #include "mozIStorageFunction.h"
 
 #include "mozStorageAsyncStatementExecution.h"
 #include "mozStorageSQLFunctions.h"
 #include "mozStorageConnection.h"
@@ -65,16 +65,18 @@
 #include "mozStoragePrivateHelpers.h"
 #include "mozStorageStatementData.h"
 #include "StorageBaseStatementInternal.h"
 #include "SQLCollations.h"
 
 #include "prlog.h"
 #include "prprf.h"
 
+#define MIN_AVAILABLE_BYTES_PER_CHUNKED_GROWTH 524288000 // 500 MiB
+
 #ifdef PR_LOGGING
 PRLogModuleInfo* gStorageLog = nsnull;
 #endif
 
 namespace mozilla {
 namespace storage {
 
 namespace {
@@ -1275,16 +1277,26 @@ Connection::RemoveProgressHandler(mozISt
 
 NS_IMETHODIMP
 Connection::SetGrowthIncrement(PRInt32 aChunkSize, const nsACString &aDatabaseName)
 {
   // Bug 597215: Disk space is extremely limited on Android
   // so don't preallocate space. This is also not effective
   // on log structured file systems used by Android devices
 #if !defined(ANDROID) && !defined(MOZ_PLATFORM_MAEMO)
+  // Don't preallocate if less than 500MiB is available.
+  nsCOMPtr<nsILocalFile> localFile = do_QueryInterface(mDatabaseFile);
+  NS_ENSURE_STATE(localFile);
+  PRInt64 bytesAvailable;
+  nsresult rv = localFile->GetDiskSpaceAvailable(&bytesAvailable);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (bytesAvailable < MIN_AVAILABLE_BYTES_PER_CHUNKED_GROWTH) {
+    return NS_ERROR_FILE_TOO_BIG;
+  }
+
   (void)::sqlite3_file_control(mDBConn,
                                aDatabaseName.Length() ? nsPromiseFlatCString(aDatabaseName).get() : NULL,
                                SQLITE_FCNTL_CHUNK_SIZE,
                                &aChunkSize);
 #endif
   return NS_OK;
 }
 
--- a/storage/test/unit/test_chunk_growth.js
+++ b/storage/test/unit/test_chunk_growth.js
@@ -21,17 +21,22 @@ function get_size(name) {
   return new_file(name).fileSize
 }
 
 function run_test()
 {
   const filename = "chunked.sqlite";
   const CHUNK_SIZE = 512 * 1024;
   var d = getDatabase(new_file(filename));
-  d.setGrowthIncrement(CHUNK_SIZE, "");
+  try {
+    d.setGrowthIncrement(CHUNK_SIZE, "");
+  } catch (e if e.result == Cr.NS_ERROR_FILE_TOO_BIG) {
+    print("Too little free space to set CHUNK_SIZE!");
+    return;
+  }
   run_sql(d, "CREATE TABLE bloat(data varchar)");
 
   var orig_size = get_size(filename);
   /* Dump in at least 32K worth of data.
    * While writing ensure that the file size growth in chunksize set above.
    */
   const str1024 = new Array(1024).join("T");
   for(var i = 0; i < 32; i++) {
--- a/toolkit/mozapps/downloads/DownloadLastDir.jsm
+++ b/toolkit/mozapps/downloads/DownloadLastDir.jsm
@@ -78,24 +78,22 @@ let observer = {
   },
   observe: function (aSubject, aTopic, aData) {
     switch (aTopic) {
       case "private-browsing":
         if (aData == "enter")
           gDownloadLastDirFile = readLastDirPref();
         else if (aData == "exit") {
           gDownloadLastDirFile = null;
-          gDownloadLastDirStore = new Dict();
         }
         break;
       case "browser:purge-session-history":
         gDownloadLastDirFile = null;
         if (Services.prefs.prefHasUserValue(LAST_DIR_PREF))
           Services.prefs.clearUserPref(LAST_DIR_PREF);
-        gDownloadLastDirStore = new Dict();
         Services.contentPrefs.removePrefsByName(LAST_DIR_PREF);
         break;
     }
   }
 };
 
 let os = Components.classes["@mozilla.org/observer-service;1"]
                    .getService(Components.interfaces.nsIObserverService);
@@ -107,31 +105,23 @@ function readLastDirPref() {
     return Services.prefs.getComplexValue(LAST_DIR_PREF, nsILocalFile);
   }
   catch (e) {
     return null;
   }
 }
 
 let gDownloadLastDirFile = readLastDirPref();
-let gDownloadLastDirStore = new Dict();
 let gDownloadLastDir = {
   // compat shims
   get file() { return this.getFile(); },
   set file(val) { this.setFile(null, val); },
   getFile: function (aURI) {
     if (aURI) {
-      let lastDir;
-      if (pbSvc && pbSvc.privateBrowsingEnabled) {
-        let group = Services.contentPrefs.grouper.group(aURI);
-        lastDir = gDownloadLastDirStore.get(group, null);
-      }
-      if (!lastDir) {
-        lastDir = Services.contentPrefs.getPref(aURI, LAST_DIR_PREF);
-      }
+      let lastDir = Services.contentPrefs.getPref(aURI, LAST_DIR_PREF);
       if (lastDir) {
         var lastDirFile = Components.classes["@mozilla.org/file/local;1"]
                                     .createInstance(Components.interfaces.nsILocalFile);
         lastDirFile.initWithPath(lastDir);
         return lastDirFile;
       }
     }
     if (gDownloadLastDirFile && !gDownloadLastDirFile.exists())
@@ -139,22 +129,17 @@ let gDownloadLastDir = {
 
     if (pbSvc && pbSvc.privateBrowsingEnabled)
       return gDownloadLastDirFile;
     else
       return readLastDirPref();
   },
   setFile: function (aURI, aFile) {
     if (aURI) {
-      if (pbSvc && pbSvc.privateBrowsingEnabled) {
-        let group = Services.contentPrefs.grouper.group(aURI);
-        gDownloadLastDirStore.set(group, aFile.path);
-      } else {
-        Services.contentPrefs.setPref(aURI, LAST_DIR_PREF, aFile.path);
-      }
+      Services.contentPrefs.setPref(aURI, LAST_DIR_PREF, aFile.path);
     }
     if (pbSvc && pbSvc.privateBrowsingEnabled) {
       if (aFile instanceof Components.interfaces.nsIFile)
         gDownloadLastDirFile = aFile.clone();
       else
         gDownloadLastDirFile = null;
     } else {
       if (aFile instanceof Components.interfaces.nsIFile)
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -188,30 +188,47 @@ AddonAuthor.prototype = {
   }
 }
 
 /**
  * This represents an screenshot for an add-on
  *
  * @param  aURL
  *         The URL to the full version of the screenshot
+ * @param  aWidth
+ *         The width in pixels of the screenshot
+ * @param  aHeight
+ *         The height in pixels of the screenshot
  * @param  aThumbnailURL
  *         The URL to the thumbnail version of the screenshot
+ * @param  aThumbnailWidth
+ *         The width in pixels of the thumbnail version of the screenshot
+ * @param  aThumbnailHeight
+ *         The height in pixels of the thumbnail version of the screenshot
  * @param  aCaption
  *         The caption of the screenshot
  */
-function AddonScreenshot(aURL, aThumbnailURL, aCaption) {
+function AddonScreenshot(aURL, aWidth, aHeight, aThumbnailURL,
+                         aThumbnailWidth, aThumbnailHeight, aCaption) {
   this.url = aURL;
-  this.thumbnailURL = aThumbnailURL;
-  this.caption = aCaption;
+  if (aWidth) this.width = aWidth;
+  if (aHeight) this.height = aHeight;
+  if (aThumbnailURL) this.thumbnailURL = aThumbnailURL;
+  if (aThumbnailWidth) this.thumbnailWidth = aThumbnailWidth;
+  if (aThumbnailHeight) this.thumbnailHeight = aThumbnailHeight;
+  if (aCaption) this.caption = aCaption;
 }
 
 AddonScreenshot.prototype = {
   url: null,
+  width: null,
+  height: null,
   thumbnailURL: null,
+  thumbnailWidth: null,
+  thumbnailHeight: null,
   caption: null,
 
   // Returns the screenshot URL, defaulting to the empty string
   toString: function() {
     return this.url || "";
   }
 }
 
--- a/toolkit/mozapps/extensions/AddonRepository.jsm
+++ b/toolkit/mozapps/extensions/AddonRepository.jsm
@@ -62,17 +62,17 @@ const PREF_GETADDONS_GETSEARCHRESULTS   
 
 const XMLURI_PARSE_ERROR  = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
 
 const API_VERSION = "1.5";
 const DEFAULT_CACHE_TYPES = "extension,theme,locale";
 
 const KEY_PROFILEDIR = "ProfD";
 const FILE_DATABASE  = "addons.sqlite";
-const DB_SCHEMA      = 1;
+const DB_SCHEMA      = 2;
 
 ["LOG", "WARN", "ERROR"].forEach(function(aName) {
   this.__defineGetter__(aName, function() {
     Components.utils.import("resource://gre/modules/AddonLogging.jsm");
 
     LogManager.getLogger("addons.repository", this);
     return this[aName];
   });
@@ -954,23 +954,35 @@ var AddonRepository = {
 
               addon.developers.push(author);
             }
           });
           break;
         case "previews":
           let previewNodes = node.getElementsByTagName("preview");
           Array.forEach(previewNodes, function(aPreviewNode) {
-            let full = self._getDescendantTextContent(aPreviewNode, "full");
+            let full = self._getUniqueDescendant(aPreviewNode, "full");
             if (full == null)
               return;
 
-            let thumbnail = self._getDescendantTextContent(aPreviewNode, "thumbnail");
+            let fullURL = self._getTextContent(full);
+            let fullWidth = full.getAttribute("width");
+            let fullHeight = full.getAttribute("height");
+
+            let thumbnailURL, thumbnailWidth, thumbnailHeight;
+            let thumbnail = self._getUniqueDescendant(aPreviewNode, "thumbnail");
+            if (thumbnail) {
+              thumbnailURL = self._getTextContent(thumbnail);
+              thumbnailWidth = thumbnail.getAttribute("width");
+              thumbnailHeight = thumbnail.getAttribute("height");
+            }
             let caption = self._getDescendantTextContent(aPreviewNode, "caption");
-            let screenshot = new AddonManagerPrivate.AddonScreenshot(full, thumbnail, caption);
+            let screenshot = new AddonManagerPrivate.AddonScreenshot(fullURL, fullWidth, fullHeight,
+                                                                     thumbnailURL, thumbnailWidth,
+                                                                     thumbnailHeight, caption);
 
             if (addon.screenshots == null)
               addon.screenshots = [];
 
             if (aPreviewNode.getAttribute("primary") == 1)
               addon.screenshots.unshift(screenshot);
             else
               addon.screenshots.push(screenshot);
@@ -1247,31 +1259,38 @@ var AddonDatabase = {
                   "contributionURL, contributionAmount, averageRating, " +
                   "reviewCount, reviewURL, totalDownloads, weeklyDownloads, " +
                   "dailyUsers, sourceURI, repositoryStatus, size, updateDate " +
                   "FROM addon",
 
     getAllDevelopers: "SELECT addon_internal_id, name, url FROM developer " +
                       "ORDER BY addon_internal_id, num",
 
-    getAllScreenshots: "SELECT addon_internal_id, url, thumbnailURL, caption " +
+    getAllScreenshots: "SELECT addon_internal_id, url, width, height, " +
+                       "thumbnailURL, thumbnailWidth, thumbnailHeight, caption " +
                        "FROM screenshot ORDER BY addon_internal_id, num",
 
     insertAddon: "INSERT INTO addon VALUES (NULL, :id, :type, :name, :version, " +
                  ":creator, :creatorURL, :description, :fullDescription, " +
                  ":developerComments, :eula, :iconURL, :homepageURL, :supportURL, " +
                  ":contributionURL, :contributionAmount, :averageRating, " +
                  ":reviewCount, :reviewURL, :totalDownloads, :weeklyDownloads, " +
                  ":dailyUsers, :sourceURI, :repositoryStatus, :size, :updateDate)",
 
     insertDeveloper:  "INSERT INTO developer VALUES (:addon_internal_id, " +
                       ":num, :name, :url)",
 
-    insertScreenshot: "INSERT INTO screenshot VALUES (:addon_internal_id, " +
-                      ":num, :url, :thumbnailURL, :caption)",
+    // We specify column names here because the columns
+    // could be out of order due to schema changes.
+    insertScreenshot: "INSERT INTO screenshot (addon_internal_id, " +
+                      "num, url, width, height, thumbnailURL, " +
+                      "thumbnailWidth, thumbnailHeight, caption) " +
+                      "VALUES (:addon_internal_id, " +
+                      ":num, :url, :width, :height, :thumbnailURL, " +
+                      ":thumbnailWidth, :thumbnailHeight, :caption)",
 
     emptyAddon:       "DELETE FROM addon"
   },
 
   /**
    * A helper function to log an SQL error.
    *
    * @param  aError
@@ -1302,35 +1321,67 @@ var AddonDatabase = {
    */
   openConnection: function AD_openConnection(aSecondAttempt) {
     this.initialized = true;
     delete this.connection;
 
     let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
     let dbMissing = !dbfile.exists();
 
+    function tryAgain() {
+      LOG("Deleting database, and attempting openConnection again");
+      this.initialized = false;
+      if (this.connection.connectionReady)
+        this.connection.close();
+      if (dbfile.exists())
+        dbfile.remove(false);
+      return this.openConnection(true);
+    }
+
     try {
       this.connection = Services.storage.openUnsharedDatabase(dbfile);
     } catch (e) {
       this.initialized = false;
       ERROR("Failed to open database", e);
       if (aSecondAttempt || dbMissing) {
         this.databaseOk = false;
         throw e;
       }
-
-      LOG("Deleting database, and attempting openConnection again");
-      dbfile.remove(false);
-      return this.openConnection(true);
+      return tryAgain();
     }
 
     this.connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
-    if (dbMissing || this.connection.schemaVersion == 0)
+    if (dbMissing)
       this._createSchema();
 
+    switch (this.connection.schemaVersion) {
+      case 0:
+        this._createSchema();
+        break;
+      case 1:
+        try {
+          this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN width INTEGER");
+          this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN height INTEGER");
+          this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailWidth INTEGER");
+          this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailHeight INTEGER");
+          this._createIndices();
+          this.connection.schemaVersion = DB_SCHEMA;
+        } catch (e) {
+          ERROR("Failed to create database schema", e);
+          this.logSQLError(this.connection.lastError, this.connection.lastErrorString);
+          this.connection.rollbackTransaction();
+          return tryAgain();
+        }
+        break;
+      case 2:
+        break;
+      default:
+        return tryAgain();
+    }
+
     return this.connection;
   },
 
   /**
    * A lazy getter for the database connection.
    */
   get connection() {
     return this.openConnection();
@@ -1728,17 +1779,21 @@ var AddonDatabase = {
    *         The index of this screenshot
    */
   _addScreenshotParams: function AD__addScreenshotParams(aParams, aInternalID,
                                                          aScreenshot, aIndex) {
     let bp = aParams.newBindingParams();
     bp.bindByName("addon_internal_id", aInternalID);
     bp.bindByName("num", aIndex);
     bp.bindByName("url", aScreenshot.url);
+    bp.bindByName("width", aScreenshot.width);
+    bp.bindByName("height", aScreenshot.height);
     bp.bindByName("thumbnailURL", aScreenshot.thumbnailURL);
+    bp.bindByName("thumbnailWidth", aScreenshot.thumbnailWidth);
+    bp.bindByName("thumbnailHeight", aScreenshot.thumbnailHeight);
     bp.bindByName("caption", aScreenshot.caption);
     aParams.addParams(bp);
   },
 
   /**
    * Make add-on from an asynchronous row
    * Note: This add-on will be lacking both developers and screenshots
    *
@@ -1791,19 +1846,24 @@ var AddonDatabase = {
    * Make a screenshot from an asynchronous row
    *
    * @param  aRow
    *         The asynchronous row to use
    * @return The created screenshot
    */
   _makeScreenshotFromAsyncRow: function AD__makeScreenshotFromAsyncRow(aRow) {
     let url = aRow.getResultByName("url");
+    let width = aRow.getResultByName("width");
+    let height = aRow.getResultByName("height");
     let thumbnailURL = aRow.getResultByName("thumbnailURL");
-    let caption =aRow.getResultByName("caption");
-    return new AddonManagerPrivate.AddonScreenshot(url, thumbnailURL, caption);
+    let thumbnailWidth = aRow.getResultByName("thumbnailWidth");
+    let thumbnailHeight = aRow.getResultByName("thumbnailHeight");
+    let caption = aRow.getResultByName("caption");
+    return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL,
+                                                   thumbnailWidth, thumbnailHeight, caption);
   },
 
   /**
    * Synchronously creates the schema in the database.
    */
   _createSchema: function AD__createSchema() {
     LOG("Creating database schema");
     this.connection.beginTransaction();
@@ -1844,28 +1904,44 @@ var AddonDatabase = {
                                   "name TEXT, " +
                                   "url TEXT, " +
                                   "PRIMARY KEY (addon_internal_id, num)");
 
       this.connection.createTable("screenshot",
                                   "addon_internal_id INTEGER, " +
                                   "num INTEGER, " +
                                   "url TEXT, " +
+                                  "width INTEGER, " +
+                                  "height INTEGER, " +
                                   "thumbnailURL TEXT, " +
+                                  "thumbnailWidth INTEGER, " +
+                                  "thumbnailHeight INTEGER, " +
                                   "caption TEXT, " +
                                   "PRIMARY KEY (addon_internal_id, num)");
 
+      this._createIndices();
+
       this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " +
         "ON addon BEGIN " +
         "DELETE FROM developer WHERE addon_internal_id=old.internal_id; " +
         "DELETE FROM screenshot WHERE addon_internal_id=old.internal_id; " +
         "END");
 
       this.connection.schemaVersion = DB_SCHEMA;
       this.connection.commitTransaction();
     } catch (e) {
       ERROR("Failed to create database schema", e);
       this.logSQLError(this.connection.lastError, this.connection.lastErrorString);
       this.connection.rollbackTransaction();
       throw e;
     }
+  },
+
+  /**
+   * Synchronously creates the indices in the database.
+   */
+  _createIndices: function AD__createIndices() {
+      this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS developer_idx " +
+                                       "ON developer (addon_internal_id)");
+      this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS screenshot_idx " +
+                                       "ON screenshot (addon_internal_id)");
   }
 };
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -2541,20 +2541,26 @@ var gDetailView = {
       version.hidden = false;
       version.value = aAddon.version;
     } else {
       version.hidden = true;
     }
 
     var screenshot = document.getElementById("detail-screenshot");
     if (aAddon.screenshots && aAddon.screenshots.length > 0) {
-      if (aAddon.screenshots[0].thumbnailURL)
+      if (aAddon.screenshots[0].thumbnailURL) {
         screenshot.src = aAddon.screenshots[0].thumbnailURL;
-      else
+        screenshot.width = aAddon.screenshots[0].thumbnailWidth;
+        screenshot.height = aAddon.screenshots[0].thumbnailHeight;
+      } else {
         screenshot.src = aAddon.screenshots[0].url;
+        screenshot.width = aAddon.screenshots[0].width;
+        screenshot.height = aAddon.screenshots[0].height;
+      }
+      screenshot.setAttribute("loading", "true");
       screenshot.hidden = false;
     } else {
       screenshot.hidden = true;
     }
 
     var desc = document.getElementById("detail-desc");
     desc.textContent = aAddon.description;
 
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -532,17 +532,19 @@
                       <label class="disabled-postfix" value="&addon.disabled.postfix;"/>
                       <label class="update-postfix" value="&addon.update.postfix;"/>
                       <spacer flex="5000"/> <!-- Necessary to allow the name to wrap -->
                     </hbox>
                     <label id="detail-creator" class="creator"/>
                   </vbox>
                   <hbox id="detail-desc-container" align="start">
                     <vbox pack="center"> <!-- Necessary to work around bug 394738 -->
-                      <image id="detail-screenshot" hidden="true"/>
+                      <image id="detail-screenshot" hidden="true"
+                             onload="this.removeAttribute('loading');"
+                             onerror="this.setAttribute('loading', 'error');"/>
                     </vbox>
                     <vbox flex="1">
                       <description id="detail-desc"/>
                       <description id="detail-fulldesc"/>
                     </vbox>
                   </hbox>
                   <vbox id="detail-contributions">
                     <description id="detail-contrib-description">
--- a/toolkit/mozapps/extensions/test/browser/browser_details.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_details.js
@@ -76,17 +76,21 @@ function test() {
     description: "Short description",
     creator: { name: "Mozilla", url: null },
     type: "extension",
     iconURL: "chrome://foo/skin/icon.png",
     contributionURL: "http://foo.com",
     contributionAmount: null,
     updateDate: gDate,
     permissions: 0,
-    screenshots: [{url: "http://example.com/screenshot"}],
+    screenshots: [{
+      url: "chrome://branding/content/about.png",
+      width: 200,
+      height: 150
+    }],
   }, {
     id: "addon3@tests.mozilla.org",
     name: "Test add-on 3",
     description: "Short description",
     creator: { name: "Mozilla", url: "http://www.mozilla.org" },
     type: "extension",
     sourceURI: Services.io.newURI("http://example.com/foo", null, null),
     updateDate: gDate,
@@ -96,17 +100,21 @@ function test() {
     isActive: false,
     isCompatible: false,
     appDisabled: true,
     permissions: AddonManager.PERM_CAN_ENABLE |
                  AddonManager.PERM_CAN_DISABLE |
                  AddonManager.PERM_CAN_UPGRADE,
     screenshots: [{
       url: "http://example.com/screenshot",
-      thumbnailURL: "http://example.com/thumbnail"
+      width: 400,
+      height: 300,
+      thumbnailURL: "chrome://branding/content/icon64.png",
+      thumbnailWidth: 160,
+      thumbnailHeight: 120
     }],
   }, {
     id: "addon4@tests.mozilla.org",
     blocklistURL: "http://example.com/addon4@tests.mozilla.org",
     name: "Test add-on 4",
     _userDisabled: true,
     isActive: false,
     blocklistState: Ci.nsIBlocklistService.STATE_SOFTBLOCKED
@@ -153,16 +161,18 @@ function end_test() {
 add_test(function() {
   open_details("addon1@tests.mozilla.org", "extension", function() {
     is(get("detail-name").textContent, "Test add-on 1", "Name should be correct");
     is_element_visible(get("detail-version"), "Version should not be hidden");
     is(get("detail-version").value, "2.1", "Version should be correct");
     is(get("detail-icon").src, "chrome://foo/skin/icon64.png", "Icon should be correct");
     is_element_hidden(get("detail-creator"), "Creator should be hidden");
     is_element_hidden(get("detail-screenshot"), "Screenshot should be hidden");
+    is(get("detail-screenshot").width, "", "Screenshot dimensions should not be set");
+    is(get("detail-screenshot").height, "", "Screenshot dimensions should not be set");
     is(get("detail-desc").textContent, "Short description", "Description should be correct");
     is(get("detail-fulldesc").textContent, "Longer description", "Full description should be correct");
 
     is_element_visible(get("detail-contributions"), "Contributions section should be visible");
     is_element_visible(get("detail-contrib-suggested"), "Contributions amount should be visible");
     ok(get("detail-contrib-suggested").value, "$0.99");
 
     is_element_hidden(get("detail-dateUpdated"), "Update date should be hidden");
@@ -257,17 +267,20 @@ add_test(function() {
     is(get("detail-icon").src, "chrome://foo/skin/icon.png", "Icon should be correct");
 
     is_element_visible(get("detail-creator"), "Creator should not be hidden");
     is_element_visible(get("detail-creator")._creatorName, "Creator name should not be hidden");
     is(get("detail-creator")._creatorName.value, "Mozilla", "Creator should be correct");
     is_element_hidden(get("detail-creator")._creatorLink, "Creator link should be hidden");
 
     is_element_visible(get("detail-screenshot"), "Screenshot should be visible");
-    is(get("detail-screenshot").src, "http://example.com/screenshot", "Should be showing the full sized screenshot");
+    is(get("detail-screenshot").src, "chrome://branding/content/about.png", "Should be showing the full sized screenshot");
+    is(get("detail-screenshot").width, 200, "Screenshot dimensions should be set");
+    is(get("detail-screenshot").height, 150, "Screenshot dimensions should be set");
+    is(get("detail-screenshot").hasAttribute("loading"), true, "Screenshot should have loading attribute");
     is(get("detail-desc").textContent, "Short description", "Description should be correct");
     is_element_hidden(get("detail-fulldesc"), "Full description should be hidden");
 
     is_element_visible(get("detail-contributions"), "Contributions section should be visible");
     is_element_hidden(get("detail-contrib-suggested"), "Contributions amount should be hidden");
 
     is_element_visible(get("detail-dateUpdated"), "Update date should not be hidden");
     is(get("detail-dateUpdated").value, formatDate(gDate), "Update date should be correct");
@@ -289,17 +302,21 @@ add_test(function() {
     is_element_hidden(get("detail-uninstall-btn"), "Remove button should be hidden");
 
     is_element_hidden(get("detail-warning"), "Warning message should be hidden");
     is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
     is_element_hidden(get("detail-error"), "Error message should be hidden");
     is_element_hidden(get("detail-error-link"), "Error link should be hidden");
     is_element_hidden(get("detail-pending"), "Pending message should be hidden");
 
-    run_next_test();
+    get("detail-screenshot").addEventListener("load", function() {
+      this.removeEventListener("load", arguments.callee, false);
+      is(this.hasAttribute("loading"), false, "Screenshot should not have loading attribute");
+      run_next_test();
+    }, false);
   });
 });
 
 // Opens and tests the details view for add-on 3
 add_test(function() {
   open_details("addon3@tests.mozilla.org", "extension", function() {
     is(get("detail-name").textContent, "Test add-on 3", "Name should be correct");
     is_element_hidden(get("detail-version"), "Version should be hidden");
@@ -307,17 +324,20 @@ add_test(function() {
 
     is_element_visible(get("detail-creator"), "Creator should not be hidden");
     is_element_hidden(get("detail-creator")._creatorName, "Creator name should be hidden");
     is_element_visible(get("detail-creator")._creatorLink, "Creator link should not be hidden");
     is(get("detail-creator")._creatorLink.value, "Mozilla", "Creator link should be correct");
     is(get("detail-creator")._creatorLink.href, "http://www.mozilla.org", "Creator link href should be correct");
 
     is_element_visible(get("detail-screenshot"), "Screenshot should be visible");
-    is(get("detail-screenshot").src, "http://example.com/thumbnail", "Should be showing the thumbnail");
+    is(get("detail-screenshot").src, "chrome://branding/content/icon64.png", "Should be showing the thumbnail");
+    is(get("detail-screenshot").width, 160, "Screenshot dimensions should be set");
+    is(get("detail-screenshot").height, 120, "Screenshot dimensions should be set");
+    is(get("detail-screenshot").hasAttribute("loading"), true, "Screenshot should have loading attribute");
 
     is_element_hidden(get("detail-contributions"), "Contributions section should be hidden");
 
     is_element_visible(get("detail-dateUpdated"), "Update date should not be hidden");
     is(get("detail-dateUpdated").value, formatDate(gDate), "Update date should be correct");
 
     is_element_visible(get("detail-rating-row"), "Rating row should not be hidden");
     is_element_hidden(get("detail-rating"), "Rating should be hidden");
@@ -364,17 +384,21 @@ add_test(function() {
 
     is_element_visible(get("detail-warning"), "Warning message should be visible");
     is(get("detail-warning").textContent, "Test add-on 3 is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
     is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
     is_element_hidden(get("detail-error"), "Error message should be hidden");
     is_element_hidden(get("detail-error-link"), "Error link should be hidden");
     is_element_hidden(get("detail-pending"), "Pending message should be hidden");
 
-    run_next_test();
+    get("detail-screenshot").addEventListener("load", function() {
+      this.removeEventListener("load", arguments.callee, false);
+      is(this.hasAttribute("loading"), false, "Screenshot should not have loading attribute");
+      run_next_test();
+    }, false);
   });
 });
 
 // Opens and tests the details view for add-on 4
 add_test(function() {
   open_details("addon4@tests.mozilla.org", "extension", function() {
     is(get("detail-name").textContent, "Test add-on 4", "Name should be correct");
 
--- a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.xml
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.xml
@@ -27,18 +27,22 @@
       <application>
         <appID>xpcshell@tests.mozilla.org</appID>
         <min_version>1</min_version>
         <max_version>1</max_version>
       </application>
     </compatible_applications>
     <previews>
       <preview primary="1">
-        <full type="image/png">http://localhost:4444/full1-1.png</full>
-        <thumbnail type="image/png">http://localhost:4444/thumbnail1-1.png</thumbnail>
+        <full type="image/png" width="400" height="300">
+          http://localhost:4444/full1-1.png
+        </full>
+        <thumbnail type="image/png" width="200" height="150">
+          http://localhost:4444/thumbnail1-1.png
+        </thumbnail>
         <caption>Caption 1 - 1</caption>
       </preview>
       <preview primary="0">
         <full type="image/png">http://localhost:4444/full2-1.png</full>
         <thumbnail type="image/png">http://localhost:4444/thumbnail2-1.png</thumbnail>
         <caption>Caption 2 - 1</caption>
       </preview>
     </previews>
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -267,17 +267,21 @@ function do_check_author(aActual, aExpec
  * @param  aActual
  *         The actual screenshot to check.
  * @param  aExpected
  *         The expected screenshot to check against.
  */
 function do_check_screenshot(aActual, aExpected) {
   do_check_eq(aActual.toString(), aExpected.url);
   do_check_eq(aActual.url, aExpected.url);
+  do_check_eq(aActual.width, aExpected.width);
+  do_check_eq(aActual.height, aExpected.height);
   do_check_eq(aActual.thumbnailURL, aExpected.thumbnailURL);
+  do_check_eq(aActual.thumbnailWidth, aExpected.thumbnailWidth);
+  do_check_eq(aActual.thumbnailHeight, aExpected.thumbnailHeight);
   do_check_eq(aActual.caption, aExpected.caption);
 }
 
 /**
  * Starts up the add-on manager as if it was started by the application.
  *
  * @param  aAppChanged
  *         An optional boolean parameter to simulate the case where the
--- a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
@@ -52,19 +52,23 @@ var GET_RESULTS = [{
                             url:  BASE_URL + "/developer1.html"
                           }],
   description:            "Test Summary 1",
   fullDescription:        "Test Description 1",
   developerComments:      "Test Developer Comments 1",
   eula:                   "Test EULA 1",
   iconURL:                BASE_URL + "/icon1.png",
   screenshots:            [{
-                            url:          BASE_URL + "/full1-1.png",
-                            thumbnailURL: BASE_URL + "/thumbnail1-1.png",
-                            caption:      "Caption 1 - 1"
+                            url:             BASE_URL + "/full1-1.png",
+                            width:           400,
+                            height:          300,
+                            thumbnailURL:    BASE_URL + "/thumbnail1-1.png",
+                            thumbnailWidth:  200,
+                            thumbnailHeight: 150,
+                            caption:         "Caption 1 - 1"
                           }, {
                             url:          BASE_URL + "/full2-1.png",
                             thumbnailURL: BASE_URL + "/thumbnail2-1.png",
                             caption:      "Caption 2 - 1"
                           }],
   homepageURL:            BASE_URL + "/learnmore1.html",
   supportURL:             BASE_URL + "/support1.html",
   contributionURL:        BASE_URL + "/meetDevelopers1.html",
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrateAddonRepository.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+
+function run_test() {
+  do_test_pending();
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+  // Write out a minimal database.
+  let dbfile = gProfD.clone();
+  dbfile.append("addons.sqlite");
+  let db = AM_Cc["@mozilla.org/storage/service;1"].
+           getService(AM_Ci.mozIStorageService).
+           openDatabase(dbfile);
+
+  db.createTable("addon",
+                 "internal_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                 "id TEXT UNIQUE, " +
+                 "type TEXT, " +
+                 "name TEXT, " +
+                 "version TEXT, " +
+                 "creator TEXT, " +
+                 "creatorURL TEXT, " +
+                 "description TEXT, " +
+                 "fullDescription TEXT, " +
+                 "developerComments TEXT, " +
+                 "eula TEXT, " +
+                 "iconURL TEXT, " +
+                 "homepageURL TEXT, " +
+                 "supportURL TEXT, " +
+                 "contributionURL TEXT, " +
+                 "contributionAmount TEXT, " +
+                 "averageRating INTEGER, " +
+                 "reviewCount INTEGER, " +
+                 "reviewURL TEXT, " +
+                 "totalDownloads INTEGER, " +
+                 "weeklyDownloads INTEGER, " +
+                 "dailyUsers INTEGER, " +
+                 "sourceURI TEXT, " +
+                 "repositoryStatus INTEGER, " +
+                 "size INTEGER, " +
+                 "updateDate INTEGER");
+
+  db.createTable("developer",
+                 "addon_internal_id INTEGER, " +
+                 "num INTEGER, " +
+                 "name TEXT, " +
+                 "url TEXT, " +
+                 "PRIMARY KEY (addon_internal_id, num)");
+
+  db.createTable("screenshot",
+                 "addon_internal_id INTEGER, " +
+                 "num INTEGER, " +
+                 "url TEXT, " +
+                 "thumbnailURL TEXT, " +
+                 "caption TEXT, " +
+                 "PRIMARY KEY (addon_internal_id, num)");
+
+  let stmt = db.createStatement("INSERT INTO addon (id) VALUES (:id)");
+  stmt.params.id = "test1@tests.mozilla.org";
+  stmt.execute();
+  stmt.finalize();
+
+  stmt = db.createStatement("INSERT INTO screenshot VALUES " +
+                            "(:addon_internal_id, :num, :url, :thumbnailURL, :caption)");
+
+  stmt.params.addon_internal_id = 1;
+  stmt.params.num = 0;
+  stmt.params.url = "http://localhost:4444/full1-1.png";
+  stmt.params.thumbnailURL = "http://localhost:4444/thumbnail1-1.png";
+  stmt.params.caption = "Caption 1 - 1";
+  stmt.execute();
+  stmt.finalize();
+
+  db.schemaVersion = 1;
+  db.close();
+
+  Services.obs.addObserver({
+    observe: function () {
+      Services.obs.removeObserver(this, "addon-repository-shutdown");
+      // Check the DB schema has changed once AddonRepository has freed it.
+      db = AM_Cc["@mozilla.org/storage/service;1"].
+           getService(AM_Ci.mozIStorageService).
+           openDatabase(dbfile);
+      do_check_eq(db.schemaVersion, 2);
+      do_check_true(db.indexExists("developer_idx"));
+      do_check_true(db.indexExists("screenshot_idx"));
+      db.close();
+      do_test_finished();
+    }
+  }, "addon-repository-shutdown", null);
+
+  Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true);
+  AddonRepository.getCachedAddonByID("test1@tests.mozilla.org", function (aAddon) {
+    do_check_neq(aAddon, null);
+    do_check_eq(aAddon.screenshots.length, 1);
+    do_check_true(aAddon.screenshots[0].width === null);
+    do_check_true(aAddon.screenshots[0].height === null);
+    do_check_true(aAddon.screenshots[0].thumbnailWidth === null);
+    do_check_true(aAddon.screenshots[0].thumbnailHeight === null);
+    AddonRepository.shutdown();
+  });
+}
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -152,16 +152,17 @@ skip-if = os == "android"
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 [test_locale.js]
 [test_locked.js]
 [test_manifest.js]
 [test_migrate1.js]
 [test_migrate2.js]
 [test_migrate3.js]
+[test_migrateAddonRepository.js]
 [test_permissions.js]
 [test_plugins.js]
 # Bug 676992: test consistently fails on Android
 fail-if = os == "android"
 [test_registry.js]
 [test_safemode.js]
 [test_startup.js]
 # Bug 676992: test consistently fails on Android
--- a/toolkit/themes/gnomestripe/mozapps/extensions/extensions.css
+++ b/toolkit/themes/gnomestripe/mozapps/extensions/extensions.css
@@ -656,16 +656,29 @@
 }
 
 #detail-screenshot {
   -moz-margin-end: 2em;
   max-width: 300px;
   max-height: 300px;
 }
 
+#detail-screenshot[loading] {
+  background-image: url("chrome://global/skin/icons/loading_16.png");
+  background-position: 50% 50%;
+  background-repeat: no-repeat;
+  border: 1px threedshadow solid;
+  border-radius: 5px;
+  -moz-box-sizing: border-box;
+}
+
+#detail-screenshot[loading="error"] {
+  background-image: url("chrome://global/skin/media/error.png");
+}
+
 #detail-desc-container {
   margin-bottom: 2em;
 }
 
 #detail-desc, #detail-fulldesc {
   -moz-margin-start: 6px;
   /* This is necessary to fix layout issues with multi-line descriptions, see
      bug 592712*/
--- a/toolkit/themes/pinstripe/mozapps/extensions/extensions.css
+++ b/toolkit/themes/pinstripe/mozapps/extensions/extensions.css
@@ -796,16 +796,29 @@
 }
 
 #detail-screenshot {
   -moz-margin-end: 2em;
   max-width: 300px;
   max-height: 300px;
 }
 
+#detail-screenshot[loading] {
+  background-image: url("chrome://global/skin/icons/loading_16.png"),
+                    -moz-linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0));
+  background-position: 50% 50%;
+  background-repeat: no-repeat;
+  border-radius: 3px;
+}
+
+#detail-screenshot[loading="error"] {
+  background-image: url("chrome://global/skin/media/error.png"),
+                    -moz-linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0));
+}
+
 #detail-desc-container {
   margin-bottom: 2em;
 }
 
 #detail-desc, #detail-fulldesc {
   -moz-margin-start: 6px;
   /* This is necessary to fix layout issues with multi-line descriptions, see
      bug 592712*/
--- a/toolkit/themes/winstripe/mozapps/extensions/extensions.css
+++ b/toolkit/themes/winstripe/mozapps/extensions/extensions.css
@@ -774,16 +774,29 @@
 }
 
 #detail-screenshot {
   -moz-margin-end: 2em;
   max-width: 300px;
   max-height: 300px;
 }
 
+#detail-screenshot[loading] {
+  background-image: url("chrome://global/skin/icons/loading_16.png"),
+                    -moz-linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0));
+  background-position: 50% 50%;
+  background-repeat: no-repeat;
+  border-radius: 3px;
+}
+
+#detail-screenshot[loading="error"] {
+  background-image: url("chrome://global/skin/media/error.png"),
+                    -moz-linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0));
+}
+
 #detail-desc-container {
   margin-bottom: 2em;
 }
 
 #detail-desc, #detail-fulldesc {
   -moz-margin-start: 6px;
   /* This is necessary to fix layout issues with multi-line descriptions, see
      bug 592712*/