Bug 831794 - Variables View: allow users to override getter properties to plain value properties, r=past

--HG--
rename : browser/devtools/debugger/test/browser_dbg_propertyview-big-data.js => browser/devtools/debugger/test/browser_dbg_propertyview-data-big.js
rename : browser/devtools/debugger/test/browser_dbg_propertyview-edit.js => browser/devtools/debugger/test/browser_dbg_propertyview-edit-value.js
This commit is contained in:
Victor Porof 2013-01-31 18:07:24 +02:00
parent 81f35d94fb
commit b98e84637f
15 changed files with 786 additions and 34 deletions

View File

@ -32,9 +32,11 @@ MOCHITEST_BROWSER_TESTS = \
browser_dbg_propertyview-08.js \
browser_dbg_propertyview-09.js \
browser_dbg_propertyview-10.js \
browser_dbg_propertyview-edit.js \
browser_dbg_propertyview-edit-value.js \
browser_dbg_propertyview-edit-watch.js \
browser_dbg_propertyview-big-data.js \
browser_dbg_propertyview-data-big.js \
browser_dbg_propertyview-data-getset-01.js \
browser_dbg_propertyview-data-getset-02.js \
browser_dbg_propertyview-data.js \
browser_dbg_propertyview-filter-01.js \
browser_dbg_propertyview-filter-02.js \

View File

@ -11,6 +11,11 @@
var a = 1;
var b = { a: a };
var c = { a: 1, b: "beta", c: true, d: b };
var myVar = {
_prop: 42,
get prop() { return this._prop; },
set prop(val) { this._prop = val; }
};
debugger;
}

View File

