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 158496 e0572cb4eb82
parent 158495 7b8cc2b3568b
child 158497 c7f35fd43aa9
push id3770
push userbbenvie@mozilla.com
push dateTue, 03 Dec 2013 18:14:59 +0000
treeherderfx-team@e0572cb4eb82 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvp, jryans
bugs808371
milestone28.0a1
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