Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 20 Aug 2013 16:27:00 -0400
changeset 156202 d136c8999d967cb04004373ba80420217e1a8ce4
parent 156176 6551f959f80d792b59071ea164c87585857254b5 (current diff)
parent 156201 ceb9333feb212fbfa040d7dffc6eda689e453563 (diff)
child 156477 7e83ba2dfb5154d03ba3071c1f0476cb132b3fff
child 156480 bae16ff9991535fb15ee06033335f3ef91696559
child 156496 3b6752031ed33f67dd8e861d11d939da14eb015a
child 156519 353b662234995befac891e3ed80724a81aab8b3a
child 158580 d4b94879db2603e0e96a29e0f36e5400d71fd9b5
push id2961
push userlsblakk@mozilla.com
push dateMon, 28 Oct 2013 21:59:28 +0000
treeherdermozilla-beta@73ef4f13486f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone26.0a1
first release with
nightly linux32
d136c8999d96 / 26.0a1 / 20130821030213 / files
nightly linux64
d136c8999d96 / 26.0a1 / 20130821030213 / files
nightly mac
d136c8999d96 / 26.0a1 / 20130821030213 / files
nightly win32
d136c8999d96 / 26.0a1 / 20130821030213 / files
nightly win64
d136c8999d96 / 26.0a1 / 20130821030213 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c.
toolkit/devtools/apps/tests/Makefile.in
--- a/browser/base/content/test/browser_tabopen_reflows.js
+++ b/browser/base/content/test/browser_tabopen_reflows.js
@@ -44,17 +44,21 @@ const EXPECTED_REFLOWS = [
   // SessionStore.getWindowDimensions()
   "ssi_getWindowDimension@resource:///modules/sessionstore/SessionStore.jsm|" +
     "@resource:///modules/sessionstore/SessionStore.jsm|" +
     "ssi_updateWindowFeatures@resource:///modules/sessionstore/SessionStore.jsm|" +
     "ssi_collectWindowData@resource:///modules/sessionstore/SessionStore.jsm|",
 
   // tabPreviews.capture()
   "tabPreviews_capture@chrome://browser/content/browser.js|" +
-    "tabPreviews_handleEvent/<@chrome://browser/content/browser.js|"
+    "tabPreviews_handleEvent/<@chrome://browser/content/browser.js|",
+
+  // tabPreviews.capture()
+  "tabPreviews_capture@chrome://browser/content/browser.js|" +
+    "@chrome://browser/content/browser.js|"
 ];
 
 const PREF_PRELOAD = "browser.newtab.preload";
 
 /*
  * This test ensures that there are no unexpected
  * uninterruptible reflows when opening new tabs.
  */
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -1214,90 +1214,111 @@ ElementEditor.prototype = {
       }
     }
   },
 
   _startModifyingAttributes: function() {
     return this.node.startModifyingAttributes();
   },
 