@ -40,7 +40,7 @@ function testFrameParameters()
is(frames.querySelectorAll(".dbg-stackframe").length, 3,
"Should have three frames.");
is(localNodes.length, 11,
is(localNodes.length, 12,
"The localScope should contain all the created variable elements.");
is(localNodes[0].querySelector(".value").getAttribute("value"), "[object Proxy]",

View File

@ -40,7 +40,7 @@ function testFrameParameters()
is(frames.querySelectorAll(".dbg-stackframe").length, 3,
"Should have three frames.");
is(localNodes.length + localNonEnums.length, 11,
is(localNodes.length + localNonEnums.length, 12,
"The localScope and localNonEnums should contain all the created variable elements.");
is(localNodes[0].querySelector(".value").getAttribute("value"), "[object Proxy]",

View File

@ -53,7 +53,7 @@ function testFrameParameters()
is(globalNodes[0].querySelector(".name").getAttribute("value"), "InstallTrigger",
"Should have the right property name for |InstallTrigger|.");
is(globalNodes[0].querySelector(".value").getAttribute("value"), "undefined",
is(globalNodes[0].querySelector(".value").getAttribute("value"), "",
"Should have the right property value for |InstallTrigger|.");
is(globalNodes[1].querySelector(".name").getAttribute("value"), "SpecialPowers",

View File

@ -64,7 +64,7 @@ function testWithFrame()
is(globalNodes[0].querySelector(".name").getAttribute("value"), "InstallTrigger",
"Should have the right property name for |InstallTrigger|.");
is(globalNodes[0].querySelector(".value").getAttribute("value"), "undefined",
is(globalNodes[0].querySelector(".value").getAttribute("value"), "",
"Should have the right property value for |InstallTrigger|.");
let len = globalNodes.length - 1;

View File

@ -0,0 +1,343 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Make sure that the property view knows how to edit getters and setters.
*/
const TAB_URL = EXAMPLE_URL + "browser_dbg_frame-parameters.html";
var gPane = null;
var gTab = null;
var gDebugger = null;
var gVars = null;
var gWatch = null;
requestLongerTimeout(2);
function test()
{
debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
gTab = aTab;
gPane = aPane;
gDebugger = gPane.panelWin;
gVars = gDebugger.DebuggerView.Variables;
gWatch = gDebugger.DebuggerView.WatchExpressions;
gVars.switch = function() {};
gVars.delete = function() {};
prepareVariablesView();
});
}
function prepareVariablesView() {
gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
Services.tm.currentThread.dispatch({ run: function() {
testVariablesView();
}}, 0);
}, false);
EventUtils.sendMouseEvent({ type: "click" },
content.document.querySelector("button"),
content.window);
}
function testVariablesView()
{
executeSoon(function() {
addWatchExpressions(function() {
testEdit("set", "this._prop = value + ' BEER CAN'", function() {
testEdit("set", "{ this._prop = value + ' BEACON' }", function() {
testEdit("set", "{ this._prop = value + ' BEACON;'; }", function() {
testEdit("set", "{ return this._prop = value + ' BEACON;;'; }", function() {
testEdit("set", "function(value) { this._prop = value + ' BACON' }", function() {
testEdit("get", "'brelx BEER CAN'", function() {
testEdit("get", "{ 'brelx BEACON' }", function() {
testEdit("get", "{ 'brelx BEACON;'; }", function() {
testEdit("get", "{ return 'brelx BEACON;;'; }", function() {
testEdit("get", "function() { return 'brelx BACON'; }", function() {
testEdit("get", "bogus", function() {
testEdit("set", "sugob", function() {
testEdit("get", "", function() {
testEdit("set", "", function() {
waitForWatchExpressions(function() {
testEdit("self", "2507", function() {
closeDebuggerAndFinish();
}, {
"myVar.prop": 2507,
"myVar.prop + 42": "250742"
});
})
gWatch.deleteExpression({ name: "myVar.prop = 'xlerb'" });
}, {
"myVar.prop": "xlerb",
"myVar.prop + 42": NaN,
"myVar.prop = 'xlerb'": "xlerb"
});
}, {
"myVar.prop": undefined,
"myVar.prop + 42": NaN,
"myVar.prop = 'xlerb'": "ReferenceError: sugob is not defined"
});
}, {
"myVar.prop": "ReferenceError: bogus is not defined",
"myVar.prop + 42": "ReferenceError: bogus is not defined",
"myVar.prop = 'xlerb'": "ReferenceError: sugob is not defined"
});
}, {
"myVar.prop": "ReferenceError: bogus is not defined",
"myVar.prop + 42": "ReferenceError: bogus is not defined",
"myVar.prop = 'xlerb'": "xlerb"
});
}, {
"myVar.prop": "brelx BACON",
"myVar.prop + 42": "brelx BACON42",
"myVar.prop = 'xlerb'": "xlerb"
});
}, {
"myVar.prop": "brelx BEACON;;",
"myVar.prop + 42": "brelx BEACON;;42",
"myVar.prop = 'xlerb'": "xlerb"
});
}, {
"myVar.prop": undefined,
"myVar.prop + 42": NaN,
"myVar.prop = 'xlerb'": "xlerb"
});
}, {
"myVar.prop": undefined,
"myVar.prop + 42": NaN,
"myVar.prop = 'xlerb'": "xlerb"
});
}, {
"myVar.prop": "brelx BEER CAN",
"myVar.prop + 42": "brelx BEER CAN42",
"myVar.prop = 'xlerb'": "xlerb"
});
}, {
"myVar.prop": "xlerb BACON",
"myVar.prop + 42": "xlerb BACON42",
"myVar.prop = 'xlerb'": "xlerb"
});
}, {
"myVar.prop": "xlerb BEACON;;",
"myVar.prop + 42": "xlerb BEACON;;42",
"myVar.prop = 'xlerb'": "xlerb"
});
}, {
"myVar.prop": "xlerb BEACON;",
"myVar.prop + 42": "xlerb BEACON;42",
"myVar.prop = 'xlerb'": "xlerb"
});
}, {
"myVar.prop": "xlerb BEACON",
"myVar.prop + 42": "xlerb BEACON42",
"myVar.prop = 'xlerb'": "xlerb"
});
}, {
"myVar.prop": "xlerb BEER CAN",
"myVar.prop + 42": "xlerb BEER CAN42",
"myVar.prop = 'xlerb'": "xlerb"
});
});
});
}
function addWatchExpressions(callback)
{
waitForWatchExpressions(function() {
let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
let scope = gVars._currHierarchy.get(label);
ok(scope, "There should be a wach expressions scope in the variables view");
is(scope._store.size, 1, "There should be 1 evaluation availalble");
let w1 = scope.get("myVar.prop");
let w2 = scope.get("myVar.prop + 42");
let w3 = scope.get("myVar.prop = 'xlerb'");
ok(w1, "The first watch expression should be present in the scope");
ok(!w2, "The second watch expression should not be present in the scope");
ok(!w3, "The third watch expression should not be present in the scope");
is(w1.value, 42, "The first value is correct.");
waitForWatchExpressions(function() {
let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
let scope = gVars._currHierarchy.get(label);
ok(scope, "There should be a wach expressions scope in the variables view");
is(scope._store.size, 2, "There should be 2 evaluations availalble");
let w1 = scope.get("myVar.prop");
let w2 = scope.get("myVar.prop + 42");
let w3 = scope.get("myVar.prop = 'xlerb'");
ok(w1, "The first watch expression should be present in the scope");
ok(w2, "The second watch expression should be present in the scope");
ok(!w3, "The third watch expression should not be present in the scope");
is(w1.value, "42", "The first expression value is correct.");
is(w2.value, "84", "The second expression value is correct.");
waitForWatchExpressions(function() {
let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
let scope = gVars._currHierarchy.get(label);
ok(scope, "There should be a wach expressions scope in the variables view");
is(scope._store.size, 3, "There should be 3 evaluations availalble");
let w1 = scope.get("myVar.prop");
let w2 = scope.get("myVar.prop + 42");
let w3 = scope.get("myVar.prop = 'xlerb'");
ok(w1, "The first watch expression should be present in the scope");
ok(w2, "The second watch expression should be present in the scope");
ok(w3, "The third watch expression should be present in the scope");
is(w1.value, "xlerb", "The first expression value is correct.");
is(w2.value, "xlerb42", "The second expression value is correct.");
is(w3.value, "xlerb", "The third expression value is correct.");
callback();
});
gWatch.addExpression("myVar.prop = 'xlerb'");
gDebugger.editor.focus();
});
gWatch.addExpression("myVar.prop + 42");
gDebugger.editor.focus();
});
gWatch.addExpression("myVar.prop");
gDebugger.editor.focus();
}
function testEdit(what, string, callback, expected)
{
let localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".scope")[1],
localNodes = localScope.querySelector(".details").childNodes,
myVar = gVars.getItemForNode(localNodes[11]);
waitForProperties(function() {
let prop = myVar.get("prop");
let getterOrSetter = (what != "self" ? prop.get(what) : prop);
EventUtils.sendMouseEvent({ type: "mousedown" },
getterOrSetter._target.querySelector(".title > .value"),
gDebugger);
waitForElement(".element-value-input", true, function() {
waitForWatchExpressions(function() {
let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
let scope = gVars._currHierarchy.get(label);
let w1 = scope.get(Object.keys(expected)[0]);
let w2 = scope.get(Object.keys(expected)[1]);
let w3 = scope.get(Object.keys(expected)[2]);
if (w1) {
if (isNaN(expected[w1.name]) && typeof expected[w1.name] == "number") {
ok(isNaN(w1.value),
"The first expression value is correct after the edit (NaN).");
} else {
is(w1.value, expected[w1.name],
"The first expression value is correct after the edit.");
}
info(w1.value + " is equal to " + expected[w1.name]);
}
if (w2) {
if (isNaN(expected[w2.name]) && typeof expected[w2.name] == "number") {
ok(isNaN(w2.value),
"The second expression value is correct after the edit (NaN).");
} else {
is(w2.value, expected[w2.name],
"The second expression value is correct after the edit.");
}
info(w2.value + " is equal to " + expected[w2.name]);
}
if (w3) {
if (isNaN(expected[w3.name]) && typeof expected[w3.name] == "number") {
ok(isNaN(w3.value),
"The third expression value is correct after the edit (NaN).");
} else {
is(w3.value, expected[w3.name],
"The third expression value is correct after the edit.");
}
info(w3.value + " is equal to " + expected[w3.name]);
}
callback();
});
info("Changing the " + what + "ter with '" + string + "'.");
write(string);
EventUtils.sendKey("RETURN", gDebugger);
});
});
myVar.expand();
gVars.clearHierarchy();
}
function waitForWatchExpressions(callback) {
gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function onFetch() {
gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", onFetch, false);
executeSoon(callback);
}, false);
}
function waitForProperties(callback) {
gDebugger.addEventListener("Debugger:FetchedProperties", function onFetch() {
gDebugger.removeEventListener("Debugger:FetchedProperties", onFetch, false);
executeSoon(callback);
}, false);
}
function waitForElement(selector, exists, callback)
{
// Poll every few milliseconds until the element are retrieved.
let count = 0;
let intervalID = window.setInterval(function() {
info("count: " + count + " ");
if (++count > 50) {
ok(false, "Timed out while polling for the element.");
window.clearInterval(intervalID);
return closeDebuggerAndFinish();
}
if (!!gVars._list.querySelector(selector) != exists) {
return;
}
// We got the element, it's safe to callback.
window.clearInterval(intervalID);
callback();
}, 100);
}
function write(text) {
if (!text) {
EventUtils.sendKey("BACK_SPACE", gDebugger);
return;
}
for (let i = 0; i < text.length; i++) {
EventUtils.sendChar(text[i], gDebugger);
}
}
registerCleanupFunction(function() {
removeTab(gTab);
gPane = null;
gTab = null;
gDebugger = null;
gVars = null;
gWatch = null;
});

View File

@ -0,0 +1,185 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Make sure that the property view is able to override getter properties
* to plain value properties.
*/
const TAB_URL = EXAMPLE_URL + "browser_dbg_frame-parameters.html";
var gPane = null;
var gTab = null;
var gDebugger = null;
var gVars = null;
var gWatch = null;
requestLongerTimeout(2);
function test()
{
debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
gTab = aTab;
gPane = aPane;
gDebugger = gPane.panelWin;
gVars = gDebugger.DebuggerView.Variables;
gWatch = gDebugger.DebuggerView.WatchExpressions;
gVars.switch = function() {};
gVars.delete = function() {};
prepareVariablesView();
});
}
function prepareVariablesView() {
gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
Services.tm.currentThread.dispatch({ run: function() {
testVariablesView();
}}, 0);
}, false);
EventUtils.sendMouseEvent({ type: "click" },
content.document.querySelector("button"),
content.window);
}
function testVariablesView()
{
executeSoon(function() {
addWatchExpressions(function() {
testEdit("\"xlerb\"", "xlerb", function() {
closeDebuggerAndFinish();
});
});
});
}
function addWatchExpressions(callback)
{
waitForWatchExpressions(function() {
let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
let scope = gVars._currHierarchy.get(label);
ok(scope, "There should be a wach expressions scope in the variables view");
is(scope._store.size, 1, "There should be 1 evaluation availalble");
let expr = scope.get("myVar.prop");
ok(expr, "The watch expression should be present in the scope");
is(expr.value, 42, "The value is correct.");
callback();
});
gWatch.addExpression("myVar.prop");
gDebugger.editor.focus();
}
function testEdit(string, expected, callback)
{
let localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".scope")[1],
localNodes = localScope.querySelector(".details").childNodes,
myVar = gVars.getItemForNode(localNodes[11]);
waitForProperties(function() {
let prop = myVar.get("prop");
is(prop.ownerView.name, "myVar",
"The right owner property name wasn't found.");
is(prop.name, "prop",
"The right property name wasn't found.");
is(prop.ownerView.value.type, "object",
"The right owner property value type wasn't found.");
is(prop.ownerView.value.class, "Object",
"The right owner property value class wasn't found.");
is(prop.name, "prop",
"The right property name wasn't found.");
is(prop.value, undefined,
"The right property value wasn't found.");
ok(prop.getter,
"The right property getter wasn't found.");
ok(prop.setter,
"The right property setter wasn't found.");
EventUtils.sendMouseEvent({ type: "mousedown" },
prop._target.querySelector(".dbg-variable-edit"),
gDebugger);
waitForElement(".element-value-input", true, function() {
waitForWatchExpressions(function() {
let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
let scope = gVars._currHierarchy.get(label);
let expr = scope.get("myVar.prop");
is(expr.value, expected, "The value is correct.");
callback();
});
write(string);
EventUtils.sendKey("RETURN", gDebugger);
});
});
myVar.expand();
gVars.clearHierarchy();
}
function waitForWatchExpressions(callback) {
gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function onFetch() {
gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", onFetch, false);
executeSoon(callback);
}, false);
}
function waitForProperties(callback) {
gDebugger.addEventListener("Debugger:FetchedProperties", function onFetch() {
gDebugger.removeEventListener("Debugger:FetchedProperties", onFetch, false);
executeSoon(callback);
}, false);
}
function waitForElement(selector, exists, callback)
{
// Poll every few milliseconds until the element are retrieved.
let count = 0;
let intervalID = window.setInterval(function() {
info("count: " + count + " ");
if (++count > 50) {
ok(false, "Timed out while polling for the element.");
window.clearInterval(intervalID);
return closeDebuggerAndFinish();
}
if (!!gVars._list.querySelector(selector) != exists) {
return;
}
// We got the element, it's safe to callback.
window.clearInterval(intervalID);
callback();
}, 100);
}
function write(text) {
if (!text) {
EventUtils.sendKey("BACK_SPACE", gDebugger);
return;
}
for (let i = 0; i < text.length; i++) {
EventUtils.sendChar(text[i], gDebugger);
}
}
registerCleanupFunction(function() {
removeTab(gTab);
gPane = null;
gTab = null;
gDebugger = null;
gVars = null;
gWatch = null;
});

