Bug 828987 - The Variables View should be keyboard accessible, r=past

This commit is contained in:
Victor Porof 2013-01-31 18:07:24 +02:00
parent a6f86fbbdc
commit 81f35d94fb
13 changed files with 864 additions and 120 deletions

View File

@ -1018,6 +1018,14 @@ FilterView.prototype = {
this._searchboxPanel.hidePopup();
},
/**
* Called when the variables focus key sequence was pressed.
*/
_doVariablesFocus: function DVG__doVariablesFocus() {
DebuggerView.showPanesSoon();
DebuggerView.Variables.focusFirstVisibleNode();
},
_searchbox: null,
_searchboxPanel: null,
_globalOperatorButton: null,

View File

@ -61,21 +61,20 @@
* Scope, variable and property elements
*/
#variables .details:not([open]) {
.details:not([open]) {
display: none;
}
.scope,
.variable,
.property {
-moz-user-focus: normal;
}
.scope[non-header] > .title,
.variable[non-header] > .title,
.property[non-header] > .title {
display: none;
}
/**
* Variables and properties searching
*/
.variable[non-match] > .title,
.property[non-header] > .title,
.property[non-match] > .title {
display: none;
}

View File

@ -43,6 +43,8 @@
oncommand="DebuggerView.Filtering._doLineSearch()"/>
<command id="variableSearchCommand"
oncommand="DebuggerView.Filtering._doVariableSearch()"/>
<command id="variablesFocusCommand"
oncommand="DebuggerView.Filtering._doVariablesFocus()"/>
<command id="addBreakpointCommand"
oncommand="DebuggerView.Breakpoints._onCmdAddBreakpoint()"/>
<command id="addConditionalBreakpointCommand"
@ -108,6 +110,11 @@
accesskey="&debuggerUI.searchVariable.key;"
key="variableSearchKey"
command="variableSearchCommand"/>
<menuitem id="se-dbg-cMenu-focusVariables"
label="&debuggerUI.focusVariables;"
accesskey="&debuggerUI.focusVariables.key;"
key="variablesFocusKey"
command="variablesFocusCommand"/>
</menupopup>
<menupopup id="debuggerWatchExpressionsContextMenu">
@ -184,6 +191,10 @@
key="&debuggerUI.searchVariable.key;"
modifiers="accel alt"
command="variableSearchCommand"/>
<key id="variablesFocusKey"
key="&debuggerUI.focusVariables.key;"
modifiers="accel shift"
command="variablesFocusCommand"/>
<key id="addBreakpointKey"
key="&debuggerUI.seMenuBreak.key;"
modifiers="accel"

View File

@ -139,8 +139,8 @@ function testSimpleCall() {
testVar.target.querySelector(".title"),
gDebugger);
ok(!testVar.expanded,
"Clicking the testVar title div shouldn't expand it.");
ok(testVar.expanded,
"Clicking the testVar title div should expand it again.");
testScope.show();
@ -185,8 +185,8 @@ function testSimpleCall() {
testVar.get("child").target.querySelector(".title"),
gDebugger);
ok(!testVar.get("child").expanded,
"Clicking the testVar child property title div shouldn't expand it.");
ok(testVar.get("child").expanded,
"Clicking the testVar child property title div should expand it again.");
gDebugger.DebuggerView.Variables.empty();

View File

