Bug 808371 - Allow adding new properties to objects in the VariablesView. r=vp, r=jryans
authorBrandon Benvie <bbenvie@mozilla.com>
Mon, 02 Dec 2013 20:07:13 -0800
changeset 158497 e0572cb4eb82
parent 158496 7b8cc2b3568b
child 158498 c7f35fd43aa9
push id25748
push userryanvm@gmail.com
push dateTue, 03 Dec 2013 21:42:03 +0000
treeherdermozilla-central@03a55dd19083 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvp, jryans
bugs808371
milestone28.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 808371 - Allow adding new properties to objects in the VariablesView. r=vp, r=jryans
browser/devtools/app-manager/content/manifest-editor.js
browser/devtools/app-manager/test/browser_manifest_editor.js
browser/devtools/app-manager/test/manifest.webapp
browser/devtools/debugger/test/browser_dbg_variables-view-04.js
browser/devtools/debugger/test/browser_dbg_variables-view-data.js
browser/devtools/netmonitor/netmonitor-view.js
browser/devtools/shared/widgets/VariablesView.jsm
browser/devtools/shared/widgets/widgets.css
browser/themes/linux/devtools/widgets.css
browser/themes/linux/jar.mn
browser/themes/osx/devtools/widgets.css
browser/themes/osx/jar.mn
browser/themes/shared/devtools/app-manager/images/add.svg
browser/themes/shared/devtools/app-manager/manifest-editor.inc.css
browser/themes/windows/devtools/widgets.css
browser/themes/windows/jar.mn
--- a/browser/devtools/app-manager/content/manifest-editor.js
+++ b/browser/devtools/app-manager/content/manifest-editor.js
@@ -11,16 +11,17 @@ const VARIABLES_VIEW_URL =
   "chrome://browser/content/devtools/widgets/VariablesView.xul";
 
 function ManifestEditor(project) {
   this.project = project;
   this._onContainerReady = this._onContainerReady.bind(this);
   this._onEval = this._onEval.bind(this);
   this._onSwitch = this._onSwitch.bind(this);
   this._onDelete = this._onDelete.bind(this);
+  this._onNew = this._onNew.bind(this);
 }
 
 ManifestEditor.prototype = {
   get manifest() { return this.project.manifest; },
 
   get editable() { return this.project.type == "packaged"; },
 
   show: function(containerElement) {
@@ -49,16 +50,17 @@ ManifestEditor.prototype = {
     editor.onlyEnumVisible = true;
     editor.alignedValues = true;
     editor.actionsFirst = true;
 
     if (this.editable) {
       editor.eval = this._onEval;
       editor.switch = this._onSwitch;
       editor.delete = this._onDelete;
+      editor.new = this._onNew;
     }
 
     return this.update();
   },
 
   _onEval: function(evalString) {
     let manifest = this.manifest;
     eval("manifest" + evalString);
@@ -82,16 +84,24 @@ ManifestEditor.prototype = {
   },
 
   _onDelete: function(variable) {
     let manifest = this.manifest;
     let evalString = "delete manifest" + variable.symbolicName;
     eval(evalString);
   },
 
+  _onNew: function(variable, newName, newValue) {
+    let manifest = this.manifest;
+    let symbolicName = variable.symbolicName + "['" + newName + "']";
+    let evalString = "manifest" + symbolicName + " = " + newValue + ";";
+    eval(evalString);
+    this.update();
+  },
+
   update: function() {
     this.editor.createHierarchy();
     this.editor.rawObject = this.manifest;
     this.editor.commitHierarchy();
 
     // Wait until the animation from commitHierarchy has completed
     let deferred = promise.defer();
     setTimeout(deferred.resolve, this.editor.lazyEmptyDelay + 1);
--- a/browser/devtools/app-manager/test/browser_manifest_editor.js
+++ b/browser/devtools/app-manager/test/browser_manifest_editor.js
@@ -1,54 +1,105 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 
 const MANIFEST_EDITOR_ENABLED = "devtools.appmanager.manifestEditor.enabled";
 
+
+let gManifestWindow, gManifestEditor;
+
 function test() {
   waitForExplicitFinish();
 
   Task.spawn(function() {
     Services.prefs.setBoolPref(MANIFEST_EDITOR_ENABLED, true);
     let tab = yield openAppManager();
     yield selectProjectsPanel();
     yield addSamplePackagedApp();
     yield showSampleProjectDetails();
+
+    gManifestWindow = getManifestWindow();
+    gManifestEditor = getProjectsWindow().UI.manifestEditor;
     yield changeManifestValue("name", "the best app");
+    yield addNewManifestProperty("developer", "foo", "bar");
+    gManifestWindow = null;
+    gManifestEditor = null;
+
     yield removeSamplePackagedApp();
     yield removeTab(tab);
     Services.prefs.setBoolPref(MANIFEST_EDITOR_ENABLED, false);
     finish();
   });
 }
 
+// Wait until the animation from commitHierarchy has completed
+function waitForUpdate() {
+  return waitForTime(gManifestEditor.editor.lazyEmptyDelay + 1);
+}
+
 function changeManifestValue(key, value) {
   return Task.spawn(function() {
-    let manifestWindow = getManifestWindow();
-    let manifestEditor = getProjectsWindow().UI.manifestEditor;
-
-    let propElem = manifestWindow.document
+    let propElem = gManifestWindow.document
                    .querySelector("[id ^= '" + key + "']");
     is(propElem.querySelector(".name").value, key,
        "Key doesn't match expected value");
 
     let valueElem = propElem.querySelector(".value");
-    EventUtils.sendMouseEvent({ type: "mousedown" }, valueElem, manifestWindow);
+    EventUtils.sendMouseEvent({ type: "mousedown" }, valueElem, gManifestWindow);
 
     let valueInput = propElem.querySelector(".element-value-input");
     valueInput.value = '"' + value + '"';
-    EventUtils.sendKey("RETURN", manifestWindow);
+    EventUtils.sendKey("RETURN", gManifestWindow);
 
-    // Wait until the animation from commitHierarchy has completed
-    yield waitForTime(manifestEditor.editor.lazyEmptyDelay + 1);
+    yield waitForUpdate();
     // Elements have all been replaced, re-select them
-    propElem = manifestWindow.document.querySelector("[id ^= '" + key + "']");
+    propElem = gManifestWindow.document.querySelector("[id ^= '" + key + "']");
     valueElem = propElem.querySelector(".value");
     is(valueElem.value, '"' + value + '"',
        "Value doesn't match expected value");
 
-    is(manifestEditor.manifest[key], value,
+    is(gManifestEditor.manifest[key], value,
        "Manifest doesn't contain expected value");
   });
 }
+
+function addNewManifestProperty(parent, key, value) {
+  return Task.spawn(function() {
+    let parentElem = gManifestWindow.document
+                     .querySelector("[id ^= '" + parent + "']");
+    ok(parentElem,
+      "Found parent element");
+    let addPropertyElem = parentElem
+                          .querySelector(".variables-view-add-property");
+    ok(addPropertyElem,
+      "Found add-property button");
+
+    EventUtils.sendMouseEvent({ type: "mousedown" }, addPropertyElem, gManifestWindow);
+
+    let nameInput = parentElem.querySelector(".element-name-input");
+    nameInput.value = key;
+    EventUtils.sendKey("TAB", gManifestWindow);
+
+    let valueInput = parentElem.querySelector(".element-value-input");
+    valueInput.value = '"' + value + '"';
+    EventUtils.sendKey("RETURN", gManifestWindow);
+
+    yield waitForUpdate();
+
+    let newElem = gManifestWindow.document.querySelector("[id ^= '" + key + "']");
+    let nameElem = newElem.querySelector(".name");
+    is(nameElem.value, key,
+       "Key doesn't match expected Key");
+
+    ok(key in gManifestEditor.manifest[parent],
+       "Manifest doesn't contain expected key");
+
+    let valueElem = newElem.querySelector(".value");
+    is(valueElem.value, '"' + value + '"',
+       "Value doesn't match expected value");
+
+    is(gManifestEditor.manifest[parent][key], value,
+       "Manifest doesn't contain expected value");
+  });
+}
--- a/browser/devtools/app-manager/test/manifest.webapp
+++ b/browser/devtools/app-manager/test/manifest.webapp
@@ -1,3 +1,6 @@
 {
-  "name": "My packaged app"
+  "name": "My packaged app",
+  "developer": {
+    "name": "Foo Bar"
+  }
 }
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-04.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-04.js
@@ -102,11 +102,49 @@ function test() {
       "Three new detail nodes should have been added in the variable tree.");
     is(testVar.get("someProp2").target.querySelector(".value").getAttribute("value"), "null",
       "The grip information for the variable wasn't set correctly (7).");
     is(testVar.get("someProp3").target.querySelector(".value").getAttribute("value"), "undefined",
       "The grip information for the variable wasn't set correctly (8).");
     is(testVar.get("someProp4").target.querySelector(".value").getAttribute("value"), "Object",
       "The grip information for the variable wasn't set correctly (9).");
 
+    let parent = testVar.get("someProp2");
+    let child = parent.addItem("child", {
+      value: {
+        type: "null"
+      }
+    });
+
+    is(variables.getItemForNode(parent.target), parent,
+       "VariablesView should have a record of the parent.");
+    is(variables.getItemForNode(child.target), child,
+       "VariablesView should have a record of the child.");
+    is([...parent].length, 1,
+       "Parent should have one child.");
+
+    parent.remove();
+
+    is(variables.getItemForNode(parent.target), undefined,
+       "VariablesView should not have a record of the parent anymore.");
+    is(parent.target.parentNode, null,
+       "Parent element should not have a parent.")
+    is(variables.getItemForNode(child.target), undefined,
+       "VariablesView should not have a record of the child anymore.");
+    is(child.target.parentNode, null,
+       "Child element should not have a parent.")
+    is([...parent].length, 0,
+       "Parent should have zero children.");
+
+    testScope.remove();
+
+    is([...variables].length, 0,
+       "VariablesView should have been emptied.");
+    is(Cu.nondeterministicGetWeakMapKeys(variables._itemsByElement).length, 0,
+       "VariablesView _itemsByElement map has been emptied.");
+    is(variables._currHierarchy.size, 0,
+       "VariablesView _currHierarchy map has been emptied.");
+    is(variables._list.children.length, 0,
+       "VariablesView element should have no children.");
+
     closeDebuggerAndFinish(aPanel);
   });
 }
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-data.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-data.js
@@ -54,16 +54,17 @@ function performTest() {
     someProp6: obj,
     get someProp7() { return arr; },
     set someProp7(value) { arr[0] = value }
   };
 
   gVariablesView.eval = function() {};
   gVariablesView.switch = function() {};
   gVariablesView.delete = function() {};
+  gVariablesView.new = function() {};
   gVariablesView.rawObject = test;
 
   testHierarchy();
   testHeader();
   testFirstLevelContents();
   testSecondLevelContents();
   testThirdLevelContents();
   testOriginalRawDataIntegrity(arr, obj);
@@ -498,18 +499,18 @@ function testAnonymousHeaders(fooScope, 
 
   is(anonymousVar.header, false,
     "An anonymous variable should have a header visible.");
   is(anonymousVar.target.hasAttribute("non-header"), true,
     "The non-header attribute should not be applied to variables without headers.");
 }
 
 function testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar, bazProperty) {
-  is(fooScope.preventDisableOnChage, gVariablesView.preventDisableOnChage,
-    "The preventDisableOnChage property should persist from the view to all scopes.");
+  is(fooScope.preventDisableOnChange, gVariablesView.preventDisableOnChange,
+    "The preventDisableOnChange property should persist from the view to all scopes.");
   is(fooScope.preventDescriptorModifiers, gVariablesView.preventDescriptorModifiers,
     "The preventDescriptorModifiers property should persist from the view to all scopes.");
   is(fooScope.editableNameTooltip, gVariablesView.editableNameTooltip,
     "The editableNameTooltip property should persist from the view to all scopes.");
   is(fooScope.editableValueTooltip, gVariablesView.editableValueTooltip,
     "The editableValueTooltip property should persist from the view to all scopes.");
   is(fooScope.editButtonTooltip, gVariablesView.editButtonTooltip,
     "The editButtonTooltip property should persist from the view to all scopes.");
@@ -520,23 +521,25 @@ function testPropertyInheritance(fooScop
   is(fooScope.separatorStr, gVariablesView.separatorStr,
     "The separatorStr property should persist from the view to all scopes.");
   is(fooScope.eval, gVariablesView.eval,
     "The eval property should persist from the view to all scopes.");
   is(fooScope.switch, gVariablesView.switch,
     "The switch property should persist from the view to all scopes.");
   is(fooScope.delete, gVariablesView.delete,
     "The delete property should persist from the view to all scopes.");
+  is(fooScope.new, gVariablesView.new,
+    "The new property should persist from the view to all scopes.");
   isnot(fooScope.eval, fooScope.switch,
     "The eval and switch functions got mixed up in the scope.");
   isnot(fooScope.switch, fooScope.delete,
     "The eval and switch functions got mixed up in the scope.");
 
-  is(barVar.preventDisableOnChage, gVariablesView.preventDisableOnChage,
-    "The preventDisableOnChage property should persist from the view to all variables.");
+  is(barVar.preventDisableOnChange, gVariablesView.preventDisableOnChange,
+    "The preventDisableOnChange property should persist from the view to all variables.");
   is(barVar.preventDescriptorModifiers, gVariablesView.preventDescriptorModifiers,
     "The preventDescriptorModifiers property should persist from the view to all variables.");
   is(barVar.editableNameTooltip, gVariablesView.editableNameTooltip,
     "The editableNameTooltip property should persist from the view to all variables.");
   is(barVar.editableValueTooltip, gVariablesView.editableValueTooltip,
     "The editableValueTooltip property should persist from the view to all variables.");
   is(barVar.editButtonTooltip, gVariablesView.editButtonTooltip,
     "The editButtonTooltip property should persist from the view to all variables.");
@@ -547,23 +550,25 @@ function testPropertyInheritance(fooScop
   is(barVar.separatorStr, gVariablesView.separatorStr,
     "The separatorStr property should persist from the view to all variables.");
   is(barVar.eval, gVariablesView.eval,
     "The eval property should persist from the view to all variables.");
   is(barVar.switch, gVariablesView.switch,
     "The switch property should persist from the view to all variables.");
   is(barVar.delete, gVariablesView.delete,
     "The delete property should persist from the view to all variables.");
+  is(barVar.new, gVariablesView.new,
+    "The new property should persist from the view to all variables.");
   isnot(barVar.eval, barVar.switch,
     "The eval and switch functions got mixed up in the variable.");
   isnot(barVar.switch, barVar.delete,
     "The eval and switch functions got mixed up in the variable.");
 
-  is(bazProperty.preventDisableOnChage, gVariablesView.preventDisableOnChage,
-    "The preventDisableOnChage property should persist from the view to all properties.");
+  is(bazProperty.preventDisableOnChange, gVariablesView.preventDisableOnChange,
+    "The preventDisableOnChange property should persist from the view to all properties.");
   is(bazProperty.preventDescriptorModifiers, gVariablesView.preventDescriptorModifiers,
     "The preventDescriptorModifiers property should persist from the view to all properties.");
   is(bazProperty.editableNameTooltip, gVariablesView.editableNameTooltip,
     "The editableNameTooltip property should persist from the view to all properties.");
   is(bazProperty.editableValueTooltip, gVariablesView.editableValueTooltip,
     "The editableValueTooltip property should persist from the view to all properties.");
   is(bazProperty.editButtonTooltip, gVariablesView.editButtonTooltip,
     "The editButtonTooltip property should persist from the view to all properties.");
@@ -574,16 +579,18 @@ function testPropertyInheritance(fooScop
   is(bazProperty.separatorStr, gVariablesView.separatorStr,
     "The separatorStr property should persist from the view to all properties.");
   is(bazProperty.eval, gVariablesView.eval,
     "The eval property should persist from the view to all properties.");
   is(bazProperty.switch, gVariablesView.switch,
     "The switch property should persist from the view to all properties.");
   is(bazProperty.delete, gVariablesView.delete,
     "The delete property should persist from the view to all properties.");
+  is(bazProperty.new, gVariablesView.new,
+    "The new property should persist from the view to all properties.");
   isnot(bazProperty.eval, bazProperty.switch,
     "The eval and switch functions got mixed up in the property.");
   isnot(bazProperty.switch, bazProperty.delete,
     "The eval and switch functions got mixed up in the property.");
 }
 
 function testClearHierarchy() {
   gVariablesView.clearHierarchy();
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -47,17 +47,17 @@ const DEFAULT_EDITOR_CONFIG = {
   lineNumbers: true
 };
 const GENERIC_VARIABLES_VIEW_SETTINGS = {
   lazyEmpty: true,
   lazyEmptyDelay: 10, // ms
   searchEnabled: true,
   editableValueTooltip: "",
   editableNameTooltip: "",
-  preventDisableOnChage: true,
+  preventDisableOnChange: true,
   preventDescriptorModifiers: true,
   eval: () => {},
   switch: () => {}
 };
 
 /**
  * Object defining the network monitor view components.
  */
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -259,20 +259,29 @@ VariablesView.prototype = {
    * user interaction. If null, then deletions are disabled.
    *
    * This property is applied recursively onto each scope in this view and
    * affects only the child nodes when they're created.
    */
   delete: null,
 
   /**
+   * Function called each time a property is added via user interaction. If
+   * null, then property additions are disabled.
+   *
+   * This property is applied recursively onto each scope in this view and
+   * affects only the child nodes when they're created.
+   */
+  new: null,
+
+  /**
    * Specifies if after an eval or switch operation, the variable or property
    * which has been edited should be disabled.
    */
-  preventDisableOnChage: false,
+  preventDisableOnChange: false,
 
   /**
    * Specifies if, whenever a variable or property descriptor is available,
    * configurable, enumerable, writable, frozen, sealed and extensible
    * attributes should not affect presentation.
    *
    * This flag is applied recursively onto each scope in this view and
    * affects only the child nodes when they're created.
@@ -820,16 +829,20 @@ VariablesView.prototype = {
 
       case e.DOM_VK_DELETE:
       case e.DOM_VK_BACK_SPACE:
         // Delete the Variable or Property if allowed.
         if (item instanceof Variable) {
           item._onDelete(e);
         }
         return;
+
+      case e.DOM_VK_INSERT:
+        item._onAddProperty(e);
+        return;
     }
   },
 
   /**
    * Listener handling a key down event on the view.
    */
   _onViewKeyDown: function(e) {
     if (e.keyCode == e.DOM_VK_C) {
@@ -907,21 +920,31 @@ VariablesView.prototype = {
     if (aFlag) {
       this._parent.setAttribute("aligned-values", "");
     } else {
       this._parent.removeAttribute("aligned-values");
     }
   },
 
   /**
+   * Gets if action buttons (like delete) should be placed at the beginning or
+   * end of a line.
+   * @return boolean
+   */
+  get actionsFirst() {
+    return this._actionsFirst;
+  },
+
+  /**
    * Sets if action buttons (like delete) should be placed at the beginning or
    * end of a line.
    * @param boolean aFlag
    */
   set actionsFirst(aFlag) {
+    this._actionsFirst = aFlag;
     if (aFlag) {
       this._parent.setAttribute("actions-first", "");
     } else {
       this._parent.removeAttribute("actions-first");
     }
   },
 
   /**
@@ -951,16 +974,18 @@ VariablesView.prototype = {
   _document: null,
   _window: null,
 
   _store: null,
   _prevHierarchy: null,
   _currHierarchy: null,
   _enumVisible: true,
   _nonEnumVisible: true,
+  _alignedValues: false,
+  _actionsFirst: false,
   _parent: null,
   _list: null,
   _searchboxNode: null,
   _searchboxContainer: null,
   _searchboxPlaceholder: "",
   _emptyTextNode: null,
   _emptyTextValue: ""
 };
@@ -1147,17 +1172,18 @@ function Scope(aView, aName, aFlags = {}
   this._openNonEnum = this._openNonEnum.bind(this);
   this._batchAppend = this._batchAppend.bind(this);
 
   // Inherit properties and flags from the parent view. You can override
   // each of these directly onto any scope, variable or property instance.
   this.eval = aView.eval;
   this.switch = aView.switch;
   this.delete = aView.delete;
-  this.preventDisableOnChage = aView.preventDisableOnChage;
+  this.new = aView.new;
+  this.preventDisableOnChange = aView.preventDisableOnChange;
   this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
   this.editableNameTooltip = aView.editableNameTooltip;
   this.editableValueTooltip = aView.editableValueTooltip;
   this.editButtonTooltip = aView.editButtonTooltip;
   this.deleteButtonTooltip = aView.deleteButtonTooltip;
   this.contextMenuId = aView.contextMenuId;
   this.separatorStr = aView.separatorStr;
 
@@ -1255,16 +1281,30 @@ Scope.prototype = {
 
       if (aOptions.callback) {
         aOptions.callback(item, descriptor.value);
       }
     }
   },
 
   /**
+   * Remove this Scope from its parent and remove all children recursively.
+   */
+  remove: function() {
+    let view = this._variablesView;
+    view._store.splice(view._store.indexOf(this), 1);
+    view._itemsByElement.delete(this._target);
+    view._currHierarchy.delete(this._nameString);
+    this._target.remove();
+    for (let variable of this._store.values()) {
+      variable.remove();
+    }
+  },
+
+  /**
    * Gets the variable in this container having the specified name.
    *
    * @param string aName
    *        The name of the variable to get.
    * @return Variable
    *         The matched variable, or null if nothing is found.
    */
   get: function(aName) {
@@ -1678,17 +1718,16 @@ Scope.prototype = {
     this._title.addEventListener("mousedown", this._onClick, false);
   },
 
   /**
    * The click listener for this scope's title.
    */
   _onClick: function(e) {
     if (e.button != 0 ||
-        e.target == this._inputNode ||
         e.target == this._editNode ||
         e.target == this._deleteNode) {
       return;
     }
     this.toggle();
     this.focus();
   },
 
@@ -2032,16 +2071,17 @@ Scope.prototype = {
   _topView: null,
   _document: null,
   _window: null,
 
   ownerView: null,
   eval: null,
   switch: null,
   delete: null,
+  new: null,
   editableValueTooltip: "",
   editableNameTooltip: "",
   editButtonTooltip: "",
   deleteButtonTooltip: "",
   preventDescriptorModifiers: false,
   contextMenuId: "",
   separatorStr: "",
 
@@ -2126,16 +2166,29 @@ Variable.prototype = Heritage.extend(Sco
    * @return Property
    *         The newly created child Property.
    */
   _createChild: function(aName, aDescriptor) {
     return new Property(this, aName, aDescriptor);
   },
 
   /**
+   * Remove this Variable from its parent and remove all children recursively.
+   */
+  remove: function() {
+    this.ownerView._store.delete(this._nameString);
+    this._variablesView._itemsByElement.delete(this._target);
+    this._variablesView._currHierarchy.delete(this._absoluteName);
+    this._target.remove();
+    for (let property of this._store.values()) {
+      property.remove();
+    }
+  },
+
+  /**
    * Populates this variable to contain all the properties of an object.
    *
    * @param object aObject
    *        The raw object you want to display.
    * @param object aOptions [optional]
    *        Additional options for adding the properties. Supported options:
    *        - sorted: true to sort all the properties before adding them
    *        - expanded: true to expand all the properties after adding them
@@ -2308,24 +2361,21 @@ Variable.prototype = Heritage.extend(Sco
    *        The variable's name.
    * @param object aDescriptor
    *        The variable's descriptor.
    */
   _init: function(aName, aDescriptor) {
     this._idString = generateId(this._nameString = aName);
     this._displayScope(aName, "variables-view-variable variable-or-property");
 
-    // Don't allow displaying variable information there's no name available.
-    if (this._nameString) {
-      this._displayVariable();
-      this._customizeVariable();
-      this._prepareTooltips();
-      this._setAttributes();
-      this._addEventListeners();
-    }
+    this._displayVariable();
+    this._customizeVariable();
+    this._prepareTooltips();
+    this._setAttributes();
+    this._addEventListeners();
 
     this._onInit(this.ownerView._store.size < LAZY_APPEND_BATCH);
   },
 
   /**
    * Called when this variable has finished initializing, and is ready to
    * be attached to the owner view.
    *
@@ -2416,22 +2466,35 @@ Variable.prototype = Heritage.extend(Sco
     let descriptor = this._initialDescriptor;
 
     if (ownerView.eval && this.getter || this.setter) {
       let editNode = this._editNode = this.document.createElement("toolbarbutton");
       editNode.className = "plain variables-view-edit";
       editNode.addEventListener("mousedown", this._onEdit.bind(this), false);
       this._title.insertBefore(editNode, this._spacer);
     }
+
     if (ownerView.delete) {
       let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
       deleteNode.className = "plain variables-view-delete";
       deleteNode.addEventListener("click", this._onDelete.bind(this), false);
       this._title.appendChild(deleteNode);
     }
+
+    let { actionsFirst } = this._variablesView;
+    if (ownerView.new || actionsFirst) {
+      let addPropertyNode = this._addPropertyNode = this.document.createElement("toolbarbutton");
+      addPropertyNode.className = "plain variables-view-add-property";
+      addPropertyNode.addEventListener("mousedown", this._onAddProperty.bind(this), false);
+      if (actionsFirst && VariablesView.isPrimitive(descriptor)) {
+        addPropertyNode.setAttribute("invisible", "");
+      }
+      this._title.appendChild(addPropertyNode);
+    }
+
     if (ownerView.contextMenuId) {
       this._title.setAttribute("context", ownerView.contextMenuId);
     }
 
     if (ownerView.preventDescriptorModifiers) {
       return;
     }
 
@@ -2578,158 +2641,52 @@ Variable.prototype = Heritage.extend(Sco
    */
   _addEventListeners: function() {
     this._name.addEventListener("dblclick", this._activateNameInput, false);
     this._valueLabel.addEventListener("mousedown", this._activateValueInput, false);
     this._title.addEventListener("mousedown", this._onClick, false);
   },
 
   /**
-   * Creates a textbox node in place of a label.
-   *
-   * @param nsIDOMNode aLabel
-   *        The label to be replaced with a textbox.
-   * @param string aClassName
-   *        The class to be applied to the textbox.
-   * @param object aCallbacks
-   *        An object containing the onKeypress and onBlur callbacks.
-   */
-  _activateInput: function(aLabel, aClassName, aCallbacks) {
-    let initialString = aLabel.getAttribute("value");
-
-    // Create a texbox input element which will be shown in the current
-    // element's specified label location.
-    let input = this.document.createElement("textbox");
-    input.className = "plain " + aClassName;
-    input.setAttribute("value", initialString);
-    if (!this._variablesView.alignedValues) {
-      input.setAttribute("flex", "1");
-    }
-
-    // Replace the specified label with a textbox input element.
-    aLabel.parentNode.replaceChild(input, aLabel);
-    this._variablesView.boxObject.ensureElementIsVisible(input);
-    input.select();
-
-    // When the value is a string (displayed as "value"), then we probably want
-    // to change it to another string in the textbox, so to avoid typing the ""
-    // again, tackle with the selection bounds just a bit.
-    if (aLabel.getAttribute("value").match(/^".+"$/)) {
-      input.selectionEnd--;
-      input.selectionStart++;
-    }
-
-    input.addEventListener("keypress", aCallbacks.onKeypress, false);
-    input.addEventListener("blur", aCallbacks.onBlur, false);
-
-    this._prevExpandable = this.twisty;
-    this._prevExpanded = this.expanded;
-    this.collapse();
-    this.hideArrow();
-    this._locked = true;
-
-    this._inputNode = input;
-    this._stopThrobber();
-  },
-
-  /**
-   * Removes the textbox node in place of a label.
-   *
-   * @param nsIDOMNode aLabel
-   *        The label which was replaced with a textbox.
-   * @param object aCallbacks
-   *        An object containing the onKeypress and onBlur callbacks.
-   */
-  _deactivateInput: function(aLabel, aInput, aCallbacks) {
-    aInput.parentNode.replaceChild(aLabel, aInput);
-    this._variablesView.boxObject.scrollBy(-this._target.clientWidth, 0);
-
-    aInput.removeEventListener("keypress", aCallbacks.onKeypress, false);
-    aInput.removeEventListener("blur", aCallbacks.onBlur, false);
-
-    this._locked = false;
-    this.twisty = this._prevExpandable;
-    this.expanded = this._prevExpanded;
-
-    this._inputNode = null;
-    this._stopThrobber();
-  },
-
-  /**
    * Makes this variable's name editable.
    */
   _activateNameInput: function(e) {
-    if (e && e.button != 0) {
-      // Only allow left-click to trigger this event.
-      return;
-    }
-    if (!this.ownerView.switch) {
-      return;
-    }
-    if (e) {
-      e.preventDefault();
-      e.stopPropagation();
+    if (!this._variablesView.alignedValues) {
+      this._separatorLabel.hidden = true;
+      this._valueLabel.hidden = true;
     }
 
-    this._onNameInputKeyPress = this._onNameInputKeyPress.bind(this);
-    this._deactivateNameInput = this._deactivateNameInput.bind(this);
-
-    this._activateInput(this._name, "element-name-input", {
-      onKeypress: this._onNameInputKeyPress,
-      onBlur: this._deactivateNameInput
-    });
-    this._separatorLabel.hidden = true;
-    this._valueLabel.hidden = true;
-  },
-
-  /**
-   * Deactivates this variable's editable name mode.
-   */
-  _deactivateNameInput: function(e) {
-    this._deactivateInput(this._name, e.target, {
-      onKeypress: this._onNameInputKeyPress,
-      onBlur: this._deactivateNameInput
-    });
-    this._separatorLabel.hidden = false;
-    this._valueLabel.hidden = false;
+    EditableName.create(this, {
+      onSave: aKey => {
+        if (!this._variablesView.preventDisableOnChange) {
+          this._disable();
+        }
+        this.ownerView.switch(this, aKey);
+      },
+      onCleanup: () => {
+        if (!this._variablesView.alignedValues) {
+          this._separatorLabel.hidden = false;
+          this._valueLabel.hidden = false;
+        }
+      }
+    }, e);
   },
 
   /**
    * Makes this variable's value editable.
    */
   _activateValueInput: function(e) {
-    if (e && e.button != 0) {
-      // Only allow left-click to trigger this event.
-      return;
-    }
-    if (!this.ownerView.eval) {
-      return;
-    }
-    if (e) {
-      e.preventDefault();
-      e.stopPropagation();
-    }
-
-    this._onValueInputKeyPress = this._onValueInputKeyPress.bind(this);
-    this._deactivateValueInput = this._deactivateValueInput.bind(this);
-
-    this._activateInput(this._valueLabel, "element-value-input", {
-      onKeypress: this._onValueInputKeyPress,
-      onBlur: this._deactivateValueInput
-    });
-  },
-
-  /**
-   * Deactivates this variable's editable value mode.
-   */
-  _deactivateValueInput: function(e) {
-    this._deactivateInput(this._valueLabel, e.target, {
-      onKeypress: this._onValueInputKeyPress,
-      onBlur: this._deactivateValueInput
-    });
+    EditableValue.create(this, {
+      onSave: aString => {
+        if (!this._variablesView.preventDisableOnChange) {
+          this._disable();
+        }
+        this.ownerView.eval(this.evaluationMacro(this, aString));
+      }
+    }, e);
   },
 
   /**
    * Disables this variable prior to a new name switch or value evaluation.
    */
   _disable: function() {
     // Prevent the variable from being collapsed or expanded.
     this.hideArrow();
@@ -2738,95 +2695,22 @@ Variable.prototype = Heritage.extend(Sco
     for (let node of this._title.childNodes) {
       node.hidden = node != this._arrow && node != this._name;
     }
     this._enum.hidden = true;
     this._nonenum.hidden = true;
   },
 
   /**
-   * Deactivates this variable's editable mode and callbacks the new name.
-   */
-  _saveNameInput: function(e) {
-    let input = e.target;
-    let initialString = this._name.getAttribute("value");
-    let currentString = input.value.trim();
-    this._deactivateNameInput(e);
-
-    if (initialString != currentString) {
-      if (!this._variablesView.preventDisableOnChage) {
-        this._disable();
-        this._name.value = currentString;
-      }
-      this.ownerView.switch(this, currentString);
-    }
-  },
-
-  /**
-   * Deactivates this variable's editable mode and evaluates the new value.
-   */
-  _saveValueInput: function(e) {
-    let input = e.target;
-    let initialString = this._valueLabel.getAttribute("value");
-    let currentString = input.value.trim();
-    this._deactivateValueInput(e);
-
-    if (initialString != currentString) {
-      if (!this._variablesView.preventDisableOnChage) {
-        this._disable();
-      }
-      this.ownerView.eval(this.evaluationMacro(this, currentString.trim()));
-    }
-  },
-
-  /**
    * The current macro used to generate the string evaluated when performing
    * a variable or property value change.
    */
   evaluationMacro: VariablesView.simpleValueEvalMacro,
 
   /**
-   * The key press listener for this variable's editable name textbox.
-   */
-  _onNameInputKeyPress: function(e) {
-    e.stopPropagation();
-
-    switch(e.keyCode) {
-      case e.DOM_VK_RETURN:
-      case e.DOM_VK_ENTER:
-        this._saveNameInput(e);
-        this.focus();
-        return;
-      case e.DOM_VK_ESCAPE:
-        this._deactivateNameInput(e);
-        this.focus();
-        return;
-    }
-  },
-
-  /**
-   * The key press listener for this variable's editable value textbox.
-   */
-  _onValueInputKeyPress: function(e) {
-    e.stopPropagation();
-
-    switch(e.keyCode) {
-      case e.DOM_VK_RETURN:
-      case e.DOM_VK_ENTER:
-        this._saveValueInput(e);
-        this.focus();
-        return;
-      case e.DOM_VK_ESCAPE:
-        this._deactivateValueInput(e);
-        this.focus();
-        return;
-    }
-  },
-
-  /**
    * The click listener for the edit button.
    */
   _onEdit: function(e) {
     if (e.button != 0) {
       return;
     }
 
     e.preventDefault();
@@ -2847,25 +2731,58 @@ Variable.prototype = Heritage.extend(Sco
 
     if (this.ownerView.delete) {
       if (!this.ownerView.delete(this)) {
         this.hide();
       }
     }
   },
 
+  /**
+   * The click listener for the add property button.
+   */
+  _onAddProperty: function(e) {
+    if ("button" in e && e.button != 0) {
+      return;
+    }
+
+    e.preventDefault();
+    e.stopPropagation();
+
+    this.expanded = true;
+
+    let item = this.addItem(" ", {
+      value: undefined,
+      configurable: true,
+      enumerable: true,
+      writable: true
+    }, true);
+
+    // Force showing the separator.
+    item._separatorLabel.hidden = false;
+
+    EditableNameAndValue.create(item, {
+      onSave: ([aKey, aValue]) => {
+        if (!this._variablesView.preventDisableOnChange) {
+          this._disable();
+        }
+        this.ownerView.new(this, aKey, aValue);
+      }
+    }, e);
+  },
+
   _symbolicName: "",
   _absoluteName: "",
   _initialDescriptor: null,
   _separatorLabel: null,
   _spacer: null,
   _valueLabel: null,
-  _inputNode: null,
   _editNode: null,
   _deleteNode: null,
+  _addPropertyNode: null,
   _tooltip: null,
   _valueGrip: null,
   _valueString: "",
   _valueClassName: "",
   _prevExpandable: false,
   _prevExpanded: false
 });
 
@@ -2890,28 +2807,25 @@ Property.prototype = Heritage.extend(Var
   /**
    * Initializes this property's id, view and binds event listeners.
    *
    * @param string aName
    *        The property's name.
    * @param object aDescriptor
    *        The property's descriptor.
    */
-  _init: function(aName, aDescriptor) {
+  _init: function(aName = "", aDescriptor) {
     this._idString = generateId(this._nameString = aName);
     this._displayScope(aName, "variables-view-property variable-or-property");
 
-    // Don't allow displaying property information there's no name available.
-    if (this._nameString) {
-      this._displayVariable();
-      this._customizeVariable();
-      this._prepareTooltips();
-      this._setAttributes();
-      this._addEventListeners();
-    }
+    this._displayVariable();
+    this._customizeVariable();
+    this._prepareTooltips();
+    this._setAttributes();
+    this._addEventListeners();
 
     this._onInit(this.ownerView._store.size < LAZY_APPEND_BATCH);
   },
 
   /**
    * Called when this property has finished initializing, and is ready to
    * be attached to the owner view.
    *
@@ -3258,8 +3172,267 @@ VariablesView.getClass = function(aGrip)
  *         A unique id.
  */
 let generateId = (function() {
   let count = 0;
   return function(aName = "") {
     return aName.toLowerCase().trim().replace(/\s+/g, "-") + (++count);
   };
 })();
+
+
+/**
+ * An Editable encapsulates the UI of an edit box that overlays a label,
+ * allowing the user to edit the value.
+ *
+ * @param Variable aVariable
+ *        The Variable or Property to make editable.
+ * @param object aOptions
+ *        - onSave
+ *          The callback to call with the value when editing is complete.
+ *        - onCleanup
+ *          The callback to call when the editable is removed for any reason.
+ */
+function Editable(aVariable, aOptions) {
+  this._variable = aVariable;
+  this._onSave = aOptions.onSave;
+  this._onCleanup = aOptions.onCleanup;
+}
+
+Editable.create = function(aVariable, aOptions, aEvent) {
+  let editable = new this(aVariable, aOptions);
+  editable.activate(aEvent);
+  return editable;
+};
+
+Editable.prototype = {
+  /**
+   * The class name for targeting this Editable type's label element. Overridden
+   * by inheriting classes.
+   */
+  className: null,
+
+  /**
+   * Boolean indicating whether this Editable should activate. Overridden by
+   * inheriting classes.
+   */
+  shouldActivate: null,
+
+  /**
+   * The label element for this Editable. Overridden by inheriting classes.
+   */
+  label: null,
+
+  /**
+   * Activate this editable by replacing the input box it overlays and
+   * initialize the handlers.
+   *
+   * @param Event e [optional]
+   *        Optionally, the Event object that was used to activate the Editable.
+   */
+  activate: function(e) {
+    if (!this.shouldActivate) {
+      return;
+    }
+
+    let { label } = this;
+    let initialString = label.getAttribute("value");
+
+    if (e) {
+      e.preventDefault();
+      e.stopPropagation();
+    }
+
+    // Create a texbox input element which will be shown in the current
+    // element's specified label location.
+    let input = this._input = this._variable.document.createElement("textbox");
+    input.className = "plain " + this.className;
+    input.setAttribute("value", initialString);
+    if (!this._variable._variablesView.alignedValues) {
+      input.setAttribute("flex", "1");
+    }
+
+    // Replace the specified label with a textbox input element.
+    label.parentNode.replaceChild(input, label);
+    this._variable._variablesView.boxObject.ensureElementIsVisible(input);
+    input.select();
+
+    // When the value is a string (displayed as "value"), then we probably want
+    // to change it to another string in the textbox, so to avoid typing the ""
+    // again, tackle with the selection bounds just a bit.
+    if (initialString.match(/^".+"$/)) {
+      input.selectionEnd--;
+      input.selectionStart++;
+    }
+
+    this._onKeypress = this._onKeypress.bind(this);
+    this._onBlur = this._onBlur.bind(this);
+    input.addEventListener("keypress", this._onKeypress);
+    input.addEventListener("blur", this._onBlur);
+
+    this._prevExpandable = this._variable.twisty;
+    this._prevExpanded = this._variable.expanded;
+    this._variable.collapse();
+    this._variable.hideArrow();
+    this._variable.locked = true;
+    this._variable._stopThrobber();
+  },
+
+  /**
+   * Remove the input box and restore the Variable or Property to its previous
+   * state.
+   */
+  deactivate: function() {
+    this._input.removeEventListener("keypress", this._onKeypress);
+    this._input.removeEventListener("blur", this.deactivate);
+    this._input.parentNode.replaceChild(this.label, this._input);
+    this._input = null;
+
+    let { boxObject } = this._variable._variablesView;
+    boxObject.scrollBy(-this._variable._target, 0);
+    this._variable.locked = false;
+    this._variable.twisty = this._prevExpandable;
+    this._variable.expanded = this._prevExpanded;
+    this._variable._stopThrobber();
+  },
+
+  /**
+   * Save the current value and deactivate the Editable.
+   */
+  _save: function() {
+    let initial = this.label.getAttribute("value");
+    let current = this._input.value.trim();
+    this.deactivate();
+    if (initial != current) {
+      this._onSave(current);
+    }
+  },
+
+  /**
+   * Called when tab is pressed, allowing subclasses to link different
+   * behavior to tabbing if desired.
+   */
+  _next: function() {
+    this._save();
+  },
+
+  /**
+   * Called when escape is pressed, indicating a cancelling of editing without
+   * saving.
+   */
+  _reset: function() {
+    this.deactivate();
+    this._variable.focus();
+  },
+
+  /**
+   * Event handler for when the input loses focus.
+   */
+  _onBlur: function() {
+    this.deactivate();
+  },
+
+  /**
+   * Event handler for when the input receives a key press.
+   */
+  _onKeypress: function(e) {
+    e.stopPropagation();
+
+    switch (e.keyCode) {
+      case e.DOM_VK_TAB:
+        this._next();
+        break;
+      case e.DOM_VK_RETURN:
+      case e.DOM_VK_ENTER:
+        this._save();
+        break;
+      case e.DOM_VK_ESCAPE:
+        this._reset();
+        break;
+    }
+  },
+};
+
+
+/**
+ * An Editable specific to editing the name of a Variable or Property.
+ */
+function EditableName(aVariable, aOptions) {
+  Editable.call(this, aVariable, aOptions);
+}
+
+EditableName.create = Editable.create;
+
+EditableName.prototype = Heritage.extend(Editable.prototype, {
+  className: "element-name-input",
+
+  get label() {
+    return this._variable._name;
+  },
+
+  get shouldActivate() {
+    return !!this._variable.ownerView.switch;
+  },
+});
+
+
+/**
+ * An Editable specific to editing the value of a Variable or Property.
+ */
+function EditableValue(aVariable, aOptions) {
+  Editable.call(this, aVariable, aOptions);
+}
+
+EditableValue.create = Editable.create;
+
+EditableValue.prototype = Heritage.extend(Editable.prototype, {
+  className: "element-value-input",
+
+  get label() {
+    return this._variable._valueLabel;
+  },
+
+  get shouldActivate() {
+    return !!this._variable.ownerView.eval;
+  },
+});
+
+
+/**
+ * An Editable specific to editing the key and value of a new property.
+ */
+function EditableNameAndValue(aVariable, aOptions) {
+  EditableName.call(this, aVariable, aOptions);
+}
+
+EditableNameAndValue.create = Editable.create;
+
+EditableNameAndValue.prototype = Heritage.extend(EditableName.prototype, {
+  _reset: function(e) {
+    // Hide the Varible or Property if the user presses escape.
+    this._variable.remove();
+    this.deactivate();
+  },
+
+  _next: function(e) {
+    // Override _next so as to set both key and value at the same time.
+    let key = this._input.value;
+    this.label.setAttribute("value", key);
+
+    let valueEditable = EditableValue.create(this._variable, {
+      onSave: aValue => {
+        this._onSave([key, aValue]);
+      },
+      onCleanup: () => {
+        this._onCleanup();
+      }
+    });
+    valueEditable._reset = () => {
+      this._variable.remove();
+      valueEditable.deactivate();
+    };
+  },
+
+  _save: function(e) {
+    // Both _save and _next activate the value edit box.
+    this._next(e);
+  }
+});
--- a/browser/devtools/shared/widgets/widgets.css
+++ b/browser/devtools/shared/widgets/widgets.css
@@ -58,15 +58,21 @@
 
 .variable-or-property:not([safe-getter]) > tooltip > label[value=WebIDL],
 .variable-or-property:not([non-extensible]) > tooltip > label[value=extensible],
 .variable-or-property:not([frozen]) > tooltip > label[value=frozen],
 .variable-or-property:not([sealed]) > tooltip > label[value=sealed] {
   display: none;
 }
 
-*:not(:hover) .variables-view-delete {
+*:not(:hover) .variables-view-delete,
+*:not(:hover) .variables-view-add-property {
   visibility: hidden;
 }
 
+.variables-view-delete > .toolbarbutton-text,
+.variables-view-add-property > .toolbarbutton-text {
+  display: none;
+}
+
 .variables-view-container[aligned-values] .title > [optional-visibility] {
   display: none;
 }
--- a/browser/themes/linux/devtools/widgets.css
+++ b/browser/themes/linux/devtools/widgets.css
@@ -610,20 +610,25 @@
 }
 
 .variables-view-container[aligned-values] .title > .element-value-input {
   width: calc(70vw - 10px);
 }
 
 /* Actions first */
 
-.variables-view-container[actions-first] .variables-view-delete {
+.variables-view-container[actions-first] .variables-view-delete,
+.variables-view-container[actions-first] .variables-view-add-property {
   -moz-box-ordinal-group: 0;
 }
 
+.variables-view-container[actions-first] [invisible] {
+  visibility: hidden;
+}
+
 /* Variables and properties tooltips */
 
 .variable-or-property > tooltip > label {
   margin: 0 2px 0 2px;
 }
 
 .variable-or-property[non-enumerable] > tooltip > label[value=enumerable],
 .variable-or-property[non-configurable] > tooltip > label[value=configurable],
@@ -649,20 +654,16 @@
 .variables-view-delete:hover {
   -moz-image-region: rect(0,32px,16px,16px);
 }
 
 .variables-view-delete:active {
   -moz-image-region: rect(0,48px,16px,32px);
 }
 
-.variables-view-delete > .toolbarbutton-text {
-  display: none;
-}
-
 .variables-view-edit {
   background: url("chrome://browser/skin/devtools/vview-edit.png") center no-repeat;
   width: 20px;
   height: 16px;
   cursor: pointer;
 }
 
 .variables-view-throbber {
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -253,16 +253,17 @@ browser.jar:
   skin/classic/browser/devtools/app-manager/index.css                 (../shared/devtools/app-manager/index.css)
   skin/classic/browser/devtools/app-manager/device.css                (../shared/devtools/app-manager/device.css)
   skin/classic/browser/devtools/app-manager/projects.css              (../shared/devtools/app-manager/projects.css)
   skin/classic/browser/devtools/app-manager/help.css                  (../shared/devtools/app-manager/help.css)
   skin/classic/browser/devtools/app-manager/warning.svg               (../shared/devtools/app-manager/images/warning.svg)
   skin/classic/browser/devtools/app-manager/error.svg                 (../shared/devtools/app-manager/images/error.svg)
   skin/classic/browser/devtools/app-manager/plus.svg                  (../shared/devtools/app-manager/images/plus.svg)
   skin/classic/browser/devtools/app-manager/remove.svg                (../shared/devtools/app-manager/images/remove.svg)
+  skin/classic/browser/devtools/app-manager/add.svg                   (../shared/devtools/app-manager/images/add.svg)
   skin/classic/browser/devtools/app-manager/index-icons.svg           (../shared/devtools/app-manager/images/index-icons.svg)
   skin/classic/browser/devtools/app-manager/rocket.svg                (../shared/devtools/app-manager/images/rocket.svg)
   skin/classic/browser/devtools/app-manager/noise.png                 (../shared/devtools/app-manager/images/noise.png)
 #ifdef MOZ_SERVICES_SYNC
   skin/classic/browser/sync-16-throbber.png
   skin/classic/browser/sync-16.png
   skin/classic/browser/sync-24-throbber.png
   skin/classic/browser/sync-32.png
--- a/browser/themes/osx/devtools/widgets.css
+++ b/browser/themes/osx/devtools/widgets.css
@@ -604,20 +604,25 @@
 }
 
 .variables-view-container[aligned-values] .title > .element-value-input {
   width: calc(70vw - 10px);
 }
 
 /* Actions first */
 
-.variables-view-container[actions-first] .variables-view-delete {
+.variables-view-container[actions-first] .variables-view-delete,
+.variables-view-container[actions-first] .variables-view-add-property {
   -moz-box-ordinal-group: 0;
 }
 
+.variables-view-container[actions-first] [invisible] {
+  visibility: hidden;
+}
+
 /* Variables and properties tooltips */
 
 .variable-or-property > tooltip > label {
   margin: 0 2px 0 2px;
 }
 
 .variable-or-property[non-enumerable] > tooltip > label[value=enumerable],
 .variable-or-property[non-configurable] > tooltip > label[value=configurable],
@@ -643,20 +648,16 @@
 .variables-view-delete:hover {
   -moz-image-region: rect(0,32px,16px,16px);
 }
 
 .variables-view-delete:active {
   -moz-image-region: rect(0,48px,16px,32px);
 }
 
-.variables-view-delete > .toolbarbutton-text {
-  display: none;
-}
-
 .variables-view-edit {
   background: url("chrome://browser/skin/devtools/vview-edit.png") center no-repeat;
   width: 20px;
   height: 16px;
   cursor: pointer;
 }
 
 .variables-view-throbber {
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -355,16 +355,17 @@ browser.jar:
   skin/classic/browser/devtools/app-manager/index.css                 (../shared/devtools/app-manager/index.css)
   skin/classic/browser/devtools/app-manager/device.css                (../shared/devtools/app-manager/device.css)
   skin/classic/browser/devtools/app-manager/projects.css              (../shared/devtools/app-manager/projects.css)
   skin/classic/browser/devtools/app-manager/help.css                  (../shared/devtools/app-manager/help.css)
   skin/classic/browser/devtools/app-manager/warning.svg               (../shared/devtools/app-manager/images/warning.svg)
   skin/classic/browser/devtools/app-manager/error.svg                 (../shared/devtools/app-manager/images/error.svg)
   skin/classic/browser/devtools/app-manager/plus.svg                  (../shared/devtools/app-manager/images/plus.svg)
   skin/classic/browser/devtools/app-manager/remove.svg                (../shared/devtools/app-manager/images/remove.svg)
+  skin/classic/browser/devtools/app-manager/add.svg                   (../shared/devtools/app-manager/images/add.svg)
   skin/classic/browser/devtools/app-manager/index-icons.svg           (../shared/devtools/app-manager/images/index-icons.svg)
   skin/classic/browser/devtools/app-manager/rocket.svg                (../shared/devtools/app-manager/images/rocket.svg)
   skin/classic/browser/devtools/app-manager/noise.png                 (../shared/devtools/app-manager/images/noise.png)
 #ifdef MOZ_SERVICES_SYNC
   skin/classic/browser/sync-throbber.png
   skin/classic/browser/sync-16.png
   skin/classic/browser/sync-32.png
   skin/classic/browser/sync-bg.png
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/devtools/app-manager/images/add.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="64px" height="64px" viewBox="0 0 64 64">
+  <path fill="#00B2F7" d="M32.336,3.894c-15.74,0-28.5,12.76-28.5,28.5s12.76,28.5,28.5,28.5s28.5-12.76,28.5-28.5
+    S48.076,3.894,32.336,3.894z M44.86,36.966h-7.823v7.62c0,2.582-2.12,4.702-4.702,4.702c-2.584,0-4.704-2.12-4.704-4.702v-7.62
+    h-7.817c-2.52,0-4.572-2.056-4.572-4.572s2.053-4.572,4.572-4.572h7.817v-7.62c0-2.582,2.12-4.702,4.704-4.702
+    c2.582,0,4.702,2.12,4.702,4.702v7.62h7.823c2.514,0,4.57,2.056,4.57,4.572S47.374,36.966,44.86,36.966z"/>
+</svg>
--- a/browser/themes/shared/devtools/app-manager/manifest-editor.inc.css
+++ b/browser/themes/shared/devtools/app-manager/manifest-editor.inc.css
@@ -1,30 +1,31 @@
 /* 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/. */
 
 /* Manifest Editor overrides */
 
 .variables-view-container.manifest-editor {
   background-color: #F5F5F5;
-  padding: 20px 13px;
+  padding: 20px 2px;
 }
 
 .manifest-editor .variable-or-property:focus > .title {
   background-color: #EDEDED;
   color: #000;
   border-radius: 4px;
 }
 
 .manifest-editor .variables-view-property > .title > .name {
   color: #27406A;
 }
 
-.manifest-editor .variable-or-property > .title > label {
+.manifest-editor .variable-or-property > .title > label,
+.manifest-editor textbox  {
   font-family: monospace;
 }
 
 .manifest-editor .variable-or-property > .title > .token-string {
   color: #54BC6A;
   font-weight: bold;
 }
 
@@ -47,21 +48,33 @@
 }
 
 .manifest-editor .variables-view-variable {
   border-bottom: none;
 }
 
 .manifest-editor .variables-view-delete,
 .manifest-editor .variables-view-delete:hover,
-.manifest-editor .variables-view-delete:active {
+.manifest-editor .variables-view-delete:active,
+.manifest-editor .variables-view-add-property,
+.manifest-editor .variables-view-add-property:hover,
+.manifest-editor .variables-view-add-property:active {
   list-style-image: none;
   -moz-image-region: initial;
 }
 
-.manifest-editor .variables-view-delete::before {
-  width: 12px;
-  height: 12px;
+.manifest-editor .variables-view-delete::before,
+.manifest-editor .variables-view-add-property::before {
+  width: 11px;
+  height: 11px;
   content: "";
   display: inline-block;
+  background-size: 11px auto;
+}
+
+.manifest-editor .variables-view-delete::before {
   background-image: url("app-manager/remove.svg");
-  background-size: 12px auto;
 }
+
+.manifest-editor .variables-view-add-property::before {
+  background-image: url("app-manager/add.svg");
+  -moz-margin-end: 2px;
+}
--- a/browser/themes/windows/devtools/widgets.css
+++ b/browser/themes/windows/devtools/widgets.css
@@ -607,20 +607,25 @@
 }
 
 .variables-view-container[aligned-values] .title > .element-value-input {
   width: calc(70vw - 10px);
 }
 
 /* Actions first */
 
-.variables-view-container[actions-first] .variables-view-delete {
+.variables-view-container[actions-first] .variables-view-delete,
+.variables-view-container[actions-first] .variables-view-add-property {
   -moz-box-ordinal-group: 0;
 }
 
+.variables-view-container[actions-first] [invisible] {
+  visibility: hidden;
+}
+
 /* Variables and properties tooltips */
 
 .variable-or-property > tooltip > label {
   margin: 0 2px 0 2px;
 }
 
 .variable-or-property[non-enumerable] > tooltip > label[value=enumerable],
 .variable-or-property[non-configurable] > tooltip > label[value=configurable],
@@ -646,20 +651,16 @@
 .variables-view-delete:hover {
   -moz-image-region: rect(0,32px,16px,16px);
 }
 
 .variables-view-delete:active {
   -moz-image-region: rect(0,48px,16px,32px);
 }
 
-.variables-view-delete > .toolbarbutton-text {
-  display: none;
-}
-
 .variables-view-edit {
   background: url("chrome://browser/skin/devtools/vview-edit.png") center no-repeat;
   width: 20px;
   height: 16px;
   cursor: pointer;
 }
 
 .variables-view-throbber {
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -277,16 +277,17 @@ browser.jar:
         skin/classic/browser/devtools/app-manager/index.css                 (../shared/devtools/app-manager/index.css)
         skin/classic/browser/devtools/app-manager/device.css                (../shared/devtools/app-manager/device.css)
         skin/classic/browser/devtools/app-manager/projects.css              (../shared/devtools/app-manager/projects.css)
         skin/classic/browser/devtools/app-manager/help.css                  (../shared/devtools/app-manager/help.css)
         skin/classic/browser/devtools/app-manager/warning.svg               (../shared/devtools/app-manager/images/warning.svg)
         skin/classic/browser/devtools/app-manager/error.svg                 (../shared/devtools/app-manager/images/error.svg)
         skin/classic/browser/devtools/app-manager/plus.svg                  (../shared/devtools/app-manager/images/plus.svg)
         skin/classic/browser/devtools/app-manager/remove.svg                (../shared/devtools/app-manager/images/remove.svg)
+        skin/classic/browser/devtools/app-manager/add.svg                   (../shared/devtools/app-manager/images/add.svg)
         skin/classic/browser/devtools/app-manager/index-icons.svg           (../shared/devtools/app-manager/images/index-icons.svg)
         skin/classic/browser/devtools/app-manager/rocket.svg                (../shared/devtools/app-manager/images/rocket.svg)
         skin/classic/browser/devtools/app-manager/noise.png                 (../shared/devtools/app-manager/images/noise.png)
 #ifdef MOZ_SERVICES_SYNC
         skin/classic/browser/sync-throbber.png
         skin/classic/browser/sync-16.png
         skin/classic/browser/sync-32.png
         skin/classic/browser/sync-128.png
@@ -578,16 +579,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/app-manager/index.css                 (../shared/devtools/app-manager/index.css)
         skin/classic/aero/browser/devtools/app-manager/device.css                (../shared/devtools/app-manager/device.css)
         skin/classic/aero/browser/devtools/app-manager/projects.css              (../shared/devtools/app-manager/projects.css)
         skin/classic/aero/browser/devtools/app-manager/help.css                  (../shared/devtools/app-manager/help.css)
         skin/classic/aero/browser/devtools/app-manager/warning.svg               (../shared/devtools/app-manager/images/warning.svg)
         skin/classic/aero/browser/devtools/app-manager/error.svg                 (../shared/devtools/app-manager/images/error.svg)
         skin/classic/aero/browser/devtools/app-manager/plus.svg                  (../shared/devtools/app-manager/images/plus.svg)
         skin/classic/aero/browser/devtools/app-manager/remove.svg                (../shared/devtools/app-manager/images/remove.svg)
+        skin/classic/aero/browser/devtools/app-manager/add.svg                   (../shared/devtools/app-manager/images/add.svg)
         skin/classic/aero/browser/devtools/app-manager/index-icons.svg           (../shared/devtools/app-manager/images/index-icons.svg)
         skin/classic/aero/browser/devtools/app-manager/rocket.svg                (../shared/devtools/app-manager/images/rocket.svg)
         skin/classic/aero/browser/devtools/app-manager/noise.png                 (../shared/devtools/app-manager/images/noise.png)
 #ifdef MOZ_SERVICES_SYNC
         skin/classic/aero/browser/sync-throbber.png
         skin/classic/aero/browser/sync-16.png
         skin/classic/aero/browser/sync-32.png
         skin/classic/aero/browser/sync-128.png