View File

@ -224,6 +224,16 @@ VariablesView.prototype = {
*/
editableNameTooltip: STR.GetStringFromName("variablesEditableNameTooltip"),
/**
* The tooltip text shown on a variable or property's edit button if an
* |eval| function is provided and a getter/setter descriptor is present,
* in order to change the variable or property to a plain value.
*
* This flag is applied recursively onto each scope in this view and
* affects only the child nodes when they're created.
*/
editButtonTooltip: STR.GetStringFromName("variablesEditButtonTooltip"),
/**
* The tooltip text shown on a variable or property's delete button if a
* |delete| function is provided, in order to delete the variable or property.
@ -884,6 +894,133 @@ VariablesView.prototype = {
_emptyTextValue: ""
};
/**
* Generates the string evaluated when performing simple value changes.
*
* @param Variable | Property aItem
* The current variable or property.
* @param string aCurrentString
* The trimmed user inputted string.
* @return string
* The string to be evaluated.
*/
VariablesView.simpleValueEvalMacro = function(aItem, aCurrentString) {
return aItem._symbolicName + "=" + aCurrentString;
};
/**
* Generates the string evaluated when overriding getters and setters with
* plain values.
*
* @param Property aItem
* The current getter or setter property.
* @param string aCurrentString
* The trimmed user inputted string.
* @return string
* The string to be evaluated.
*/
VariablesView.overrideValueEvalMacro = function(aItem, aCurrentString) {
let property = "\"" + aItem._nameString + "\"";
let parent = aItem.ownerView._symbolicName || "this";
return "Object.defineProperty(" + parent + "," + property + "," +
"{ value: " + aCurrentString +
", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
", configurable: true" +
", writable: true" +
"})";
};
/**
* Generates the string evaluated when performing getters and setters changes.
*
* @param Property aItem
* The current getter or setter property.
* @param string aCurrentString
* The trimmed user inputted string.
* @return string
* The string to be evaluated.
*/
VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString) {
let type = aItem._nameString;
let propertyObject = aItem.ownerView;
let parentObject = propertyObject.ownerView;
let property = "\"" + propertyObject._nameString + "\"";
let parent = parentObject._symbolicName || "this";
switch (aCurrentString) {
case "":
case "null":
case "undefined":
let mirrorType = type == "get" ? "set" : "get";
let mirrorLookup = type == "get" ? "__lookupSetter__" : "__lookupGetter__";
// If the parent object will end up without any getter or setter,
// morph it into a plain value.
if ((type == "set" && propertyObject.getter.type == "undefined") ||
(type == "get" && propertyObject.setter.type == "undefined")) {
return VariablesView.overrideValueEvalMacro(propertyObject, "undefined");
}
// Construct and return the getter/setter removal evaluation string.
// e.g: Object.defineProperty(foo, "bar", {
// get: foo.__lookupGetter__("bar"),
// set: undefined,
// enumerable: true,
// configurable: true
// })
return "Object.defineProperty(" + parent + "," + property + "," +
"{" + mirrorType + ":" + parent + "." + mirrorLookup + "(" + property + ")" +
"," + type + ":" + undefined +
", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
", configurable: true" +
"})";
default:
// Wrap statements inside a function declaration if not already wrapped.
if (aCurrentString.indexOf("function") != 0) {
let header = "function(" + (type == "set" ? "value" : "") + ")";
let body = "";
// If there's a return statement explicitly written, always use the
// standard function definition syntax
if (aCurrentString.indexOf("return ") != -1) {
body = "{" + aCurrentString + "}";
}
// If block syntax is used, use the whole string as the function body.
else if (aCurrentString.indexOf("{") == 0) {
body = aCurrentString;
}
// Prefer an expression closure.
else {
body = "(" + aCurrentString + ")";
}
aCurrentString = header + body;
}
// Determine if a new getter or setter should be defined.
let defineType = type == "get" ? "__defineGetter__" : "__defineSetter__";
// Make sure all quotes are escaped in the expression's syntax,
let defineFunc = "eval(\"(" + aCurrentString.replace(/"/g, "\\$&") + ")\")";
// Construct and return the getter/setter evaluation string.
// e.g: foo.__defineGetter__("bar", eval("(function() { return 42; })"))
return parent + "." + defineType + "(" + property + "," + defineFunc + ")";
}
};
/**
* Function invoked when a getter or setter is deleted.
*
* @param Property aItem
* The current getter or setter property.
*/
VariablesView.getterOrSetterDeleteCallback = function(aItem) {
aItem._disable();
aItem.ownerView.eval(VariablesView.getterOrSetterEvalMacro(aItem, ""));
return true; // Don't hide the element.
};
/**
* A Scope is an object holding Variable instances.
* Iterable via "for (let [name, variable] in instance) { }".
@ -911,6 +1048,7 @@ function Scope(aView, aName, aFlags = {}) {
this.delete = aView.delete;
this.editableValueTooltip = aView.editableValueTooltip;
this.editableNameTooltip = aView.editableNameTooltip;
this.editButtonTooltip = aView.editButtonTooltip;
this.deleteButtonTooltip = aView.deleteButtonTooltip;
this.descriptorTooltip = aView.descriptorTooltip;
this.contextMenuId = aView.contextMenuId;
@ -1636,6 +1774,7 @@ Scope.prototype = {
delete: null,
editableValueTooltip: "",
editableNameTooltip: "",
editButtonTooltip: "",
deleteButtonTooltip: "",
descriptorTooltip: true,
contextMenuId: "",
@ -1681,10 +1820,6 @@ function Variable(aScope, aName, aDescriptor) {
this._displayTooltip = this._displayTooltip.bind(this);
this._activateNameInput = this._activateNameInput.bind(this);
this._activateValueInput = this._activateValueInput.bind(this);
this._deactivateNameInput = this._deactivateNameInput.bind(this);
this._deactivateValueInput = this._deactivateValueInput.bind(this);
this._onNameInputKeyPress = this._onNameInputKeyPress.bind(this);
this._onValueInputKeyPress = this._onValueInputKeyPress.bind(this);
Scope.call(this, aScope, aName, this._initialDescriptor = aDescriptor);
this.setGrip(aDescriptor.value);
@ -1886,6 +2021,11 @@ create({ constructor: Variable, proto: Scope.prototype }, {
if (!this._nameString) {
return;
}
// Getters and setters should display grip information in sub-properties.
if (!this._isUndefined && (this.getter || this.setter)) {
this._valueLabel.setAttribute("value", "");
return;
}
if (aGrip === undefined) {
aGrip = { type: "undefined" };
@ -1965,20 +2105,27 @@ create({ constructor: Variable, proto: Scope.prototype }, {
this._title.appendChild(separatorLabel);
this._title.appendChild(valueLabel);
let isPrimitive = VariablesView.isPrimitive(descriptor);
let isUndefined = VariablesView.isUndefined(descriptor);
let isPrimitive = this._isPrimitive = VariablesView.isPrimitive(descriptor);
let isUndefined = this._isUndefined = VariablesView.isUndefined(descriptor);
if (isPrimitive || isUndefined) {
this.hideArrow();
}
if (!isUndefined && (descriptor.get || descriptor.set)) {
// FIXME: editing getters and setters is not allowed yet. Bug 831794.
this.eval = null;
this.addProperty("get", { value: descriptor.get });
this.addProperty("set", { value: descriptor.set });
this.expand();
separatorLabel.hidden = true;
valueLabel.hidden = true;
this.delete = VariablesView.getterOrSetterDeleteCallback;
this.evaluationMacro = VariablesView.overrideValueEvalMacro;
let getter = this.addProperty("get", { value: descriptor.get });
let setter = this.addProperty("set", { value: descriptor.set });
getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
getter.hideArrow();
setter.hideArrow();
this.expand();
}
},
@ -1986,11 +2133,21 @@ create({ constructor: Variable, proto: Scope.prototype }, {
* Adds specific nodes for this variable based on custom flags.
*/
_customizeVariable: function V__customizeVariable() {
if (this.ownerView.eval) {
if (!this._isUndefined && (this.getter || this.setter)) {
let editNode = this._editNode = this.document.createElement("toolbarbutton");
editNode.className = "plain dbg-variable-edit";
editNode.addEventListener("mousedown", this._onEdit.bind(this), false);
this._title.appendChild(editNode);
}
}
if (this.ownerView.delete) {
let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
deleteNode.className = "plain dbg-variable-delete devtools-closebutton";
deleteNode.addEventListener("click", this._onDelete.bind(this), false);
this._title.appendChild(deleteNode);
if (!this._isUndefined || !(this.ownerView.getter && this.ownerView.setter)) {
let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
deleteNode.className = "plain dbg-variable-delete devtools-closebutton";
deleteNode.addEventListener("click", this._onDelete.bind(this), false);
this._title.appendChild(deleteNode);
}
}
if (this.ownerView.contextMenuId) {
this._title.setAttribute("context", this.ownerView.contextMenuId);
@ -2031,6 +2188,9 @@ create({ constructor: Variable, proto: Scope.prototype }, {
this._target.appendChild(tooltip);
this._target.setAttribute("tooltip", tooltip.id);
}
if (this.ownerView.eval && !this._isUndefined && (this.getter || this.setter)) {
this._editNode.setAttribute("tooltiptext", this.ownerView.editButtonTooltip);
}
if (this.ownerView.eval) {
this._valueLabel.setAttribute("tooltiptext", this.ownerView.editableValueTooltip);
}
@ -2056,7 +2216,7 @@ create({ constructor: Variable, proto: Scope.prototype }, {
if (!descriptor.enumerable) {
this._target.setAttribute("non-enumerable", "");
}
if (!descriptor.writable) {
if (!descriptor.writable && !this.ownerView.get && !this.ownerView.set) {
this._target.setAttribute("non-writable", "");
}
if (name == "this") {
@ -2158,6 +2318,9 @@ create({ constructor: Variable, proto: Scope.prototype }, {
e.stopPropagation();
}
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
@ -2194,6 +2357,9 @@ create({ constructor: Variable, proto: Scope.prototype }, {
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
@ -2214,11 +2380,18 @@ create({ constructor: Variable, proto: Scope.prototype }, {
* Disables this variable prior to a new name switch or value evaluation.
*/
_disable: function V__disable() {
this.twisty = false;
this.hideArrow();
this._separatorLabel.hidden = true;
this._valueLabel.hidden = true;
this._enum.hidden = true;
this._nonenum.hidden = true;
if (this._editNode) {
this._editNode.hidden = true;
}
if (this._deleteNode) {
this._deleteNode.hidden = true;
}
},
/**
@ -2248,10 +2421,16 @@ create({ constructor: Variable, proto: Scope.prototype }, {
if (initialString != currentString) {
this._disable();
this.ownerView.eval(this._symbolicName + "=" + currentString);
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.
*/
@ -2290,6 +2469,15 @@ create({ constructor: Variable, proto: Scope.prototype }, {
}
},
/**
* The click listener for the edit button.
*/
_onEdit: function V__onEdit(e) {
e.preventDefault();
e.stopPropagation();
this._activateValueInput();
},
/**
* The click listener for the delete button.
*/
@ -2298,16 +2486,20 @@ create({ constructor: Variable, proto: Scope.prototype }, {
e.stopPropagation();
if (this.ownerView.delete) {
this.ownerView.delete(this);
this.hide();
if (!this.ownerView.delete(this)) {
this.hide();
}
}
},
_symbolicName: "",
_absoluteName: "",
_initialDescriptor: null,
_isPrimitive: false,
_isUndefined: false,
_separatorLabel: null,
_valueLabel: null,
_editNode: null,
_deleteNode: null,
_tooltip: null,
_valueGrip: null,

View File

@ -196,13 +196,17 @@ ToolboxDebugger.tooltip=JavaScript Debugger
variablesEditableNameTooltip=Double click to edit
# LOCALIZATION NOTE (variablesEditableValueTooltip): The text that is displayed
# in the variables list on an item with an editable name.
# in the variables list on an item with an editable value.
variablesEditableValueTooltip=Click to change value
# LOCALIZATION NOTE (variablesCloseButtonTooltip): The text that is displayed
# in the variables list on an item with which can be removed.
# in the variables list on an item which can be removed.
variablesCloseButtonTooltip=Click to remove
# LOCALIZATION NOTE (variablesEditButtonTooltip): The text that is displayed
# in the variables list on a getter or setter which can be edited.
variablesEditButtonTooltip=Click to set value
# LOCALIZATION NOTE (variablesSeparatorLabel): The text that is displayed
# in the variables list as a separator between the name and value.
variablesSeparatorLabel=:

View File

@ -294,8 +294,15 @@
opacity: 0.5;
}
.dbg-variable-edit {
background: url("chrome://browser/skin/tabview/edit-light.png") center no-repeat;
width: 20px;
height: 16px;
cursor: pointer;
}
.dbg-variable-throbber {
background: url("chrome://global/skin/icons/loading_16.png");
background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
width: 16px;
height: 16px;
}
@ -316,7 +323,7 @@
.scope > .details {
-moz-margin-start: 2px;
-moz-margin-end: 2px;
-moz-margin-end: 1px;
}
.scope > .details.nonenum:not(:empty) {

View File

@ -301,8 +301,15 @@
opacity: 0.5;
}
.dbg-variable-edit {
background: url("chrome://browser/skin/tabview/edit-light.png") center no-repeat;
width: 20px;
height: 16px;
cursor: pointer;
}
.dbg-variable-throbber {
background: url("chrome://global/skin/icons/loading_16.png");
background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
width: 16px;
height: 16px;
}
@ -323,7 +330,7 @@
.scope > .details {
-moz-margin-start: 2px;
-moz-margin-end: 2px;
-moz-margin-end: 1px;
}
.scope > .details.nonenum:not(:empty) {

View File

@ -307,8 +307,15 @@
opacity: 0.5;
}
.dbg-variable-edit {
background: url("chrome://browser/skin/tabview/edit-light.png") center no-repeat;
width: 20px;
height: 16px;
cursor: pointer;
}
.dbg-variable-throbber {
background: url("chrome://global/skin/icons/loading_16.png");
background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
width: 16px;
height: 16px;
}
@ -329,7 +336,7 @@
.scope > .details {
-moz-margin-start: 2px;
-moz-margin-end: 2px;
-moz-margin-end: 1px;
}
.scope > .details.nonenum:not(:empty) {