From 53febe327af3e3519264282cb29cb7f51361b44a Mon Sep 17 00:00:00 2001 From: Paul Rouget Date: Fri, 1 Jun 2012 21:39:00 +0200 Subject: [PATCH] Bug 719845 - [markup panel] The HTML Tree should have its own keybindings. r=dcamp --- browser/devtools/highlighter/InsideOutBox.jsm | 146 +++++++++++++++++- browser/devtools/highlighter/TreePanel.jsm | 58 ++++++- browser/devtools/highlighter/inspector.jsm | 4 +- browser/devtools/highlighter/test/Makefile.in | 2 + ...rowser_inspector_treePanel_navigation.html | 26 ++++ .../browser_inspector_treePanel_navigation.js | 103 ++++++++++++ 6 files changed, 328 insertions(+), 11 deletions(-) create mode 100644 browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.html create mode 100644 browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.js diff --git a/browser/devtools/highlighter/InsideOutBox.jsm b/browser/devtools/highlighter/InsideOutBox.jsm index 5b07e4b10532..b15dd5ebf873 100644 --- a/browser/devtools/highlighter/InsideOutBox.jsm +++ b/browser/devtools/highlighter/InsideOutBox.jsm @@ -214,12 +214,13 @@ InsideOutBox.prototype = this.selectObjectBox(objectBox, forceOpen); if (makeBoxVisible) { this.openObjectBox(objectBox); - if (scrollIntoView) { - // We want to center the label of the element, not the whole tag - // (which includes all of its children, and is vertically huge). - LayoutHelpers.scrollIntoViewIfNeeded(objectBox.firstElementChild); - } } + if (scrollIntoView) { + // We want to center the label of the element, not the whole tag + // (which includes all of its children, and is vertically huge). + LayoutHelpers.scrollIntoViewIfNeeded(objectBox.firstElementChild); + } + return objectBox; }, @@ -340,6 +341,141 @@ InsideOutBox.prototype = } }, + /** + * Returns the next object box in the tree for navigation purposes. + */ + nextObjectBox: function IOBox_nextObjectBox(aBoxObject) + { + let candidate; + let boxObject = aBoxObject || this.selectedObjectBox; + if (!boxObject) + return this.rootObjectBox; + + // If expanded, return the first child. + let isOpen = this.view.hasClass(boxObject, "open"); + let childObjectBox = this.getChildObjectBox(boxObject); + if (isOpen && childObjectBox && childObjectBox.firstChild) { + candidate = childObjectBox.firstChild; + } else { + // Otherwise we get the next available sibling. + while (boxObject) { + if (boxObject.nextSibling) { + boxObject = boxObject.nextSibling; + break; + } + boxObject = this.getParentObjectBox(boxObject); + } + candidate = boxObject; + } + + // If the node is not an element (comments or text nodes), we + // jump to the next line. + if (candidate && + candidate.repObject.nodeType != candidate.repObject.ELEMENT_NODE) { + return this.nextObjectBox(candidate); + } + + return candidate; + }, + + /** + * Returns the next object in the tree for navigation purposes. + */ + nextObject: function IOBox_nextObject() + { + let next = this.nextObjectBox(); + return next ? next.repObject : null; + }, + + /** + * Returns the object that is below the selection. + * + * @param aDistance Number of lines to jump. + */ + farNextObject: function IOBox_farPreviousProject(aDistance) + { + let boxObject = this.selectedObjectBox; + while (aDistance-- > 0) { + let newBoxObject = this.nextObjectBox(boxObject); + if (!newBoxObject) { + break; + } + boxObject = newBoxObject; + } + return boxObject ? boxObject.repObject : null; + }, + + /** + * Returns the last visible child box of an object box. + */ + lastVisible: function IOBox_lastVisibleChild(aNode) + { + if (!this.view.hasClass(aNode, "open")) + return aNode; + + let childBox = this.getChildObjectBox(aNode); + if (!childBox || !childBox.lastChild) + return aNode; + + return this.lastVisible(childBox.lastChild); + }, + + /** + * Returns the previous object box in the tree for navigation purposes. + */ + previousObjectBox: function IOBox_previousObjectBox(aBoxObject) + { + let boxObject = aBoxObject || this.selectedObjectBox; + if (!boxObject) + return this.rootObjectBox; + + let candidate; + let sibling = boxObject.previousSibling; + if (sibling) { + candidate = this.lastVisible(sibling); + } else { + candidate = this.getParentObjectBox(boxObject); + } + + // If the node is not an element (comments or text nodes), we + // jump to the previous line. + if (candidate && + candidate.repObject.nodeType != candidate.repObject.ELEMENT_NODE) { + return this.previousObjectBox(candidate); + } + + return candidate; + }, + + /** + * Returns the previous object in the tree for navigation purposes. + */ + previousObject: function IOBox_previousObject() + { + let boxObject = this.previousObjectBox(); + return boxObject ? boxObject.repObject : null; + }, + + /** + * Returns the object that is above the selection. + * + * @param aDistance Number of lines to jump. + */ + farPreviousObject: function IOBox_farPreviousProject(aDistance) + { + let boxObject = this.selectedObjectBox; + while (aDistance-- > 0) { + let newBoxObject = this.previousObjectBox(boxObject); + if (!newBoxObject) { + break; + } + boxObject = newBoxObject; + if (boxObject === this.rootObjectBox) + break; + } + return boxObject ? boxObject.repObject : null; + }, + /** * Open the ancestors of the given object box. * @param aObjectBox diff --git a/browser/devtools/highlighter/TreePanel.jsm b/browser/devtools/highlighter/TreePanel.jsm index 991388ac7026..55de7dc6f030 100644 --- a/browser/devtools/highlighter/TreePanel.jsm +++ b/browser/devtools/highlighter/TreePanel.jsm @@ -5,11 +5,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const Cu = Components.utils; +const Ci = Components.interfaces; Cu.import("resource:///modules/domplate.jsm"); Cu.import("resource:///modules/InsideOutBox.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource:///modules/inspector.jsm"); +Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); var EXPORTED_SYMBOLS = ["TreePanel", "DOMHelpers"]; @@ -80,6 +82,8 @@ TreePanel.prototype = { this.ioBox = new InsideOutBox(this, this.treePanelDiv); this.ioBox.createObjectBox(this.IUI.win.document.documentElement); this.treeLoaded = true; + this._boundTreeKeyPress = this.onTreeKeyPress.bind(this); + this.treeIFrame.addEventListener("keypress", this._boundTreeKeyPress.bind(this), true); this.treeIFrame.addEventListener("click", this.onTreeClick.bind(this), false); this.treeIFrame.addEventListener("dblclick", this.onTreeDblClick.bind(this), false); this.treeIFrame.focus(); @@ -182,6 +186,7 @@ TreePanel.prototype = { this.treePanelDiv.ownerPanel = null; let parent = this.treePanelDiv.parentNode; parent.removeChild(this.treePanelDiv); + this.treeIFrame.removeEventListener("keypress", this._boundTreeKeyPress, true); delete this.treePanelDiv; delete this.treeBrowserDocument; } @@ -272,8 +277,7 @@ TreePanel.prototype = { if (this.IUI.inspecting) { this.IUI.stopInspecting(true); } else { - this.IUI.select(node, true, false); - this.IUI.highlighter.highlight(node); + this.navigate(node); } } } @@ -316,6 +320,52 @@ TreePanel.prototype = { } }, + navigate: function TP_navigate(node) + { + if (!node) + return; + this.ioBox.select(node, false, false, true); + + if (this.IUI.highlighter.isNodeHighlightable(node)) { + this.IUI.select(node, true, false, "treepanel"); + this.IUI.highlighter.highlight(node); + } + }, + + onTreeKeyPress: function TP_onTreeKeyPress(aEvent) + { + let handled = true; + switch(aEvent.keyCode) { + case Ci.nsIDOMKeyEvent.DOM_VK_LEFT: + this.ioBox.contractObjectBox(this.ioBox.selectedObjectBox); + break; + case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: + this.ioBox.expandObjectBox(this.ioBox.selectedObjectBox); + break; + case Ci.nsIDOMKeyEvent.DOM_VK_UP: + this.navigate(this.ioBox.previousObject()); + break; + case Ci.nsIDOMKeyEvent.DOM_VK_DOWN: + this.navigate(this.ioBox.nextObject()); + break; + case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: + this.navigate(this.ioBox.farPreviousObject(10)); + break; + case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: + this.navigate(this.ioBox.farNextObject(10)); + break; + case Ci.nsIDOMKeyEvent.DOM_VK_HOME: + this.navigate(this.ioBox.rootObject); + break; + default: + handled = false; + } + if (handled) { + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + }, + /** * Starts the editor for an attribute name or value. * @param aAttrObj @@ -542,10 +592,10 @@ TreePanel.prototype = { * @param aNode the DOM node in the content document to select. * @param aScroll boolean scroll to the visible node? */ - select: function TP_select(aNode, aScroll) + select: function TP_select(aNode, aScroll, aFrom) { if (this.ioBox) { - this.ioBox.select(aNode, true, true, aScroll); + this.ioBox.select(aNode, true, aFrom != "treepanel", aScroll); } else { this.pendingSelection = { node: aNode, scroll: aScroll }; } diff --git a/browser/devtools/highlighter/inspector.jsm b/browser/devtools/highlighter/inspector.jsm index 596ed61e9a4b..4e67acf00126 100644 --- a/browser/devtools/highlighter/inspector.jsm +++ b/browser/devtools/highlighter/inspector.jsm @@ -440,7 +440,7 @@ InspectorUI.prototype = { /** * Toggle the TreePanel. */ - toggleHTMLPanel: function TP_toggleHTMLPanel() + toggleHTMLPanel: function IUI_toggleHTMLPanel() { if (this.treePanel.isOpen()) { this.treePanel.close(); @@ -849,7 +849,7 @@ InspectorUI.prototype = { this.breadcrumbs.update(); this.chromeWin.Tilt.update(aNode); - this.treePanel.select(aNode, aScroll); + this.treePanel.select(aNode, aScroll, aFrom); this._notifySelected(aFrom); }, diff --git a/browser/devtools/highlighter/test/Makefile.in b/browser/devtools/highlighter/test/Makefile.in index bae8d3589d37..0db71e10830f 100644 --- a/browser/devtools/highlighter/test/Makefile.in +++ b/browser/devtools/highlighter/test/Makefile.in @@ -42,6 +42,8 @@ _BROWSER_FILES = \ browser_inspector_pseudoClass_menu.js \ browser_inspector_destroyselection.html \ browser_inspector_destroyselection.js \ + browser_inspector_treePanel_navigation.html \ + browser_inspector_treePanel_navigation.js \ head.js \ $(NULL) diff --git a/browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.html b/browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.html new file mode 100644 index 000000000000..fe5561181b14 --- /dev/null +++ b/browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.html @@ -0,0 +1,26 @@ + + + + + + +
+

line1

+

line2

+

line3

+ +

line4 + line5 + line6 + + line7line8 + line9 + line10 + line11 + line12line13 +

+

line14

+

line15

+
+ + diff --git a/browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.js b/browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.js new file mode 100644 index 000000000000..ec3a5aed01f9 --- /dev/null +++ b/browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + + +function test() { + + waitForExplicitFinish(); + + let doc; + + let keySequence = "right down right "; + keySequence += "down down down down right "; + keySequence += "down down down right "; + keySequence += "down down down down down right "; + keySequence += "down down down down down "; + keySequence += "up up up left down home "; + keySequence += "pagedown left down down pageup pageup left down"; + + keySequence = keySequence.split(" "); + + let keySequenceRes = "body node0 node0 "; + keySequenceRes += "node1 node2 node3 node4 node4 "; + keySequenceRes += "node5 node6 node7 node7 "; + keySequenceRes += "node8 node9 node10 node11 node12 node12 "; + keySequenceRes += "node13 node14 node15 node15 node15 "; + keySequenceRes += "node14 node13 node12 node12 node14 html "; + keySequenceRes += "node7 node7 node9 node10 body html html html"; + + keySequenceRes = keySequenceRes.split(" "); + + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + doc = content.document; + waitForFocus(setupTest, content); + }, true); + + content.location = "http://mochi.test:8888/browser/browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.html"; + + function setupTest() { + Services.obs.addObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false); + InspectorUI.toggleInspectorUI(); + } + + function runTests() { + Services.obs.removeObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED); + Services.obs.addObserver(startNavigation, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false); + InspectorUI.select(doc.body, true, true, true); + InspectorUI.toggleHTMLPanel(); + } + + function startNavigation() { + Services.obs.removeObserver(startNavigation, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY); + nextStep(0); + } + + function nextStep(cursor) { + let key = keySequence[cursor]; + let className = keySequenceRes[cursor]; + switch(key) { + case "right": + EventUtils.synthesizeKey("VK_RIGHT", {}); + break; + case "down": + EventUtils.synthesizeKey("VK_DOWN", {}); + break; + case "left": + EventUtils.synthesizeKey("VK_LEFT", {}); + break; + case "up": + EventUtils.synthesizeKey("VK_UP", {}); + break; + case "pageup": + EventUtils.synthesizeKey("VK_PAGE_UP", {}); + break; + case "pagedown": + EventUtils.synthesizeKey("VK_PAGE_DOWN", {}); + break; + case "home": + EventUtils.synthesizeKey("VK_HOME", {}); + break; + } + + executeSoon(function() { + if (cursor >= keySequence.length) { + Services.obs.addObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false); + InspectorUI.closeInspectorUI(); + } else { + let node = InspectorUI.treePanel.ioBox.selectedObjectBox.repObject; + is(node.className, className, "[" + cursor + "] right node selected: " + className); + nextStep(cursor + 1); + } + }); + } + + function finishUp() { + Services.obs.removeObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED); + doc = null; + gBrowser.removeCurrentTab(); + finish(); + } +}