Bug 830715 - Some Variables View flags should propagate across scopes, variables and properties, r=msucan

This commit is contained in:
Victor Porof 2013-01-24 20:59:44 +02:00
parent cad95bb97f
commit 74c6a0f4cc
3 changed files with 285 additions and 136 deletions

View File

@ -661,13 +661,12 @@ StackFrames.prototype = {
// to contain all the values.
if (this.syncedWatchExpressions && watchExpressionsEvaluation) {
let label = L10N.getStr("watchExpressionsScopeLabel");
let arrow = L10N.getStr("watchExpressionsSeparatorLabel");
let scope = DebuggerView.Variables.addScope(label);
scope.separator = arrow;
scope.showDescriptorTooltip = false;
scope.allowNameInput = true;
scope.allowDeletion = true;
scope.contextMenu = "debuggerWatchExpressionsContextMenu";
// Customize the scope for holding watch expressions evaluations.
scope.descriptorTooltip = false;
scope.contextMenuId = "debuggerWatchExpressionsContextMenu";
scope.separatorStr = L10N.getStr("watchExpressionsSeparatorLabel");
scope.switch = DebuggerView.WatchExpressions.switchExpression;
scope.delete = DebuggerView.WatchExpressions.deleteExpression;
@ -769,6 +768,12 @@ StackFrames.prototype = {
let expVal = ownProperties[i].value;
let expRef = aScope.addVar(name, ownProperties[i]);
this._addVarExpander(expRef, expVal);
// Revert some of the custom watch expressions scope presentation flags.
expRef.switch = null;
expRef.delete = null;
expRef.descriptorTooltip = true;
expRef.separatorStr = L10N.getStr("variablesSeparatorLabel");
}
// Signal that watch expressions have been fetched.

View File

@ -70,37 +70,20 @@ function testVariablesView()
testThirdLevelContents();
testIntegrity(arr, obj);
gVariablesView.eval = function() {};
gVariablesView.switch = function() {};
gVariablesView.delete = function() {};
let fooScope = gVariablesView.addScope("foo");
let anonymousVar = fooScope.addVar();
let anonymousScope = gVariablesView.addScope();
let barVar = anonymousScope.addVar("bar");
let bazProperty = barVar.addProperty("baz");
is(fooScope.header, true,
"A named scope should have a header visible.");
is(fooScope.target.hasAttribute("non-header"), false,
"The non-header attribute should not be applied to scopes with headers.");
is(anonymousScope.header, false,
"An anonymous scope should have a header visible.");
is(anonymousScope.target.hasAttribute("non-header"), true,
"The non-header attribute should not be applied to scopes without headers.");
is(barVar.header, true,
"A named variable should have a header visible.");
is(barVar.target.hasAttribute("non-header"), false,
"The non-header attribute should not be applied to variables with headers.");
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.");
gVariablesView.clearHierarchy();
is (gVariablesView._prevHierarchy.size, 0,
"The previous hierarchy should have been cleared.");
is (gVariablesView._currHierarchy.size, 0,
"The current hierarchy should have been cleared.");
testAnonymousHeaders(fooScope, anonymousVar, anonymousScope, barVar, bazProperty);
testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar, bazProperty);
testClearHierarchy();
closeDebuggerAndFinish();
}
@ -506,6 +489,107 @@ function testIntegrity(arr, obj) {
is(obj.p6.prop2, 6, "The seventh object property should not have changed");
}
function testAnonymousHeaders(fooScope, anonymousVar, anonymousScope, barVar, bazProperty) {
is(fooScope.header, true,
"A named scope should have a header visible.");
is(fooScope.target.hasAttribute("non-header"), false,
"The non-header attribute should not be applied to scopes with headers.");
is(anonymousScope.header, false,
"An anonymous scope should have a header visible.");
is(anonymousScope.target.hasAttribute("non-header"), true,
"The non-header attribute should not be applied to scopes without headers.");
is(barVar.header, true,
"A named variable should have a header visible.");
is(barVar.target.hasAttribute("non-header"), false,
"The non-header attribute should not be applied to variables with headers.");
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.editableValueTooltip, gVariablesView.editableValueTooltip,
"The editableValueTooltip 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.deleteButtonTooltip, gVariablesView.deleteButtonTooltip,
"The deleteButtonTooltip property should persist from the view to all scopes.");
is(fooScope.descriptorTooltip, gVariablesView.descriptorTooltip,
"The descriptorTooltip property should persist from the view to all scopes.");
is(fooScope.contextMenuId, gVariablesView.contextMenuId,
"The contextMenuId property should persist from the view to all scopes.");
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.");
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.editableValueTooltip, gVariablesView.editableValueTooltip,
"The editableValueTooltip 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.deleteButtonTooltip, gVariablesView.deleteButtonTooltip,
"The deleteButtonTooltip property should persist from the view to all variables.");
is(barVar.descriptorTooltip, gVariablesView.descriptorTooltip,
"The descriptorTooltip property should persist from the view to all variables.");
is(barVar.contextMenuId, gVariablesView.contextMenuId,
"The contextMenuId property should persist from the view to all variables.");
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.");
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.editableValueTooltip, gVariablesView.editableValueTooltip,
"The editableValueTooltip 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.deleteButtonTooltip, gVariablesView.deleteButtonTooltip,
"The deleteButtonTooltip property should persist from the view to all properties.");
is(bazProperty.descriptorTooltip, gVariablesView.descriptorTooltip,
"The descriptorTooltip property should persist from the view to all properties.");
is(bazProperty.contextMenuId, gVariablesView.contextMenuId,
"The contextMenuId property should persist from the view to all properties.");
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.");
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();
is (gVariablesView._prevHierarchy.size, 0,
"The previous hierarchy should have been cleared.");
is (gVariablesView._currHierarchy.size, 0,
"The current hierarchy should have been cleared.");
}
registerCleanupFunction(function() {
removeTab(gTab);
gPane = null;

View File

@ -20,6 +20,11 @@ XPCOMUtils.defineLazyModuleGetter(this,
this.EXPORTED_SYMBOLS = ["VariablesView", "create"];
/**
* Debugger localization strings.
*/
const STR = Services.strings.createBundle(DBG_STRINGS_URI);
/**
* A tree view for inspecting scopes, objects and properties.
* Iterable via "for (let [id, scope] in instance) { }".
@ -144,6 +149,97 @@ VariablesView.prototype = {
}.bind(this), aTimeout);
},
/**
* Specifies if this view may be emptied lazily.
* @see VariablesView.prototype.empty
*/
lazyEmpty: false,
/**
* Specifies if nodes in this view may be added lazily.
* @see Scope.prototype._lazyAppend
*/
lazyAppend: true,
/**
* Function called each time a variable or property's value is changed via
* user interaction. If null, then value changes are disabled.
*
* This property is applied recursively onto each scope in this view and
* affects only the child nodes when they're created.
*/
eval: null,
/**
* Function called each time a variable or property's name is changed via
* user interaction. If null, then name changes are disabled.
*
* This property is applied recursively onto each scope in this view and
* affects only the child nodes when they're created.
*/
switch: null,
/**
* Function called each time a variable or property is deleted via
* 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,
/**
* The tooltip text shown on a variable or property's value if an |eval|
* function is provided, in order to change the variable or property's value.
*
* This flag is applied recursively onto each scope in this view and
* affects only the child nodes when they're created.
*/
editableValueTooltip: STR.GetStringFromName("variablesEditableValueTooltip"),
/**
* The tooltip text shown on a variable or property's name if a |switch|
* function is provided, in order to change the variable or property's name.
*
* This flag is applied recursively onto each scope in this view and
* affects only the child nodes when they're created.
*/
editableNameTooltip: STR.GetStringFromName("variablesEditableNameTooltip"),
/**
* 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.
*
* This flag is applied recursively onto each scope in this view and
* affects only the child nodes when they're created.
*/
deleteButtonTooltip: STR.GetStringFromName("variablesCloseButtonTooltip"),
/**
* Specifies if the configurable, enumerable or writable tooltip should be
* shown whenever a variable or property descriptor is available.
*
* This flag is applied recursively onto each scope in this view and
* affects only the child nodes when they're created.
*/
descriptorTooltip: true,
/**
* Specifies the context menu attribute set on variables and properties.
*
* This flag is applied recursively onto each scope in this view and
* affects only the child nodes when they're created.
*/
contextMenuId: "",
/**
* The separator label between the variables or properties name and value.
*
* This flag is applied recursively onto each scope in this view and
* affects only the child nodes when they're created.
*/
separatorStr: STR.GetStringFromName("variablesSeparatorLabel"),
/**
* Specifies if enumerable properties and variables should be displayed.
* These variables and properties are visible by default.
@ -477,19 +573,14 @@ VariablesView.prototype = {
_document: null,
_window: null,
eval: null,
switch: null,
delete: null,
lazyEmpty: false,
lazyAppend: true,
_store: null,
_prevHierarchy: null,
_currHierarchy: null,
_enumVisible: true,
_nonEnumVisible: true,
_emptyTimeout: null,
_searchTimeout: null,
_searchFunction: null,
_enumVisible: true,
_nonEnumVisible: true,
_parent: null,
_list: null,
_searchboxNode: null,
@ -511,6 +602,8 @@ VariablesView.prototype = {
* Additional options or flags for this scope.
*/
function Scope(aView, aName, aFlags = {}) {
this.ownerView = aView;
this.expand = this.expand.bind(this);
this.toggle = this.toggle.bind(this);
this._openEnum = this._openEnum.bind(this);
@ -518,10 +611,17 @@ function Scope(aView, aName, aFlags = {}) {
this._batchAppend = this._batchAppend.bind(this);
this._batchItems = [];
this.ownerView = aView;
// 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.editableValueTooltip = aView.editableValueTooltip;
this.editableNameTooltip = aView.editableNameTooltip;
this.deleteButtonTooltip = aView.deleteButtonTooltip;
this.descriptorTooltip = aView.descriptorTooltip;
this.contextMenuId = aView.contextMenuId;
this.separatorStr = aView.separatorStr;
this._store = new Map();
this._init(aName.trim(), aFlags);
@ -632,12 +732,12 @@ Scope.prototype = {
// even if they were already displayed before. In this case, show a throbber
// to suggest that this scope is expanding.
if (!this._isExpanding &&
this._store.size > LAZY_APPEND_BATCH && this._variablesView.lazyAppend) {
this._variablesView.lazyAppend && this._store.size > LAZY_APPEND_BATCH) {
this._isExpanding = true;
// Start spinning a throbber in this scope's title.
// Start spinning a throbber in this scope's title and allow a few
// milliseconds for it to be painted.
this._startThrobber();
// Allow the trobber to be painted.
this.window.setTimeout(this.expand, LAZY_EXPAND_DELAY);
return;
}
@ -820,36 +920,6 @@ Scope.prototype = {
this._title.removeEventListener(aName, aCallback, aCapture);
},
/**
* Specifies if the configurable/enumerable/writable tooltip should be shown
* whenever a variable or property descriptor is available.
* This flag applies non-recursively to the current scope.
*/
showDescriptorTooltip: true,
/**
* Specifies if editing variable or property names is allowed.
* This flag applies non-recursively to the current scope.
*/
allowNameInput: false,
/**
* Specifies if editing variable or property values is allowed.
* This flag applies non-recursively to the current scope.
*/
allowValueInput: true,
/**
* Specifies if removing variables or properties values is allowed.
* This flag applies non-recursively to the current scope.
*/
allowDeletion: false,
/**
* Specifies the context menu attribute set on variables and properties.
*/
contextMenu: "",
/**
* Gets the id associated with this item.
* @return string
@ -1041,7 +1111,7 @@ Scope.prototype = {
for (let [, variable] of this._store) {
variable._enumVisible = aFlag;
if (!this.expanded) {
if (!this._isExpanded) {
continue;
}
if (aFlag) {
@ -1060,7 +1130,7 @@ Scope.prototype = {
for (let [, variable] of this._store) {
variable._nonEnumVisible = aFlag;
if (!this.expanded) {
if (!this._isExpanded) {
continue;
}
if (aFlag) {
@ -1185,6 +1255,13 @@ Scope.prototype = {
eval: null,
switch: null,
delete: null,
editableValueTooltip: "",
editableNameTooltip: "",
deleteButtonTooltip: "",
descriptorTooltip: true,
contextMenuId: "",
separatorStr: "",
_store: null,
_fetched: false,
_retrieved: false,
@ -1382,18 +1459,28 @@ create({ constructor: Variable, proto: Scope.prototype }, {
return propertyItem;
},
/**
* Gets this variable's path to the topmost scope.
* For example, a symbolic name may look like "arguments['0']['foo']['bar']".
* @return string
*/
get symbolicName() this._symbolicName,
/**
* Returns this variable's value from the descriptor if available.
* @return any
*/
get value() this._initialDescriptor.value,
/**
* Returns this variable's getter from the descriptor if available.
* @return object
*/
get getter() this._initialDescriptor.get,
/**
* Returns this variable's getter from the descriptor if available.
* @return object
*/
get setter() this._initialDescriptor.set,
@ -1489,7 +1576,7 @@ create({ constructor: Variable, proto: Scope.prototype }, {
let separatorLabel = this._separatorLabel = document.createElement("label");
separatorLabel.className = "plain";
separatorLabel.setAttribute("value", this.ownerView.separator);
separatorLabel.setAttribute("value", this.ownerView.separatorStr);
let valueLabel = this._valueLabel = document.createElement("label");
valueLabel.className = "plain value";
@ -1517,14 +1604,14 @@ create({ constructor: Variable, proto: Scope.prototype }, {
* Adds specific nodes for this variable based on custom flags.
*/
_customizeVariable: function V__customizeVariable() {
if (this.ownerView.allowDeletion) {
let closeNode = this._closeNode = this.document.createElement("toolbarbutton");
closeNode.className = "plain dbg-variable-delete devtools-closebutton";
closeNode.addEventListener("click", this._onClose.bind(this), false);
this._title.appendChild(closeNode);
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.ownerView.contextMenu) {
this._title.setAttribute("context", this.ownerView.contextMenu);
if (this.ownerView.contextMenuId) {
this._title.setAttribute("context", this.ownerView.contextMenuId);
}
},
@ -1541,7 +1628,7 @@ create({ constructor: Variable, proto: Scope.prototype }, {
_displayTooltip: function V__displayTooltip() {
this._target.removeEventListener("mouseover", this._displayTooltip, false);
if (this.ownerView.showDescriptorTooltip) {
if (this.ownerView.descriptorTooltip) {
let document = this.document;
let tooltip = document.createElement("tooltip");
@ -1562,14 +1649,14 @@ create({ constructor: Variable, proto: Scope.prototype }, {
this._target.appendChild(tooltip);
this._target.setAttribute("tooltip", tooltip.id);
}
if (this.ownerView.allowNameInput) {
this._name.setAttribute("tooltiptext", L10N.getStr("variablesEditableNameTooltip"));
if (this.ownerView.eval) {
this._valueLabel.setAttribute("tooltiptext", this.ownerView.editableValueTooltip);
}
if (this.ownerView.allowValueInput) {
this._valueLabel.setAttribute("tooltiptext", L10N.getStr("variablesEditableValueTooltip"));
if (this.ownerView.switch) {
this._name.setAttribute("tooltiptext", this.ownerView.editableNameTooltip);
}
if (this.ownerView.allowDeletion) {
this._closeNode.setAttribute("tooltiptext", L10N.getStr("variablesCloseButtonTooltip"));
if (this.ownerView.delete) {
this._deleteNode.setAttribute("tooltiptext", this.ownerView.deleteButtonTooltip);
}
},
@ -1578,27 +1665,25 @@ create({ constructor: Variable, proto: Scope.prototype }, {
* and specifies if it's a 'this', '<exception>' or '__proto__' reference.
*/
_setAttributes: function V__setAttributes() {
let name = this._nameString;
let descriptor = this._initialDescriptor;
let name = this._nameString;
if (descriptor) {
if (!descriptor.configurable) {
this._target.setAttribute("non-configurable", "");
}
if (!descriptor.enumerable) {
this._target.setAttribute("non-enumerable", "");
}
if (!descriptor.writable) {
this._target.setAttribute("non-writable", "");
}
if (!descriptor.configurable) {
this._target.setAttribute("non-configurable", "");
}
if (!descriptor.enumerable) {
this._target.setAttribute("non-enumerable", "");
}
if (!descriptor.writable) {
this._target.setAttribute("non-writable", "");
}
if (name == "this") {
this._target.setAttribute("self", "");
}
if (name == "<exception>") {
else if (name == "<exception>") {
this._target.setAttribute("exception", "");
}
if (name == "__proto__") {
else if (name == "__proto__") {
this._target.setAttribute("proto", "");
}
},
@ -1684,7 +1769,7 @@ create({ constructor: Variable, proto: Scope.prototype }, {
// Only allow left-click to trigger this event.
return;
}
if (!this.ownerView.allowNameInput || !this.switch) {
if (!this.ownerView.switch) {
return;
}
this._activateInput(this._name, "element-name-input", {
@ -1715,7 +1800,7 @@ create({ constructor: Variable, proto: Scope.prototype }, {
// Only allow left-click to trigger this event.
return;
}
if (!this.ownerView.allowValueInput || !this.eval) {
if (!this.ownerView.eval) {
return;
}
this._activateInput(this._valueLabel, "element-value-input", {
@ -1757,7 +1842,7 @@ create({ constructor: Variable, proto: Scope.prototype }, {
if (initialString != currentString) {
this._disable();
this._name.value = currentString;
this.switch(this, currentString);
this.ownerView.switch(this, currentString);
}
},
@ -1772,7 +1857,7 @@ create({ constructor: Variable, proto: Scope.prototype }, {
if (initialString != currentString) {
this._disable();
this.eval(this._symbolicName + "=" + currentString);
this.ownerView.eval(this._symbolicName + "=" + currentString);
}
},
@ -1807,13 +1892,13 @@ create({ constructor: Variable, proto: Scope.prototype }, {
},
/**
* The click listener for the close button.
* The click listener for the delete button.
*/
_onClose: function V__onClose() {
_onDelete: function V__onDelete() {
this.hide();
if (this.delete) {
this.delete(this);
if (this.ownerView.delete) {
this.ownerView.delete(this);
}
},
@ -1822,7 +1907,7 @@ create({ constructor: Variable, proto: Scope.prototype }, {
_initialDescriptor: null,
_separatorLabel: null,
_valueLabel: null,
_closeNode: null,
_deleteNode: null,
_tooltip: null,
_valueGrip: null,
_valueString: "",
@ -2156,31 +2241,6 @@ VariablesView.getClass = function VV_getClass(aGrip) {
return "token-other";
};
/**
* Localization convenience methods.
*/
let L10N = {
/**
* L10N shortcut function.
*
* @param string aName
* @return string
*/
getStr: function L10N_getStr(aName) {
return this.stringBundle.GetStringFromName(aName);
}
};
XPCOMUtils.defineLazyGetter(L10N, "stringBundle", function() {
return Services.strings.createBundle(DBG_STRINGS_URI);
});
/**
* The separator label between the variables or properties name and value.
* This property applies non-recursively to the current scope.
*/
Scope.prototype.separator = L10N.getStr("variablesSeparatorLabel");
/**
* A monotonically-increasing counter, that guarantees the uniqueness of scope,
* variables and properties ids.