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 76655 2857288e9a3faa4660e1800a61fb887be2a85e95
parent 76654 19db4fd8a771eaf3065098336bcd7ef92b062fd0 (current diff)
parent 76569 1f5cd567c93a7ef6db27aa0539bdc2a4099935d0 (diff)
child 76656 ab1e3be27b4315cca736ec10486089786b2e3127
push id3
push userfelipc@gmail.com
push dateFri, 30 Sep 2011 20:09:13 +0000
milestone9.0a1
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*/