-  _createAttribute: function EE_createAttribute(aAttr, aBefore)
+  _createAttribute: function EE_createAttribute(aAttr, aBefore = null)
   {
-    if (this.attrs.hasOwnProperty(aAttr.name)) {
-      var attr = this.attrs[aAttr.name];
-      var name = attr.querySelector(".attrname");
-      var val = attr.querySelector(".attrvalue");
-    } else {
-      // Create the template editor, which will save some variables here.
-      let data = {
-        attrName: aAttr.name,
-      };
-      this.template("attribute", data);
-      var {attr, inner, name, val} = data;
+    // Create the template editor, which will save some variables here.
+    let data = {
+      attrName: aAttr.name,
+    };
+    this.template("attribute", data);
+    var {attr, inner, name, val} = data;
 
-      // Figure out where we should place the attribute.
-      let before = aBefore || null;
-      if (aAttr.name == "id") {
-        before = this.attrList.firstChild;
-      } else if (aAttr.name == "class") {
-        let idNode = this.attrs["id"];
-        before = idNode ? idNode.nextSibling : this.attrList.firstChild;
-      }
-      this.attrList.insertBefore(attr, before);
+    // Double quotes need to be handled specially to prevent DOMParser failing.
+    // name="v"a"l"u"e" when editing -> name='v"a"l"u"e"'
+    // name="v'a"l'u"e" when editing -> name="v'a&quot;l'u&quot;e"
+    let editValueDisplayed = aAttr.value;
+    let hasDoubleQuote = editValueDisplayed.contains('"');
+    let hasSingleQuote = editValueDisplayed.contains("'");
+    let initial = aAttr.name + '="' + editValueDisplayed + '"';
+
+    // Can't just wrap value with ' since the value contains both " and '.
+    if (hasDoubleQuote && hasSingleQuote) {
+        editValueDisplayed = editValueDisplayed.replace(/\"/g, "&quot;");
+        initial = aAttr.name + '="' + editValueDisplayed + '"';
+    }
+
+    // Wrap with ' since there are no single quotes in the attribute value.
+    if (hasDoubleQuote && !hasSingleQuote) {
+        initial = aAttr.name + "='" + editValueDisplayed + "'";
+    }
 
-      // Make the attribute editable.
-      editableField({
-        element: inner,
-        trigger: "dblclick",
-        stopOnReturn: true,
-        selectAll: false,
-        contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
-        popup: this.markup.popup,
-        start: (aEditor, aEvent) => {
-          // If the editing was started inside the name or value areas,
-          // select accordingly.
-          if (aEvent && aEvent.target === name) {
-            aEditor.input.setSelectionRange(0, name.textContent.length);
-          } else if (aEvent && aEvent.target === val) {
-            let length = val.textContent.length;
-            let editorLength = aEditor.input.value.length;
-            let start = editorLength - (length + 1);
-            aEditor.input.setSelectionRange(start, start + length);
-          } else {
-            aEditor.input.select();
-          }
-        },
-        done: (aVal, aCommit) => {
-          if (!aCommit) {
-            return;
-          }
+    // Make the attribute editable.
+    editableField({
+      element: inner,
+      trigger: "dblclick",
+      stopOnReturn: true,
+      selectAll: false,
+      initial: initial,
+      contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+      popup: this.markup.popup,
+      start: (aEditor, aEvent) => {
+        // If the editing was started inside the name or value areas,
+        // select accordingly.
+        if (aEvent && aEvent.target === name) {
+          aEditor.input.setSelectionRange(0, name.textContent.length);
+        } else if (aEvent && aEvent.target === val) {
+          let length = editValueDisplayed.length;
+          let editorLength = aEditor.input.value.length;
+          let start = editorLength - (length + 1);
+          aEditor.input.setSelectionRange(start, start + length);
+        } else {
+          aEditor.input.select();
+        }
+      },
+      done: (aVal, aCommit) => {
+        if (!aCommit) {
+          return;
+        }
+
+        let doMods = this._startModifyingAttributes();
+        let undoMods = this._startModifyingAttributes();
 
-          let doMods = this._startModifyingAttributes();
-          let undoMods = this._startModifyingAttributes();
+        // Remove the attribute stored in this editor and re-add any attributes
+        // parsed out of the input element. Restore original attribute if
+        // parsing fails.
+        try {
+          this._saveAttribute(aAttr.name, undoMods);
+          doMods.removeAttribute(aAttr.name);
+          this._applyAttributes(aVal, attr, doMods, undoMods);
+          this.undo.do(() => {
+            doMods.apply();
+          }, () => {
+            undoMods.apply();
+          })
+        } catch(ex) {
+          console.error(ex);
+        }
+      }
+    });
 
-          // Remove the attribute stored in this editor and re-add any attributes
-          // parsed out of the input element. Restore original attribute if
-          // parsing fails.
-          try {
-            this._saveAttribute(aAttr.name, undoMods);
-            doMods.removeAttribute(aAttr.name);
-            this._applyAttributes(aVal, attr, doMods, undoMods);
-            this.undo.do(() => {
-              doMods.apply();
-            }, () => {
-              undoMods.apply();
-            })
-          } catch(ex) {
-            console.error(ex);
-          }
-        }
-      });
 
-      this.attrs[aAttr.name] = attr;
+    // Figure out where we should place the attribute.
+    let before = aBefore;
+    if (aAttr.name == "id") {
+      before = this.attrList.firstChild;
+    } else if (aAttr.name == "class") {
+      let idNode = this.attrs["id"];
+      before = idNode ? idNode.nextSibling : this.attrList.firstChild;
     }
+    this.attrList.insertBefore(attr, before);
+
+    // Remove the old version of this attribute from the DOM.
+    let oldAttr = this.attrs[aAttr.name];
+    if (oldAttr && oldAttr.parentNode) {
+      oldAttr.parentNode.removeChild(oldAttr);
+    }
+
+    this.attrs[aAttr.name] = attr;
 
     name.textContent = aAttr.name;
     val.textContent = aAttr.value;
 
     return attr;
   },
 
   /**
--- a/browser/devtools/markupview/test/browser_inspector_markup_edit.html
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.html
@@ -35,10 +35,12 @@
     </div>
     <div id="node22" class="unchanged"></div>
     <div id="node23"></div>
     <div id="node24"></div>
     <div id="retag-me">
       <div id="retag-me-2"></div>
     </div>
     <div id="node25"></div>
+    <div id="node26" style='background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F");'></div>
+    <div id="node27" class="Double &quot; and single &apos;"></div>
   </body>
 </html>
--- a/browser/devtools/markupview/test/browser_inspector_markup_edit.js
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.js
@@ -234,16 +234,89 @@ function test() {
       },
       after: function() {
         assertAttributes(doc.querySelector("#node25"), {
           id: "node25",
           src: "somefile.html?param1=<a>&param2=\xfc&param3='\"'"
         });
       }
     },
+
+    {
+      desc: "Modify inline style containing \"",
+      before: function() {
+        assertAttributes(doc.querySelector("#node26"), {
+          id: "node26",
+          style: 'background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F");'
+        });
+      },
+      execute: function(after) {
+        inspector.once("markupmutation", after);
+        let editor = getContainerForRawNode(markup, doc.querySelector("#node26")).editor;
+        let attr = editor.attrs["style"].querySelector(".editable");
+
+
+        attr.focus();
+        EventUtils.sendKey("return", inspector.panelWin);
+
+        let input = inplaceEditor(attr).input;
+        let value = input.value;
+
+        is (value,
+          "style='background-image: url(\"moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F\");'",
+          "Value contains actual double quotes"
+        );
+
+        value = value.replace(/mozilla\.org/, "mozilla.com");
+        input.value = value;
+
+        EventUtils.sendKey("return", inspector.panelWin);
+      },
+      after: function() {
+        assertAttributes(doc.querySelector("#node26"), {
+          id: "node26",
+          style: 'background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.com%2F");'
+        });
+      }
+    },
+
+    {
+      desc: "Modify inline style containing \" and \'",
+      before: function() {
+        assertAttributes(doc.querySelector("#node27"), {
+          id: "node27",
+          class: 'Double " and single \''
+        });
+      },
+      execute: function(after) {
+        inspector.once("markupmutation", after);
+        let editor = getContainerForRawNode(markup, doc.querySelector("#node27")).editor;
+        let attr = editor.attrs["class"].querySelector(".editable");
+
+        attr.focus();
+        EventUtils.sendKey("return", inspector.panelWin);
+
+        let input = inplaceEditor(attr).input;
+        let value = input.value;
+
+        is (value, "class=\"Double &quot; and single '\"", "Value contains &quot;");
+
+        value = value.replace(/Double/, "&quot;").replace(/single/, "'");
+        input.value = value;
+
+        EventUtils.sendKey("return", inspector.panelWin);
+      },
+      after: function() {
+        assertAttributes(doc.querySelector("#node27"), {
+          id: "node27",
+          class: '" " and \' \''
+        });
+      }
+    },
+
     {
       desc: "Add an attribute value without closing \"",
       enteredText: 'style="display: block;',
       expectedAttributes: {
         style: "display: block;"
       }
     },
     {
--- a/browser/devtools/responsivedesign/responsivedesign.jsm
+++ b/browser/devtools/responsivedesign/responsivedesign.jsm
@@ -682,16 +682,17 @@ ResponsiveUI.prototype = {
 
     this.saveCustomSize();
 
     delete this._resizing;
     if (this.transitionsEnabled) {
       this.stack.removeAttribute("notransition");
     }
     this.ignoreY = false;
+    this.ignoreX = false;
     this.isResizing = false;
   },
 
   /**
    * Store the custom size as a pref.
    */
    saveCustomSize: function RUI_saveCustomSize() {
      Services.prefs.setIntPref("devtools.responsiveUI.customWidth", this.customPreset.width);
--- a/browser/devtools/styleinspector/test/browser_ruleview_inherit.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_inherit.js
@@ -37,17 +37,17 @@ function simpleInherit(aInspector, aRule
     ok(!!inheritRule.inherited, "Rule should consider itself inherited.");
     is(inheritRule.textProps.length, 1, "Should only display one inherited style");
     let inheritProp = inheritRule.textProps[0];
     is(inheritProp.name, "color", "color should have been inherited.");
 
     styleNode.parentNode.removeChild(styleNode);
 
     emptyInherit();
-  }).then(null, console.error);
+  });
 }
 
 function emptyInherit()
 {
   // No inheritable styles, this rule shouldn't show up.
   let style = '' +
     '#test2 {' +
     '  background-color: green;' +
@@ -63,17 +63,17 @@ function emptyInherit()
     is(elementStyle.rules.length, 1, "Should have 1 rule.");
 
     let elementRule = elementStyle.rules[0];
     ok(!elementRule.inherited, "Element style attribute should not consider itself inherited.");
 
     styleNode.parentNode.removeChild(styleNode);
 
     elementStyleInherit();
-  }).then(null, console.error);
+  });
 }
 
 function elementStyleInherit()
 {
   doc.body.innerHTML = '<div id="test2" style="color: red"><div id="test1">Styled Node</div></div>';
 
   inspector.selection.setNode(doc.getElementById("test1"));
   inspector.once("inspector-updated", () => {
@@ -87,17 +87,17 @@ function elementStyleInherit()
     let inheritRule = elementStyle.rules[1];
     is(inheritRule.domRule.type, ELEMENT_STYLE, "Inherited rule should be an element style, not a rule.");
     ok(!!inheritRule.inherited, "Rule should consider itself inherited.");
     is(inheritRule.textProps.length, 1, "Should only display one inherited style");
     let inheritProp = inheritRule.textProps[0];
     is(inheritProp.name, "color", "color should have been inherited.");
 
     finishTest();
-  }).then(null, console.error);
+  });
 }
 
 function finishTest()
 {
   doc = null;
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/styleinspector/test/browser_styleinspector_bug_672744_search_filter.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_672744_search_filter.js
@@ -32,17 +32,17 @@ function SI_inspectNode()
   var span = doc.querySelector("#matches");
   ok(span, "captain, we have the matches span");
 
   inspector.selection.setNode(span);
   inspector.once("inspector-updated", () => {
     is(span, computedView.viewedElement.rawNode(),
       "style inspector node matches the selected node");
     SI_toggleDefaultStyles();
-  }).then(null, (err) => console.error(err));
+  });
 }
 
 function SI_toggleDefaultStyles()
 {
   info("checking \"Browser styles\" checkbox");
 
   let doc = computedView.styleDocument;
   let checkbox = doc.querySelector(".includebrowserstyles");
--- a/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
@@ -55,17 +55,17 @@
 <!ENTITY btnPageCSS.label   "CSS">
 <!ENTITY btnPageCSS.tooltip "Log CSS parsing errors">
 <!ENTITY btnPageCSS.accesskey "C">
 <!ENTITY btnPageJS.label    "JS">
 <!ENTITY btnPageJS.tooltip  "Log JavaScript exceptions">
 <!ENTITY btnPageJS.accesskey  "J">
 <!ENTITY btnPageSecurity.label "Security">
 <!ENTITY btnPageSecurity.tooltip "Log security errors and warnings">
-<!ENTITY btnPageSecurity.accesskey "S">
+<!ENTITY btnPageSecurity.accesskey "u">
 
 <!-- LOCALIZATION NOTE (btnPageLogging): This is used as the text of the
   -  the toolbar. It shows or hides messages that the web developer inserted on
   -  the page for debugging purposes, using calls such console.log() and
   -  console.error(). -->
 <!ENTITY btnPageLogging.label   "Logging">
 <!ENTITY btnPageLogging.tooltip "Log messages sent to the window.console object">
 <!ENTITY btnPageLogging.accesskey2 "R">
--- a/browser/themes/shared/devtools/markup-view.css
+++ b/browser/themes/shared/devtools/markup-view.css
@@ -4,22 +4,16 @@
 
 * {
   padding: 0;
   margin: 0;
 }
 
 .newattr {
   cursor: pointer;
-}
-
-/* Give some padding to focusable elements to match the editor input
- * that will replace them. */
-span[tabindex] {
-  display: inline-block;
   padding: 1px 0;
 }
 
 li.container {
   padding: 2px 0 0 2px;
 }
 
 .codebox {
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -25,16 +25,17 @@ endif
   HOST_CSRCS \
   HOST_LIBRARY_NAME \
   MODULE \
   NO_DIST_INSTALL \
   PARALLEL_DIRS \
   TEST_DIRS \
   TIERS \
   TOOL_DIRS \
+  XPCSHELL_TESTS \
   XPIDL_MODULE \
   $(NULL)
 
 _DEPRECATED_VARIABLES := \
   XPIDL_FLAGS \
   $(NULL)
 
 ifndef EXTERNALLY_MANAGED_MAKE_FILE
--- a/dom/locales/en-US/chrome/layout/HtmlForm.properties
+++ b/dom/locales/en-US/chrome/layout/HtmlForm.properties
@@ -23,13 +23,16 @@ NoFileSelected=No file selected.
 # LOCALIZATION NOTE (NoFilesSelected): this string is shown on a
 # <input type='file' multiple> when there is no file selected yet.
 NoFilesSelected=No files selected.
 # LOCALIZATION NOTE (XFilesSelected): this string is shown on a
 # <input type='file' multiple> when there are more than one selected file.
 # %S will be a number greater or equal to 2.
 XFilesSelected=%S files selected.
 ColorPicker=Choose a color
-# LOCALIZATION NOTE (AndXMoreFiles): this string is shown at the end of the
-# tooltip text for <input type='file' multiple> when there are more than 21
-# files selected (when we will only list the first 20, plus an "and X more"
-# line). %S will be the number of files minus 20. 
-AndXMoreFiles=and %S more
+# LOCALIZATION NOTE (AndNMoreFiles): Semi-colon list of plural forms. 
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals 
+# This string is shown at the end of the tooltip text for <input type='file'
+# multiple> when there are more than 21 files selected (when we will only list
+# the first 20, plus an "and X more" line). #1 represents the number of files
+# minus 20 and will always be a number equal to or greater than 2. So the
+# singular case will never be used.
+AndNMoreFiles=and one more;and #1 more
--- a/js/src/config/rules.mk
+++ b/js/src/config/rules.mk
@@ -25,16 +25,17 @@ endif
   HOST_CSRCS \
   HOST_LIBRARY_NAME \
   MODULE \
   NO_DIST_INSTALL \
   PARALLEL_DIRS \
   TEST_DIRS \
   TIERS \
   TOOL_DIRS \
+  XPCSHELL_TESTS \
   XPIDL_MODULE \
   $(NULL)
 
 _DEPRECATED_VARIABLES := \
   XPIDL_FLAGS \
   $(NULL)
 
 ifndef EXTERNALLY_MANAGED_MAKE_FILE
--- a/mobile/android/base/PageActionLayout.java
+++ b/mobile/android/base/PageActionLayout.java
@@ -26,39 +26,39 @@ import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
 import java.util.UUID;
-import java.util.LinkedHashMap;
+import java.util.ArrayList;
 
 public class PageActionLayout extends LinearLayout implements GeckoEventListener,
                                                               View.OnClickListener,
                                                               View.OnLongClickListener {
     private final String LOGTAG = "GeckoPageActionLayout";
     private final String MENU_BUTTON_KEY = "MENU_BUTTON_KEY";
     private final int DEFAULT_PAGE_ACTIONS_SHOWN = 2;
 
-    private LinkedHashMap<String, PageAction> mPageActionList;
+    private ArrayList<PageAction> mPageActionList;
     private GeckoPopupMenu mPageActionsMenu;
     private Context mContext;
     private LinearLayout mLayout;
 
     // By default it's two, can be changed by calling setNumberShown(int)
     private int mMaxVisiblePageActions;
 
     public PageActionLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
         mLayout = this;
 
-        mPageActionList = new LinkedHashMap<String, PageAction>();
+        mPageActionList = new ArrayList<PageAction>();
         setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN);
         refreshPageActionIcons();
 
         registerEventListener("PageActions:Add");
         registerEventListener("PageActions:Remove");
     }
 
     public void setNumberShown(int count) {
@@ -86,57 +86,68 @@ public class PageActionLayout extends Li
 
     @Override
     public void handleMessage(String event, JSONObject message) {
         try {
             if (event.equals("PageActions:Add")) {
                 final String id = message.getString("id");
                 final String title = message.getString("title");
                 final String imageURL = message.optString("icon");
+                final boolean mImportant = message.getBoolean("important");
 
                 addPageAction(id, title, imageURL, new OnPageActionClickListeners() {
                     @Override
                     public void onClick(String id) {
                         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:Clicked", id));
                     }
 
                     @Override
                     public boolean onLongClick(String id) {
                         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:LongClicked", id));
                         return true;
                     }
-                });
+                }, mImportant);
             } else if (event.equals("PageActions:Remove")) {
                 final String id = message.getString("id");
 
                 removePageAction(id);
             }
         } catch(JSONException ex) {
             Log.e(LOGTAG, "Error deocding", ex);
         }
     }
 
