Bug 893965 - Autocomplete CSS properties in the rule view, r=mratcliffe, msucan

This commit is contained in:
Girish Sharma 2013-07-26 04:35:05 +05:30
parent dd52d56902
commit 5600aa21af
9 changed files with 478 additions and 16 deletions

View File

@ -4,6 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const Cu = Components.utils;
const Ci = Components.interfaces;
// The XUL and XHTML namespace.
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@ -11,6 +12,10 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "gDevTools", function() {
return Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools;
});
this.EXPORTED_SYMBOLS = ["AutocompletePopup"];
/**
@ -49,6 +54,14 @@ function AutocompletePopup(aDocument, aOptions = {})
let id = aOptions.panelId || "devtools_autoCompletePopup";
let theme = aOptions.theme || "dark";
// If theme is auto, use the devtools.theme pref
if (theme == "auto") {
theme = Services.prefs.getCharPref("devtools.theme");
this.autoThemeEnabled = true;
// Setup theme change listener.
this._handleThemeChange = this._handleThemeChange.bind(this);
gDevTools.on("pref-changed", this._handleThemeChange);
}
// Reuse the existing popup elements.
this._panel = this._document.getElementById(id);
if (!this._panel) {
@ -173,6 +186,10 @@ AutocompletePopup.prototype = {
this._list.removeEventListener("keypress", this.onKeypress, false);
}
if (this.autoThemeEnabled) {
gDevTools.off("pref-changed", this._handleThemeChange);
}
this._document = null;
this._list = null;
this._panel = null;
@ -440,7 +457,7 @@ AutocompletePopup.prototype = {
this.selectedIndex++;
}
else {
this.selectedIndex = -1;
this.selectedIndex = 0;
}
return this.selectedItem;
@ -454,7 +471,7 @@ AutocompletePopup.prototype = {
*/
selectPreviousItem: function AP_selectPreviousItem()
{
if (this.selectedIndex > -1) {
if (this.selectedIndex > 0) {
this.selectedIndex--;
}
else {
@ -496,5 +513,28 @@ AutocompletePopup.prototype = {
return this.__scrollbarWidth;
},
};
/**
* Manages theme switching for the popup based on the devtools.theme pref.
*
* @private
*
* @param String aEvent
* The name of the event. In this case, "pref-changed".
* @param Object aData
* An object passed by the emitter of the event. In this case, the
* object consists of three properties:
* - pref {String} The name of the preference that was modified.
* - newValue {Object} The new value of the preference.
* - oldValue {Object} The old value of the preference.
*/
_handleThemeChange: function AP__handleThemeChange(aEvent, aData)
{
if (aData.pref == "devtools.theme") {
this._panel.classList.toggle(aData.oldValue + "-theme", false);
this._panel.classList.toggle(aData.newValue + "-theme", true);
this._list.classList.toggle(aData.oldValue + "-theme", false);
this._list.classList.toggle(aData.newValue + "-theme", true);
}
},
};

View File

@ -24,9 +24,16 @@
"use strict";
const {Ci, Cu} = require("chrome");
const {Ci, Cu, Cc} = require("chrome");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const CONTENT_TYPES = {
PLAIN_TEXT: 0,
CSS_VALUE: 1,
CSS_MIXED: 2,
CSS_PROPERTY: 3,
};
const MAX_POPUP_ENTRIES = 10;
const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD;
const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD;
@ -160,6 +167,8 @@ function InplaceEditor(aOptions, aEvent)
this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
this.multiline = aOptions.multiline || false;
this.stopOnReturn = !!aOptions.stopOnReturn;
this.contentType = aOptions.contentType || CONTENT_TYPES.PLAIN_TEXT;
this.popup = aOptions.popup;
this._onBlur = this._onBlur.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
@ -208,6 +217,8 @@ function InplaceEditor(aOptions, aEvent)
exports.InplaceEditor = InplaceEditor;
InplaceEditor.CONTENT_TYPES = CONTENT_TYPES;
InplaceEditor.prototype = {
_createInput: function InplaceEditor_createEditor()
{
@ -679,6 +690,10 @@ InplaceEditor.prototype = {
return;
}
if (this.popup) {
this.popup.hidePopup();
}
this._applied = true;
if (this.done) {
@ -732,9 +747,31 @@ InplaceEditor.prototype = {
increment *= smallIncrement;
}
let cycling = false;
if (increment && this._incrementValue(increment) ) {
this._updateSize();
prevent = true;
} else if (increment && this.popup && this.popup.isOpen) {
cycling = true;
prevent = true;
if (increment > 0) {
this.popup.selectPreviousItem();
} else {
this.popup.selectNextItem();
}
this.input.value = this.popup.selectedItem.label;
this._updateSize();
}
if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE ||
aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DELETE ||
aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_LEFT ||
aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RIGHT) {
if (this.popup && this.popup.isOpen) {
this.popup.hidePopup();
}
} else if (!cycling) {
this._maybeSuggestCompletion();
}
if (this.multiline &&
@ -756,6 +793,9 @@ InplaceEditor.prototype = {
direction = null;
}
// Now we don't want to suggest anything as we are moving out.
this._preventSuggestions = true;
let input = this.input;
this._apply();
@ -775,6 +815,8 @@ InplaceEditor.prototype = {
this._clear();
} else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
// Cancel and blur ourselves.
// Now we don't want to suggest anything as we are moving out.
this._preventSuggestions = true;
prevent = true;
this.cancelled = true;
this._apply();
@ -821,6 +863,79 @@ InplaceEditor.prototype = {
if (this.change) {
this.change(this.input.value.trim());
}
},
/**
* Handles displaying suggestions based on the current input.
*/
_maybeSuggestCompletion: function() {
// Since we are calling this method from a keypress event handler, the
// |input.value| does not include currently typed character. Thus we perform
// this method async.
this.doc.defaultView.setTimeout(() => {
if (this._preventSuggestions) {
this._preventSuggestions = false;
return;
}
if (this.contentType == CONTENT_TYPES.PLAIN_TEXT) {
return;
}
let input = this.input;
// Input can be null in cases when you intantaneously switch out of it.
if (!input) {
return;
}
let query = input.value.slice(0, input.selectionStart);
if (!query) {
return;
}
let list = [];
if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
list = CSSPropertyList;
}
list.some(item => {
if (item.startsWith(query)) {
input.value = item;
input.setSelectionRange(query.length, item.length);
this._updateSize();
return true;
}
});
if (!this.popup) {
return;
}
let finalList = [];
let length = list.length;
for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) {
if (list[i].startsWith(query)) {
count++;
finalList.push({
preLabel: query,
label: list[i]
});
}
else if (count > 0) {
// Since count was incremented, we had already crossed the entries
// which would have started with query, assuming that list is sorted.
break;
}
else if (list[i][0] > query[0]) {
// We have crossed all possible matches alphabetically.
break;
}
}
if (finalList.length > 1) {
this.popup.setItems(finalList);
this.popup.openPopup(this.input);
} else {
this.popup.hidePopup();
}
}, 0);
}
};
@ -849,3 +964,8 @@ function moveFocus(aWin, aDirection)
XPCOMUtils.defineLazyGetter(this, "focusManager", function() {
return Services.focus;
});
XPCOMUtils.defineLazyGetter(this, "CSSPropertyList", function() {
let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
return domUtils.getCSSPropertyNames(domUtils.INCLUDE_ALIASES).sort();
});

View File

@ -876,6 +876,13 @@ function CssRuleView(aDoc, aStore)
this._boundCopy = this._onCopy.bind(this);
this.element.addEventListener("copy", this._boundCopy);
let options = {
fixedWidth: true,
autoSelect: true,
theme: "auto"
};
this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options);
this._showEmpty();
}
@ -902,6 +909,8 @@ CssRuleView.prototype = {
if (this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
this.popup.destroy();
},
/**
@ -1270,7 +1279,9 @@ RuleEditor.prototype = {
element: this.newPropSpan,
done: this._onNewProperty,
destroy: this._newPropertyDestroy,
advanceChars: ":"
advanceChars: ":",
contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
popup: this.ruleView.popup
});
},
@ -1322,6 +1333,7 @@ RuleEditor.prototype = {
function TextPropertyEditor(aRuleEditor, aProperty)
{
this.doc = aRuleEditor.doc;
this.popup = aRuleEditor.ruleView.popup;
this.prop = aProperty;
this.prop.editor = this;
this.browserWindow = this.doc.defaultView.top;
@ -1390,7 +1402,9 @@ TextPropertyEditor.prototype = {
start: this._onStartEditing,
element: this.nameSpan,
done: this._onNameDone,
advanceChars: ':'
advanceChars: ':',
contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
popup: this.popup
});
appendText(this.nameContainer, ": ");
@ -1873,3 +1887,7 @@ XPCOMUtils.defineLazyGetter(this, "_strings", function() {
XPCOMUtils.defineLazyGetter(this, "domUtils", function() {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
});
XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function() {
return Cu.import("resource:///modules/devtools/AutocompletePopup.jsm", {}).AutocompletePopup;
});

View File

@ -38,6 +38,8 @@ MOCHITEST_BROWSER_FILES = \
browser_computedview_734259_style_editor_link.js \
browser_computedview_copy.js\
browser_styleinspector_bug_677930_urls_clickable.js \
browser_bug893965_css_property_completion_new_property.js \
browser_bug893965_css_property_completion_existing_property.js \
head.js \
$(NULL)

View File

@ -0,0 +1,149 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that CSS property names are autocompleted and cycled correctly.
const MAX_ENTRIES = 10;
let doc;
let inspector;
let ruleViewWindow;
let editor;
let state;
// format :
// [
// what key to press,
// expected input box value after keypress,
// selectedIndex of the popup,
// total items in the popup
// ]
let testData = [
["VK_RIGHT", "border", -1, 0],
["-","border-bottom", 0, 10],
["b","border-bottom", 0, 6],
["VK_BACK_SPACE", "border-b", -1, 0],
["VK_BACK_SPACE", "border-", -1, 0],
["VK_BACK_SPACE", "border", -1, 0],
["VK_BACK_SPACE", "borde", -1, 0],
["VK_BACK_SPACE", "bord", -1, 0],
["VK_BACK_SPACE", "bor", -1, 0],
["VK_BACK_SPACE", "bo", -1, 0],
["VK_BACK_SPACE", "b", -1, 0],
["VK_BACK_SPACE", "", -1, 0],
["d", "direction", 0, 3],
["VK_DOWN", "display", 1, 3],
["VK_DOWN", "dominant-baseline", 2, 3],
["VK_DOWN", "direction", 0, 3],
["VK_DOWN", "display", 1, 3],
["VK_UP", "direction", 0, 3],
["VK_UP", "dominant-baseline", 2, 3],
["VK_UP", "display", 1, 3],
["VK_BACK_SPACE", "displa", -1, 0],
["VK_BACK_SPACE", "displ", -1, 0],
["VK_BACK_SPACE", "disp", -1, 0],
["VK_BACK_SPACE", "dis", -1, 0],
["VK_BACK_SPACE", "di", -1, 0],
["VK_BACK_SPACE", "d", -1, 0],
["i", "direction", 0, 2],
["s", "display", -1, 0],
["VK_BACK_SPACE", "dis", -1, 0],
["VK_BACK_SPACE", "di", -1, 0],
["VK_BACK_SPACE", "d", -1, 0],
["VK_BACK_SPACE", "", -1, 0],
["f", "fill", 0, MAX_ENTRIES],
["i", "fill", 0, 4],
["VK_ESCAPE", null, -1, 0],
];
function openRuleView()
{
var target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
inspector = toolbox.getCurrentPanel();
inspector.sidebar.select("ruleview");
// Highlight a node.
let node = content.document.getElementsByTagName("h1")[0];
inspector.selection.setNode(node);
inspector.sidebar.once("ruleview-ready", testCompletion);
});
}
function testCompletion()
{
ruleViewWindow = inspector.sidebar.getWindowForTab("ruleview");
let brace = ruleViewWindow.document.querySelector(".ruleview-propertyname");
waitForEditorFocus(brace.parentNode, function onNewElement(aEditor) {
editor = aEditor;
editor.input.addEventListener("keypress", checkState, false);
checkStateAndMoveOn(0);
});
brace.click();
}
function checkStateAndMoveOn(index) {
if (index == testData.length) {
finishUp();
return;
}
let [key] = testData[index];
state = index;
info("pressing key " + key + " to get result: [" + testData[index].slice(1) +
"] for state " + state);
EventUtils.synthesizeKey(key, {}, ruleViewWindow);
}
function checkState(event) {
// The keypress handler is async and can take some time to compute, filter and
// display the popup and autocompletion so we need to use setTimeout here.
window.setTimeout(function() {
info("After keypress for state " + state);
let [key, completion, index, total] = testData[state];
if (completion != null) {
is(editor.input.value, completion,
"Correct value is autocompleted for state " + state);
}
if (total == 0) {
ok(!(editor.popup && editor.popup.isOpen), "Popup is closed for state " +
state);
}
else {
ok(editor.popup._panel.state == "open" ||
editor.popup._panel.state == "showing",
"Popup is open for state " + state);
is(editor.popup.getItems().length, total,
"Number of suggestions match for state " + state);
is(editor.popup.selectedIndex, index,
"Correct item is selected for state " + state);
}
checkStateAndMoveOn(state + 1);
}, 200);
}
function finishUp()
{
doc = inspector = editor = ruleViewWindow = state = null;
gBrowser.removeCurrentTab();
finish();
}
function test()
{
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
doc = content.document;
doc.title = "Rule View Test";
waitForFocus(openRuleView, content);
}, true);
content.location = "data:text/html,<h1 style='border: 1px solid red'>Filename" +
": browser_bug893965_css_property_completion_existing_property.js</h1>";
}

View File

@ -0,0 +1,137 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that CSS property names are autocompleted and cycled correctly.
const MAX_ENTRIES = 10;
let doc;
let inspector;
let ruleViewWindow;
let editor;
let state;
// format :
// [
// what key to press,
// expected input box value after keypress,
// selectedIndex of the popup,
// total items in the popup
// ]
let testData = [
["d", "direction", 0, 3],
["VK_DOWN", "display", 1, 3],
["VK_DOWN", "dominant-baseline", 2, 3],
["VK_DOWN", "direction", 0, 3],
["VK_DOWN", "display", 1, 3],
["VK_UP", "direction", 0, 3],
["VK_UP", "dominant-baseline", 2, 3],
["VK_UP", "display", 1, 3],
["VK_BACK_SPACE", "displa", -1, 0],
["VK_BACK_SPACE", "displ", -1, 0],
["VK_BACK_SPACE", "disp", -1, 0],
["VK_BACK_SPACE", "dis", -1, 0],
["VK_BACK_SPACE", "di", -1, 0],
["VK_BACK_SPACE", "d", -1, 0],
["i", "direction", 0, 2],
["s", "display", -1, 0],
["VK_BACK_SPACE", "dis", -1, 0],
["VK_BACK_SPACE", "di", -1, 0],
["VK_BACK_SPACE", "d", -1, 0],
["VK_BACK_SPACE", "", -1, 0],
["f", "fill", 0, MAX_ENTRIES],
["i", "fill", 0, 4],
["VK_ESCAPE", null, -1, 0],
];
function openRuleView()
{
var target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
inspector = toolbox.getCurrentPanel();
inspector.sidebar.select("ruleview");
// Highlight a node.
let node = content.document.getElementsByTagName("h1")[0];
inspector.selection.setNode(node);
inspector.sidebar.once("ruleview-ready", testCompletion);
});
}
function testCompletion()
{
ruleViewWindow = inspector.sidebar.getWindowForTab("ruleview");
let brace = ruleViewWindow.document.querySelector(".ruleview-ruleclose");
waitForEditorFocus(brace.parentNode, function onNewElement(aEditor) {
editor = aEditor;
editor.input.addEventListener("keypress", checkState, false);
checkStateAndMoveOn(0);
});
brace.click();
}
function checkStateAndMoveOn(index) {
if (index == testData.length) {
finishUp();
return;
}
let [key] = testData[index];
state = index;
info("pressing key " + key + " to get result: [" + testData[index].slice(1) +
"] for state " + state);
EventUtils.synthesizeKey(key, {}, ruleViewWindow);
}
function checkState(event) {
// The keypress handler is async and can take some time to compute, filter and
// display the popup and autocompletion so we need to use setTimeout here.
window.setTimeout(function() {
info("After keypress for state " + state);
let [key, completion, index, total] = testData[state];
if (completion != null) {
is(editor.input.value, completion,
"Correct value is autocompleted for state " + state);
}
if (total == 0) {
ok(!(editor.popup && editor.popup.isOpen), "Popup is closed for state " +
state);
}
else {
ok(editor.popup._panel.state == "open" ||
editor.popup._panel.state == "showing",
"Popup is open for state " + state);
is(editor.popup.getItems().length, total,
"Number of suggestions match for state " + state);
is(editor.popup.selectedIndex, index,
"Correct item is selected for state " + state);
}
checkStateAndMoveOn(state + 1);
}, 200);
}
function finishUp()
{
doc = inspector = editor = ruleViewWindow = state = null;
gBrowser.removeCurrentTab();
finish();
}
function test()
{
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
doc = content.document;
doc.title = "Rule View Test";
waitForFocus(openRuleView, content);
}, true);
content.location = "data:text/html,<h1 style='border: 1px solid red'>Filename:" +
"browser_bug893965_css_property_completion_new_property.js</h1>";
}

View File

@ -69,7 +69,6 @@ function consoleOpened(aHud) {
is(popup.selectedIndex, 17,
"Index of the first item from bottom is selected.");
EventUtils.synthesizeKey("VK_DOWN", {});
EventUtils.synthesizeKey("VK_DOWN", {});
let prefix = jsterm.inputNode.value.replace(/[\S]/g, " ");
@ -123,7 +122,6 @@ function popupHideAfterTab()
is(popup.selectedIndex, 17, "First index from bottom is selected");
EventUtils.synthesizeKey("VK_DOWN", {});
EventUtils.synthesizeKey("VK_DOWN", {});
let prefix = jsterm.inputNode.value.replace(/[\S]/g, " ");
@ -169,7 +167,6 @@ function testReturnKey()
is(popup.selectedIndex, 17, "First index from bottom is selected");
EventUtils.synthesizeKey("VK_DOWN", {});
EventUtils.synthesizeKey("VK_DOWN", {});
let prefix = jsterm.inputNode.value.replace(/[\S]/g, " ");
@ -292,7 +289,6 @@ function testCompletionInText()
ok(popup.isOpen, "popup is open");
is(popup.itemCount, 2, "popup.itemCount is correct");
EventUtils.synthesizeKey("VK_DOWN", {});
EventUtils.synthesizeKey("VK_DOWN", {});
is(popup.selectedIndex, 0, "popup.selectedIndex is correct");
ok(!completeNode.value, "completeNode.value is empty");

View File

@ -64,10 +64,10 @@ function consoleOpened(HUD) {
is(popup.selectedIndex, 2, "index 2 is selected");
is(popup.selectedItem, items[2], "item2 is selected");
ok(!popup.selectNextItem(), "selectPreviousItem() works");
ok(popup.selectNextItem(), "selectPreviousItem() works");
is(popup.selectedIndex, -1, "no index is selected");
ok(!popup.selectedItem, "no item is selected");
is(popup.selectedIndex, 0, "index 0 is selected");
is(popup.selectedItem, items[0], "item0 is selected");
items.push({label: "label3", value: "value3"});
popup.appendItem(items[3]);

View File

@ -66,21 +66,21 @@ function testCompletion(hud) {
yield undefined;
is(input.value, "document.getElem", "'document.getElem' completion");
is(jsterm.completeNode.value, "", "'document.getElem' completion");
is(jsterm.completeNode.value, " entsByTagNameNS", "'document.getElem' completion");
// Test pressing tab another time.
jsterm.complete(jsterm.COMPLETE_FORWARD, testNext);
yield undefined;
is(input.value, "document.getElem", "'document.getElem' completion");
is(jsterm.completeNode.value, " entsByTagNameNS", "'document.getElem' another tab completion");
is(jsterm.completeNode.value, " entsByTagName", "'document.getElem' another tab completion");
// Test pressing shift_tab.
jsterm.complete(jsterm.COMPLETE_BACKWARD, testNext);
yield undefined;
is(input.value, "document.getElem", "'document.getElem' untab completion");
is(jsterm.completeNode.value, "", "'document.getElem' completion");
is(jsterm.completeNode.value, " entsByTagNameNS", "'document.getElem' completion");
jsterm.clearOutput();