Bug 917863 - Add XUL context menu back into rule and computed views. r=jwalker

This commit is contained in:
Michael Ratcliffe 2013-09-20 01:15:34 +01:00
parent 886d4ca125
commit 5306e29c30
6 changed files with 416 additions and 110 deletions

View File

@ -139,11 +139,15 @@ function CssHtmlTree(aStyleInspector, aPageStyle)
this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
// Create bound methods.
this.siFocusWindow = this.focusWindow.bind(this);
this.siBoundCopy = this.computedViewCopy.bind(this);
this.focusWindow = this.focusWindow.bind(this);
this._onContextMenu = this._onContextMenu.bind(this);
this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
this._onSelectAll = this._onSelectAll.bind(this);
this._onCopy = this._onCopy.bind(this);
this.styleDocument.addEventListener("copy", this.siBoundCopy);
this.styleDocument.addEventListener("mousedown", this.siFocusWindow);
this.styleDocument.addEventListener("copy", this._onCopy);
this.styleDocument.addEventListener("mousedown", this.focusWindow);
this.styleDocument.addEventListener("contextmenu", this._onContextMenu);
// Nodes used in templating
this.root = this.styleDocument.getElementById("root");
@ -161,6 +165,8 @@ function CssHtmlTree(aStyleInspector, aPageStyle)
// The element that we're inspecting, and the document that it comes from.
this.viewedElement = null;
this._buildContextMenu();
this.createStyleViews();
}
@ -466,32 +472,128 @@ CssHtmlTree.prototype = {
*
* @param aEvent The event object
*/
focusWindow: function si_focusWindow(aEvent)
focusWindow: function(aEvent)
{
let win = this.styleDocument.defaultView;
win.focus();
},
/**
* Copy selected text.
*
* @param aEvent The event object
* Create a context menu.
*/
computedViewCopy: function si_computedViewCopy(aEvent)
_buildContextMenu: function()
{
let doc = this.styleDocument.defaultView.parent.document;
this._contextmenu = this.styleDocument.createElementNS(XUL_NS, "menupopup");
this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
this._contextmenu.id = "computed-view-context-menu";
// Select All
this.menuitemSelectAll = createMenuItem(this._contextmenu, {
label: "computedView.contextmenu.selectAll",
accesskey: "computedView.contextmenu.selectAll.accessKey",
command: this._onSelectAll
});
// Copy
this.menuitemCopy = createMenuItem(this._contextmenu, {
label: "computedView.contextmenu.copy",
accesskey: "computedView.contextmenu.copy.accessKey",
command: this._onCopy
});
let popupset = doc.documentElement.querySelector("popupset");
if (!popupset) {
popupset = doc.createElementNS(XUL_NS, "popupset");
doc.documentElement.appendChild(popupset);
}
popupset.appendChild(this._contextmenu);
},
/**
* Update the context menu. This means enabling or disabling menuitems as
* appropriate.
*/
_contextMenuUpdate: function()
{
let win = this.styleDocument.defaultView;
let text = win.getSelection().toString();
let disable = win.getSelection().isCollapsed;
this.menuitemCopy.disabled = disable;
},
// Tidy up block headings by moving CSS property names and their values onto
// the same line and inserting a colon between them.
text = text.replace(/(.+)\r\n(.+)/g, "$1: $2;");
text = text.replace(/(.+)\n(.+)/g, "$1: $2;");
/**
* Context menu handler.
*/
_onContextMenu: function(event) {
try {
this.styleDocument.defaultView.focus();
let outerDoc = this.styleInspector.outerIFrame.ownerDocument;
clipboardHelper.copyString(text, outerDoc);
this._contextmenu.openPopup(
event.target.ownerDocument.documentElement,
"overlap", event.clientX, event.clientY, true, false, null);
} catch(e) {
console.error(e);
}
},
if (aEvent) {
aEvent.preventDefault();
/**
* Select all text.
*/
_onSelectAll: function()
{
try {
let win = this.styleDocument.defaultView;
let selection = win.getSelection();
selection.selectAllChildren(this.styleDocument.documentElement);
} catch(e) {
console.error(e);
}
},
/**
* Copy selected text.
*
* @param event The event object
*/
_onCopy: function(event)
{
try {
let win = this.styleDocument.defaultView;
let text = win.getSelection().toString().trim();
// Tidy up block headings by moving CSS property names and their values onto
// the same line and inserting a colon between them.
let textArray = text.split(/[\r\n]+/);
let result = "";
// Parse text array to output string.
if (textArray.length > 1) {
for (let prop of textArray) {
if (CssHtmlTree.propertyNames.indexOf(prop) !== -1) {
// Property name
result += prop;
} else {
// Property value
result += ": " + prop;
if (result.length > 0) {
result += ";\n";
}
}
}
} else {
// Short text fragment.
result = textArray[0];
}
clipboardHelper.copyString(result, this.styleDocument);
if (event) {
event.preventDefault();
}
} catch(e) {
console.error(e);
}
},
@ -517,32 +619,25 @@ CssHtmlTree.prototype = {
}
// Remove context menu
let outerDoc = this.styleInspector.outerIFrame.ownerDocument;
let menu = outerDoc.querySelector("#computed-view-context-menu");
if (menu) {
// Copy selected
let menuitem = outerDoc.querySelector("#computed-view-copy");
menuitem.removeEventListener("command", this.siBoundCopy);
if (this._contextmenu) {
// Destroy the Select All menuitem.
this.menuitemCopy.removeEventListener("command", this._onCopy);
this.menuitemCopy = null;
// Copy property
menuitem = outerDoc.querySelector("#computed-view-copy-declaration");
menuitem.removeEventListener("command", this.siBoundCopyDeclaration);
// Destroy the Copy menuitem.
this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
this.menuitemSelectAll = null;
// Copy property name
menuitem = outerDoc.querySelector("#computed-view-copy-property");
menuitem.removeEventListener("command", this.siBoundCopyProperty);
// Copy property value
menuitem = outerDoc.querySelector("#computed-view-copy-property-value");
menuitem.removeEventListener("command", this.siBoundCopyPropertyValue);
menu.removeEventListener("popupshowing", this.siBoundMenuUpdate);
menu.parentNode.removeChild(menu);
// Destroy the context menu.
this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
this._contextmenu.parentNode.removeChild(this._contextmenu);
this._contextmenu = null;
}
// Remove bound listeners
this.styleDocument.removeEventListener("copy", this.siBoundCopy);
this.styleDocument.removeEventListener("mousedown", this.siFocusWindow);
this.styleDocument.removeEventListener("contextmenu", this._onContextMenu);
this.styleDocument.removeEventListener("copy", this._onCopy);
this.styleDocument.removeEventListener("mousedown", this.focusWindow);
// Nodes used in templating
delete this.root;
@ -573,6 +668,19 @@ PropertyInfo.prototype = {
}
};
function createMenuItem(aMenu, aAttributes)
{
let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
item.setAttribute("label", CssHtmlTree.l10n(aAttributes.label));
item.setAttribute("accesskey", CssHtmlTree.l10n(aAttributes.accesskey));
item.addEventListener("command", aAttributes.command);
aMenu.appendChild(item);
return item;
}
/**
* A container to give easy access to property data from the template engine.
*

View File

@ -1047,8 +1047,12 @@ function CssRuleView(aDoc, aStore, aPageStyle)
this.element.className = "ruleview devtools-monospace";
this.element.flex = 1;
this._boundCopy = this._onCopy.bind(this);
this.element.addEventListener("copy", this._boundCopy);
this._buildContextMenu = this._buildContextMenu.bind(this);
this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
this._onSelectAll = this._onSelectAll.bind(this);
this._onCopy = this._onCopy.bind(this);
this.element.addEventListener("copy", this._onCopy);
this._handlePrefChange = this._handlePrefChange.bind(this);
gDevTools.on("pref-changed", this._handlePrefChange);
@ -1060,6 +1064,7 @@ function CssRuleView(aDoc, aStore, aPageStyle)
};
this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options);
this._buildContextMenu();
this._showEmpty();
}
@ -1069,6 +1074,118 @@ CssRuleView.prototype = {
// The element that we're inspecting.
_viewedElement: null,
/**
* Build the context menu.
*/
_buildContextMenu: function() {
let doc = this.doc.defaultView.parent.document;
this._contextmenu = doc.createElementNS(XUL_NS, "menupopup");
this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
this._contextmenu.id = "rule-view-context-menu";
this.menuitemSelectAll = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.selectAll",
accesskey: "ruleView.contextmenu.selectAll.accessKey",
command: this._onSelectAll
});
this.menuitemCopy = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.copy",
accesskey: "ruleView.contextmenu.copy.accessKey",
command: this._onCopy
});
let popupset = doc.documentElement.querySelector("popupset");
if (!popupset) {
popupset = doc.createElementNS(XUL_NS, "popupset");
doc.documentElement.appendChild(popupset);
}
popupset.appendChild(this._contextmenu);
},
/**
* Update the context menu. This means enabling or disabling menuitems as
* appropriate.
*/
_contextMenuUpdate: function() {
let win = this.doc.defaultView;
// Copy selection.
let selection = win.getSelection();
let copy;
if (selection.toString()) {
// Panel text selected
copy = true;
} else if (selection.anchorNode) {
// input type="text"
let { selectionStart, selectionEnd } = this.doc.popupNode;
if (isFinite(selectionStart) && isFinite(selectionEnd) &&
selectionStart !== selectionEnd) {
copy = true;
}
} else {
// No text selected, disable copy.
copy = false;
}
this.menuitemCopy.disabled = !copy;
},
/**
* Select all text.
*/
_onSelectAll: function()
{
let win = this.doc.defaultView;
let selection = win.getSelection();
selection.selectAllChildren(this.doc.documentElement);
},
/**
* Copy selected text from the rule view.
*
* @param {Event} event
* The event object.
*/
_onCopy: function(event)
{
try {
let target = event.target;
let text;
if (event.target.nodeName === "menuitem") {
target = this.doc.popupNode;
}
if (target.nodeName == "input") {
let start = Math.min(target.selectionStart, target.selectionEnd);
let end = Math.max(target.selectionStart, target.selectionEnd);
let count = end - start;
text = target.value.substr(start, count);
} else {
let win = this.doc.defaultView;
text = win.getSelection().toString();
// Remove any double newlines.
text = text.replace(/(\r?\n)\r?\n/g, "$1");
// Remove "inline"
let inline = _strings.GetStringFromName("rule.sourceInline");
let rx = new RegExp("^" + inline + "\\r?\\n?", "g");
text = text.replace(rx, "");
}
clipboardHelper.copyString(text, this.doc);
event.preventDefault();
} catch(e) {
console.error(e);
}
},
setPageStyle: function(aPageStyle) {
this.pageStyle = aPageStyle;
},
@ -1094,8 +1211,27 @@ CssRuleView.prototype = {
gDevTools.off("pref-changed", this._handlePrefChange);
this.element.removeEventListener("copy", this._boundCopy);
delete this._boundCopy;
this.element.removeEventListener("copy", this._onCopy);
delete this._onCopy;
// Remove context menu
if (this._contextmenu) {
// Destroy the Select All menuitem.
this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
this.menuitemSelectAll = null;
// Destroy the Copy menuitem.
this.menuitemCopy.removeEventListener("command", this._onCopy);
this.menuitemCopy = null;
// Destroy the context menu.
this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
this._contextmenu.parentNode.removeChild(this._contextmenu);
this._contextmenu = null;
}
// We manage the popupNode ourselves so we also need to destroy it.
this.doc.popupNode = null;
if (this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
@ -1342,41 +1478,6 @@ CssRuleView.prototype = {
this.togglePseudoElementVisibility(this.showPseudoElements);
},
/**
* Copy selected text from the rule view.
*
* @param {Event} aEvent
* The event object.
*/
_onCopy: function CssRuleView_onCopy(aEvent)
{
let target = aEvent.target;
let text;
if (target.nodeName == "input") {
let start = Math.min(target.selectionStart, target.selectionEnd);
let end = Math.max(target.selectionStart, target.selectionEnd);
let count = end - start;
text = target.value.substr(start, count);
} else {
let win = this.doc.defaultView;
text = win.getSelection().toString();
// Remove any double newlines.
text = text.replace(/(\r?\n)\r?\n/g, "$1");
// Remove "inline"
let inline = _strings.GetStringFromName("rule.sourceInline");
let rx = new RegExp("^" + inline + "\\r?\\n?", "g");
text = text.replace(rx, "");
}
clipboardHelper.copyString(text, this.doc);
aEvent.preventDefault();
},
};
/**
@ -1458,6 +1559,22 @@ RuleEditor.prototype = {
this.doc.defaultView.focus();
}.bind(this), false);
this.element.addEventListener("contextmenu", event => {
try {
// In the sidebar we do not have this.doc.popupNode so we need to save
// the node ourselves.
this.doc.popupNode = event.explicitOriginalTarget;
let win = this.doc.defaultView;
win.focus();
this.ruleView._contextmenu.openPopup(
event.target.ownerDocument.documentElement,
"overlap", event.clientX, event.clientY, true, false, null);
} catch(e) {
console.error(e);
}
}, false);
this.propertyList = createChild(code, "ul", {
class: "ruleview-propertylist"
});
@ -2196,6 +2313,7 @@ function createChild(aParent, aTag, aAttributes)
function createMenuItem(aMenu, aAttributes)
{
let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
item.setAttribute("label", _strings.GetStringFromName(aAttributes.label));
item.setAttribute("accesskey", _strings.GetStringFromName(aAttributes.accesskey));
item.addEventListener("command", aAttributes.command);

View File

@ -32,7 +32,7 @@ function createDocument()
'</div>';
doc.title = "Computed view context menu test";
openComputedView(selectNode)
openComputedView(selectNode);
}
function selectNode(aInspector, aComputedView)
@ -64,24 +64,51 @@ function checkCopySelection()
ok(props, "captain, we have the property-view nodes");
let range = document.createRange();
range.setStart(props[0], 0);
range.setStart(props[1], 0);
range.setEnd(props[3], 3);
win.getSelection().addRange(range);
info("Checking that cssHtmlTree.siBoundCopy() " +
" returns the correct clipboard value");
let expectedPattern = "color: #FF0;[\\r\\n]+" +
"font-family: helvetica,sans-serif;[\\r\\n]+" +
"font-size: 16px;[\\r\\n]+" +
"font-variant: small-caps;[\\r\\n]*";
let expectedPattern = "font-family: helvetica,sans-serif;[\\r\\n]+" +
"font-size: 16px;[\\r\\n]+" +
"font-variant: small-caps;[\\r\\n]*";
SimpleTest.waitForClipboard(function CS_boundCopyCheck() {
return checkClipboardData(expectedPattern);
},
function() {fireCopyEvent(props[0])}, closeStyleInspector, function() {
failedClipboard(expectedPattern, closeStyleInspector);
});
return checkClipboardData(expectedPattern);
},
function() {
fireCopyEvent(props[0]);
},
checkSelectAll,
function() {
failedClipboard(expectedPattern, checkSelectAll);
});
}
function checkSelectAll()
{
let contentDoc = computedView.styleDocument;
let prop = contentDoc.querySelector(".property-view");
info("Checking that _SelectAll() then copy returns the correct clipboard value");
computedView._onSelectAll();
let expectedPattern = "color: #FF0;[\\r\\n]+" +
"font-family: helvetica,sans-serif;[\\r\\n]+" +
"font-size: 16px;[\\r\\n]+" +
"font-variant: small-caps;[\\r\\n]*";
SimpleTest.waitForClipboard(function() {
return checkClipboardData(expectedPattern);
},
function() {
fireCopyEvent(prop);
},
finishUp,
function() {
failedClipboard(expectedPattern, finishUp);
});
}
function checkClipboardData(aExpectedPattern)
@ -113,11 +140,6 @@ function failedClipboard(aExpectedPattern, aCallback)
aCallback();
}
function closeStyleInspector()
{
finishUp();
}
function finishUp()
{
computedView = doc = win = null;

View File

@ -56,18 +56,17 @@ function highlightNode()
function checkCopySelection()
{
let contentDoc = win.document;
let props = contentDoc.querySelectorAll(".ruleview-property");
let prop = contentDoc.querySelector(".ruleview-property");
let values = contentDoc.querySelectorAll(".ruleview-propertycontainer");
let range = document.createRange();
range.setStart(props[0], 0);
let range = contentDoc.createRange();
range.setStart(prop, 0);
range.setEnd(values[4], 2);
let selection = win.getSelection();
selection.addRange(range);
info("Checking that _boundCopy() returns the correct " +
"clipboard value");
info("Checking that _Copy() returns the correct clipboard value");
let expectedPattern = " margin: 10em;[\\r\\n]+" +
" font-size: 14pt;[\\r\\n]+" +
" font-family: helvetica,sans-serif;[\\r\\n]+" +
@ -76,20 +75,47 @@ function checkCopySelection()
"html {[\\r\\n]+" +
" color: #000;[\\r\\n]*";
SimpleTest.waitForClipboard(function IUI_boundCopyCheck() {
SimpleTest.waitForClipboard(function() {
return checkClipboardData(expectedPattern);
},function() {fireCopyEvent(props[0])}, finishup, function() {
failedClipboard(expectedPattern, finishup);
},
function() {
fireCopyEvent(prop);
},
checkSelectAll,
function() {
failedClipboard(expectedPattern, checkSelectAll);
});
}
function selectNode(aNode) {
let doc = aNode.ownerDocument;
let win = doc.defaultView;
let range = doc.createRange();
function checkSelectAll()
{
let contentDoc = win.document;
let _ruleView = ruleView();
let prop = contentDoc.querySelector(".ruleview-property");
range.selectNode(aNode);
win.getSelection().addRange(range);
info("Checking that _SelectAll() then copy returns the correct clipboard value");
_ruleView._onSelectAll();
let expectedPattern = "[\\r\\n]+" +
"element {[\\r\\n]+" +
" margin: 10em;[\\r\\n]+" +
" font-size: 14pt;[\\r\\n]+" +
" font-family: helvetica,sans-serif;[\\r\\n]+" +
" color: #AAA;[\\r\\n]+" +
"}[\\r\\n]+" +
"html {[\\r\\n]+" +
" color: #000;[\\r\\n]+" +
"}[\\r\\n]*";
SimpleTest.waitForClipboard(function() {
return checkClipboardData(expectedPattern);
},
function() {
fireCopyEvent(prop);
},
finishup,
function() {
failedClipboard(expectedPattern, finishup);
});
}
function checkClipboardData(aExpectedPattern)

View File

@ -64,8 +64,8 @@ function openComputedView(callback)
openInspector(inspector => {
inspector.sidebar.once("computedview-ready", () => {
inspector.sidebar.select("computedview");
let ruleView = inspector.sidebar.getWindowForTab("computedview").computedview.view;
callback(inspector, ruleView);
let computedView = inspector.sidebar.getWindowForTab("computedview").computedview.view;
callback(inspector, computedView);
})
});
}

View File

@ -57,3 +57,35 @@ rule.warning.title=Invalid property value
# LOCALIZATION NOTE (ruleView.empty): Text displayed when the highlighter is
# first opened and there's no node selected in the rule view.
rule.empty=No element selected.
# LOCALIZATION NOTE (ruleView.contextmenu.selectAll): Text displayed in the
# rule view context menu.
ruleView.contextmenu.selectAll=Select all
# LOCALIZATION NOTE (ruleView.contextmenu.selectAll.accessKey): Access key for
# the rule view context menu "Select all" entry.
ruleView.contextmenu.selectAll.accessKey=A
# LOCALIZATION NOTE (ruleView.contextmenu.copy): Text displayed in the rule view
# context menu.
ruleView.contextmenu.copy=Copy
# LOCALIZATION NOTE (ruleView.contextmenu.copy.accessKey): Access key for
# the rule view context menu "Select all" entry.
ruleView.contextmenu.copy.accessKey=C
# LOCALIZATION NOTE (computedView.contextmenu.selectAll): Text displayed in the
# computed view context menu.
computedView.contextmenu.selectAll=Select all
# LOCALIZATION NOTE (computedView.contextmenu.selectAll.accessKey): Access key for
# the computed view context menu "Select all" entry.
computedView.contextmenu.selectAll.accessKey=A
# LOCALIZATION NOTE (computedView.contextmenu.copy): Text displayed in the
# computed view context menu.
computedView.contextmenu.copy=Copy
# LOCALIZATION NOTE (computedView.contextmenu.copy.accessKey): Access key for
# the computed view context menu "Select all" entry.
computedView.contextmenu.copy.accessKey=C