-    public void addPageAction(final String id, final String title, final String imageData, final OnPageActionClickListeners mOnPageActionClickListeners) {
-        final PageAction pageAction = new PageAction(id, title, null, mOnPageActionClickListeners);
-        mPageActionList.put(id, pageAction);
+    public void addPageAction(final String id, final String title, final String imageData, final OnPageActionClickListeners mOnPageActionClickListeners, boolean mImportant) {
+        final PageAction pageAction = new PageAction(id, title, null, mOnPageActionClickListeners, mImportant);
+
+        int insertAt = mPageActionList.size();
+        while(insertAt > 0 && mPageActionList.get(insertAt-1).isImportant()) {
+          insertAt--;
+        }
+        mPageActionList.add(insertAt, pageAction);
 
         BitmapUtils.getDrawable(mContext, imageData, new BitmapUtils.BitmapLoader() {
             @Override
             public void onBitmapFound(final Drawable d) {
-                if (mPageActionList.containsKey(id)) {
+                if (mPageActionList.contains(pageAction)) {
                     pageAction.setDrawable(d);
                     refreshPageActionIcons();
                 }
             }
         });
     }
 
     public void removePageAction(String id) {
-        mPageActionList.remove(id);
-        refreshPageActionIcons();
+        for(int i = 0; i < mPageActionList.size(); i++) {
+            if (mPageActionList.get(i).getID().equals(id)) {
+                mPageActionList.remove(i);
+                refreshPageActionIcons();
+                return;
+            }
+        }
     }
 
     private ImageButton createImageButton() {
         ImageButton imageButton = new ImageButton(mContext, null, R.style.AddressBar_ImageButton_Icon);
         imageButton.setLayoutParams(new LayoutParams(mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width), LayoutParams.MATCH_PARENT));
         imageButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
         imageButton.setOnClickListener(this);
         imageButton.setOnLongClickListener(this);
@@ -145,29 +156,29 @@ public class PageActionLayout extends Li
 
     @Override
     public void onClick(View v) {
         String buttonClickedId = (String)v.getTag();
         if (buttonClickedId != null) {
             if (buttonClickedId.equals(MENU_BUTTON_KEY)) {
                 showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1);
             } else {
-                mPageActionList.get(buttonClickedId).onClick();
+                getPageActionWithId(buttonClickedId).onClick();
             }
         }
     }
 
     @Override
     public boolean onLongClick(View v) {
         String buttonClickedId = (String)v.getTag();
         if (buttonClickedId.equals(MENU_BUTTON_KEY)) {
             showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1);
             return true;
         } else {
-            return mPageActionList.get(buttonClickedId).onLongClick();
+            return getPageActionWithId(buttonClickedId).onLongClick();
         }
     }
 
     private void setActionForView(final ImageButton view, final PageAction pageAction) {
         if (pageAction == null) {
             view.setTag(null);
             ThreadUtils.postToUiThread(new Runnable() {
                 @Override
@@ -225,79 +236,86 @@ public class PageActionLayout extends Li
          * and hence we maintain the insertion order of the child Views which is essentially the reverse of their index
          */
 
         int buttonIndex = (this.getChildCount() - 1) - index;
         int totalVisibleButtons = ((mPageActionList.size() < this.getChildCount()) ? mPageActionList.size() : this.getChildCount());
 
         if (mPageActionList.size() > buttonIndex) {
             // Return the pageactions starting from the end of the list for the number of visible pageactions.
-            return getPageActionAt((mPageActionList.size() - totalVisibleButtons) + buttonIndex);
+            return mPageActionList.get((mPageActionList.size() - totalVisibleButtons) + buttonIndex);
         }
         return null;
     }
 
-    private PageAction getPageActionAt(int index) {
-        int count = 0;
-        for(PageAction pageAction : mPageActionList.values()) {
-            if (count == index) {
+    private PageAction getPageActionWithId(String id) {
+        for(int i = 0; i < mPageActionList.size(); i++) {
+            PageAction pageAction = mPageActionList.get(i);
+            if (pageAction.getID().equals(id)) {
                 return pageAction;
             }
-            count++;
         }
         return null;
     }
 
     private void showMenu(View mPageActionButton, int toShow) {
         if (mPageActionsMenu == null) {
             mPageActionsMenu = new GeckoPopupMenu(mPageActionButton.getContext(), mPageActionButton);
             mPageActionsMenu.inflate(0);
             mPageActionsMenu.setOnMenuItemClickListener(new GeckoPopupMenu.OnMenuItemClickListener() {
                 @Override
                 public boolean onMenuItemClick(MenuItem item) {
-                    for(PageAction pageAction : mPageActionList.values()) {
-                        if (pageAction.key() == item.getItemId()) {
+                    int id = item.getItemId();
+                    for(int i = 0; i < mPageActionList.size(); i++) {
+                        PageAction pageAction = mPageActionList.get(i);
+                        if (pageAction.key() == id) {
                             pageAction.onClick();
+                            return true;
                         }
                     }
-                    return true;
+                    return false;
                 }
             });
         }
         Menu menu = mPageActionsMenu.getMenu();
         menu.clear();
 
-        int count = 0;
-        for(PageAction pageAction : mPageActionList.values()) {
-            if (count < toShow) {
+        for(int i = 0; i < mPageActionList.size(); i++) {
+            if (i < toShow) {
+                PageAction pageAction = mPageActionList.get(i);
                 MenuItem item = menu.add(Menu.NONE, pageAction.key(), Menu.NONE, pageAction.getTitle());
                 item.setIcon(pageAction.getDrawable());
             }
-            count++;
         }
         mPageActionsMenu.show();
     }
 
     public static interface OnPageActionClickListeners {
         public void onClick(String id);
         public boolean onLongClick(String id);
     }
 
     private static class PageAction {
         private OnPageActionClickListeners mOnPageActionClickListeners;
         private Drawable mDrawable;
         private String mTitle;
         private String mId;
         private int key;
+        private boolean mImportant;
 
-        public PageAction(String id, String title, Drawable image, OnPageActionClickListeners mOnPageActionClickListeners) {
+        public PageAction(String id,
+                          String title,
+                          Drawable image,
+                          OnPageActionClickListeners mOnPageActionClickListeners,
+                          boolean mImportant) {
             this.mId = id;
             this.mTitle = title;
             this.mDrawable = image;
             this.mOnPageActionClickListeners = mOnPageActionClickListeners;
+            this.mImportant = mImportant;
 
             this.key = UUID.fromString(mId.subSequence(1, mId.length() - 2).toString()).hashCode();
         }
 
         public Drawable getDrawable() {
             return mDrawable;
         }
 
@@ -312,16 +330,20 @@ public class PageActionLayout extends Li
         public String getID() {
             return mId;
         }
 
         public int key() {
             return key;
         }
 
+        public boolean isImportant() {
+            return mImportant;
+        }
+
         public void onClick() {
             if (mOnPageActionClickListeners != null) {
                 mOnPageActionClickListeners.onClick(mId);
             }
         }
 
         public boolean onLongClick() {
             if (mOnPageActionClickListeners != null) {
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -69,23 +69,27 @@ var SelectionHandler = {
     switch (aTopic) {
       case "Gesture:SingleTap": {
         if (this._activeType == this.TYPE_SELECTION) {
           let data = JSON.parse(aData);
           if (this._pointInSelection(data.x, data.y))
             this.copySelection();
           else
             this._closeSelection();
+        } else if (this._activeType == this.TYPE_CURSOR) {
+          // attachCaret() is called in the "Gesture:SingleTap" handler in BrowserEventHandler
+          // We're guaranteed to call this first, because this observer was added last
+          this._closeSelection();
         }
         break;
       }
       case "Tab:Selected":
         this._closeSelection();
         break;
-  
+
       case "Window:Resize": {
         if (this._activeType == this.TYPE_SELECTION) {
           // Knowing when the page is done drawing is hard, so let's just cancel
           // the selection when the window changes. We should fix this later.
           this._closeSelection();
         }
         break;
       }
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1623,17 +1623,18 @@ var NativeWindow = {
   pageactions: {
     _items: { },
     add: function(aOptions) {
       let id = uuidgen.generateUUID().toString();
       sendMessageToJava({
         type: "PageActions:Add",
         id: id,
         title: aOptions.title,
-        icon: resolveGeckoURI(aOptions.icon)
+        icon: resolveGeckoURI(aOptions.icon),
+        important: "important" in aOptions ? aOptions.important : false
       });
       this._items[id] = {
         clickCallback: aOptions.clickCallback,
         longClickCallback: aOptions.longClickCallback
       };
       return id;
     },
     remove: function(id) {
@@ -7152,24 +7153,26 @@ let Reader = {
       NativeWindow.pageactions.remove(this.pageAction.id);
       delete this.pageAction.id;
     }
 
     if (tab.readerActive) {
       this.pageAction.id = NativeWindow.pageactions.add({
         title: Strings.browser.GetStringFromName("readerMode.exit"),
         icon: "drawable://reader_active",
-        clickCallback: this.pageAction.readerModeCallback
+        clickCallback: this.pageAction.readerModeCallback,
+        important: true
       });
     } else if (tab.readerEnabled) {
       this.pageAction.id = NativeWindow.pageactions.add({
         title: Strings.browser.GetStringFromName("readerMode.enter"),
         icon: "drawable://reader",
         clickCallback:this.pageAction.readerModeCallback,
-        longClickCallback: this.pageAction.readerModeActiveCallback
+        longClickCallback: this.pageAction.readerModeActiveCallback,
+        important: true
       });
     }
   },
 
   observe: function(aMessage, aTopic, aData) {
     switch(aTopic) {
       case "Reader:Add": {
         let args = JSON.parse(aData);
--- a/services/common/tests/unit/test_storage_server.js
+++ b/services/common/tests/unit/test_storage_server.js
@@ -341,17 +341,17 @@ add_test(function test_bso_if_unmodified
   server.createContents("123", {
     test: {bso: {foo: "bar"}}
   });
   server.startSynchronous();
 
   let coll = server.user("123").collection("test");
   do_check_neq(coll, null);
 
-  let time = coll.timestamp;
+  let time = coll.bso("bso").modified;
 
   _("Ensure we get a 412 for specified times older than server time.");
   let request = localRequest(server, "/2.0/123/storage/test/bso",
                              "123", "password");
   request.setHeader("X-If-Unmodified-Since", time - 5000);
   request.setHeader("Content-Type", "application/json");
   let payload = JSON.stringify({"payload": "foobar"});
   let error = doPutRequest(request, payload);
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -3361,16 +3361,28 @@
     "description": "The time (in milliseconds) that it took a 'prototypeAndProperties' request to go round trip."
   },
   "DEVTOOLS_DEBUGGER_RDP_REMOTE_PROTOTYPEANDPROPERTIES_MS": {
     "kind": "exponential",
     "high": "10000",
     "n_buckets": "1000",
     "description": "The time (in milliseconds) that it took a 'prototypeAndProperties' request to go round trip."
   },
+  "DEVTOOLS_DEBUGGER_RDP_LOCAL_PROTOTYPESANDPROPERTIES_MS": {
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'prototypesAndProperties' request to go round trip."
+  },
+  "DEVTOOLS_DEBUGGER_RDP_REMOTE_PROTOTYPESANDPROPERTIES_MS": {
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'prototypesAndProperties' request to go round trip."
+  },
   "DEVTOOLS_DEBUGGER_RDP_LOCAL_PROPERTY_MS": {
     "kind": "exponential",
     "high": "10000",
     "n_buckets": "1000",
     "description": "The time (in milliseconds) that it took a 'property' request to go round trip."
   },
   "DEVTOOLS_DEBUGGER_RDP_REMOTE_PROPERTY_MS": {
     "kind": "exponential",
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -585,21 +585,21 @@
                 const TRUNCATED_FILE_COUNT = 20;
                 let count = Math.min(files.length, TRUNCATED_FILE_COUNT);
                 for (let i = 1; i < count; ++i) {
                   titleText += "\n" + files[i].name;
                 }
                 if (files.length == TRUNCATED_FILE_COUNT + 1) {
                   titleText += "\n" + files[TRUNCATED_FILE_COUNT].name;
                 } else if (files.length > TRUNCATED_FILE_COUNT + 1) {
-                  let xmoreStr = bundle.GetStringFromName("AndXMoreFiles");
+                  let xmoreStr = bundle.GetStringFromName("AndNMoreFiles");
                   let xmoreNum = files.length - TRUNCATED_FILE_COUNT;
                   let tmp = {};
                   Components.utils.import("resource://gre/modules/PluralForm.jsm", tmp);
-                  let andXMoreStr = tmp.PluralForm.get(xmoreNum, xmoreStr).replace("%S", xmoreNum);
+                  let andXMoreStr = tmp.PluralForm.get(xmoreNum, xmoreStr).replace("#1", xmoreNum);
                   titleText += "\n" + andXMoreStr;
                 }
               }
             } catch(e) {}
           }
 
           while ((titleText == null) && (XLinkTitleText == null) &&
                  (SVGTitleText == null) && tipElement) {
--- a/toolkit/devtools/DevToolsUtils.js
+++ b/toolkit/devtools/DevToolsUtils.js
@@ -1,35 +1,46 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /* General utilities used throughout devtools. */
 
-/* Turn the error e into a string, without fail. */
+/**
+ * Turn the error |aError| into a string, without fail.
+ */
 this.safeErrorString = function safeErrorString(aError) {
   try {
-    var s = aError.toString();
-    if (typeof s === "string")
-      return s;
+    let errorString = aError.toString();
+    if (typeof errorString === "string") {
+      // Attempt to attach a stack to |errorString|. If it throws an error, or
+      // isn't a string, don't use it.
+      try {
+        if (aError.stack) {
+          let stack = aError.stack.toString();
+          if (typeof stack === "string") {
+            errorString += "\nStack: " + stack;
+          }
+        }
+      } catch (ee) { }
+
+      return errorString;
+    }
   } catch (ee) { }
 
   return "<failed trying to find error description>";
 }
 
 /**
  * Report that |aWho| threw an exception, |aException|.
  */
 this.reportException = function reportException(aWho, aException) {
   let msg = aWho + " threw an exception: " + safeErrorString(aException);
-  if (aException.stack) {
-    msg += "\nCall stack:\n" + aException.stack;
-  }
 
   dump(msg + "\n");
 
   if (Components.utils.reportError) {
     /*
      * Note that the xpcshell test harness registers an observer for
      * console messages, so when we're running tests, this will cause
      * the test to quit.
deleted file mode 100644
--- a/toolkit/devtools/apps/tests/Makefile.in
+++ /dev/null
@@ -1,15 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-DEPTH           = @DEPTH@
-topsrcdir       = @top_srcdir@
-srcdir          = @srcdir@
-VPATH           = @srcdir@
-relativesrcdir = @relativesrcdir@
-
-include $(DEPTH)/config/autoconf.mk
-
-XPCSHELL_TESTS = unit
-
-include $(topsrcdir)/config/rules.mk
--- a/toolkit/devtools/apps/tests/moz.build
+++ b/toolkit/devtools/apps/tests/moz.build
@@ -1,7 +1,9 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 MODULE = 'test_webapps_actor'
+
+XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
--- a/toolkit/devtools/apps/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/apps/tests/unit/xpcshell.ini
@@ -1,6 +1,10 @@
 [DEFAULT]
 head = head_apps.js
 tail = tail_apps.js
 
 [test_webappsActor.js]
+# Persistent failures.
+skip-if = true
 [test_appInstall.js]
+# Persistent failures.
+skip-if = true
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -1621,18 +1621,32 @@ ThreadClient.prototype = {
    */
   source: function TC_source(aForm) {
     if (aForm.actor in this._threadGrips) {
       return this._threadGrips[aForm.actor];
     }
 
     return this._threadGrips[aForm.actor] = new SourceClient(this._client,
                                                              aForm);
-  }
+  },
 
+  /**
+   * Request the prototype and own properties of mutlipleObjects.
+   *
+   * @param aOnResponse function
+   *        Called with the request's response.
+   * @param actors [string]
+   *        List of actor ID of the queried objects.
+   */
+  getPrototypesAndProperties: DebuggerClient.requester({
+    type: "prototypesAndProperties",
+    actors: args(0)
+  }, {
+    telemetry: "PROTOTYPESANDPROPERTIES"
+  })
 };
 
 eventSource(ThreadClient.prototype);
 
 /**
  * Creates a tracing profiler client for the remote debugging protocol
  * server. This client is a front to the trace actor created on the
  * server side, hiding the protocol details in a traditional
--- a/toolkit/devtools/moz.build
+++ b/toolkit/devtools/moz.build
@@ -1,14 +1,16 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+TEST_DIRS += ['tests']
+
 PARALLEL_DIRS += [
     'server',
     'client',
     'gcli',
     'sourcemap',
     'webconsole',
     'apps',
     'styleinspector'
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -2065,31 +2065,60 @@ ThreadActor.prototype = {
           && bp.line <= endLine) {
         this._setBreakpoint(bp);
       }
     }
 
     return true;
   },
 
+
+  /**
+   * Get prototypes and properties of multiple objects.
+   */
+  onPrototypesAndProperties: function TA_onPrototypesAndProperties(aRequest) {
+    let result = {};
+    for (let actorID of aRequest.actors) {
+      // This code assumes that there are no lazily loaded actors returned
+      // by this call.
+      let actor = this.conn.getActor(actorID);
+      if (!actor) {
+        return { from: this.actorID,
+                 error: "noSuchActor" };
+      }
+      let handler = actor.onPrototypeAndProperties;
+      if (!handler) {
+        return { from: this.actorID,
+                 error: "unrecognizedPacketType",
+                 message: ('Actor "' + actorID +
+                           '" does not recognize the packet type ' +
+                           '"prototypeAndProperties"') };
+      }
+      result[actorID] = handler.call(actor, {});
+    }
+    return { from: this.actorID,
+             actors: result };
+  }
+
 };
 
 ThreadActor.prototype.requestTypes = {
   "attach": ThreadActor.prototype.onAttach,
   "detach": ThreadActor.prototype.onDetach,
   "reconfigure": ThreadActor.prototype.onReconfigure,
   "resume": ThreadActor.prototype.onResume,
   "clientEvaluate": ThreadActor.prototype.onClientEvaluate,
   "frames": ThreadActor.prototype.onFrames,
   "interrupt": ThreadActor.prototype.onInterrupt,
   "eventListeners": ThreadActor.prototype.onEventListeners,
   "releaseMany": ThreadActor.prototype.onReleaseMany,
   "setBreakpoint": ThreadActor.prototype.onSetBreakpoint,
   "sources": ThreadActor.prototype.onSources,
-  "threadGrips": ThreadActor.prototype.onThreadGrips
+  "threadGrips": ThreadActor.prototype.onThreadGrips,
+  "prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties
 };
 
 
 /**
  * Creates a PauseActor.
  *
  * PauseActors exist for the lifetime of a given debuggee pause.  Used to
  * scope pause-lifetime grips.
--- a/toolkit/devtools/server/actors/tracer.js
+++ b/toolkit/devtools/server/actors/tracer.js
@@ -530,17 +530,17 @@ function timeSinceTraceStarted({ startTi
 /**
  * Creates a value grip for the given completion value, to be
  * serialized by JSON.stringify.
  *
  * @param aType string
  *        The type of completion value to serialize (return, throw, or yield).
  */
 function serializeCompletionValue(aType, { value }) {
-  if (typeof value[aType] === "undefined") {
+  if (!Object.hasOwnProperty.call(value, aType)) {
     return undefined;
   }
   return createValueGrip(value[aType], true);
 }
 
 
 // Serialization helper functions. Largely copied from script.js and modified
 // for use in serialization rather than object actor requests.
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -829,23 +829,22 @@ DebuggerServerConnection.prototype = {
       if (pool.has(aActorID)) {
         return pool;
       }
     }
     return null;
   },
 
   _unknownError: function DSC__unknownError(aPrefix, aError) {
-    let errorString = safeErrorString(aError);
-    errorString += "\n" + aError.stack;
+    let errorString = aPrefix + ": " + safeErrorString(aError);
     Cu.reportError(errorString);
     dumpn(errorString);
     return {
       error: "unknownError",
-      message: (aPrefix + "': " + errorString)
+      message: errorString
     };
   },
 
   /* Forwarding packets to other transports based on actor name prefixes. */
 
   /*
    * Arrange to forward packets to another server. This is how we
    * forward debugging connections to child processes.
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-09.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/**
+ * This tests exercises getProtypesAndProperties message accepted
+ * by a thread actor.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-grips");
+  gDebuggee.eval(function stopMe(arg1, arg2) {
+    debugger;
+  }.toString());
+
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_object_grip();
+    });
+  });
+  do_test_pending();
+}
+
+function test_object_grip()
+{
+  gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
+    let args = aPacket.frame.arguments;
+
+    gThreadClient.getPrototypesAndProperties([args[0].actor, args[1].actor], function(aResponse) {
+      let obj1 = aResponse.actors[args[0].actor];
+      let obj2 = aResponse.actors[args[1].actor];
+      do_check_eq(obj1.ownProperties.x.configurable, true);
+      do_check_eq(obj1.ownProperties.x.enumerable, true);
+      do_check_eq(obj1.ownProperties.x.writable, true);
+      do_check_eq(obj1.ownProperties.x.value, 10);
+
+      do_check_eq(obj1.ownProperties.y.configurable, true);
+      do_check_eq(obj1.ownProperties.y.enumerable, true);
+      do_check_eq(obj1.ownProperties.y.writable, true);
+      do_check_eq(obj1.ownProperties.y.value, "kaiju");
+
+      do_check_eq(obj2.ownProperties.z.configurable, true);
+      do_check_eq(obj2.ownProperties.z.enumerable, true);
+      do_check_eq(obj2.ownProperties.z.writable, true);
+      do_check_eq(obj2.ownProperties.z.value, 123);
+
+      do_check_true(obj1.prototype != undefined);
+      do_check_true(obj2.prototype != undefined);
+
+      gThreadClient.resume(function() {
+        finishClient(gClient);
+      });
+    });
+
+  });
+
+  gDebuggee.eval("stopMe({ x: 10, y: 'kaiju'}, { z: 123 })");
+}
+
--- a/toolkit/devtools/server/tests/unit/test_trace_actor-06.js
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-06.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Tests that objects are correctly serialized and sent in exitedFrame
- * packets.
+ * Tests that values are correctly serialized and sent in enteredFrame
+ * and exitedFrame packets.
  */
 
 let {defer} = devtools.require("sdk/core/promise");
 
 var gDebuggee;
 var gClient;
 var gTraceClient;
 
@@ -25,122 +25,199 @@ function run_test()
       });
     });
   });
   do_test_pending();
 }
 
 function test_enter_exit_frame()
 {
-  gTraceClient.addListener("exitedFrame", function(aEvent, aPacket) {
-    if (aPacket.sequence === 3) {
-      let obj = aPacket.return;
-      do_check_eq(typeof obj, "object",
-                  'exitedFrame response should have return value');
-      do_check_eq(typeof obj.prototype, "object",
-                  'return value should have prototype');
-      do_check_eq(typeof obj.ownProperties, "object",
-                  'return value should have ownProperties list');
-      do_check_eq(typeof obj.safeGetterValues, "object",
-                  'return value should have safeGetterValues');
-
-      do_check_eq(typeof obj.ownProperties.num, "object",
-                  'return value should have property "num"');
-      do_check_eq(typeof obj.ownProperties.str, "object",
-                  'return value should have property "str"');
-      do_check_eq(typeof obj.ownProperties.bool, "object",
-                  'return value should have property "bool"');
-      do_check_eq(typeof obj.ownProperties.undef, "object",
-                  'return value should have property "undef"');
-      do_check_eq(typeof obj.ownProperties.undef.value, "object",
-                  'return value property "undef" should be a grip');
-      do_check_eq(typeof obj.ownProperties.nil, "object",
-                  'return value should have property "nil"');
-      do_check_eq(typeof obj.ownProperties.nil.value, "object",
-                  'return value property "nil" should be a grip');
-      do_check_eq(typeof obj.ownProperties.obj, "object",
-                  'return value should have property "obj"');
-      do_check_eq(typeof obj.ownProperties.obj.value, "object",
-                  'return value property "obj" should be a grip');
-      do_check_eq(typeof obj.ownProperties.arr, "object",
-                  'return value should have property "arr"');
-      do_check_eq(typeof obj.ownProperties.arr.value, "object",
-                  'return value property "arr" should be a grip');
-      do_check_eq(typeof obj.ownProperties.inf, "object",
-                  'return value should have property "inf"');
-      do_check_eq(typeof obj.ownProperties.inf.value, "object",
-                  'return value property "inf" should be a grip');
-      do_check_eq(typeof obj.ownProperties.ninf, "object",
-                  'return value should have property "ninf"');
-      do_check_eq(typeof obj.ownProperties.ninf.value, "object",
-                  'return value property "ninf" should be a grip');
-      do_check_eq(typeof obj.ownProperties.nan, "object",
-                  'return value should have property "nan"');
-      do_check_eq(typeof obj.ownProperties.nan.value, "object",
-                  'return value property "nan" should be a grip');
-      do_check_eq(typeof obj.ownProperties.nzero, "object",
-                  'return value should have property "nzero"');
-      do_check_eq(typeof obj.ownProperties.nzero.value, "object",
-                  'return value property "nzero" should be a grip');
-
-      do_check_eq(obj.prototype.type, "object");
-      do_check_eq(obj.ownProperties.num.value, 25);
-      do_check_eq(obj.ownProperties.str.value, "foo");
-      do_check_eq(obj.ownProperties.bool.value, false);
-      do_check_eq(obj.ownProperties.undef.value.type, "undefined");
-      do_check_eq(obj.ownProperties.nil.value.type, "null");
-      do_check_eq(obj.ownProperties.obj.value.type, "object");
-      do_check_eq(obj.ownProperties.obj.value.class, "Object");
-      do_check_eq(obj.ownProperties.arr.value.type, "object");
-      do_check_eq(obj.ownProperties.arr.value.class, "Array");
-      do_check_eq(obj.ownProperties.inf.value.type, "Infinity");
-      do_check_eq(obj.ownProperties.ninf.value.type, "-Infinity");
-      do_check_eq(obj.ownProperties.nan.value.type, "NaN");
-      do_check_eq(obj.ownProperties.nzero.value.type, "-0");
-    }
-  });
+  gTraceClient.addListener("enteredFrame", check_packet);
+  gTraceClient.addListener("exitedFrame", check_packet);
 
   start_trace()
     .then(eval_code)
     .then(stop_trace)
     .then(function() {
       finishClient(gClient);
     });
 }
 
 function start_trace()
 {
   let deferred = defer();
-  gTraceClient.startTrace(["return"], null, function() { deferred.resolve(); });
+  gTraceClient.startTrace(["arguments", "return"], null, function() { deferred.resolve(); });
   return deferred.promise;
 }
 
 function eval_code()
 {
   gDebuggee.eval("(" + function() {
-    function foo() {
-      let obj = {};
-      obj.self = obj;
+    function identity(x) {
+      return x;
+    }
+
+    let circular = {};
+    circular.self = circular;
 
-      return {
-        num: 25,
-        str: "foo",
-        bool: false,
-        undef: undefined,
-        nil: null,
-        obj: obj,
-        arr: [1,2,3,4,5],
-        inf: Infinity,
-        ninf: -Infinity,
-        nan: NaN,
-        nzero: -0
-      };
-    }
-    foo();
+    let obj = {
+      num: 0,
+      str: "foo",
+      bool: false,
+      undef: undefined,
+      nil: null,
+      inf: Infinity,
+      ninf: -Infinity,
+      nan: NaN,
+      nzero: -0,
+      obj: circular,
+      arr: [1,2,3,4,5]
+    };
+
+    identity();
+    identity(0);
+    identity("");
+    identity(false);
+    identity(undefined);
+    identity(null);
+    identity(Infinity);
+    identity(-Infinity);
+    identity(NaN);
+    identity(-0);
+    identity(obj);
   } + ")()");
 }
 
 function stop_trace()
 {
   let deferred = defer();
   gTraceClient.stopTrace(null, function() { deferred.resolve(); });
   return deferred.promise;
 }
+
+function check_packet(aEvent, aPacket)
+{
+  let value = (aPacket.type === "enteredFrame" && aPacket.arguments)
+        ? aPacket.arguments[0]
+        : aPacket.return;
+  switch(aPacket.sequence) {
+  case 2:
+    do_check_eq(typeof aPacket.arguments, "object",
+                "zero-argument function call should send arguments list");
+    do_check_eq(aPacket.arguments.length, 0,
+                "zero-argument function call should send zero-length arguments list");
+    break;
+  case 3:
+    check_value(value, "object", "undefined");
+    break;
+  case 4:
+  case 5:
+    check_value(value, "number", 0);
+    break;
+  case 6:
+  case 7:
+    check_value(value, "string", "");
+    break;
+  case 8:
+  case 9:
+    check_value(value, "boolean", false);
+    break;
+  case 10:
+  case 11:
+    check_value(value, "object", "undefined");
+    break;
+  case 12:
+  case 13:
+    check_value(value, "object", "null");
+    break;
+  case 14:
+  case 15:
+    check_value(value, "object", "Infinity");
+    break;
+  case 16:
+  case 17:
+    check_value(value, "object", "-Infinity");
+    break;
+  case 18:
+  case 19:
+    check_value(value, "object", "NaN");
+    break;
+  case 20:
+  case 21:
+    check_value(value, "object", "-0");
+    break;
+  case 22:
+  case 23:
+    check_object(aPacket.type, value);
+    break;
+  }
+}
+
+function check_value(aActual, aExpectedType, aExpectedValue)
+{
+  do_check_eq(typeof aActual, aExpectedType);
+  do_check_eq(aExpectedType === "object" ? aActual.type : aActual, aExpectedValue);
+}
+
+function check_object(aType, aObj) {
+  do_check_eq(typeof aObj, "object",
+              'serialized object should be present in packet');
+  do_check_eq(typeof aObj.prototype, "object",
+              'serialized object should have prototype');
+  do_check_eq(typeof aObj.ownProperties, "object",
+              'serialized object should have ownProperties list');
+  do_check_eq(typeof aObj.safeGetterValues, "object",
+              'serialized object should have safeGetterValues');
+
+  do_check_eq(typeof aObj.ownProperties.num, "object",
+              'serialized object should have property "num"');
+  do_check_eq(typeof aObj.ownProperties.str, "object",
+              'serialized object should have property "str"');
+  do_check_eq(typeof aObj.ownProperties.bool, "object",
+              'serialized object should have property "bool"');
+  do_check_eq(typeof aObj.ownProperties.undef, "object",
+              'serialized object should have property "undef"');
+  do_check_eq(typeof aObj.ownProperties.undef.value, "object",
+              'serialized object property "undef" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.nil, "object",
+              'serialized object should have property "nil"');
+  do_check_eq(typeof aObj.ownProperties.nil.value, "object",
+              'serialized object property "nil" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.obj, "object",
+              'serialized object should have property "aObj"');
+  do_check_eq(typeof aObj.ownProperties.obj.value, "object",
+              'serialized object property "aObj" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.arr, "object",
+              'serialized object should have property "arr"');
+  do_check_eq(typeof aObj.ownProperties.arr.value, "object",
+              'serialized object property "arr" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.inf, "object",
+              'serialized object should have property "inf"');
+  do_check_eq(typeof aObj.ownProperties.inf.value, "object",
+              'serialized object property "inf" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.ninf, "object",
+              'serialized object should have property "ninf"');
+  do_check_eq(typeof aObj.ownProperties.ninf.value, "object",
+              'serialized object property "ninf" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.nan, "object",
+              'serialized object should have property "nan"');
+  do_check_eq(typeof aObj.ownProperties.nan.value, "object",
+              'serialized object property "nan" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.nzero, "object",
+              'serialized object should have property "nzero"');
+  do_check_eq(typeof aObj.ownProperties.nzero.value, "object",
+              'serialized object property "nzero" should be a grip');
+
+  do_check_eq(aObj.prototype.type, "object");
+  do_check_eq(aObj.ownProperties.num.value, 0);
+  do_check_eq(aObj.ownProperties.str.value, "foo");
+  do_check_eq(aObj.ownProperties.bool.value, false);
+  do_check_eq(aObj.ownProperties.undef.value.type, "undefined");
+  do_check_eq(aObj.ownProperties.nil.value.type, "null");
+  do_check_eq(aObj.ownProperties.obj.value.type, "object");
+  do_check_eq(aObj.ownProperties.obj.value.class, "Object");
+  do_check_eq(aObj.ownProperties.arr.value.type, "object");
+  do_check_eq(aObj.ownProperties.arr.value.class, "Array");
+  do_check_eq(aObj.ownProperties.inf.value.type, "Infinity");
+  do_check_eq(aObj.ownProperties.ninf.value.type, "-Infinity");
+  do_check_eq(aObj.ownProperties.nan.value.type, "NaN");
+  do_check_eq(aObj.ownProperties.nzero.value.type, "-0");
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -122,16 +122,17 @@ reason = bug 820380
 [test_objectgrips-01.js]
 [test_objectgrips-02.js]
 [test_objectgrips-03.js]
 [test_objectgrips-04.js]
 [test_objectgrips-05.js]
 [test_objectgrips-06.js]
 [test_objectgrips-07.js]
 [test_objectgrips-08.js]
+[test_objectgrips-09.js]
 [test_interrupt.js]
 [test_stepping-01.js]
 [test_stepping-02.js]
 [test_stepping-03.js]
 [test_stepping-04.js]
 [test_stepping-05.js]
 [test_stepping-06.js]
 [test_framebindings-01.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tests/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MODULE = 'test_devtools'
+
+XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tests/unit/head_devtools.js
@@ -0,0 +1,42 @@
+"use strict";
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
+
+// Register a console listener, so console messages don't just disappear
+// into the ether.
+let errorCount = 0;
+let listener = {
+  observe: function (aMessage) {
+    errorCount++;
+    try {
+      // If we've been given an nsIScriptError, then we can print out
+      // something nicely formatted, for tools like Emacs to pick up.
+      var scriptError = aMessage.QueryInterface(Ci.nsIScriptError);
+      dump(aMessage.sourceName + ":" + aMessage.lineNumber + ": " +
+           scriptErrorFlagsToKind(aMessage.flags) + ": " +
+           aMessage.errorMessage + "\n");
+      var string = aMessage.errorMessage;
+    } catch (x) {
+      // Be a little paranoid with message, as the whole goal here is to lose
+      // no information.
+      try {
+        var string = "" + aMessage.message;
+      } catch (x) {
+        var string = "<error converting error message to string>";
+      }
+    }
+
+    // Make sure we exit all nested event loops so that the test can finish.
+    while (DebuggerServer.xpcInspector.eventLoopNestLevel > 0) {
+      DebuggerServer.xpcInspector.exitNestedEventLoop();
+    }
+    do_throw("head_dbg.js got console message: " + string + "\n");
+  }
+};
+
+let consoleService = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService);
+consoleService.registerListener(listener);
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tests/unit/test_safeErrorString.js
@@ -0,0 +1,54 @@
+/* -*- Mode: js; js-indent-level: 2; -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test DevToolsUtils.safeErrorString
+
+function run_test() {
+  test_with_error();
+  test_with_tricky_error();
+  test_with_string();
+  test_with_thrower();
+  test_with_psychotic();
+}
+
+function test_with_error() {
+  let s = DevToolsUtils.safeErrorString(new Error("foo bar"));
+  // Got the message.
+  do_check_true(s.contains("foo bar"));
+  // Got the stack.
+  do_check_true(s.contains("test_with_error"))
+  do_check_true(s.contains("test_safeErrorString.js"));
+}
+
+function test_with_tricky_error() {
+  let e = new Error("batman");
+  e.stack = { toString: Object.create(null) };
+  let s = DevToolsUtils.safeErrorString(e);
+  // Still got the message, despite a bad stack property.
+  do_check_true(s.contains("batman"));
+}
+
+function test_with_string() {
+  let s = DevToolsUtils.safeErrorString("not really an error");
+  // Still get the message.
+  do_check_true(s.contains("not really an error"));
+}
+
+function test_with_thrower() {
+  let s = DevToolsUtils.safeErrorString({
+    toString: () => {
+      throw new Error("Muahahaha");
+    }
+  });
+  // Still don't fail, get string back.
+  do_check_eq(typeof s, "string");
+}
+
+function test_with_psychotic() {
+  let s = DevToolsUtils.safeErrorString({
+    toString: () => Object.create(null)
+  });
+  // Still get a string out, and no exceptions thrown
+  do_check_eq(typeof s, "string");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tests/unit/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head = head_devtools.js
+tail =
+
+[test_safeErrorString.js]
\ No newline at end of file
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -2202,16 +2202,21 @@ NS_IMETHODIMP nsExternalAppHandler::Canc
 {
   NS_ENSURE_ARG(NS_FAILED(aReason));
   // XXX should not ignore the reason
 
   mCanceled = true;
   if (mSaver) {
     mSaver->Finish(aReason);
     mSaver = nullptr;
+  } else if (mStopRequestIssued && mTempFile) {
+    // This branch can only happen when the user cancels the helper app dialog
+    // when the request has completed. The temp file has to be removed here,
+    // because mSaver has been released at that time with the temp file left.
+    (void)mTempFile->Remove(false);
   }
 
   // Break our reference cycle with the helper app dialog (set up in
   // OnStartRequest)
   mDialog = nullptr;
 
   mRequest = nullptr;
 
--- a/webapprt/ContentPermission.js
+++ b/webapprt/ContentPermission.js
@@ -1,84 +1,105 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
+const UNKNOWN_FAIL = ["geolocation", "desktop-notification"];
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://webapprt/modules/WebappRT.jsm");
 
 function ContentPermission() {}
 
 ContentPermission.prototype = {
   classID: Components.ID("{07ef5b2e-88fb-47bd-8cec-d3b0bef11ac4}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]),
 
+  _getChromeWindow: function(aWindow) { 
+    return aWindow
+      .QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIWebNavigation)
+      .QueryInterface(Ci.nsIDocShellTreeItem)
+      .rootTreeItem
+      .QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIDOMWindow)
+      .QueryInterface(Ci.nsIDOMChromeWindow);
+  },
+
   prompt: function(request) {
-    // Only handle geolocation requests for now
-    if (request.type != "geolocation") {
-      return;
+    // Reuse any remembered permission preferences
+    let result =
+      Services.perms.testExactPermissionFromPrincipal(request.principal,
+                                                      request.type);
+
+    // We used to use the name "geo" for the geolocation permission, now we're
+    // using "geolocation".  We need to check both to support existing
+    // installations.
+    if ((result == Ci.nsIPermissionManager.UNKNOWN_ACTION ||
+         result == Ci.nsIPermissionManager.PROMPT_ACTION) &&
+        request.type == "geolocation") {
+      let geoResult = Services.perms.testExactPermission(request.principal.URI,
+                                                         "geo");
+      // We override the result only if the "geo" permission was allowed or
+      // denied.
+      if (geoResult == Ci.nsIPermissionManager.ALLOW_ACTION ||
+          geoResult == Ci.nsIPermissionManager.DENY_ACTION) {
+        result = geoResult;
+      }
     }
 
-    // Reuse any remembered permission preferences
-    let result = Services.perms.testExactPermissionFromPrincipal(request.principal, "geo");
     if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
       request.allow();
       return;
-    }
-    else if (result == Ci.nsIPermissionManager.DENY_ACTION) {
+    } else if (result == Ci.nsIPermissionManager.DENY_ACTION ||
+               (result == Ci.nsIPermissionManager.UNKNOWN_ACTION &&
+                UNKNOWN_FAIL.indexOf(request.type) >= 0)) {
       request.cancel();
       return;
     }
 
-    function getChromeWindow(aWindow) {
-      var chromeWin = aWindow
-        .QueryInterface(Ci.nsIInterfaceRequestor)
-        .getInterface(Ci.nsIWebNavigation)
-        .QueryInterface(Ci.nsIDocShellTreeItem)
-        .rootTreeItem
-        .QueryInterface(Ci.nsIInterfaceRequestor)
-        .getInterface(Ci.nsIDOMWindow)
-        .QueryInterface(Ci.nsIDOMChromeWindow);
-      return chromeWin;
-    }
-
     // Display a prompt at the top level
     let {name} = WebappRT.config.app.manifest;
     let requestingWindow = request.window.top;
-    let chromeWin = getChromeWindow(requestingWindow);
+    let chromeWin = this._getChromeWindow(requestingWindow);
     let bundle = Services.strings.createBundle("chrome://webapprt/locale/webapp.properties");
 
     // Construct a prompt with share/don't and remember checkbox
     let remember = {value: false};
     let choice = Services.prompt.confirmEx(
       chromeWin,
-      bundle.formatStringFromName("geolocation.title", [name], 1),
-      bundle.GetStringFromName("geolocation.description"),
+      bundle.formatStringFromName(request.type + ".title", [name], 1),
+      bundle.GetStringFromName(request.type + ".description"),
       // Set both buttons to strings with the cancel button being default
       Ci.nsIPromptService.BUTTON_POS_1_DEFAULT |
         Ci.nsIPromptService.BUTTON_TITLE_IS_STRING * Ci.nsIPromptService.BUTTON_POS_0 |
         Ci.nsIPromptService.BUTTON_TITLE_IS_STRING * Ci.nsIPromptService.BUTTON_POS_1,
-      bundle.GetStringFromName("geolocation.sharelocation"),
-      bundle.GetStringFromName("geolocation.dontshare"),
+      bundle.GetStringFromName(request.type + ".allow"),
+      bundle.GetStringFromName(request.type + ".deny"),
       null,
-      bundle.GetStringFromName("geolocation.remember"),
+      bundle.GetStringFromName(request.type + ".remember"),
       remember);
 
-    // Persist the choice if the user wants to remember
+    let action = Ci.nsIPermissionManager.ALLOW_ACTION;
+    if (choice != 0) {
+      action = Ci.nsIPermissionManager.DENY_ACTION;
+    }
+
     if (remember.value) {
-      let action = Ci.nsIPermissionManager.ALLOW_ACTION;
-      if (choice != 0) {
-        action = Ci.nsIPermissionManager.DENY_ACTION;
-      }
-      Services.perms.addFromPrincipal(request.principal, "geo", action);
+      // Persist the choice if the user wants to remember
+      Services.perms.addFromPrincipal(request.principal, request.type, action);
+    } else {
+      // Otherwise allow the permission for the current session
+      Services.perms.addFromPrincipal(request.principal, request.type, action,
+                                      Ci.nsIPermissionManager.EXPIRE_SESSION);
     }
 
     // Trigger the selected choice
     if (choice == 0) {
       request.allow();
     }
     else {
       request.cancel();
--- a/webapprt/content/webapp.xul
+++ b/webapprt/content/webapp.xul
@@ -40,17 +40,17 @@
        key="&copyCmd.key;"
        modifiers="accel"/>
   <key id="key_paste"
        key="&pasteCmd.key;"
        modifiers="accel"/>
   <key id="key_delete" keycode="VK_DELETE" command="cmd_delete"/>
   <key id="key_selectAll" key="&selectAllCmd.key;" modifiers="accel"/>
   <key id="key_quitApplication"
-       key="&quitApplicationCmdMac.key;"
+       key="&quitApplicationCmdUnix.key;"
        command="cmd_quitApplication"
        modifiers="accel"/>
   <key id="key_hideThisAppCmdMac"
        key="&hideThisAppCmdMac.key;"
        modifiers="accel"/>
   <key id="key_hideOtherAppsCmdMac"
        key="&hideOtherAppsCmdMac.key;"
        modifiers="accel,alt"/>
--- a/webapprt/locales/en-US/webapprt/webapp.dtd
+++ b/webapprt/locales/en-US/webapprt/webapp.dtd
@@ -13,22 +13,24 @@
 
 <!ENTITY quitApplicationCmdWin.label        "Exit">
 <!ENTITY quitApplicationCmdWin.accesskey    "x">
 <!ENTITY quitApplicationCmd.label           "Quit">
 <!ENTITY quitApplicationCmd.accesskey       "Q">
 <!-- On Mac, we create the Quit and Hide command labels dynamically,
    - using properties in window.properties, in order to include the name
    - of the webapp in the labels without creating a DTD file for it. -->
-<!ENTITY quitApplicationCmdMac.key          "Q">
 <!ENTITY hideThisAppCmdMac.key              "H">
 <!ENTITY hideOtherAppsCmdMac.label          "Hide Others">
 <!ENTITY hideOtherAppsCmdMac.key            "H">
 <!ENTITY showAllAppsCmdMac.label            "Show All">
 
+<!-- LOCALIZATION NOTE(quitApplicationCmdUnix.key): This keyboard shortcut is used by both Linux and OSX -->
+<!ENTITY quitApplicationCmdUnix.key          "Q">
+
 <!ENTITY editMenu.label                     "Edit">
 <!ENTITY editMenu.accesskey                 "E">
 <!ENTITY undoCmd.label                      "Undo">
 <!ENTITY undoCmd.key                        "Z">
 <!ENTITY undoCmd.accesskey                  "U">
 <!ENTITY redoCmd.label                      "Redo">
 <!ENTITY redoCmd.key                        "Y">
 <!ENTITY redoCmd.accesskey                  "R">
--- a/webapprt/locales/en-US/webapprt/webapp.properties
+++ b/webapprt/locales/en-US/webapprt/webapp.properties
@@ -15,20 +15,28 @@ quitApplicationCmdMac.label=Quit %S
 # LOCALIZATION NOTE (hideApplicationCmdMac.label): %S will be replaced with
 # the name of the webapp.
 hideApplicationCmdMac.label=Hide %S
 
 # LOCALIZATION NOTE (geolocation.title): %S will be replaced with the name of
 # the webapp.
 geolocation.title=%S - Share Location
 geolocation.description=Do you want to share your location?
-geolocation.sharelocation=Share Location
-geolocation.dontshare=Don't Share
+geolocation.allow=Share Location
+geolocation.deny=Don't Share
 geolocation.remember=Remember my choice
 
+# LOCALIZATION NOTE (desktop-notification.title): %S will be replaced with the
+# name of the webapp.
+desktop-notification.title=%S - Show notifications
+desktop-notification.description=Do you want to allow notifications?
+desktop-notification.allow=Show
+desktop-notification.deny=Don't show
+desktop-notification.remember=Remember my choice
+
 # LOCALIZATION NOTE (webapps.install.title): %S will be replaced with the name
 # of the webapp being installed.
 webapps.install.title=Install %S
 # LOCALIZATION NOTE (webapps.install.description): %S will be replaced with the
 # name of the webapp being installed.
 webapps.install.description=Do you want to install %S?
 webapps.install.install=Install App
 webapps.install.dontinstall=Don't Install