@ -74,9 +74,9 @@ function testFrameParameters()
"The global scope should be collapsed by default.");
let thisNode = gVars.getVariableOrPropertyForNode(localNodes[0]);
let argumentsNode = gVars.getVariableOrPropertyForNode(localNodes[8]);
let cNode = gVars.getVariableOrPropertyForNode(localNodes[10]);
let thisNode = gVars.getItemForNode(localNodes[0]);
let argumentsNode = gVars.getItemForNode(localNodes[8]);
let cNode = gVars.getItemForNode(localNodes[10]);
is(thisNode.expanded, false,
"The thisNode should not be expanded at this point.");
@ -180,20 +180,20 @@ function testFrameParameters()
"Should have the right value for 'c.c'.");
is(gVars.getVariableOrPropertyForNode(
is(gVars.getItemForNode(
cNode.target.querySelectorAll(".property")[0]).target,
cNode.target.querySelectorAll(".property")[0],
"getVariableOrPropertyForNode([0]) didn't return the expected property.");
"getItemForNode([0]) didn't return the expected property.");
is(gVars.getVariableOrPropertyForNode(
is(gVars.getItemForNode(
cNode.target.querySelectorAll(".property")[1]).target,
cNode.target.querySelectorAll(".property")[1],
"getVariableOrPropertyForNode([1]) didn't return the expected property.");
"getItemForNode([1]) didn't return the expected property.");
is(gVars.getVariableOrPropertyForNode(
is(gVars.getItemForNode(
cNode.target.querySelectorAll(".property")[2]).target,
cNode.target.querySelectorAll(".property")[2],
"getVariableOrPropertyForNode([2]) didn't return the expected property.");
"getItemForNode([2]) didn't return the expected property.");
is(cNode.find(

View File

@ -21,6 +21,7 @@ function test()
gDebugger = gPane.panelWin;
gVariablesView = gDebugger.DebuggerView.Variables;
gDebugger.DebuggerView.togglePanes({ visible: true, animated: false });
testVariablesView();
});
}
@ -61,6 +62,9 @@ function testVariablesView()
set someProp7(value) { arr[0] = value }
};
gVariablesView.eval = function() {};
gVariablesView.switch = function() {};
gVariablesView.delete = function() {};
gVariablesView.rawObject = test;
testHierarchy();
@ -70,10 +74,6 @@ function testVariablesView()
testThirdLevelContents();
testIntegrity(arr, obj);
gVariablesView.eval = function() {};
gVariablesView.switch = function() {};
gVariablesView.delete = function() {};
let fooScope = gVariablesView.addScope("foo");
let anonymousVar = fooScope.addVar();
@ -83,9 +83,14 @@ function testVariablesView()
testAnonymousHeaders(fooScope, anonymousVar, anonymousScope, barVar, bazProperty);
testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar, bazProperty);
testClearHierarchy();
closeDebuggerAndFinish();
executeSoon(function() {
testKeyboardAccessibility(function() {
testClearHierarchy();
closeDebuggerAndFinish();
});
});
}
function testHierarchy() {
@ -582,11 +587,268 @@ function testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar,
"The eval and switch functions got mixed up in the property.");
}
function testKeyboardAccessibility(callback) {
gDebugger.DebuggerView.Filtering._doVariablesFocus();
gDebugger.DebuggerView.Variables.pageSize = 5;
is(gVariablesView.getFocusedItem().name, "someProp0",
"The someProp0 item should be focused.");
gVariablesView.focusNextItem();
is(gVariablesView.getFocusedItem().name, "someProp1",
"The someProp1 item should be focused.");
gVariablesView.focusPrevItem();
is(gVariablesView.getFocusedItem().name, "someProp0",
"The someProp0 item should be focused again.");
ok(!gVariablesView._list.querySelector(".element-value-input"),
"There shouldn't be a value input element created.");
EventUtils.synthesizeKey("VK_ENTER", {}, gDebugger);
waitForElement(".element-value-input", true, function() {
ok(gVariablesView._list.querySelector(".element-value-input"),
"There should be a value input element created.");
EventUtils.sendKey("ESCAPE", gDebugger);
waitForElement(".element-value-input", false, function() {
ok(!gVariablesView._list.querySelector(".element-value-input"),
"There shouldn't be a value input element anymore.");
ok(!gVariablesView._list.querySelector(".element-name-input"),
"There shouldn't be a name input element created.");
EventUtils.synthesizeKey("VK_ENTER", { shiftKey: true }, gDebugger);
waitForElement(".element-name-input", true, function() {
ok(gVariablesView._list.querySelector(".element-name-input"),
"There should be a name input element created.");
EventUtils.sendKey("ESCAPE", gDebugger);
waitForElement(".element-name-input", false, function() {
ok(!gVariablesView._list.querySelector(".element-name-input"),
"There shouldn't be a name input element anymore.");
EventUtils.sendKey("DOWN", gDebugger);
executeSoon(function() {
is(gVariablesView._parent.scrollTop, 0,
"The variables view shouldn't scroll when pressing the DOWN key.");
EventUtils.sendKey("UP", gDebugger);
executeSoon(function() {
is(gVariablesView._parent.scrollTop, 0,
"The variables view shouldn't scroll when pressing the UP key.");
EventUtils.sendKey("PAGE_DOWN", gDebugger);
is(gVariablesView.getFocusedItem().name, "someProp5",
"The someProp5 item should be focused now.");
EventUtils.sendKey("DOWN", gDebugger);
is(gVariablesView.getFocusedItem().name, "0",
"The 0 item should be focused now.");
EventUtils.sendKey("END", gDebugger);
is(gVariablesView.getFocusedItem().name, "foo",
"The foo item should be focused now.");
EventUtils.sendKey("DOWN", gDebugger);
is(gVariablesView.getFocusedItem().name, "foo",
"The foo item should still be focused now.");
EventUtils.sendKey("RIGHT", gDebugger);
is(gVariablesView.getFocusedItem().name, "bar",
"The bar item should still be focused now.");
EventUtils.sendKey("PAGE_DOWN", gDebugger);
is(gVariablesView.getFocusedItem().name, "foo",
"The foo item should still be focused now.");
EventUtils.sendKey("PAGE_UP", gDebugger);
is(gVariablesView.getFocusedItem().name, "__proto__",
"The __proto__ item should be focused now.");
EventUtils.sendKey("UP", gDebugger);
is(gVariablesView.getFocusedItem().name, "set",
"The set item should be focused now.");
EventUtils.sendKey("UP", gDebugger);
is(gVariablesView.getFocusedItem().name, "get",
"The get item should be focused now.");
EventUtils.sendKey("UP", gDebugger);
is(gVariablesView.getFocusedItem().name, "p8",
"The p8 item should be focused now.");
EventUtils.sendKey("HOME", gDebugger);
is(gVariablesView.getFocusedItem().name, "someProp0",
"The someProp0 item should be focused now.");
EventUtils.sendKey("UP", gDebugger);
is(gVariablesView.getFocusedItem().name, "someProp0",
"The someProp0 item should still be focused now.");
EventUtils.sendKey("LEFT", gDebugger);
is(gVariablesView.getFocusedItem().name, "someProp0",
"The someProp0 item should still be focused now.");
EventUtils.sendKey("PAGE_UP", gDebugger);
is(gVariablesView.getFocusedItem().name, "someProp0",
"The someProp0 item should still be focused now.");
for (let i = 0; i < 16; i++) {
// Advance to the first collapsed __proto__ property.
EventUtils.sendKey("RIGHT", gDebugger);
}
is(gVariablesView.getFocusedItem().name, "__proto__",
"The __proto__ item should be focused now.");
is(gVariablesView.getFocusedItem().expanded, false,
"The __proto__ item shouldn't be expanded yet.");
EventUtils.sendKey("RIGHT", gDebugger);
is(gVariablesView.getFocusedItem().name, "__proto__",
"The __proto__ item should still be focused.");
is(gVariablesView.getFocusedItem().expanded, true,
"The __proto__ item should be expanded now.");
for (let i = 0; i < 2; i++) {
// Advance to the fifth top-level someProp5 property.
EventUtils.sendKey("LEFT", gDebugger);
}
is(gVariablesView.getFocusedItem().name, "5",
"The fifth array item should be focused.");
is(gVariablesView.getFocusedItem().expanded, false,
"The fifth array item should not be expanded now.");
for (let i = 0; i < 6; i++) {
// Advance to the fifth top-level someProp5 property.
EventUtils.sendKey("UP", gDebugger);
}
is(gVariablesView.getFocusedItem().name, "someProp5",
"The someProp5 item should be focused now.");
is(gVariablesView.getFocusedItem().expanded, true,
"The someProp5 item should already be expanded.");
EventUtils.sendKey("LEFT", gDebugger);
is(gVariablesView.getFocusedItem().name, "someProp5",
"The someProp5 item should still be focused.");
is(gVariablesView.getFocusedItem().expanded, false,
"The someProp5 item should not be expanded now.");
EventUtils.sendKey("LEFT", gDebugger);
is(gVariablesView.getFocusedItem().name, "someProp4",
"The someProp4 item should be focused.");
EventUtils.sendKey("LEFT", gDebugger);
is(gVariablesView.getFocusedItem().name, "someProp3",
"The someProp3 item should be focused.");
EventUtils.sendKey("LEFT", gDebugger);
is(gVariablesView.getFocusedItem().name, "someProp2",
"The someProp2 item should be focused.");
EventUtils.sendKey("LEFT", gDebugger);
is(gVariablesView.getFocusedItem().name, "someProp1",
"The someProp1 item should be focused.");
EventUtils.sendKey("LEFT", gDebugger);
is(gVariablesView.getFocusedItem().name, "someProp0",
"The someProp0 item should be focused.");
EventUtils.sendKey("LEFT", gDebugger);
is(gVariablesView.getFocusedItem().name, "someProp0",
"The someProp0 item should still be focused.");
for (let i = 0; i < 32; i++) {
// Advance to the last property in this scope.
EventUtils.sendKey("DOWN", gDebugger);
}
is(gVariablesView.getFocusedItem().name, "__proto__",
"The top-level __proto__ item should be focused.");
EventUtils.sendKey("DOWN", gDebugger);
is(gVariablesView.getFocusedItem().name, "foo",
"The foo scope should be focused now.");
is(gVariablesView.getFocusedItem().expanded, true,
"The foo scope should already be expanded yet.");
EventUtils.sendKey("LEFT", gDebugger);
is(gVariablesView.getFocusedItem().name, "foo",
"The foo scope should be focused now.");
is(gVariablesView.getFocusedItem().expanded, false,
"The foo scope shouldn't be expanded now.");
EventUtils.sendKey("DOWN", gDebugger);
is(gVariablesView.getFocusedItem().name, "foo",
"The foo scope should still be focused.");
is(gVariablesView.getFocusedItem().expanded, true,
"The foo scope should be expanded now.");
EventUtils.sendKey("DOWN", gDebugger);
is(gVariablesView.getFocusedItem().name, "bar",
"The bar variable should still be focused.");
is(gVariablesView.getFocusedItem().expanded, false,
"The bar variable shouldn't be expanded.");
is(gVariablesView.getFocusedItem().visible, true,
"The bar variable shouldn't be hidden.");
EventUtils.sendKey("BACK_SPACE", gDebugger);
is(gVariablesView.getFocusedItem().name, "bar",
"The bar variable should still be focused.");
is(gVariablesView.getFocusedItem().expanded, false,
"The bar variable should still not be expanded.");
is(gVariablesView.getFocusedItem().visible, false,
"The bar variable should be hidden.");
EventUtils.sendKey("UP", gDebugger);
is(gVariablesView.getFocusedItem().name, "foo",
"The foo scope should be focused.");
EventUtils.sendKey("UP", gDebugger);
is(gVariablesView.getFocusedItem().name, "__proto__",
"The top-level __proto__ item should be focused.");
executeSoon(callback);
});
});
});
});
});
});
}
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 (!!gVariablesView._list.querySelector(selector) != exists) {
return;
}
// We got the element, it's safe to callback.
window.clearInterval(intervalID);
callback();
}, 100);
}
function testClearHierarchy() {
gVariablesView.clearHierarchy();
is (gVariablesView._prevHierarchy.size, 0,
ok(!gVariablesView._prevHierarchy.size,
"The previous hierarchy should have been cleared.");
is (gVariablesView._currHierarchy.size, 0,
ok(!gVariablesView._currHierarchy.size,
"The current hierarchy should have been cleared.");
}

View File

@ -68,7 +68,7 @@ function testFrameEval() {
ok(scope, "There should be a wach expressions scope in the variables view");
is(scope._store.size, 5, "There should be 5 evaluations availalble");
is(scope.get("this")._isShown, true,
is(scope.get("this")._isContentVisible, true,
"Should have the right visibility state for 'this'.");
is(scope.get("this").target.querySelectorAll(".dbg-variable-delete").length, 1,
"Should have the one close button visible for 'this'.");
@ -79,7 +79,7 @@ function testFrameEval() {
is(scope.get("this").value.class, "Proxy",
"Should have the right value type for 'this'.");
is(scope.get("ermahgerd")._isShown, true,
is(scope.get("ermahgerd")._isContentVisible, true,
"Should have the right visibility state for 'ermahgerd'.");
is(scope.get("ermahgerd").target.querySelectorAll(".dbg-variable-delete").length, 1,
"Should have the one close button visible for 'ermahgerd'.");
@ -90,7 +90,7 @@ function testFrameEval() {
is(scope.get("ermahgerd").value.class, "Function",
"Should have the right value type for 'ermahgerd'.");
is(scope.get("aArg")._isShown, true,
is(scope.get("aArg")._isContentVisible, true,
"Should have the right visibility state for 'aArg'.");
is(scope.get("aArg").target.querySelectorAll(".dbg-variable-delete").length, 1,
"Should have the one close button visible for 'aArg'.");
@ -99,7 +99,7 @@ function testFrameEval() {
is(scope.get("aArg").value, undefined,
"Should have the right value for 'aArg'.");
is(scope.get("document.title")._isShown, true,
is(scope.get("document.title")._isContentVisible, true,
"Should have the right visibility state for 'document.title'.");
is(scope.get("document.title").target.querySelectorAll(".dbg-variable-delete").length, 1,
"Should have the one close button visible for 'document.title'.");
@ -110,7 +110,7 @@ function testFrameEval() {
is(typeof scope.get("document.title").value, "string",
"Should have the right value type for 'document.title'.");
is(scope.get("document.title = 42")._isShown, true,
is(scope.get("document.title = 42")._isContentVisible, true,
"Should have the right visibility state for 'document.title = 42'.");
is(scope.get("document.title = 42").target.querySelectorAll(".dbg-variable-delete").length, 1,
"Should have the one close button visible for 'document.title = 42'.");

View File

@ -69,7 +69,7 @@ function testModification(aVar, aCallback, aNewValue, aNewResult) {
EventUtils.sendKey("RETURN", gDebugger);
}
EventUtils.sendMouseEvent({ type: "click" },
EventUtils.sendMouseEvent({ type: "mousedown" },
aVar.querySelector(".value"),
gDebugger);

View File

@ -5,15 +5,20 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Ci = Components.interfaces;
const Cu = Components.utils;
const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
const LAZY_EMPTY_DELAY = 150; // ms
const LAZY_EXPAND_DELAY = 50; // ms
const LAZY_APPEND_DELAY = 100; // ms
const LAZY_APPEND_BATCH = 100; // nodes
const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
const PAGE_SIZE_MAX_JUMPS = 30;
const SEARCH_ACTION_MAX_DELAY = 1000; // ms
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this,
"WebConsoleUtils", "resource://gre/modules/devtools/WebConsoleUtils.jsm");
@ -40,6 +45,7 @@ const STR = Services.strings.createBundle(DBG_STRINGS_URI);
*/
this.VariablesView = function VariablesView(aParentNode) {
this._store = new Map();
this._itemsByElement = new WeakMap();
this._prevHierarchy = new Map();
this._currHierarchy = new Map();
@ -48,9 +54,12 @@ this.VariablesView = function VariablesView(aParentNode) {
this._onSearchboxInput = this._onSearchboxInput.bind(this);
this._onSearchboxKeyPress = this._onSearchboxKeyPress.bind(this);
this._onViewKeyPress = this._onViewKeyPress.bind(this);
// Create an internal list container.
this._list = this.document.createElement("vbox");
this._list = this.document.createElement("scrollbox");
this._list.setAttribute("orient", "vertical");
this._list.addEventListener("keypress", this._onViewKeyPress, false);
this._parent.appendChild(this._list);
};
@ -82,6 +91,7 @@ VariablesView.prototype = {
let scope = new Scope(this, aName);
this._store.set(scope.id, scope);
this._currHierarchy.set(aName, scope);
this._itemsByElement.set(scope._target, scope);
scope.header = !!aName;
return scope;
},
@ -112,6 +122,8 @@ VariablesView.prototype = {
}
this._store = new Map();
this._itemsByElement = new WeakMap();
this._appendEmptyNotice();
this._toggleSearchVisibility(false);
},
@ -133,12 +145,18 @@ VariablesView.prototype = {
*/
_emptySoon: function VV__emptySoon(aTimeout) {
let prevList = this._list;
let currList = this._list = this.document.createElement("vbox");
let currList = this._list = this.document.createElement("scrollbox");
this._store = new Map();
this._itemsByElement = new WeakMap();
this._emptyTimeout = this.window.setTimeout(function() {
this._emptyTimeout = null;
prevList.removeEventListener("keypress", this._onViewKeyPress, false);
currList.addEventListener("keypress", this._onViewKeyPress, false);
currList.setAttribute("orient", "vertical");
this._parent.removeChild(prevList);
this._parent.appendChild(currList);
@ -468,15 +486,74 @@ VariablesView.prototype = {
*/
expandFirstSearchResults: function VV_expandFirstSearchResults() {
for (let [, scope] of this._store) {
for (let [, variable] of scope._store) {
if (variable._isMatch) {
variable.expand();
break;
}
let match = scope._firstMatch;
if (match) {
match.expand();
}
}
},
/**
* Focuses the first visible variable or property in this container.
*/
focusFirstVisibleNode: function VV_focusFirstVisibleNode() {
let property, variable, scope;
for (let [, item] of this._currHierarchy) {
if (!item.focusable) {
continue;
}
if (item instanceof Property) {
property = item;
break;
} else if (item instanceof Variable) {
variable = item;
break;
} else if (item instanceof Scope) {
scope = item;
break;
}
}
if (scope) {
this._focusItem(scope);
} else if (variable) {
this._focusItem(variable);
} else if (property) {
this._focusItem(property);
}
this._parent.scrollTop = 0;
this._parent.scrollLeft = 0;
},
/**
* Focuses the last visible variable or property in this container.
*/
focusLastVisibleNode: function VV_focusLastVisibleNode() {
let property, variable, scope;
for (let [, item] of this._currHierarchy) {
if (!item.focusable) {
continue;
}
if (item instanceof Property) {
property = item;
} else if (item instanceof Variable) {
variable = item;
} else if (item instanceof Scope) {
scope = item;
}
}
if (property && (!variable || property.isDescendantOf(variable))) {
this._focusItem(property);
} else if (variable && (!scope || variable.isDescendantOf(scope))) {
this._focusItem(variable);
} else if (scope) {
this._focusItem(scope);
this._parent.scrollTop = this._parent.scrollHeight;
this._parent.scrollLeft = 0;
}
},
/**
* Searches for the scope in this container displayed by the specified node.
*
@ -486,33 +563,250 @@ VariablesView.prototype = {
* The matched scope, or null if nothing is found.
*/
getScopeForNode: function VV_getScopeForNode(aNode) {
for (let [, scope] of this._store) {
if (scope._target == aNode) {
return scope;
}
let item = this._itemsByElement.get(aNode);
if (item && !(item instanceof Variable) && !(item instanceof Property)) {
return item;
}
return null;
},
/**
* Recursively searches all the scopes for the variable or property
* Recursively searches this container for the scope, variable or property
* displayed by the specified node.
*
* @param nsIDOMNode aNode
* The node to search for.
* @return Variable | Property
* The matched variable or property, or null if nothing is found.
* @return Scope | Variable | Property
* The matched scope, variable or property, or null if nothing is found.
*/
getVariableOrPropertyForNode: function VV_getVariableOrPropertyForNode(aNode) {
for (let [, scope] of this._store) {
let match = scope.find(aNode);
if (match) {
return match;
}
}
return null;
getItemForNode: function VV_getItemForNode(aNode) {
return this._itemsByElement.get(aNode);
},
/**
* Gets the currently focused scope, variable or property in this view.
*
* @return Scope | Variable | Property
* The focused scope, variable or property, or null if nothing is found.
*/
getFocusedItem: function VV_getFocusedItem() {
let focused = this.document.commandDispatcher.focusedElement;
return this.getItemForNode(focused);
},
/**
* Focuses the next scope, variable or property in this view.
* @see VariablesView.prototype._focusChange
*/
focusNextItem: function VV_focusNextItem(aMaintainViewFocusedFlag)
this._focusChange("advanceFocus", aMaintainViewFocusedFlag),
/**
* Focuses the previous scope, variable or property in this view.
* @see VariablesView.prototype._focusChange
*/
focusPrevItem: function VV_focusPrevItem(aMaintainViewFocusedFlag)
this._focusChange("rewindFocus", aMaintainViewFocusedFlag),
/**
* Focuses the next or previous scope, variable or property in this view.
*
* @param string aDirection
* Either "advanceFocus" or "rewindFocus".
* @param boolean aMaintainViewFocusedFlag
* True too keep this view focused if the element is out of bounds.
* @return boolean
* True if the focus went out of bounds and the first or last element
* in this view was focused instead.
*/
_focusChange: function VV__changeFocus(aDirection, aMaintainViewFocusedFlag) {
let commandDispatcher = this.document.commandDispatcher;
let item;
do {
commandDispatcher[aDirection]();
// If maintaining this view focused is not mandatory, a simple
// "advanceFocus" or "rewindFocus" command dispatch is sufficient.
if (!aMaintainViewFocusedFlag) {
return false;
}
// Make sure the newly focused target is a part of this view.
item = this.getFocusedItem();
if (!item) {
if (aDirection == "advanceFocus") {
this.focusLastVisibleNode();
} else {
this.focusFirstVisibleNode();
}
// Focus went out of bounds so the first or last element in this view
// was focused instead.
return true;
}
} while (!item.focusable);
// Focus remained within bounds.
return false;
},
/**
* Focuses a scope, variable or property and makes sure it's visible.
*
* @param aItem Scope | Variable | Property
* The item to focus.
* @param boolean aCollapseFlag
* True if the focused item should also be collapsed.
* @return boolean
* True if the item was successfully focused.
*/
_focusItem: function VV__focusItem(aItem, aCollapseFlag) {
if (!aItem.focusable) {
return false;
}
if (aCollapseFlag) {
aItem.collapse();
}
aItem._target.focus();
let boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
boxObject.ensureElementIsVisible(aItem._title);
boxObject.scrollBy(-this._list.clientWidth, 0);
return true;
},
/**
* Listener handling a key press event on the view.
*/
_onViewKeyPress: function VV__onViewKeyPress(e) {
let item = this.getFocusedItem();
switch (e.keyCode) {
case e.DOM_VK_UP:
case e.DOM_VK_DOWN:
case e.DOM_VK_LEFT:
case e.DOM_VK_RIGHT:
case e.DOM_VK_PAGE_UP:
case e.DOM_VK_PAGE_DOWN:
case e.DOM_VK_HOME:
case e.DOM_VK_END:
// Prevent scrolling when pressing navigation keys.
e.preventDefault();
e.stopPropagation();
}
switch (e.keyCode) {
case e.DOM_VK_UP:
// Always rewind focus.
this.focusPrevItem(true);
return;
case e.DOM_VK_DOWN:
// Only expand scopes before advancing focus.
if (!(item instanceof Variable) &&
!(item instanceof Property) &&
!item._isExpanded && item._isArrowVisible) {
item.expand();
} else {
this.focusNextItem(true);
}
return;
case e.DOM_VK_LEFT:
// If this is a collapsed or un-expandable item that has an expandable
// variable or property parent, collapse and focus the owner view.
if (!item._isExpanded || !item._isArrowVisible) {
let ownerView = item.ownerView;
if ((ownerView instanceof Variable ||
ownerView instanceof Property) &&
ownerView._isExpanded && ownerView._isArrowVisible) {
if (this._focusItem(ownerView, true)) {
return;
}
}
}
// Collapse scopes, variables and properties before rewinding focus.
if (item._isExpanded && item._isArrowVisible) {
item.collapse();
} else {
this.focusPrevItem(true);
}
return;
case e.DOM_VK_RIGHT:
// Expand scopes, variables and properties before advancing focus.
if (!item._isExpanded && item._isArrowVisible) {
item.expand();
} else {
this.focusNextItem(true);
}
return;
case e.DOM_VK_PAGE_UP:
// Rewind a certain number of elements based on the container height.
var jumps = this.pageSize || Math.min(Math.floor(this._list.scrollHeight /
PAGE_SIZE_SCROLL_HEIGHT_RATIO),
PAGE_SIZE_MAX_JUMPS);
while (jumps--) {
if (this.focusPrevItem(true)) {
return;
}
}
return;
case e.DOM_VK_PAGE_DOWN:
// Advance a certain number of elements based on the container height.
var jumps = this.pageSize || Math.min(Math.floor(this._list.scrollHeight /
PAGE_SIZE_SCROLL_HEIGHT_RATIO),
PAGE_SIZE_MAX_JUMPS);
while (jumps--) {
if (this.focusNextItem(true)) {
return;
}
}
return;
case e.DOM_VK_HOME:
this.focusFirstVisibleNode();
return;
case e.DOM_VK_END:
this.focusLastVisibleNode();
return;
case e.DOM_VK_RETURN:
case e.DOM_VK_ENTER:
// Start editing the value or name of the variable or property.
if (item instanceof Variable ||
item instanceof Property) {
if (e.metaKey || e.altKey || e.shiftKey) {
item._activateNameInput();
} else {
item._activateValueInput();
}
}
return;
case e.DOM_VK_DELETE:
case e.DOM_VK_BACK_SPACE:
// Delete the variable or property if allowed.
if (item instanceof Variable ||
item instanceof Property) {
item._onDelete(e);
}
return;
}
},
/**
* The number of elements in this container to jump when Page Up or Page Down
* keys are pressed. If falsy, then the page size will be based on the
* container height.
*/
pageSize: 0,
/**
* Sets the text displayed in this container when there are no available items.
* @param string aValue
@ -604,8 +898,7 @@ VariablesView.prototype = {
function Scope(aView, aName, aFlags = {}) {
this.ownerView = aView;
this.expand = this.expand.bind(this);
this.toggle = this.toggle.bind(this);
this._onClick = this._onClick.bind(this);
this._openEnum = this._openEnum.bind(this);
this._openNonEnum = this._openNonEnum.bind(this);
this._batchAppend = this._batchAppend.bind(this);
@ -656,6 +949,7 @@ Scope.prototype = {
let variable = new Variable(this, aName, aDescriptor);
this._store.set(aName, variable);
this._variablesView._currHierarchy.set(variable._absoluteName, variable);
this._variablesView._itemsByElement.set(variable._target, variable);
variable.header = !!aName;
return variable;
},
@ -696,12 +990,45 @@ Scope.prototype = {
return null;
},
/**
* Determines if this scope is a direct child of a parent variables view,
* scope, variable or property.
*
* @param VariablesView | Scope | Variable | Property
* The parent to check.
* @return boolean
* True if the specified item is a direct child, false otherwise.
*/
isChildOf: function S_isChildOf(aParent) {
return this.ownerView == aParent;
},
/**
* Determines if this scope is a descendant of a parent variables view,
* scope, variable or property.
*
* @param VariablesView | Scope | Variable | Property
* The parent to check.
* @return boolean
* True if the specified item is a descendant, false otherwise.
*/
isDescendantOf: function S_isDescendantOf(aParent) {
if (this.isChildOf(aParent)) {
return true;
}
if (this.ownerView instanceof Scope ||
this.ownerView instanceof Variable ||
this.ownerView instanceof Property) {
return this.ownerView.isDescendantOf(aParent);
}
},
/**
* Shows the scope.
*/
show: function S_show() {
this._target.hidden = false;
this._isShown = true;
this._isContentVisible = true;
if (this.onshow) {
this.onshow(this);
@ -713,7 +1040,7 @@ Scope.prototype = {
*/
hide: function S_hide() {
this._target.hidden = true;
this._isShown = false;
this._isContentVisible = false;
if (this.onhide) {
this.onhide(this);
@ -738,7 +1065,7 @@ Scope.prototype = {
// Start spinning a throbber in this scope's title and allow a few
// milliseconds for it to be painted.
this._startThrobber();
this.window.setTimeout(this.expand, LAZY_EXPAND_DELAY);
this.window.setTimeout(this.expand.bind(this), LAZY_EXPAND_DELAY);
return;
}
@ -787,7 +1114,7 @@ Scope.prototype = {
// Make sure the scope and its contents are visibile.
for (let [, variable] of this._store) {
variable.header = true;
variable._match = true;
variable._matched = true;
}
if (this.ontoggle) {
this.ontoggle(this);
@ -844,7 +1171,7 @@ Scope.prototype = {
* Gets the visibility state.
* @return boolean
*/
get visible() this._isShown,
get visible() this._isContentVisible,
/**
* Gets the expanded state.
@ -864,6 +1191,12 @@ Scope.prototype = {
*/
get twisty() this._isArrowVisible,
/**
* Gets the expand lock state.
* @return boolean
*/
get locked() this._locked,
/**
* Sets the visibility state.
* @param boolean aFlag
@ -888,18 +1221,37 @@ Scope.prototype = {
*/
set twisty(aFlag) aFlag ? this.showArrow() : this.hideArrow(),
/**
* Gets the expand lock state.
* @return boolean
*/
get locked() this._locked,
/**
* Sets the expand lock state.
* @param boolean aFlag
*/
set locked(aFlag) this._locked = aFlag,
/**
* Specifies if this target node may be focused.
* @return boolean
*/
get focusable() {
// Check if this target node is actually visibile.
if (!this._nameString ||
!this._isContentVisible ||
!this._isHeaderVisible ||
!this._isMatch) {
return false;
}
// Check if all parent objects are expanded.
let item = this;
while ((item = item.ownerView) && /* Parent object exists. */
(item instanceof Scope ||
item instanceof Variable ||
item instanceof Property)) {
if (!item._isExpanded) {
return false;
}
}
return true;
},
/**
* Adds an event listener for a certain event on this scope's title.
* @param string aName
@ -998,7 +1350,15 @@ Scope.prototype = {
* Adds the necessary event listeners for this scope.
*/
_addEventListeners: function S__addEventListeners() {
this._title.addEventListener("mousedown", this.toggle, false);
this._title.addEventListener("mousedown", this._onClick, false);
},
/**
* The click listener for this scope's title.
*/
_onClick: function S__onClick() {
this.toggle();
this._variablesView._focusItem(this);
},
/**
@ -1157,11 +1517,11 @@ Scope.prototype = {
// Non-matched variables or properties require a corresponding attribute.
if (!lowerCaseName.contains(aLowerCaseQuery) &&
!lowerCaseValue.contains(aLowerCaseQuery)) {
variable._match = false;
variable._matched = false;
}
// Variable or property is matched.
else {
variable._match = true;
variable._matched = true;
// If the variable was ever expanded, there's a possibility it may
// contain some matched properties, so make sure they're visible
@ -1184,7 +1544,7 @@ Scope.prototype = {
variable instanceof Property)) {
// Show and expand the parent, as it is certainly accessible.
variable._match = true;
variable._matched = true;
aLowerCaseQuery && variable.expand();
}
}
@ -1199,10 +1559,10 @@ Scope.prototype = {
},
/**
* Sets if this object instance is a match or non-match.
* Sets if this object instance is a matched or non-matched item.
* @param boolean aStatus
*/
set _match(aStatus) {
set _matched(aStatus) {
if (this._isMatch == aStatus) {
return;
}
@ -1215,6 +1575,25 @@ Scope.prototype = {
}
},
/**
* Gets the first search results match in this scope.
* @return Variable | Property
*/
get _firstMatch() {
for (let [, variable] of this._store) {
let match;
if (variable._isMatch) {
match = variable;
} else {
match = variable._firstMatch;
}
if (match) {
return match;
}
}
return null;
},
/**
* Gets top level variables view instance.
* @return VariablesView
@ -1269,10 +1648,10 @@ Scope.prototype = {
_batchItems: null,
_batchTimeout: null,
_locked: false,
_isShown: true,
_isExpanding: false,
_isExpanded: false,
_wasToggled: false,
_isContentVisible: true,
_isHeaderVisible: true,
_isArrowVisible: true,
_isMatch: true,
@ -1342,6 +1721,7 @@ create({ constructor: Variable, proto: Scope.prototype }, {
let property = new Property(this, aName, aDescriptor);
this._store.set(aName, property);
this._variablesView._currHierarchy.set(property._absoluteName, property);
this._variablesView._itemsByElement.set(property._target, property);
property.header = !!aName;
return property;
},
@ -1536,7 +1916,7 @@ create({ constructor: Variable, proto: Scope.prototype }, {
*/
_init: function V__init(aName, aDescriptor) {
this._idString = generateId(this._nameString = aName);
this._displayScope(aName, "variable");
this._displayScope(aName, "variable variable-or-property");
// Don't allow displaying variable information there's no name available.
if (this._nameString) {
@ -1694,10 +2074,9 @@ create({ constructor: Variable, proto: Scope.prototype }, {
* Adds the necessary event listeners for this variable.
*/
_addEventListeners: function V__addEventListeners() {
this._arrow.addEventListener("mousedown", this.toggle, false);
this._name.addEventListener("mousedown", this.toggle, false);
this._name.addEventListener("dblclick", this._activateNameInput, false);
this._valueLabel.addEventListener("click", this._activateValueInput, false);
this._valueLabel.addEventListener("mousedown", this._activateValueInput, false);
this._title.addEventListener("mousedown", this._onClick, false);
},
/**
@ -1774,6 +2153,11 @@ create({ constructor: Variable, proto: Scope.prototype }, {
if (!this.ownerView.switch) {
return;
}
if (e) {
e.preventDefault();
e.stopPropagation();
}
this._activateInput(this._name, "element-name-input", {
onKeypress: this._onNameInputKeyPress,
onBlur: this._deactivateNameInput
@ -1805,6 +2189,11 @@ create({ constructor: Variable, proto: Scope.prototype }, {
if (!this.ownerView.eval) {
return;
}
if (e) {
e.preventDefault();
e.stopPropagation();
}
this._activateInput(this._valueLabel, "element-value-input", {
onKeypress: this._onValueInputKeyPress,
onBlur: this._deactivateValueInput
@ -1867,13 +2256,17 @@ create({ constructor: Variable, proto: Scope.prototype }, {
* The key press listener for this variable's editable name textbox.
*/
_onNameInputKeyPress: function V__onNameInputKeyPress(e) {
e.stopPropagation();
switch(e.keyCode) {
case e.DOM_VK_RETURN:
case e.DOM_VK_ENTER:
this._saveNameInput(e);
this._variablesView._focusItem(this);
return;
case e.DOM_VK_ESCAPE:
this._deactivateNameInput(e);
this._variablesView._focusItem(this);
return;
}
},
@ -1882,13 +2275,17 @@ create({ constructor: Variable, proto: Scope.prototype }, {
* The key press listener for this variable's editable value textbox.
*/
_onValueInputKeyPress: function V__onValueInputKeyPress(e) {
e.stopPropagation();
switch(e.keyCode) {
case e.DOM_VK_RETURN:
case e.DOM_VK_ENTER:
this._saveValueInput(e);
this._variablesView._focusItem(this);
return;
case e.DOM_VK_ESCAPE:
this._deactivateValueInput(e);
this._variablesView._focusItem(this);
return;
}
},
@ -1896,11 +2293,13 @@ create({ constructor: Variable, proto: Scope.prototype }, {
/**
* The click listener for the delete button.
*/
_onDelete: function V__onDelete() {
this.hide();
_onDelete: function V__onDelete(e) {
e.preventDefault();
e.stopPropagation();
if (this.ownerView.delete) {
this.ownerView.delete(this);
this.hide();
}
},
@ -1946,7 +2345,7 @@ create({ constructor: Property, proto: Variable.prototype }, {
*/
_init: function P__init(aName, aDescriptor) {
this._idString = generateId(this._nameString = aName);
this._displayScope(aName, "property");
this._displayScope(aName, "property variable-or-property");
// Don't allow displaying property information there's no name available.
if (this._nameString) {

View File

@ -94,6 +94,11 @@
<!ENTITY debuggerUI.searchVariable "Filter variables">
<!ENTITY debuggerUI.searchVariable.key "V">
<!-- LOCALIZATION NOTE (debuggerUI.focusVariables): This is the text that appears
- in the source editor's context menu for the variables focus operation. -->
<!ENTITY debuggerUI.focusVariables "Focus variables tree">
<!ENTITY debuggerUI.focusVariables.key "V">
<!-- LOCALIZATION NOTE (debuggerUI.condBreakPanelTitle): This is the text that
- appears in the conditional breakpoint panel popup as a description. -->
<!ENTITY debuggerUI.condBreakPanelTitle "This breakpoint will stop execution only if the following expression is true">

View File

@ -304,6 +304,11 @@
* Scope element
*/
.scope:focus > .title {
background: Highlight;
color: HighlightText;
}
.scope > .title {
text-shadow: 0 1px #222;
color: #fff;
@ -335,11 +340,20 @@
transition-duration: 0.4s;
}
.variable:focus > .title {
background: Highlight;
color: HighlightText;
border-radius: 4px;
}
.variable > .title > .name {
color: #048;
font-weight: 600;
}
.variable:not(:focus) > .title > .name {
color: #048;
}
.variable > .title > .value {
-moz-padding-start: 6px;
-moz-padding-end: 4px;
@ -363,7 +377,13 @@
transition-duration: 0.4s;
}
.property > .title > .name {
.property:focus > .title {
background: Highlight;
color: HighlightText;
border-radius: 4px;
}
.property:not(:focus) > .title > .name {
color: #881090;
}
@ -377,7 +397,7 @@
}
/**
* Non enumerable, configurable and writable variables and properties.
* Non enumerable, configurable and writable variables and properties
*/
.variable[proto] > .title > .name,
@ -415,8 +435,8 @@
}
}
.variable[exception] > .title > .name,
.property[exception] > .title > .name {
.variable[exception]:not(:focus) > .title > .name,
.property[exception]:not(:focus) > .title > .name {
color: #a00;
text-shadow: 0 0 8px #fcc;
}
@ -469,28 +489,28 @@
* Token value colors
*/
.token-undefined {
.variable-or-property:not(:focus) > .title > .token-undefined {
color: #bbb;
}
.token-null {
.variable-or-property:not(:focus) > .title > .token-null {
color: #999;
}
.token-boolean {
.variable-or-property:not(:focus) > .title > .token-boolean {
color: #777;
}
.token-number {
.variable-or-property:not(:focus) > .title > .token-number {
color: #c40a16;
}
.token-string {
.variable-or-property:not(:focus) > .title > .token-string {
max-width: 30em;
color: #1c00cf;
}
.token-other {
.variable-or-property:not(:focus) > .title > .token-other {
color: #333;
}

View File

@ -311,6 +311,11 @@
* Scope element
*/
.scope:focus > .title {
background: Highlight;
color: HighlightText;
}
.scope > .title {
text-shadow: 0 1px #222;
color: #fff;
@ -342,11 +347,20 @@
transition-duration: 0.4s;
}
.variable:focus > .title {
background: Highlight;
color: HighlightText;
border-radius: 4px;
}
.variable > .title > .name {
color: #048;
font-weight: 600;
}
.variable:not(:focus) > .title > .name {
color: #048;
}
.variable > .title > .value {
-moz-padding-start: 6px;
-moz-padding-end: 4px;
@ -370,7 +384,13 @@
transition-duration: 0.4s;
}
.property > .title > .name {
.property:focus > .title {
background: Highlight;
color: HighlightText;
border-radius: 4px;
}
.property:not(:focus) > .title > .name {
color: #881090;
}
@ -384,7 +404,7 @@
}
/**
* Non enumerable, configurable and writable variables and properties.
* Non enumerable, configurable and writable variables and properties
*/
.variable[proto] > .title > .name,
@ -422,8 +442,8 @@
}
}
.variable[exception] > .title > .name,
.property[exception] > .title > .name {
.variable[exception]:not(:focus) > .title > .name,
.property[exception]:not(:focus) > .title > .name {
color: #a00;
text-shadow: 0 0 8px #fcc;
}
@ -476,28 +496,28 @@
* Token value colors
*/
.token-undefined {
.variable-or-property:not(:focus) > .title > .token-undefined {
color: #bbb;
}
.token-null {
.variable-or-property:not(:focus) > .title > .token-null {
color: #999;
}
.token-boolean {
.variable-or-property:not(:focus) > .title > .token-boolean {
color: #777;
}
.token-number {
.variable-or-property:not(:focus) > .title > .token-number {
color: #c40a16;
}
.token-string {
.variable-or-property:not(:focus) > .title > .token-string {
max-width: 30em;
color: #1c00cf;
}
.token-other {
.variable-or-property:not(:focus) > .title > .token-other {
color: #333;
}

View File

@ -317,6 +317,11 @@
* Scope element
*/
.scope:focus > .title {
background: Highlight;
color: HighlightText;
}
.scope > .title {
text-shadow: 0 1px #222;
color: #fff;
@ -348,11 +353,20 @@
transition-duration: 0.4s;
}
.variable:focus > .title {
background: Highlight;
color: HighlightText;
border-radius: 4px;
}
.variable > .title > .name {
color: #048;
font-weight: 600;
}
.variable:not(:focus) > .title > .name {
color: #048;
}
.variable > .title > .value {
-moz-padding-start: 6px;
-moz-padding-end: 4px;
@ -376,7 +390,13 @@
transition-duration: 0.4s;
}
.property > .title > .name {
.property:focus > .title {
background: Highlight;
color: HighlightText;
border-radius: 4px;
}
.property:not(:focus) > .title > .name {
color: #881090;
}
@ -390,7 +410,7 @@
}
/**
* Non enumerable, configurable and writable variables and properties.
* Non enumerable, configurable and writable variables and properties
*/
.variable[proto] > .title > .name,
@ -428,8 +448,8 @@
}
}
.variable[exception] > .title > .name,
.property[exception] > .title > .name {
.variable[exception]:not(:focus) > .title > .name,
.property[exception]:not(:focus) > .title > .name {
color: #a00;
text-shadow: 0 0 8px #fcc;
}
@ -482,28 +502,28 @@
* Token value colors
*/
.token-undefined {
.variable-or-property:not(:focus) > .title > .token-undefined {
color: #bbb;
}
.token-null {
.variable-or-property:not(:focus) > .title > .token-null {
color: #999;
}
.token-boolean {
.variable-or-property:not(:focus) > .title > .token-boolean {
color: #777;
}
.token-number {
.variable-or-property:not(:focus) > .title > .token-number {
color: #c40a16;
}
.token-string {
.variable-or-property:not(:focus) > .title > .token-string {
max-width: 30em;
color: #1c00cf;
}
.token-other {
.variable-or-property:not(:focus) > .title > .token-other {
color: #333;
}