Bug 1014547 - Add a css transform highlighter to the style-inspector; r=bgrins

This commit is contained in:
Patrick Brosset 2014-06-13 16:27:10 +02:00
parent 8b0f429a75
commit f7fa4585a3
41 changed files with 2111 additions and 1220 deletions

View File

@ -71,3 +71,10 @@ html|*.highlighter-nodeinfobar-tagname {
html|*.highlighter-nodeinfobar-tagname {
text-transform: lowercase;
}
/*
* Css transform highlighter
*/
svg|svg.css-transform-root[hidden] {
display: none;
}

View File

@ -0,0 +1,286 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cc, Ci, Cu} = require("chrome");
const {Promise: promise} = require("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource:///modules/devtools/gDevTools.jsm");
/**
* Client-side highlighter shared module.
* To be used by toolbox panels that need to highlight DOM elements.
*
* Highlighting and selecting elements is common enough that it needs to be at
* toolbox level, accessible by any panel that needs it.
* That's why the toolbox is the one that initializes the inspector and
* highlighter. It's also why the API returned by this module needs a reference
* to the toolbox which should be set once only.
*/
/**
* Get the highighterUtils instance for a given toolbox.
* This should be done once only by the toolbox itself and stored there so that
* panels can get it from there. That's because the API returned has a stateful
* scope that would be different for another instance returned by this function.
*
* @param {Toolbox} toolbox
* @return {Object} the highlighterUtils public API
*/
exports.getHighlighterUtils = function(toolbox) {
if (!toolbox || !toolbox.target) {
throw new Error("Missing or invalid toolbox passed to getHighlighterUtils");
return;
}
// Exported API properties will go here
let exported = {};
// The current toolbox target
let target = toolbox.target;
// Is the highlighter currently in pick mode
let isPicking = false;
/**
* Release this utils, nullifying the references to the toolbox
*/
exported.release = function() {
toolbox = target = null;
}
/**
* Does the target have the highlighter actor.
* The devtools must be backwards compatible with at least B2G 1.3 (28),
* which doesn't have the highlighter actor. This can be removed as soon as
* the minimal supported version becomes 1.4 (29)
*/
let isRemoteHighlightable = exported.isRemoteHighlightable = function() {
return target.client.traits.highlightable;
}
/**
* Does the target support custom highlighters.
*/
let supportsCustomHighlighters = function() {
return !!target.client.traits.customHighlighters;
}
/**
* Is typeName a known custom highlighter
* @param {String} typeName
* @return {Boolean}
*/
let hasCustomHighlighter = exported.hasCustomHighlighter = function(typeName) {
return supportsCustomHighlighters() &&
target.client.traits.customHighlighters.indexOf(typeName) !== -1;
}
/**
* Make a function that initializes the inspector before it runs.
* Since the init of the inspector is asynchronous, the return value will be
* produced by Task.async and the argument should be a generator
* @param {Function*} generator A generator function
* @return {Function} A function
*/
let isInspectorInitialized = false;
let requireInspector = generator => {
return Task.async(function*(...args) {
if (!isInspectorInitialized) {
yield toolbox.initInspector();
isInspectorInitialized = true;
}
return yield generator.apply(null, args);
});
};
/**
* Start/stop the element picker on the debuggee target.
* @return A promise that resolves when done
*/
let togglePicker = exported.togglePicker = function() {
if (isPicking) {
return stopPicker();
} else {
return startPicker();
}
}
/**
* Start the element picker on the debuggee target.
* This will request the inspector actor to start listening for mouse events
* on the target page to highlight the hovered/picked element.
* Depending on the server-side capabilities, this may fire events when nodes
* are hovered.
* @return A promise that resolves when the picker has started or immediately
* if it is already started
*/
let startPicker = exported.startPicker = requireInspector(function*() {
if (isPicking) {
return;
}
isPicking = true;
toolbox.pickerButtonChecked = true;
yield toolbox.selectTool("inspector");
toolbox.on("select", stopPicker);
if (isRemoteHighlightable()) {
toolbox.walker.on("picker-node-hovered", onPickerNodeHovered);
toolbox.walker.on("picker-node-picked", onPickerNodePicked);
yield toolbox.highlighter.pick();
toolbox.emit("picker-started");
} else {
// If the target doesn't have the highlighter actor, we can use the
// walker's pick method instead, knowing that it only responds when a node
// is picked (instead of emitting events)
toolbox.emit("picker-started");
let node = yield toolbox.walker.pick();
onPickerNodePicked({node: node});
}
});
/**
* Stop the element picker. Note that the picker is automatically stopped when
* an element is picked
* @return A promise that resolves when the picker has stopped or immediately
* if it is already stopped
*/
let stopPicker = exported.stopPicker = requireInspector(function*() {
if (!isPicking) {
return;
}
isPicking = false;
toolbox.pickerButtonChecked = false;
if (isRemoteHighlightable()) {
yield toolbox.highlighter.cancelPick();
toolbox.walker.off("picker-node-hovered", onPickerNodeHovered);
toolbox.walker.off("picker-node-picked", onPickerNodePicked);
} else {
// If the target doesn't have the highlighter actor, use the walker's
// cancelPick method instead
yield toolbox.walker.cancelPick();
}
toolbox.off("select", stopPicker);
toolbox.emit("picker-stopped");
});
/**
* When a node is hovered by the mouse when the highlighter is in picker mode
* @param {Object} data Information about the node being hovered
*/
function onPickerNodeHovered(data) {
toolbox.emit("picker-node-hovered", data.node);
}
/**
* When a node has been picked while the highlighter is in picker mode
* @param {Object} data Information about the picked node
*/
function onPickerNodePicked(data) {
toolbox.selection.setNodeFront(data.node, "picker-node-picked");
stopPicker();
}
/**
* Show the box model highlighter on a node in the content page.
* The node needs to be a NodeFront, as defined by the inspector actor
* @see toolkit/devtools/server/actors/inspector.js
* @param {NodeFront} nodeFront The node to highlight
* @param {Object} options
* @return A promise that resolves when the node has been highlighted
*/
let highlightNodeFront = exported.highlightNodeFront = requireInspector(
function*(nodeFront, options={}) {
if (!nodeFront) {
return;
}
if (isRemoteHighlightable()) {
yield toolbox.highlighter.showBoxModel(nodeFront, options);
} else {
// If the target doesn't have the highlighter actor, revert to the
// walker's highlight method, which draws a simple outline
yield toolbox.walker.highlight(nodeFront);
}
toolbox.emit("node-highlight", nodeFront);
});
/**
* This is a convenience method in case you don't have a nodeFront but a
* valueGrip. This is often the case with VariablesView properties.
* This method will simply translate the grip into a nodeFront and call
* highlightNodeFront, so it has the same signature.
* @see highlightNodeFront
*/
let highlightDomValueGrip = exported.highlightDomValueGrip = requireInspector(
function*(valueGrip, options={}) {
let nodeFront = yield gripToNodeFront(valueGrip);
if (nodeFront) {
yield highlightNodeFront(nodeFront, options);
} else {
throw new Error("The ValueGrip passed could not be translated to a NodeFront");
}
});
/**
* Translate a debugger value grip into a node front usable by the inspector
* @param {ValueGrip}
* @return a promise that resolves to the node front when done
*/
let gripToNodeFront = exported.gripToNodeFront = requireInspector(
function*(grip) {
return yield toolbox.walker.getNodeActorFromObjectActor(grip.actor);
});
/**
* Hide the highlighter.
* @param {Boolean} forceHide Only really matters in test mode (when
* gDevTools.testing is true). In test mode, hovering over several nodes in
* the markup view doesn't hide/show the highlighter to ease testing. The
* highlighter stays visible at all times, except when the mouse leaves the
* markup view, which is when this param is passed to true
* @return a promise that resolves when the highlighter is hidden
*/
let unhighlight = exported.unhighlight = Task.async(
function*(forceHide=false) {
forceHide = forceHide || !gDevTools.testing;
// Note that if isRemoteHighlightable is true, there's no need to hide the
// highlighter as the walker uses setTimeout to hide it after some time
if (forceHide && toolbox.highlighter && isRemoteHighlightable()) {
yield toolbox.highlighter.hideBoxModel();
}
toolbox.emit("node-unhighlight");
});
/**
* If the main, box-model, highlighter isn't enough, or if multiple
* highlighters are needed in parallel, this method can be used to return a
* new instance of a highlighter actor, given a type.
* The type of the highlighter passed must be known by the server.
* The highlighter actor returned will have the show(nodeFront) and hide()
* methods and needs to be released by the consumer when not needed anymore.
* @return a promise that resolves to the highlighter
*/
let getHighlighterByType = exported.getHighlighterByType = requireInspector(
function*(typeName) {
if (hasCustomHighlighter(typeName)) {
return yield toolbox.inspector.getHighlighterByType(typeName);
} else {
throw "The target doesn't support creating highlighters by types or " +
typeName + " is unknown";
}
});
// Return the public API
return exported;
};

View File

@ -13,6 +13,7 @@ let {Cc, Ci, Cu} = require("chrome");
let {Promise: promise} = require("resource://gre/modules/Promise.jsm");
let EventEmitter = require("devtools/toolkit/event-emitter");
let Telemetry = require("devtools/shared/telemetry");
let {getHighlighterUtils} = require("devtools/framework/toolbox-highlighter-utils");
let HUDService = require("devtools/webconsole/hudservice");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -68,7 +69,7 @@ function Toolbox(target, selectedTool, hostType, hostOptions) {
this._refreshHostTitle = this._refreshHostTitle.bind(this);
this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this)
this.destroy = this.destroy.bind(this);
this.highlighterUtils = new ToolboxHighlighterUtils(this);
this.highlighterUtils = getHighlighterUtils(this);
this._highlighterReady = this._highlighterReady.bind(this);
this._highlighterHidden = this._highlighterHidden.bind(this);
@ -187,13 +188,11 @@ Toolbox.prototype = {
/**
* Get the toolbox highlighter front. Note that it may not always have been
* initialized first. Use `initInspector()` if needed.
* Consider using highlighterUtils instead, it exposes the highlighter API in
* a useful way for the toolbox panels
*/
get highlighter() {
if (this.highlighterUtils.isRemoteHighlightable) {
return this._highlighter;
} else {
return null;
}
return this._highlighter;
},
/**
@ -581,6 +580,18 @@ Toolbox.prototype = {
this._pickerButton.addEventListener("command", this._togglePicker, false);
},
/**
* Setter for the checked state of the picker button in the toolbar
* @param {Boolean} isChecked
*/
set pickerButtonChecked(isChecked) {
if (isChecked) {
this._pickerButton.setAttribute("checked", "true");
} else {
this._pickerButton.removeAttribute("checked");
}
},
/**
* Return all toolbox buttons (command buttons, plus any others that were
* added manually).
@ -1142,12 +1153,11 @@ Toolbox.prototype = {
this._walker = yield this._inspector.getWalker();
this._selection = new Selection(this._walker);
if (this.highlighterUtils.isRemoteHighlightable) {
let autohide = !gDevTools.testing;
if (this.highlighterUtils.isRemoteHighlightable()) {
this.walker.on("highlighter-ready", this._highlighterReady);
this.walker.on("highlighter-hide", this._highlighterHidden);
let autohide = !gDevTools.testing;
this._highlighter = yield this._inspector.getHighlighter(autohide);
}
}.bind(this));
@ -1277,6 +1287,7 @@ Toolbox.prototype = {
}
let target = this._target;
this._target = null;
this.highlighterUtils.release();
target.off("close", this.destroy);
return target.destroy();
}).then(() => {
@ -1294,202 +1305,5 @@ Toolbox.prototype = {
_highlighterHidden: function() {
this.emit("highlighter-hide");
},
};
/**
* The ToolboxHighlighterUtils is what you should use for anything related to
* node highlighting and picking.
* It encapsulates the logic to connecting to the HighlighterActor.
*/
function ToolboxHighlighterUtils(toolbox) {
this.toolbox = toolbox;
this._onPickerNodeHovered = this._onPickerNodeHovered.bind(this);
this._onPickerNodePicked = this._onPickerNodePicked.bind(this);
this.stopPicker = this.stopPicker.bind(this);
}
ToolboxHighlighterUtils.prototype = {
/**
* Indicates whether the highlighter actor exists on the server.
*/
get isRemoteHighlightable() {
return this.toolbox._target.client.traits.highlightable;
},
/**
* Start/stop the element picker on the debuggee target.
*/
togglePicker: function() {
if (this._isPicking) {
return this.stopPicker();
} else {
return this.startPicker();
}
},
_onPickerNodeHovered: function(res) {
this.toolbox.emit("picker-node-hovered", res.node);
},
_onPickerNodePicked: function(res) {
this.toolbox.selection.setNodeFront(res.node, "picker-node-picked");
this.stopPicker();
},
/**
* Start the element picker on the debuggee target.
* This will request the inspector actor to start listening for mouse/touch
* events on the target to highlight the hovered/picked element.
* Depending on the server-side capabilities, this may fire events when nodes
* are hovered.
* @return A promise that resolves when the picker has started or immediately
* if it is already started
*/
startPicker: function() {
if (this._isPicking) {
return promise.resolve();
}
let deferred = promise.defer();
let done = () => {
this._isPicking = true;
this.toolbox.emit("picker-started");
this.toolbox.on("select", this.stopPicker);
deferred.resolve();
};
promise.all([
this.toolbox.initInspector(),
this.toolbox.selectTool("inspector")
]).then(() => {
this.toolbox._pickerButton.setAttribute("checked", "true");
if (this.isRemoteHighlightable) {
this.toolbox.walker.on("picker-node-hovered", this._onPickerNodeHovered);
this.toolbox.walker.on("picker-node-picked", this._onPickerNodePicked);
this.toolbox.highlighter.pick().then(done);
} else {
return this.toolbox.walker.pick().then(node => {
this.toolbox.selection.setNodeFront(node, "picker-node-picked").then(() => {
this.stopPicker();
done();
});
});
}
});
return deferred.promise;
},
/**
* Stop the element picker
* @return A promise that resolves when the picker has stopped or immediately
* if it is already stopped
*/
stopPicker: function() {
if (!this._isPicking) {
return promise.resolve();
}
let deferred = promise.defer();
let done = () => {
this.toolbox.emit("picker-stopped");
this.toolbox.off("select", this.stopPicker);
deferred.resolve();
};
this.toolbox.initInspector().then(() => {
this._isPicking = false;
this.toolbox._pickerButton.removeAttribute("checked");
if (this.isRemoteHighlightable) {
this.toolbox.highlighter.cancelPick().then(done);
this.toolbox.walker.off("picker-node-hovered", this._onPickerNodeHovered);
this.toolbox.walker.off("picker-node-picked", this._onPickerNodePicked);
} else {
this.toolbox.walker.cancelPick().then(done);
}
});
return deferred.promise;
},
/**
* Show the box model highlighter on a node, given its NodeFront (this type
* of front is normally returned by the WalkerActor).
* @return a promise that resolves to the nodeFront when the node has been
* highlit
*/
highlightNodeFront: function(nodeFront, options={}) {
let deferred = promise.defer();
// If the remote highlighter exists on the target, use it
if (this.isRemoteHighlightable) {
this.toolbox.initInspector().then(() => {
this.toolbox.highlighter.showBoxModel(nodeFront, options).then(() => {
this.toolbox.emit("node-highlight", nodeFront);
deferred.resolve(nodeFront);
});
});
}
// Else, revert to the "older" version of the highlighter in the walker
// actor
else {
this.toolbox.walker.highlight(nodeFront).then(() => {
this.toolbox.emit("node-highlight", nodeFront);
deferred.resolve(nodeFront);
});
}
return deferred.promise;
},
/**
* This is a convenience method in case you don't have a nodeFront but a
* valueGrip. This is often the case with VariablesView properties.
* This method will simply translate the grip into a nodeFront and call
* highlightNodeFront
* @return a promise that resolves to the nodeFront when the node has been
* highlit
*/
highlightDomValueGrip: function(valueGrip, options={}) {
return this._translateGripToNodeFront(valueGrip).then(nodeFront => {
if (nodeFront) {
return this.highlightNodeFront(nodeFront, options);
} else {
return promise.reject();
}
});
},
_translateGripToNodeFront: function(grip) {
return this.toolbox.initInspector().then(() => {
return this.toolbox.walker.getNodeActorFromObjectActor(grip.actor);
});
},
/**
* Hide the highlighter.
* @return a promise that resolves when the highlighter is hidden
*/
unhighlight: function(forceHide=false) {
let unhighlightPromise;
forceHide = forceHide || !gDevTools.testing;
if (forceHide && this.isRemoteHighlightable && this.toolbox.highlighter) {
// If the remote highlighter exists on the target, use it
unhighlightPromise = this.toolbox.highlighter.hideBoxModel();
} else {
// If not, no need to unhighlight as the older highlight method uses a
// setTimeout to hide itself
unhighlightPromise = promise.resolve();
}
return unhighlightPromise.then(() => {
this.toolbox.emit("node-unhighlight");
});
}
};

View File

@ -42,7 +42,7 @@ function testNonTransformedBoxModelDimensionsNoZoom() {
info("Highlighted the non-rotated div");
isNodeCorrectlyHighlighted(div, "non-zoomed");
inspector.toolbox.once("highlighter-ready", testNonTransformedBoxModelDimensionsZoomed);
waitForBoxModelUpdate().then(testNonTransformedBoxModelDimensionsZoomed);
contentViewer = gBrowser.selectedBrowser.docShell.contentViewer
.QueryInterface(Ci.nsIMarkupDocumentViewer);
contentViewer.fullZoom = 2;
@ -52,19 +52,22 @@ function testNonTransformedBoxModelDimensionsZoomed() {
info("Highlighted the zoomed, non-rotated div");
isNodeCorrectlyHighlighted(div, "zoomed");
inspector.toolbox.once("highlighter-ready", testMouseOverRotatedHighlights);
waitForBoxModelUpdate().then(testMouseOverRotatedHighlights);
contentViewer.fullZoom = 1;
}
function testMouseOverRotatedHighlights() {
inspector.toolbox.once("highlighter-ready", () => {
ok(isHighlighting(), "Highlighter is shown");
info("Highlighted the rotated div");
isNodeCorrectlyHighlighted(rotated, "rotated");
executeSoon(finishUp);
});
let onBoxModelUpdate = waitForBoxModelUpdate();
inspector.selection.setNode(rotated);
inspector.once("inspector-updated", () => {
onBoxModelUpdate.then(() => {
ok(isHighlighting(), "Highlighter is shown");
info("Highlighted the rotated div");
isNodeCorrectlyHighlighted(rotated, "rotated");
executeSoon(finishUp);
});
});
}
function finishUp() {

View File

@ -18,14 +18,18 @@ function test() {
}
function runTest() {
info("Checking that the highlighter has the right size");
let rect = getSimpleBorderRect();
is(rect.width, 100, "outline has the right width");
waitForBoxModelUpdate().then(testRectWidth);
info("Changing the test element's size");
div.style.width = "200px";
inspector.toolbox.once("highlighter-ready", testRectWidth);
}
function testRectWidth() {
info("Checking that the highlighter has the right size after update");
let rect = getSimpleBorderRect();
is(rect.width, 200, "outline updated");
finishUp();

View File

@ -309,6 +309,26 @@ function isHighlighting()
return !root.hasAttribute("hidden");
}
/**
* Observes mutation changes on the box-model highlighter and returns a promise
* that resolves when one of the attributes changes.
* If an attribute changes in the box-model, it means its position/dimensions
* got updated
*/
function waitForBoxModelUpdate() {
let def = promise.defer();
let root = getBoxModelRoot();
let polygon = root.querySelector(".box-model-content");
let observer = new polygon.ownerDocument.defaultView.MutationObserver(() => {
observer.disconnect();
def.resolve();
});
observer.observe(polygon, {attributes: true});
return def.promise;
}
function getHighlitNode()
{
if (isHighlighting()) {
@ -437,49 +457,15 @@ function isNodeCorrectlyHighlighted(node, prefix="") {
prefix += (node.classList.length ? "." + [...node.classList].join(".") : "");
prefix += " ";
let quads = helper.getAdjustedQuads(node, "content");
let {p1:cp1, p2:cp2, p3:cp3, p4:cp4} = boxModel.content.points;
is(cp1.x, quads.p1.x, prefix + "content point 1 x co-ordinate is correct");
is(cp1.y, quads.p1.y, prefix + "content point 1 y co-ordinate is correct");
is(cp2.x, quads.p2.x, prefix + "content point 2 x co-ordinate is correct");
is(cp2.y, quads.p2.y, prefix + "content point 2 y co-ordinate is correct");
is(cp3.x, quads.p3.x, prefix + "content point 3 x co-ordinate is correct");
is(cp3.y, quads.p3.y, prefix + "content point 3 y co-ordinate is correct");
is(cp4.x, quads.p4.x, prefix + "content point 4 x co-ordinate is correct");
is(cp4.y, quads.p4.y, prefix + "content point 4 y co-ordinate is correct");
quads = helper.getAdjustedQuads(node, "padding");
let {p1:pp1, p2:pp2, p3:pp3, p4:pp4} = boxModel.padding.points;
is(pp1.x, quads.p1.x, prefix + "padding point 1 x co-ordinate is correct");
is(pp1.y, quads.p1.y, prefix + "padding point 1 y co-ordinate is correct");
is(pp2.x, quads.p2.x, prefix + "padding point 2 x co-ordinate is correct");
is(pp2.y, quads.p2.y, prefix + "padding point 2 y co-ordinate is correct");
is(pp3.x, quads.p3.x, prefix + "padding point 3 x co-ordinate is correct");
is(pp3.y, quads.p3.y, prefix + "padding point 3 y co-ordinate is correct");
is(pp4.x, quads.p4.x, prefix + "padding point 4 x co-ordinate is correct");
is(pp4.y, quads.p4.y, prefix + "padding point 4 y co-ordinate is correct");
quads = helper.getAdjustedQuads(node, "border");
let {p1:bp1, p2:bp2, p3:bp3, p4:bp4} = boxModel.border.points;
is(bp1.x, quads.p1.x, prefix + "border point 1 x co-ordinate is correct");
is(bp1.y, quads.p1.y, prefix + "border point 1 y co-ordinate is correct");
is(bp2.x, quads.p2.x, prefix + "border point 2 x co-ordinate is correct");
is(bp2.y, quads.p2.y, prefix + "border point 2 y co-ordinate is correct");
is(bp3.x, quads.p3.x, prefix + "border point 3 x co-ordinate is correct");
is(bp3.y, quads.p3.y, prefix + "border point 3 y co-ordinate is correct");
is(bp4.x, quads.p4.x, prefix + "border point 4 x co-ordinate is correct");
is(bp4.y, quads.p4.y, prefix + "border point 4 y co-ordinate is correct");
quads = helper.getAdjustedQuads(node, "margin");
let {p1:mp1, p2:mp2, p3:mp3, p4:mp4} = boxModel.margin.points;
is(mp1.x, quads.p1.x, prefix + "margin point 1 x co-ordinate is correct");
is(mp1.y, quads.p1.y, prefix + "margin point 1 y co-ordinate is correct");
is(mp2.x, quads.p2.x, prefix + "margin point 2 x co-ordinate is correct");
is(mp2.y, quads.p2.y, prefix + "margin point 2 y co-ordinate is correct");
is(mp3.x, quads.p3.x, prefix + "margin point 3 x co-ordinate is correct");
is(mp3.y, quads.p3.y, prefix + "margin point 3 y co-ordinate is correct");
is(mp4.x, quads.p4.x, prefix + "margin point 4 x co-ordinate is correct");
is(mp4.y, quads.p4.y, prefix + "margin point 4 y co-ordinate is correct");
for (let boxType of ["content", "padding", "border", "margin"]) {
let quads = helper.getAdjustedQuads(node, boxType);
for (let point in boxModel[boxType].points) {
is(boxModel[boxType].points[point].x, quads[point].x,
prefix + boxType + " point " + point + " x coordinate is correct");
is(boxModel[boxType].points[point].y, quads[point].y,
prefix + boxType + " point " + point + " y coordinate is correct");
}
}
}
function getContainerForRawNode(markupView, rawNode)

View File

@ -24,8 +24,7 @@ let test = asyncTest(function*() {
let {toolbox, inspector, view} = yield openLayoutView();
yield runTests(inspector, view);
// TODO: Closing the toolbox in this test leaks - bug 994314
// yield destroyToolbox(inspector);
yield destroyToolbox(inspector);
});
addTest("Test that editing margin dynamically updates the document, pressing escape cancels the changes",

View File

@ -23,8 +23,7 @@ let test = asyncTest(function*() {
let {toolbox, inspector, view} = yield openLayoutView();
yield runTests(inspector, view);
// TODO: Closing the toolbox in this test leaks - bug 994314
// yield destroyToolbox(inspector);
yield destroyToolbox(inspector);
});
addTest("When all properties are set on the node editing one should work",

View File

@ -23,8 +23,7 @@ let test = asyncTest(function*() {
let {toolbox, inspector, view} = yield openLayoutView();
yield runTests(inspector, view);
// TODO: Closing the toolbox in this test leaks - bug 994314
// yield destroyToolbox(inspector);
yield destroyToolbox(inspector);
});
addTest("Test that adding a border applies a border style when necessary",

View File

@ -24,8 +24,7 @@ let test = asyncTest(function*() {
let {toolbox, inspector, view} = yield openLayoutView();
yield runTests(inspector, view);
// TODO: Closing the toolbox in this test leaks - bug 994314
// yield destroyToolbox(inspector);
yield destroyToolbox(inspector);
});
addTest("Test that entering units works",

View File

@ -175,10 +175,26 @@ MarkupView.prototype = {
this._hoveredNode = null;
},
/**
* Show the box model highlighter on a given node front
* @param {NodeFront} nodeFront The node to show the highlighter for
* @param {Object} options Options for the highlighter
* @return a promise that resolves when the highlighter for this nodeFront is
* shown, taking into account that there could already be highlighter requests
* queued up
*/
_showBoxModel: function(nodeFront, options={}) {
this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
return this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
},
/**
* Hide the box model highlighter on a given node front
* @param {NodeFront} nodeFront The node to hide the highlighter for
* @param {Boolean} forceHide See toolbox-highlighter-utils/unhighlight
* @return a promise that resolves when the highlighter for this nodeFront is
* hidden, taking into account that there could already be highlighter requests
* queued up
*/
_hideBoxModel: function(forceHide) {
return this._inspector.toolbox.highlighterUtils.unhighlight(forceHide);
},

View File

@ -3,6 +3,7 @@ skip-if = e10s # Bug ?????? - devtools tests disabled with e10s
subsuite = devtools
support-files =
browser_layoutHelpers.html
browser_layoutHelpers-getBoxQuads.html
browser_layoutHelpers_iframe.html
browser_templater_basic.html
browser_toolbar_basic.html
@ -22,6 +23,7 @@ support-files =
[browser_graphs-09.js]
[browser_graphs-10.js]
[browser_layoutHelpers.js]
[browser_layoutHelpers-getBoxQuads.js]
[browser_observableobject.js]
[browser_outputparser.js]
[browser_require_basic.js]
@ -49,4 +51,3 @@ support-files =
[browser_tableWidget_keyboard_interaction.js]
[browser_tableWidget_mouse_interaction.js]
[browser_spectrum.js]
[browser_csstransformpreview.js]

View File

@ -1,139 +0,0 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the spectrum color picker works correctly
const TEST_URI = "data:text/html;charset=utf-8,<div></div>";
const {CSSTransformPreviewer} = devtools.require("devtools/shared/widgets/CSSTransformPreviewer");
let doc, root;
function test() {
waitForExplicitFinish();
addTab(TEST_URI, () => {
doc = content.document;
root = doc.querySelector("div");
startTests();
});
}
function endTests() {
doc = root = null;
gBrowser.removeCurrentTab();
finish();
}
function startTests() {
testCreateAndDestroyShouldAppendAndRemoveElements();
}
function testCreateAndDestroyShouldAppendAndRemoveElements() {
ok(root, "We have the root node to append the preview to");
is(root.childElementCount, 0, "Root node is empty");
let p = new CSSTransformPreviewer(root);
p.preview("matrix(1, -0.2, 0, 1, 0, 0)");
ok(root.childElementCount > 0, "Preview has appended elements");
ok(root.querySelector("canvas"), "Canvas preview element is here");
p.destroy();
is(root.childElementCount, 0, "Destroying preview removed all nodes");
testCanvasDimensionIsConstrainedByMaxDim();
}
function testCanvasDimensionIsConstrainedByMaxDim() {
let p = new CSSTransformPreviewer(root);
p.MAX_DIM = 500;
p.preview("scale(1)", "center", 1000, 1000);
let canvas = root.querySelector("canvas");
is(canvas.width, 500, "Canvas width is correct");
is(canvas.height, 500, "Canvas height is correct");
p.destroy();
testCallingPreviewSeveralTimesReusesTheSameCanvas();
}
function testCallingPreviewSeveralTimesReusesTheSameCanvas() {
let p = new CSSTransformPreviewer(root);
p.preview("scale(1)", "center", 1000, 1000);
let canvas = root.querySelector("canvas");
p.preview("rotate(90deg)");
let canvases = root.querySelectorAll("canvas");
is(canvases.length, 1, "Still one canvas element");
is(canvases[0], canvas, "Still the same canvas element");
p.destroy();
testCanvasDimensionAreCorrect();
}
function testCanvasDimensionAreCorrect() {
// Only test a few simple transformations
let p = new CSSTransformPreviewer(root);
// Make sure we have a square
let w = 200, h = w;
p.MAX_DIM = w;
// We can't test the content of the canvas here, just that, given a max width
// the aspect ratio of the canvas seems correct.
// Translate a square by its width, should be a rectangle
p.preview("translateX(200px)", "center", w, h);
let canvas = root.querySelector("canvas");
is(canvas.width, w, "width is correct");
is(canvas.height, h/2, "height is half of the width");
// Rotate on the top right corner, should be a rectangle
p.preview("rotate(-90deg)", "top right", w, h);
is(canvas.width, w, "width is correct");
is(canvas.height, h/2, "height is half of the width");
// Rotate on the bottom left corner, should be a rectangle
p.preview("rotate(90deg)", "top right", w, h);
is(canvas.width, w/2, "width is half of the height");
is(canvas.height, h, "height is correct");
// Scale from center, should still be a square
p.preview("scale(2)", "center", w, h);
is(canvas.width, w, "width is correct");
is(canvas.height, h, "height is correct");
// Skew from center, 45deg, should be a rectangle
p.preview("skew(45deg)", "center", w, h);
is(canvas.width, w, "width is correct");
is(canvas.height, h/2, "height is half of the height");
p.destroy();
testPreviewingInvalidTransformReturnsFalse();
}
function testPreviewingInvalidTransformReturnsFalse() {
let p = new CSSTransformPreviewer(root);
ok(!p.preview("veryWow(muchPx) suchTransform(soDeg)"), "Returned false for invalid transform");
ok(!p.preview("rotae(3deg)"), "Returned false for invalid transform");
// Verify the canvas is empty by checking the image data
let canvas = root.querySelector("canvas"), ctx = canvas.getContext("2d");
let data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
for (let i = 0, n = data.length; i < n; i += 4) {
// Let's not log 250*250*4 asserts! Instead, just log when it fails
let red = data[i];
let green = data[i + 1];
let blue = data[i + 2];
let alpha = data[i + 3];
if (red !== 0 || green !== 0 || blue !== 0 || alpha !== 0) {
ok(false, "Image data is not empty after an invalid transformed was previewed");
break;
}
}
is(p.preview("translateX(30px)"), true, "Returned true for a valid transform");
endTests();
}

View File

@ -0,0 +1,62 @@
<!doctype html>
<meta charset=utf-8>
<title>Layout Helpers</title>
<style id="styles">
body {
margin: 0;
padding: 0;
}
#hidden-node {
display: none;
}
#simple-node-with-margin-padding-border {
width: 200px;
height: 200px;
background: #f06;
padding: 20px;
margin: 50px;
border: 10px solid black;
}
#scrolled-node {
position: absolute;
top: 0;
left: 0;
width: 300px;
height: 100px;
overflow: scroll;
background: linear-gradient(red, pink);
}
#sub-scrolled-node {
width: 200px;
height: 200px;
overflow: scroll;
background: linear-gradient(yellow, green);
}
#inner-scrolled-node {
width: 100px;
height: 400px;
background: linear-gradient(black, white);
}
</style>
<div id="hidden-node"></div>
<div id="simple-node-with-margin-padding-border"></div>
<!-- The inline encoded code below corresponds to:
<iframe style="margin:10px;border:0;width:300px;height:300px;">
<iframe style="margin:10px;border:0;width:200px;height:200px;">
<div id="inner-node" style="width:100px;height:100px;border:10px solid red;margin:10px;padding:10px;"></div>
</iframe>
</iframe>
-->
<iframe src="data:text/html,%3Cstyle%3Ebody%7Bmargin:0;padding:0;%7D%3C/style%3E%3Ciframe%20src=%22data:text/html,%253Cstyle%253Ebody%257Bmargin:0;padding:0;%257D%253C/style%253E%253Cdiv%2520id='inner-node'%2520style='width:100px;height:100px;border:10px%2520solid%2520red;margin:10px;padding:10px;'%253E%253C/div%253E%22%20style=%22margin:10px;border:0;width:200px;height:200px;%22%3E%3C/iframe%3E" style="margin:10px;border:0;width:300px;height:300px;"></iframe>
<div id="scrolled-node">
<div id="sub-scrolled-node">
<div id="inner-scrolled-node"></div>
</div>
</div>

View File

@ -0,0 +1,209 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that LayoutHelpers.getAdjustedQuads works properly in a variety of use
// cases including iframes, scroll and zoom
const {utils: Cu} = Components;
const {LayoutHelpers} = Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", {});
const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers-getBoxQuads.html";
function test() {
addTab(TEST_URI, function(browser, tab) {
let doc = browser.contentDocument;
let win = doc.defaultView;
info("Creating a new LayoutHelpers instance for the test window");
let helper = new LayoutHelpers(win);
ok(helper.getAdjustedQuads, "getAdjustedQuads is defined");
info("Running tests");
returnsTheRightDataStructure(doc, helper);
returnsNullForMissingNode(doc, helper);
returnsNullForHiddenNodes(doc, helper);
defaultsToBorderBoxIfNoneProvided(doc, helper);
returnsLikeGetBoxQuadsInSimpleCase(doc, helper);
takesIframesOffsetsIntoAccount(doc, helper);
takesScrollingIntoAccount(doc, helper);
takesZoomIntoAccount(doc, helper);
gBrowser.removeCurrentTab();
finish();
});
}
function returnsTheRightDataStructure(doc, helper) {
info("Checks that the returned data contains bounds and 4 points");
let node = doc.querySelector("body");
let res = helper.getAdjustedQuads(node, "content");
ok("bounds" in res, "The returned data has a bounds property");
ok("p1" in res, "The returned data has a p1 property");
ok("p2" in res, "The returned data has a p2 property");
ok("p3" in res, "The returned data has a p3 property");
ok("p4" in res, "The returned data has a p4 property");
for (let boundProp of
["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
ok(boundProp in res.bounds, "The bounds has a " + boundProp + " property");
}
for (let point of ["p1", "p2", "p3", "p4"]) {
for (let pointProp of ["x", "y", "z", "w"]) {
ok(pointProp in res[point], point + " has a " + pointProp + " property");
}
}
}
function returnsNullForMissingNode(doc, helper) {
info("Checks that null is returned for invalid nodes");
for (let input of [null, undefined, "", 0]) {
ok(helper.getAdjustedQuads(input) === null, "null is returned for input " +
input);
}
}
function returnsNullForHiddenNodes(doc, helper) {
info("Checks that null is returned for nodes that aren't rendered");
let style = doc.querySelector("#styles");
ok(helper.getAdjustedQuads(style) === null,
"null is returned for a <style> node");
let hidden = doc.querySelector("#hidden-node");
ok(helper.getAdjustedQuads(hidden) === null,
"null is returned for a hidden node");
}
function defaultsToBorderBoxIfNoneProvided(doc, helper) {
info("Checks that if no boxtype is passed, then border is the default one");
let node = doc.querySelector("#simple-node-with-margin-padding-border");
let withBoxType = helper.getAdjustedQuads(node, "border");
let withoutBoxType = helper.getAdjustedQuads(node);
for (let boundProp of
["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
is(withBoxType.bounds[boundProp], withoutBoxType.bounds[boundProp],
boundProp + " bound is equal with or without the border box type");
}
for (let point of ["p1", "p2", "p3", "p4"]) {
for (let pointProp of ["x", "y", "z", "w"]) {
is(withBoxType[point][pointProp], withoutBoxType[point][pointProp],
point + "." + pointProp +
" is equal with or without the border box type");
}
}
}
function returnsLikeGetBoxQuadsInSimpleCase(doc, helper) {
info("Checks that for an element in the main frame, without scroll nor zoom" +
"that the returned value is similar to the returned value of getBoxQuads");
let node = doc.querySelector("#simple-node-with-margin-padding-border");
for (let region of ["content", "padding", "border", "margin"]) {
let expected = node.getBoxQuads({
box: region
})[0];
let actual = helper.getAdjustedQuads(node, region);
for (let boundProp of
["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
is(actual.bounds[boundProp], expected.bounds[boundProp],
boundProp + " bound is equal to the one returned by getBoxQuads for " +
region + " box");
}
for (let point of ["p1", "p2", "p3", "p4"]) {
for (let pointProp of ["x", "y", "z", "w"]) {
is(actual[point][pointProp], expected[point][pointProp],
point + "." + pointProp +
" is equal to the one returned by getBoxQuads for " + region + " box");
}
}
}
}
function takesIframesOffsetsIntoAccount(doc, helper) {
info("Checks that the quad returned for a node inside iframes that have " +
"margins takes those offsets into account");
let rootIframe = doc.querySelector("iframe");
let subIframe = rootIframe.contentDocument.querySelector("iframe");
let innerNode = subIframe.contentDocument.querySelector("#inner-node");
let quad = helper.getAdjustedQuads(innerNode, "content");
//rootIframe margin + subIframe margin + node margin + node border + node padding
let p1x = 10 + 10 + 10 + 10 + 10;
is(quad.p1.x, p1x, "The inner node's p1 x position is correct");
// Same as p1x + the inner node width
let p2x = p1x + 100;
is(quad.p2.x, p2x, "The inner node's p2 x position is correct");
}
function takesScrollingIntoAccount(doc, helper) {
info("Checks that the quad returned for a node inside multiple scrolled " +
"containers takes the scroll values into account");
// For info, the container being tested here is absolutely positioned at 0 0
// to simplify asserting the coordinates
info("Scroll the container nodes down");
let scrolledNode = doc.querySelector("#scrolled-node");
scrolledNode.scrollTop = 100;
let subScrolledNode = doc.querySelector("#sub-scrolled-node");
subScrolledNode.scrollTop = 200;
let innerNode = doc.querySelector("#inner-scrolled-node");
let quad = helper.getAdjustedQuads(innerNode, "content");
is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling down");
is(quad.p1.y, -300, "p1.y of the scrolled node is correct after scrolling down");
info("Scrolling back up");
scrolledNode.scrollTop = 0;
subScrolledNode.scrollTop = 0;
let quad = helper.getAdjustedQuads(innerNode, "content");
is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling up");
is(quad.p1.y, 0, "p1.y of the scrolled node is correct after scrolling up");
}
function takesZoomIntoAccount(doc, helper) {
info("Checks that if the page is zoomed in/out, the quad returned is correct");
// Hard-coding coordinates in this zoom test is a bad idea as it can vary
// depending on the platform, so we simply test that zooming in produces a
// bigger quad and zooming out produces a smaller quad
let node = doc.querySelector("#simple-node-with-margin-padding-border");
let defaultQuad = helper.getAdjustedQuads(node);
info("Zoom in");
window.FullZoom.enlarge();
let zoomedInQuad = helper.getAdjustedQuads(node);
ok(zoomedInQuad.bounds.width > defaultQuad.bounds.width,
"The zoomed in quad is bigger than the default one");
ok(zoomedInQuad.bounds.height > defaultQuad.bounds.height,
"The zoomed in quad is bigger than the default one");
info("Zoom out");
window.FullZoom.reset();
window.FullZoom.reduce();
let zoomedOutQuad = helper.getAdjustedQuads(node);
ok(zoomedOutQuad.bounds.width < defaultQuad.bounds.width,
"The zoomed out quad is smaller than the default one");
ok(zoomedOutQuad.bounds.height < defaultQuad.bounds.height,
"The zoomed out quad is smaller than the default one");
window.FullZoom.reset();
}

View File

@ -12,7 +12,7 @@ registerCleanupFunction(function () {
let LayoutHelpers = imported.LayoutHelpers;
const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_layoutHelpers.html";
const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers.html";
function test() {
addTab(TEST_URI, function(browser, tab) {

View File

@ -12,7 +12,7 @@
var promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}).Promise;
var template = Cu.import("resource://gre/modules/devtools/Templater.jsm", {}).template;
const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_templater_basic.html";
const TEST_URI = TEST_URI_ROOT + "browser_templater_basic.html";
function test() {
addTab(TEST_URI, function() {

View File

@ -3,7 +3,7 @@
// Tests that the developer toolbar works properly
const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_toolbar_basic.html";
const TEST_URI = TEST_URI_ROOT + "browser_toolbar_basic.html";
function test() {
addTab(TEST_URI, function(browser, tab) {

View File

@ -4,8 +4,7 @@
// Tests that the developer toolbar errors count works properly.
function test() {
const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/" +
"browser_toolbar_webconsole_errors_count.html";
const TEST_URI = TEST_URI_ROOT + "browser_toolbar_webconsole_errors_count.html";
let gDevTools = Cu.import("resource:///modules/devtools/gDevTools.jsm",
{}).gDevTools;

View File

@ -11,6 +11,8 @@ SimpleTest.registerCleanupFunction(() => {
gDevTools.testing = false;
});
const TEST_URI_ROOT = "http://example.com/browser/browser/devtools/shared/test/";
/**
* Open a new tab at a URL and call a callback on load
*/

View File

@ -1,389 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* The CSSTransformPreview module displays, using a <canvas> a rectangle, with
* a given width and height and its transformed version, given a css transform
* property and origin. It also displays arrows from/to each corner.
*
* It is useful to visualize how a css transform affected an element. It can
* help debug tricky transformations. It is used today in a tooltip, and this
* tooltip is shown when hovering over a css transform declaration in the rule
* and computed view panels.
*
* TODO: For now, it multiplies matrices itself to calculate the coordinates of
* the transformed box, but that should be removed as soon as we can get access
* to getQuads().
*/
const HTML_NS = "http://www.w3.org/1999/xhtml";
/**
* The TransformPreview needs an element to output a canvas tag.
*
* Usage example:
*
* let t = new CSSTransformPreviewer(myRootElement);
* t.preview("rotate(45deg)", "top left", 200, 400);
* t.preview("skew(19deg)", "center", 100, 500);
* t.preview("matrix(1, -0.2, 0, 1, 0, 0)");
* t.destroy();
*
* @param {nsIDOMElement} parentEl
* Where the canvas will go
*/
function CSSTransformPreviewer(parentEl) {
this.parentEl = parentEl;
this.doc = this.parentEl.ownerDocument;
this.canvas = null;
this.ctx = null;
}
module.exports.CSSTransformPreviewer = CSSTransformPreviewer;
CSSTransformPreviewer.prototype = {
/**
* The preview look-and-feel can be changed using these properties
*/
MAX_DIM: 250,
PAD: 5,
ORIGINAL_FILL: "#1F303F",
ORIGINAL_STROKE: "#B2D8FF",
TRANSFORMED_FILL: "rgba(200, 200, 200, .5)",
TRANSFORMED_STROKE: "#B2D8FF",
ARROW_STROKE: "#329AFF",
ORIGIN_STROKE: "#329AFF",
ARROW_TIP_HEIGHT: 10,
ARROW_TIP_WIDTH: 8,
CORNER_SIZE_RATIO: 6,
/**
* Destroy removes the canvas from the parentelement passed in the constructor
*/
destroy: function() {
if (this.canvas) {
this.parentEl.removeChild(this.canvas);
}
if (this._hiddenDiv) {
this.parentEl.removeChild(this._hiddenDiv);
}
this.parentEl = this.canvas = this.ctx = this.doc = null;
},
_createMarkup: function() {
this.canvas = this.doc.createElementNS(HTML_NS, "canvas");
this.canvas.setAttribute("id", "canvas");
this.canvas.setAttribute("width", this.MAX_DIM);
this.canvas.setAttribute("height", this.MAX_DIM);
this.canvas.style.position = "relative";
this.parentEl.appendChild(this.canvas);
this.ctx = this.canvas.getContext("2d");
},
_getComputed: function(name, value, width, height) {
if (!this._hiddenDiv) {
// Create a hidden element to apply the style to
this._hiddenDiv = this.doc.createElementNS(HTML_NS, "div");
this._hiddenDiv.style.visibility = "hidden";
this._hiddenDiv.style.position = "absolute";
this.parentEl.appendChild(this._hiddenDiv);
}
// Camelcase the name
name = name.replace(/-([a-z]{1})/g, (m, letter) => letter.toUpperCase());
// Apply width and height to make sure computation is made correctly
this._hiddenDiv.style.width = width + "px";
this._hiddenDiv.style.height = height + "px";
// Show the hidden div, apply the style, read the computed style, hide the
// hidden div again
this._hiddenDiv.style.display = "block";
this._hiddenDiv.style[name] = value;
let computed = this.doc.defaultView.getComputedStyle(this._hiddenDiv);
let computedValue = computed[name];
this._hiddenDiv.style.display = "none";
return computedValue;
},
_getMatrixFromTransformString: function(transformStr) {
let matrix = transformStr.substring(0, transformStr.length - 1).
substring(transformStr.indexOf("(") + 1).split(",");
matrix.forEach(function(value, index) {
matrix[index] = parseFloat(value, 10);
});
let transformMatrix = null;
if (matrix.length === 6) {
// 2d transform
transformMatrix = [
[matrix[0], matrix[2], matrix[4], 0],
[matrix[1], matrix[3], matrix[5], 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
} else {
// 3d transform
transformMatrix = [
[matrix[0], matrix[4], matrix[8], matrix[12]],
[matrix[1], matrix[5], matrix[9], matrix[13]],
[matrix[2], matrix[6], matrix[10], matrix[14]],
[matrix[3], matrix[7], matrix[11], matrix[15]]
];
}
return transformMatrix;
},
_getOriginFromOriginString: function(originStr) {
let offsets = originStr.split(" ");
offsets.forEach(function(item, index) {
offsets[index] = parseInt(item, 10);
});
return offsets;
},
_multiply: function(m1, m2) {
let m = [];
for (let m1Line = 0; m1Line < m1.length; m1Line++) {
m[m1Line] = 0;
for (let m2Col = 0; m2Col < m2.length; m2Col++) {
m[m1Line] += m1[m1Line][m2Col] * m2[m2Col];
}
}
return [m[0], m[1]];
},
_getTransformedPoint: function(matrix, point, origin) {
let pointMatrix = [point[0] - origin[0], point[1] - origin[1], 1, 1];
return this._multiply(matrix, pointMatrix);
},
_getTransformedPoints: function(matrix, rect, origin) {
return rect.map(point => {
let tPoint = this._getTransformedPoint(matrix, [point[0], point[1]], origin);
return [tPoint[0] + origin[0], tPoint[1] + origin[1]];
});
},
/**
* For canvas to avoid anti-aliasing
*/
_round: x => Math.round(x) + .5,
_drawShape: function(points, fillStyle, strokeStyle) {
this.ctx.save();
this.ctx.lineWidth = 1;
this.ctx.strokeStyle = strokeStyle;
this.ctx.fillStyle = fillStyle;
this.ctx.beginPath();
this.ctx.moveTo(this._round(points[0][0]), this._round(points[0][1]));
for (var i = 1; i < points.length; i++) {
this.ctx.lineTo(this._round(points[i][0]), this._round(points[i][1]));
}
this.ctx.lineTo(this._round(points[0][0]), this._round(points[0][1]));
this.ctx.fill();
this.ctx.stroke();
this.ctx.restore();
},
_drawArrow: function(x1, y1, x2, y2) {
// do not draw if the line is too small
if (Math.abs(x2-x1) < 20 && Math.abs(y2-y1) < 20) {
return;
}
this.ctx.save();
this.ctx.strokeStyle = this.ARROW_STROKE;
this.ctx.fillStyle = this.ARROW_STROKE;
this.ctx.lineWidth = 1;
this.ctx.beginPath();
this.ctx.moveTo(this._round(x1), this._round(y1));
this.ctx.lineTo(this._round(x2), this._round(y2));
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.translate(x2, y2);
let radians = Math.atan((y1 - y2) / (x1 - x2));
radians += ((x1 >= x2) ? -90 : 90) * Math.PI / 180;
this.ctx.rotate(radians);
this.ctx.moveTo(0, 0);
this.ctx.lineTo(this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
this.ctx.lineTo(-this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
this.ctx.closePath();
this.ctx.fill();
this.ctx.restore();
},
_drawOrigin: function(x, y) {
this.ctx.save();
this.ctx.strokeStyle = this.ORIGIN_STROKE;
this.ctx.fillStyle = this.ORIGIN_STROKE;
this.ctx.beginPath();
this.ctx.arc(x, y, 4, 0, 2 * Math.PI, false);
this.ctx.stroke();
this.ctx.fill();
this.ctx.restore();
},
/**
* Computes the largest width and height of all the given shapes and changes
* all of the shapes' points (by reference) so they fit into the configured
* MAX_DIM - 2*PAD area.
* @return {Object} A {w, h} giving the size the canvas should be
*/
_fitAllShapes: function(allShapes) {
let allXs = [], allYs = [];
for (let shape of allShapes) {
for (let point of shape) {
allXs.push(point[0]);
allYs.push(point[1]);
}
}
let minX = Math.min.apply(Math, allXs);
let maxX = Math.max.apply(Math, allXs);
let minY = Math.min.apply(Math, allYs);
let maxY = Math.max.apply(Math, allYs);
let spanX = maxX - minX;
let spanY = maxY - minY;
let isWide = spanX > spanY;
let cw = isWide ? this.MAX_DIM :
this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
let ch = !isWide ? this.MAX_DIM :
this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
let mapX = x => this.PAD + ((cw - 2 * this.PAD) / (maxX - minX)) * (x - minX);
let mapY = y => this.PAD + ((ch - 2 * this.PAD) / (maxY - minY)) * (y - minY);
for (let shape of allShapes) {
for (let point of shape) {
point[0] = mapX(point[0]);
point[1] = mapY(point[1]);
}
}
return {w: cw, h: ch};
},
_drawShapes: function(shape, corner, transformed, transformedCorner) {
this._drawOriginal(shape);
this._drawOriginalCorner(corner);
this._drawTransformed(transformed);
this._drawTransformedCorner(transformedCorner);
},
_drawOriginal: function(points) {
this._drawShape(points, this.ORIGINAL_FILL, this.ORIGINAL_STROKE);
},
_drawTransformed: function(points) {
this._drawShape(points, this.TRANSFORMED_FILL, this.TRANSFORMED_STROKE);
},
_drawOriginalCorner: function(points) {
this._drawShape(points, this.ORIGINAL_STROKE, this.ORIGINAL_STROKE);
},
_drawTransformedCorner: function(points) {
this._drawShape(points, this.TRANSFORMED_STROKE, this.TRANSFORMED_STROKE);
},
_drawArrows: function(shape, transformed) {
this._drawArrow(shape[0][0], shape[0][1], transformed[0][0], transformed[0][1]);
this._drawArrow(shape[1][0], shape[1][1], transformed[1][0], transformed[1][1]);
this._drawArrow(shape[2][0], shape[2][1], transformed[2][0], transformed[2][1]);
this._drawArrow(shape[3][0], shape[3][1], transformed[3][0], transformed[3][1]);
},
/**
* Draw a transform preview
*
* @param {String} transform
* The css transform value as a string, as typed by the user, as long
* as it can be computed by the browser
* @param {String} origin
* Same as above for the transform-origin value. Defaults to "center"
* @param {Number} width
* The width of the container. Defaults to 200
* @param {Number} height
* The height of the container. Defaults to 200
* @return {Boolean} Whether or not the preview could be created. Will return
* false for instance if the transform is invalid
*/
preview: function(transform, origin="center", width=200, height=200) {
// Create/clear the canvas
if (!this.canvas) {
this._createMarkup();
}
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Get computed versions of transform and origin
transform = this._getComputed("transform", transform, width, height);
if (transform && transform !== "none") {
origin = this._getComputed("transform-origin", origin, width, height);
// Get the matrix, origin and width height data for the previewed element
let originData = this._getOriginFromOriginString(origin);
let matrixData = this._getMatrixFromTransformString(transform);
// Compute the original box rect and transformed box rect
let shapePoints = [
[0, 0],
[width, 0],
[width, height],
[0, height]
];
let transformedPoints = this._getTransformedPoints(matrixData, shapePoints, originData);
// Do the same for the corner triangle shape
let cornerSize = Math.min(shapePoints[2][1] - shapePoints[1][1],
shapePoints[1][0] - shapePoints[0][0]) / this.CORNER_SIZE_RATIO;
let cornerPoints = [
[shapePoints[1][0], shapePoints[1][1]],
[shapePoints[1][0], shapePoints[1][1] + cornerSize],
[shapePoints[1][0] - cornerSize, shapePoints[1][1]]
];
let transformedCornerPoints = this._getTransformedPoints(matrixData, cornerPoints, originData);
// Resize points to fit everything in the canvas
let {w, h} = this._fitAllShapes([
shapePoints,
transformedPoints,
cornerPoints,
transformedCornerPoints,
[originData]
]);
this.canvas.setAttribute("width", w);
this.canvas.setAttribute("height", h);
this._drawShapes(shapePoints, cornerPoints, transformedPoints, transformedCornerPoints)
this._drawArrows(shapePoints, transformedPoints);
this._drawOrigin(originData[0], originData[1]);
return true;
} else {
return false;
}
}
};

View File

@ -12,7 +12,6 @@ const {Spectrum} = require("devtools/shared/widgets/Spectrum");
const EventEmitter = require("devtools/toolkit/event-emitter");
const {colorUtils} = require("devtools/css-color");
const Heritage = require("sdk/core/heritage");
const {CSSTransformPreviewer} = require("devtools/shared/widgets/CSSTransformPreviewer");
const {Eyedropper} = require("devtools/eyedropper/eyedropper");
Cu.import("resource://gre/modules/Services.jsm");
@ -737,45 +736,6 @@ Tooltip.prototype = {
return def.promise;
},
/**
* Set the content of the tooltip to be the result of CSSTransformPreviewer.
* Meaning a canvas previewing a css transformation.
*
* @param {String} transform
* The CSS transform value (e.g. "rotate(45deg) translateX(50px)")
* @param {PageStyleActor} pageStyle
* An instance of the PageStyleActor that will be used to retrieve
* computed styles
* @param {NodeActor} node
* The NodeActor for the currently selected node
* @return A promise that resolves when the tooltip content is ready, or
* rejects if no transform is provided or the transform is invalid
*/
setCssTransformContent: Task.async(function*(transform, pageStyle, node) {
if (!transform) {
throw "Missing transform";
}
// Look into the computed styles to find the width and height and possibly
// the origin if it hadn't been provided
let styles = yield pageStyle.getComputed(node, {
filter: "user",
markMatched: false,
onlyMatched: false
});
let origin = styles["transform-origin"].value;
let width = parseInt(styles["width"].value);
let height = parseInt(styles["height"].value);
let root = this.doc.createElementNS(XHTML_NS, "div");
let previewer = new CSSTransformPreviewer(root);
this.content = root;
if (!previewer.preview(transform, origin, width, height)) {
throw "Invalid transform";
}
}),
/**
* Set the content of the tooltip to display a font family preview.
* This is based on Lea Verou's Dablet. See https://github.com/LeaVerou/dabblet

View File

@ -26,6 +26,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
const FILTER_CHANGED_TIMEOUT = 300;
const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const TRANSFORM_HIGHLIGHTER_TYPE = "CssTransformHighlighter";
/**
* Helper for long-running processes that should yield occasionally to
@ -187,6 +188,12 @@ function CssHtmlTree(aStyleInspector, aPageStyle)
this._buildContextMenu();
this.createStyleViews();
// Initialize the css transform highlighter if the target supports it
let hUtils = this.styleInspector.inspector.toolbox.highlighterUtils;
if (hUtils.hasCustomHighlighter(TRANSFORM_HIGHLIGHTER_TYPE)) {
this._initTransformHighlighter();
}
}
/**
@ -513,6 +520,65 @@ CssHtmlTree.prototype = {
win.focus();
},
/**
* Get the css transform highlighter front, initializing it if needed
* @param a promise that resolves to the highlighter
*/
getTransformHighlighter: function() {
if (this.transformHighlighterPromise) {
return this.transformHighlighterPromise;
}
let utils = this.styleInspector.inspector.toolbox.highlighterUtils;
this.transformHighlighterPromise =
utils.getHighlighterByType(TRANSFORM_HIGHLIGHTER_TYPE).then(highlighter => {
this.transformHighlighter = highlighter;
return this.transformHighlighter;
});
return this.transformHighlighterPromise;
},
_initTransformHighlighter: function() {
this.isTransformHighlighterShown = false;
this._onMouseMove = this._onMouseMove.bind(this);
this._onMouseLeave = this._onMouseLeave.bind(this);
this.propertyContainer.addEventListener("mousemove", this._onMouseMove, false);
this.propertyContainer.addEventListener("mouseleave", this._onMouseLeave, false);
},
_onMouseMove: function(event) {
if (event.target === this._lastHovered) {
return;
}
if (this.isTransformHighlighterShown) {
this.isTransformHighlighterShown = false;
this.getTransformHighlighter().then(highlighter => highlighter.hide());
}
this._lastHovered = event.target;
if (this._lastHovered.classList.contains("property-value")) {
let propName = this._lastHovered.parentNode.querySelector(".property-name");
if (propName.textContent === "transform") {
this.isTransformHighlighterShown = true;
let node = this.styleInspector.inspector.selection.nodeFront;
this.getTransformHighlighter().then(highlighter => highlighter.show(node));
}
}
},
_onMouseLeave: function(event) {
this._lastHovered = null;
if (this.isTransformHighlighterShown) {
this.isTransformHighlighterShown = false;
this.getTransformHighlighter().then(highlighter => highlighter.hide());
}
},
/**
* Executed by the tooltip when the pointer hovers over an element of the view.
* Used to decide whether the tooltip should be shown or not and to actually
@ -538,12 +604,6 @@ CssHtmlTree.prototype = {
let propValue = target;
let propName = target.parentNode.querySelector(".property-name");
// Test for css transform
if (propName.textContent === "transform") {
return this.tooltip.setCssTransformContent(propValue.textContent,
this.pageStyle, this.viewedElement);
}
// Test for font family
if (propName.textContent === "font-family") {
let prop = propValue.textContent.toLowerCase();
@ -812,6 +872,16 @@ CssHtmlTree.prototype = {
this.tooltip.stopTogglingOnHover(this.propertyContainer);
this.tooltip.destroy();
if (this.transformHighlighter) {
this.transformHighlighter.finalize();
this.transformHighlighter = null;
this.propertyContainer.removeEventListener("mousemove", this._onMouseMove, false);
this.propertyContainer.removeEventListener("mouseleave", this._onMouseLeave, false);
this._lastHovered = null;
}
// Remove bound listeners
this.styleDocument.removeEventListener("contextmenu", this._onContextMenu);
this.styleDocument.removeEventListener("copy", this._onCopy);

View File

@ -24,6 +24,7 @@ const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
const TRANSFORM_HIGHLIGHTER_TYPE = "CssTransformHighlighter";
/**
* These regular expressions are adapted from firebug's css.js, and are
@ -1030,7 +1031,6 @@ TextProperty.prototype = {
}
};
/**
* View hierarchy mostly follows the model hierarchy.
*
@ -1111,6 +1111,12 @@ function CssRuleView(aInspector, aDoc, aStore, aPageStyle) {
this._buildContextMenu();
this._showEmpty();
// Initialize the css transform highlighter if the target supports it
let hUtils = this.inspector.toolbox.highlighterUtils;
if (hUtils.hasCustomHighlighter(TRANSFORM_HIGHLIGHTER_TYPE)) {
this._initTransformHighlighter();
}
}
exports.CssRuleView = CssRuleView;
@ -1159,21 +1165,75 @@ CssRuleView.prototype = {
popupset.appendChild(this._contextmenu);
},
/**
* Get the css transform highlighter front, initializing it if needed
* @param a promise that resolves to the highlighter
*/
getTransformHighlighter: function() {
if (this.transformHighlighterPromise) {
return this.transformHighlighterPromise;
}
let utils = this.inspector.toolbox.highlighterUtils;
this.transformHighlighterPromise =
utils.getHighlighterByType(TRANSFORM_HIGHLIGHTER_TYPE).then(highlighter => {
this.transformHighlighter = highlighter;
return this.transformHighlighter;
});
return this.transformHighlighterPromise;
},
_initTransformHighlighter: function() {
this.isTransformHighlighterShown = false;
this._onMouseMove = this._onMouseMove.bind(this);
this._onMouseLeave = this._onMouseLeave.bind(this);
this.element.addEventListener("mousemove", this._onMouseMove, false);
this.element.addEventListener("mouseleave", this._onMouseLeave, false);
},
_onMouseMove: function(event) {
if (event.target === this._lastHovered) {
return;
}
if (this.isTransformHighlighterShown) {
this.isTransformHighlighterShown = false;
this.getTransformHighlighter().then(highlighter => highlighter.hide());
}
this._lastHovered = event.target;
let prop = event.target.textProperty;
let isHighlightable = prop && prop.name === "transform" &&
prop.enabled && !prop.overridden &&
!prop.rule.pseudoElement;
if (isHighlightable) {
this.isTransformHighlighterShown = true;
let node = this.inspector.selection.nodeFront;
this.getTransformHighlighter().then(highlighter => highlighter.show(node));
}
},
_onMouseLeave: function(event) {
this._lastHovered = null;
if (this.isTransformHighlighterShown) {
this.isTransformHighlighterShown = false;
this.getTransformHighlighter().then(highlighter => highlighter.hide());
}
},
/**
* Which type of hover-tooltip should be shown for the given element?
* This depends on the element: does it contain an image URL, a CSS transform,
* a font-family, ...
* This depends on the element: does it contain a URL, a font-family, ...
* @param {DOMNode} el The element to test
* @return {String} The type of hover-tooltip
*/
_getHoverTooltipTypeForTarget: function(el) {
let prop = el.textProperty;
// Test for css transform
if (prop && prop.name === "transform") {
return "transform";
}
// Test for image
let isUrl = el.classList.contains("theme-link") &&
el.parentNode.classList.contains("ruleview-propertyvalue");
@ -1218,10 +1278,6 @@ CssRuleView.prototype = {
this.colorPicker.hide();
}
if (tooltipType === "transform") {
return this.previewTooltip.setCssTransformContent(target.textProperty.value,
this.pageStyle, this._viewedElement);
}
if (tooltipType === "image") {
let prop = target.parentNode.textProperty;
let dim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
@ -1465,6 +1521,16 @@ CssRuleView.prototype = {
this.previewTooltip.destroy();
this.colorPicker.destroy();
if (this.transformHighlighter) {
this.transformHighlighter.finalize();
this.transformHighlighter = null;
this.element.removeEventListener("mousemove", this._onMouseMove, false);
this.element.removeEventListener("mouseleave", this._onMouseLeave, false);
this._lastHovered = null;
}
if (this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}

View File

@ -99,4 +99,7 @@ skip-if = os == "win" && debug # bug 963492
[browser_styleinspector_tooltip-longhand-fontfamily.js]
[browser_styleinspector_tooltip-shorthand-fontfamily.js]
[browser_styleinspector_tooltip-size.js]
[browser_styleinspector_tooltip-transform.js]
[browser_styleinspector_transform-highlighter-01.js]
[browser_styleinspector_transform-highlighter-02.js]
[browser_styleinspector_transform-highlighter-03.js]
[browser_styleinspector_transform-highlighter-04.js]

View File

@ -4,8 +4,7 @@
"use strict";
// Test that the color picker tooltip hides when an image or transform
// tooltip appears
// Test that the color picker tooltip hides when an image tooltip appears
const PAGE_CONTENT = [
'<style type="text/css">',
@ -14,7 +13,6 @@ const PAGE_CONTENT = [
' background-color: #ededed;',
' background-image: url(chrome://global/skin/icons/warning-64.png);',
' border: 2em solid rgba(120, 120, 120, .5);',
' transform: skew(-16deg);',
' }',
'</style>',
'Testing the color picker tooltip!'
@ -29,7 +27,6 @@ let test = asyncTest(function*() {
.querySelector(".ruleview-colorswatch");
yield testColorPickerHidesWhenImageTooltipAppears(view, swatch);
yield testColorPickerHidesWhenTransformTooltipAppears(view, swatch);
});
function* testColorPickerHidesWhenImageTooltipAppears(view, swatch) {
@ -49,20 +46,3 @@ function* testColorPickerHidesWhenImageTooltipAppears(view, swatch) {
ok(true, "The color picker closed when the image preview tooltip appeared");
}
function* testColorPickerHidesWhenTransformTooltipAppears(view, swatch) {
let transformSpan = getRuleViewProperty(view, "body", "transform").valueSpan;
let tooltip = view.colorPicker.tooltip;
info("Showing the color picker tooltip by clicking on the color swatch");
let onShown = tooltip.once("shown");
swatch.click();
yield onShown;
info("Now showing the transform preview tooltip to hide the color picker");
let onHidden = tooltip.once("hidden");
yield assertHoverTooltipOn(view.previewTooltip, transformSpan);
yield onHidden;
ok(true, "The color picker closed when the transform preview tooltip appeared");
}

View File

@ -12,7 +12,6 @@ const TEST_PAGE = [
'<style type="text/css">',
' div {',
' width: 300px;height: 300px;border-radius: 50%;',
' transform: skew(45deg);',
' background: red url(chrome://global/skin/icons/warning-64.png);',
' }',
'</style>',
@ -25,42 +24,10 @@ let test = asyncTest(function*() {
yield selectNode("div", inspector);
yield testTransformDimension(view);
yield testImageDimension(view);
yield testPickerDimension(view);
});
function* testTransformDimension(ruleView) {
info("Testing css transform tooltip dimensions");
let tooltip = ruleView.previewTooltip;
let panel = tooltip.panel;
let {valueSpan} = getRuleViewProperty(ruleView, "div", "transform");
// Make sure there is a hover tooltip for this property, this also will fill
// the tooltip with its content
yield assertHoverTooltipOn(tooltip, valueSpan);
info("Showing the tooltip");
let onShown = tooltip.once("shown");
tooltip.show();
yield onShown;
// Let's not test for a specific size, but instead let's make sure it's at
// least as big as the preview canvas
let canvas = panel.querySelector("canvas");
let w = canvas.width;
let h = canvas.height;
let panelRect = panel.getBoundingClientRect();
ok(panelRect.width >= w, "The panel is wide enough to show the canvas");
ok(panelRect.height >= h, "The panel is high enough to show the canvas");
let onHidden = tooltip.once("hidden");
tooltip.hide();
yield onHidden;
}
function* testImageDimension(ruleView) {
info("Testing background-image tooltip dimensions");

View File

@ -1,87 +0,0 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the css transform preview tooltip is shown on transform properties
const PAGE_CONTENT = [
'<style type="text/css">',
' #testElement {',
' width: 500px;',
' height: 300px;',
' background: red;',
' transform: skew(16deg);',
' }',
' .test-element {',
' transform-origin: top left;',
' transform: rotate(45deg);',
' }',
' div {',
' transform: scaleX(1.5);',
' transform-origin: bottom right;',
' }',
' [attr] {',
' }',
'</style>',
'<div id="testElement" class="test-element" attr="value">transformed element</div>'
].join("\n");
let test = asyncTest(function*() {
yield addTab("data:text/html,rule view css transform tooltip test");
content.document.body.innerHTML = PAGE_CONTENT;
let {toolbox, inspector, view} = yield openRuleView();
info("Selecting the test node");
yield selectNode("#testElement", inspector);
info("Checking that transforms tooltips are shown in various rule-view properties");
for (let selector of [".test-element", "div", "#testElement"]) {
yield testTransformTooltipOnSelector(view, selector);
}
info("Checking that the transform tooltip doesn't appear for invalid transforms");
yield testTransformTooltipNotShownOnInvalidTransform(view);
info("Checking transforms in the computed-view");
let {view} = yield openComputedView();
yield testTransformTooltipOnComputedView(view);
});
function* testTransformTooltipOnSelector(view, selector) {
info("Testing that a transform tooltip appears on transform in " + selector);
let {valueSpan} = getRuleViewProperty(view, selector, "transform");
ok(valueSpan, "The transform property was found");
yield assertHoverTooltipOn(view.previewTooltip, valueSpan);
// The transform preview is canvas, so there's not much we can test, so for
// now, let's just be happy with the fact that the tooltips is shown!
ok(true, "Tooltip shown on the transform property in " + selector);
}
function* testTransformTooltipNotShownOnInvalidTransform(view) {
let ruleEditor;
for (let rule of view._elementStyle.rules) {
if (rule.matchedSelectors[0] === "[attr]") {
ruleEditor = rule.editor;
}
}
ruleEditor.addProperty("transform", "muchTransform(suchAngle)", "");
let {valueSpan} = getRuleViewProperty(view, "[attr]", "transform");
let isValid = yield isHoverTooltipTarget(view.previewTooltip, valueSpan);
ok(!isValid, "The tooltip did not appear on hover of an invalid transform value");
}
function* testTransformTooltipOnComputedView(view) {
info("Testing that a transform tooltip appears in the computed view too");
let {valueSpan} = getComputedViewProperty(view, "transform");
yield assertHoverTooltipOn(view.tooltip, valueSpan);
// The transform preview is canvas, so there's not much we can test, so for
// now, let's just be happy with the fact that the tooltips is shown!
ok(true, "Tooltip shown on the computed transform property");
}

View File

@ -0,0 +1,38 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the css transform highlighter is created only when asked
const PAGE_CONTENT = [
'<style type="text/css">',
' body {',
' transform: skew(16deg);',
' }',
'</style>',
'Test the css transform highlighter'
].join("\n");
let test = asyncTest(function*() {
yield addTab("data:text/html," + PAGE_CONTENT);
let {view: rView} = yield openRuleView();
ok(!rView.transformHighlighter, "No highlighter exists in the rule-view");
let h = yield rView.getTransformHighlighter();
ok(rView.transformHighlighter, "The highlighter has been created in the rule-view");
is(h, rView.transformHighlighter, "The right highlighter has been created");
let h2 = yield rView.getTransformHighlighter();
is(h, h2, "The same instance of highlighter is returned everytime in the rule-view");
let {view: cView} = yield openComputedView();
ok(!cView.transformHighlighter, "No highlighter exists in the computed-view");
let h = yield cView.getTransformHighlighter();
ok(cView.transformHighlighter, "The highlighter has been created in the computed-view");
is(h, cView.transformHighlighter, "The right highlighter has been created");
let h2 = yield cView.getTransformHighlighter();
is(h, h2, "The same instance of highlighter is returned everytime in the computed-view");
});

View File

@ -0,0 +1,54 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the css transform highlighter is created when hovering over a
// transform property
const PAGE_CONTENT = [
'<style type="text/css">',
' body {',
' transform: skew(16deg);',
' color: yellow;',
' }',
'</style>',
'Test the css transform highlighter'
].join("\n");
let test = asyncTest(function*() {
yield addTab("data:text/html," + PAGE_CONTENT);
let {view: rView} = yield openRuleView();
ok(!rView.transformHighlighter, "No highlighter exists in the rule-view (1)");
info("Faking a mousemove on a non-transform property");
let {valueSpan} = getRuleViewProperty(rView, "body", "color");
rView._onMouseMove({target: valueSpan});
ok(!rView.transformHighlighter, "No highlighter exists in the rule-view (2)");
ok(!rView.transformHighlighterPromise, "No highlighter is being initialized");
info("Faking a mousemove on a transform property");
let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
rView._onMouseMove({target: valueSpan});
ok(rView.transformHighlighterPromise, "The highlighter is being initialized");
let h = yield rView.transformHighlighterPromise;
is(h, rView.transformHighlighter, "The initialized highlighter is the right one");
let {view: cView} = yield openComputedView();
ok(!cView.transformHighlighter, "No highlighter exists in the computed-view (1)");
info("Faking a mousemove on a non-transform property");
let {valueSpan} = getComputedViewProperty(cView, "color");
cView._onMouseMove({target: valueSpan});
ok(!cView.transformHighlighter, "No highlighter exists in the computed-view (2)");
ok(!cView.transformHighlighterPromise, "No highlighter is being initialized");
info("Faking a mousemove on a transform property");
let {valueSpan} = getComputedViewProperty(cView, "transform");
cView._onMouseMove({target: valueSpan});
ok(cView.transformHighlighterPromise, "The highlighter is being initialized");
let h = yield cView.transformHighlighterPromise;
is(h, cView.transformHighlighter, "The initialized highlighter is the right one");
});

View File

@ -0,0 +1,89 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the css transform highlighter is shown when hovering over transform
// properties
// Note that in this test, we mock the highlighter front, merely testing the
// behavior of the style-inspector UI for now
const PAGE_CONTENT = [
'<style type="text/css">',
' html {',
' transform: scale(.9);',
' }',
' body {',
' transform: skew(16deg);',
' color: purple;',
' }',
'</style>',
'Test the css transform highlighter'
].join("\n");
let test = asyncTest(function*() {
yield addTab("data:text/html," + PAGE_CONTENT);
let {inspector, view: rView} = yield openRuleView();
// Mock the highlighter front to get the reference of the NodeFront
let HighlighterFront = {
isShown: false,
nodeFront: null,
nbOfTimesShown: 0,
show: function(nodeFront) {
this.nodeFront = nodeFront;
this.isShown = true;
this.nbOfTimesShown ++;
},
hide: function() {
this.nodeFront = null;
this.isShown = false;
}
};
// Inject the mock highlighter in the rule-view
rView.transformHighlighterPromise = {
then: function(cb) {
cb(HighlighterFront);
}
};
let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
info("Checking that the HighlighterFront's show/hide methods are called");
rView._onMouseMove({target: valueSpan});
ok(HighlighterFront.isShown, "The highlighter is shown");
rView._onMouseLeave();
ok(!HighlighterFront.isShown, "The highlighter is hidden");
info("Checking that hovering several times over the same property doesn't" +
" show the highlighter several times");
let nb = HighlighterFront.nbOfTimesShown;
rView._onMouseMove({target: valueSpan});
is(HighlighterFront.nbOfTimesShown, nb + 1, "The highlighter was shown once");
rView._onMouseMove({target: valueSpan});
rView._onMouseMove({target: valueSpan});
is(HighlighterFront.nbOfTimesShown, nb + 1,
"The highlighter was shown once, after several mousemove");
info("Checking that the right NodeFront reference is passed");
yield selectNode(content.document.documentElement, inspector);
let {valueSpan} = getRuleViewProperty(rView, "html", "transform");
rView._onMouseMove({target: valueSpan});
is(HighlighterFront.nodeFront.tagName, "HTML",
"The right NodeFront is passed to the highlighter (1)");
yield selectNode("body", inspector);
let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
rView._onMouseMove({target: valueSpan});
is(HighlighterFront.nodeFront.tagName, "BODY",
"The right NodeFront is passed to the highlighter (2)");
info("Checking that the highlighter gets hidden when hovering a non-transform property");
let {valueSpan} = getRuleViewProperty(rView, "body", "color");
rView._onMouseMove({target: valueSpan});
ok(!HighlighterFront.isShown, "The highlighter is hidden");
});

View File

@ -0,0 +1,58 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the css transform highlighter is shown only when hovering over a
// transform declaration that isn't overriden or disabled
// Note that unlike the other browser_styleinspector_transform-highlighter-N.js
// tests, this one only tests the rule-view as only this view features disabled
// and overriden properties
const PAGE_CONTENT = [
'<style type="text/css">',
' div {',
' background: purple;',
' width:300px;height:300px;',
' transform: rotate(16deg);',
' }',
' .test {',
' transform: skew(25deg);',
' }',
'</style>',
'<div class="test"></div>'
].join("\n");
let test = asyncTest(function*() {
yield addTab("data:text/html," + PAGE_CONTENT);
let {view: rView, inspector} = yield openRuleView();
yield selectNode(".test", inspector);
info("Faking a mousemove on the overriden property");
let {valueSpan} = getRuleViewProperty(rView, "div", "transform");
rView._onMouseMove({target: valueSpan});
ok(!rView.transformHighlighter, "No highlighter was created for the overriden property");
ok(!rView.transformHighlighterPromise, "And no highlighter is being initialized either");
info("Disabling the applied property");
let classRuleEditor = rView.element.children[1]._ruleEditor;
let propEditor = classRuleEditor.rule.textProps[0].editor;
propEditor.enable.click();
yield classRuleEditor.rule._applyingModifications;
info("Faking a mousemove on the disabled property");
let {valueSpan} = getRuleViewProperty(rView, ".test", "transform");
rView._onMouseMove({target: valueSpan});
ok(!rView.transformHighlighter, "No highlighter was created for the disabled property");
ok(!rView.transformHighlighterPromise, "And no highlighter is being initialized either");
info("Faking a mousemove on the now unoverriden property");
let {valueSpan} = getRuleViewProperty(rView, "div", "transform");
rView._onMouseMove({target: valueSpan});
ok(rView.transformHighlighterPromise, "The highlighter is being initialized now");
let h = yield rView.transformHighlighterPromise;
is(h, rView.transformHighlighter, "The initialized highlighter is the right one");
});

View File

@ -5,6 +5,7 @@
%endif
/* Box model highlighter */
svg|g.box-model-container {
opacity: 0.4;
}
@ -107,3 +108,23 @@ html|*.highlighter-nodeinfobar-dimensions {
.highlighter-nodeinfobar-container[hide-arrow] > .highlighter-nodeinfobar {
margin: 7px 0;
}
/* Css transform highlighter */
svg|polygon.css-transform-transformed {
fill: #80d4ff;
opacity: 0.8;
}
svg|polygon.css-transform-untransformed {
fill: #66cc52;
opacity: 0.8;
}
svg|polygon.css-transform-transformed,
svg|polygon.css-transform-untransformed,
svg|line.css-transform-line {
stroke: #08C;
stroke-dasharray: 5 3;
stroke-width: 2;
}

View File

@ -25,16 +25,18 @@ LayoutHelpers.prototype = {
/**
* Get box quads adjusted for iframes and zoom level.
*
* @param {DOMNode} node
* The node for which we are to get the box model region quads
* @param {String} region
* The box model region to return:
* "content", "padding", "border" or "margin"
* @return {Object} An object that has the same structure as one quad returned
* by getBoxQuads
*/
getAdjustedQuads: function(node, region) {
if (!node) {
return;
if (!node || !node.getBoxQuads) {
return null;
}
let [quads] = node.getBoxQuads({
@ -42,10 +44,10 @@ LayoutHelpers.prototype = {
});
if (!quads) {
return;
return null;
}
let [xOffset, yOffset] = this._getNodeOffsets(node);
let [xOffset, yOffset] = this.getFrameOffsets(node);
let scale = this.calculateScale(node);
return {
@ -86,6 +88,12 @@ LayoutHelpers.prototype = {
};
},
/**
* Get the current zoom factor applied to the container window of a given node
* @param {DOMNode}
* The node for which the zoom factor should be calculated
* @return {Number}
*/
calculateScale: function(node) {
let win = node.ownerDocument.defaultView;
let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
@ -97,12 +105,14 @@ LayoutHelpers.prototype = {
* Compute the absolute position and the dimensions of a node, relativalely
* to the root window.
*
* @param nsIDOMNode aNode
* @param {DOMNode} aNode
* a DOM element to get the bounds for
* @param nsIWindow aContentWindow
* @param {DOMWindow} aContentWindow
* the content window holding the node
* @return {Object}
* A rect object with the {top, left, width, height} properties
*/
getRect: function LH_getRect(aNode, aContentWindow) {
getRect: function(aNode, aContentWindow) {
let frameWin = aNode.ownerDocument.defaultView;
let clientRect = aNode.getBoundingClientRect();
@ -115,7 +125,6 @@ LayoutHelpers.prototype = {
// We iterate through all the parent windows.
while (true) {
// Are we in the top-level window?
if (this.isTopLevelWindow(frameWin)) {
break;
@ -149,15 +158,15 @@ LayoutHelpers.prototype = {
* suitable API for determining the offset between the iframe's content and
* its bounding client rect. Bug 626359 should provide us with such an API.
*
* @param aIframe
* @param {DOMNode} aIframe
* The iframe.
* @returns array [offsetTop, offsetLeft]
* offsetTop is the distance from the top of the iframe and the
* top of the content document.
* offsetLeft is the distance from the left of the iframe and the
* left of the content document.
* @return {Array} [offsetTop, offsetLeft]
* offsetTop is the distance from the top of the iframe and the top of
* the content document.
* offsetLeft is the distance from the left of the iframe and the left
* of the content document.
*/
getIframeContentOffset: function LH_getIframeContentOffset(aIframe) {
getIframeContentOffset: function(aIframe) {
let style = aIframe.contentWindow.getComputedStyle(aIframe, null);
// In some cases, the computed style is null
@ -178,12 +187,14 @@ LayoutHelpers.prototype = {
* Find an element from the given coordinates. This method descends through
* frames to find the element the user clicked inside frames.
*
* @param DOMDocument aDocument the document to look into.
* @param integer aX
* @param integer aY
* @returns Node|null the element node found at the given coordinates.
* @param {DOMDocument} aDocument the document to look into.
* @param {Number} aX
* @param {Number} aY
* @return {DOMNode}
* the element node found at the given coordinates, or null if no node
* was found
*/
getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY) {
getElementFromPoint: function(aDocument, aX, aY) {
let node = aDocument.elementFromPoint(aX, aY);
if (node && node.contentDocument) {
if (node instanceof Ci.nsIDOMHTMLIFrameElement) {
@ -214,10 +225,12 @@ LayoutHelpers.prototype = {
/**
* Scroll the document so that the element "elem" appears in the viewport.
*
* @param Element elem the element that needs to appear in the viewport.
* @param bool centered true if you want it centered, false if you want it to
* appear on the top of the viewport. It is true by default, and that is
* usually what you want.
* @param {DOMNode} elem
* The element that needs to appear in the viewport.
* @param {Boolean} centered
* true if you want it centered, false if you want it to appear on the
* top of the viewport. It is true by default, and that is usually what
* you want.
*/
scrollIntoViewIfNeeded: function(elem, centered) {
// We want to default to centering the element in the page,
@ -293,10 +306,10 @@ LayoutHelpers.prototype = {
* Check if a node and its document are still alive
* and attached to the window.
*
* @param aNode
* @param {DOMNode} aNode
* @return {Boolean}
*/
isNodeConnected: function LH_isNodeConnected(aNode)
{
isNodeConnected: function(aNode) {
try {
let connected = (aNode.ownerDocument && aNode.ownerDocument.defaultView &&
!(aNode.compareDocumentPosition(aNode.ownerDocument.documentElement) &
@ -310,8 +323,11 @@ LayoutHelpers.prototype = {
/**
* like win.parent === win, but goes through mozbrowsers and mozapps iframes.
*
* @param {DOMWindow} win
* @return {Boolean}
*/
isTopLevelWindow: function LH_isTopLevelWindow(win) {
isTopLevelWindow: function(win) {
let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
@ -321,6 +337,9 @@ LayoutHelpers.prototype = {
/**
* Check a window is part of the top level window.
*
* @param {DOMWindow} win
* @return {Boolean}
*/
isIncludedInTopLevelWindow: function LH_isIncludedInTopLevelWindow(win) {
if (this.isTopLevelWindow(win)) {
@ -337,8 +356,11 @@ LayoutHelpers.prototype = {
/**
* like win.parent, but goes through mozbrowsers and mozapps iframes.
*
* @param {DOMWindow} win
* @return {DOMWindow}
*/
getParentWindow: function LH_getParentWindow(win) {
getParentWindow: function(win) {
if (this.isTopLevelWindow(win)) {
return null;
}
@ -358,10 +380,12 @@ LayoutHelpers.prototype = {
/**
* like win.frameElement, but goes through mozbrowsers and mozapps iframes.
*
* @param DOMWindow win The window to get the frame for
* @return DOMElement The element in which the window is embedded.
* @param {DOMWindow} win
* The window to get the frame for
* @return {DOMNode}
* The element in which the window is embedded.
*/
getFrameElement: function LH_getFrameElement(win) {
getFrameElement: function(win) {
if (this.isTopLevelWindow(win)) {
return null;
}
@ -374,12 +398,14 @@ LayoutHelpers.prototype = {
},
/**
* Get the x and y offsets for a node taking iframes into account.
* Get the x/y offsets for of all the parent frames of a given node
*
* @param {DOMNode} node
* The node for which we are to get the offset
* @return {Array}
* The frame offset [x, y]
*/
_getNodeOffsets: function(node) {
getFrameOffsets: function(node) {
let xOffset = 0;
let yOffset = 0;
let frameWin = node.ownerDocument.defaultView;
@ -412,4 +438,61 @@ LayoutHelpers.prototype = {
return [xOffset * scale, yOffset * scale];
},
/**
* Get the 4 bounding points for a node taking iframes into account.
* Note that for transformed nodes, this will return the untransformed bound.
*
* @param {DOMNode} node
* @return {Object}
* An object with p1,p2,p3,p4 properties being {x,y} objects
*/
getNodeBounds: function(node) {
if (!node) {
return;
}
let scale = this.calculateScale(node);
// Find out the offset of the node in its current frame
let offsetLeft = 0;
let offsetTop = 0;
let el = node;
while (el && el.parentNode) {
offsetLeft += el.offsetLeft;
offsetTop += el.offsetTop;
el = el.offsetParent;
}
// Also take scrolled containers into account
let el = node;
while (el && el.parentNode) {
if (el.scrollTop) {
offsetTop -= el.scrollTop;
}
if (el.scrollLeft) {
offsetLeft -= el.scrollLeft;
}
el = el.parentNode;
}
// And add the potential frame offset if the node is nested
let [xOffset, yOffset] = this.getFrameOffsets(node);
xOffset += offsetLeft;
yOffset += offsetTop;
xOffset *= scale;
yOffset *= scale;
// Get the width and height
let width = node.offsetWidth * scale;
let height = node.offsetHeight * scale;
return {
p1: {x: xOffset, y: yOffset},
p2: {x: xOffset + width, y: yOffset},
p3: {x: xOffset + width, y: yOffset + height},
p4: {x: xOffset, y: yOffset + height}
};
}
};

View File

@ -9,6 +9,7 @@ const Services = require("Services");
const protocol = require("devtools/server/protocol");
const {Arg, Option, method} = protocol;
const events = require("sdk/event/core");
const Heritage = require("sdk/core/heritage");
const EventEmitter = require("devtools/toolkit/event-emitter");
const GUIDE_STROKE_WIDTH = 1;
@ -28,18 +29,42 @@ const XHTML_NS = "http://www.w3.org/1999/xhtml";
const SVG_NS = "http://www.w3.org/2000/svg";
const HIGHLIGHTER_PICKED_TIMER = 1000;
const INFO_BAR_OFFSET = 5;
// The minimum distance a line should be before it has an arrow marker-end
const ARROW_LINE_MIN_DISTANCE = 10;
// All possible highlighter classes
let HIGHLIGHTER_CLASSES = exports.HIGHLIGHTER_CLASSES = {
"BoxModelHighlighter": BoxModelHighlighter,
"CssTransformHighlighter": CssTransformHighlighter
};
/**
* The HighlighterActor is the server-side entry points for any tool that wishes
* to highlight elements in the content document.
* The Highlighter is the server-side entry points for any tool that wishes to
* highlight elements in some way in the content document.
*
* The highlighter can be retrieved via the inspector's getHighlighter method.
* A little bit of vocabulary:
* - <something>HighlighterActor classes are the actors that can be used from
* the client. They do very little else than instantiate a given
* <something>Highlighter and use it to highlight elements.
* - <something>Highlighter classes aren't actors, they're just JS classes that
* know how to create and attach the actual highlighter elements on top of the
* content
*
* The most used highlighter actor is the HighlighterActor which can be
* conveniently retrieved via the InspectorActor's 'getHighlighter' method.
* The InspectorActor will always return the same instance of
* HighlighterActor if asked several times and this instance is used in the
* toolbox to highlighter elements's box-model from the markup-view, layout-view,
* console, debugger, ... as well as select elements with the pointer (pick).
*
* Other types of highlighter actors exist and can be accessed via the
* InspectorActor's 'getHighlighterByType' method.
*/
/**
* The HighlighterActor class
*/
let HighlighterActor = protocol.ActorClass({
let HighlighterActor = exports.HighlighterActor = protocol.ActorClass({
typeName: "highlighter",
initialize: function(inspector, autohide) {
@ -53,7 +78,7 @@ let HighlighterActor = protocol.ActorClass({
this._highlighterReady = this._highlighterReady.bind(this);
this._highlighterHidden = this._highlighterHidden.bind(this);
if (this._supportsBoxModelHighlighter()) {
if (supportXULBasedHighlighter(this._tabActor)) {
this._boxModelHighlighter =
new BoxModelHighlighter(this._tabActor, this._inspector);
@ -66,18 +91,6 @@ let HighlighterActor = protocol.ActorClass({
get conn() this._inspector && this._inspector.conn,
/**
* Can the host support the box model highlighter which requires a parent
* XUL node to attach itself.
*/
_supportsBoxModelHighlighter: function() {
// Note that <browser>s on Fennec also have a XUL parentNode but the box
// model highlighter doesn't display correctly on Fennec (bug 993190)
return this._tabActor.browser &&
!!this._tabActor.browser.parentNode &&
Services.appinfo.ID !== "{aa3c5121-dab2-40e2-81ca-7ea25febc110}";
},
destroy: function() {
protocol.Actor.prototype.destroy.call(this);
if (this._boxModelHighlighter) {
@ -103,7 +116,7 @@ let HighlighterActor = protocol.ActorClass({
* all options may be supported by all types of highlighters.
*/
showBoxModel: method(function(node, options={}) {
if (node && this._isNodeValidForHighlighting(node.rawNode)) {
if (node && isNodeValid(node.rawNode)) {
this._boxModelHighlighter.show(node.rawNode, options);
} else {
this._boxModelHighlighter.hide();
@ -115,25 +128,6 @@ let HighlighterActor = protocol.ActorClass({
}
}),
_isNodeValidForHighlighting: function(node) {
// Is it null or dead?
let isNotDead = node && !Cu.isDeadWrapper(node);
// Is it connected to the document?
let isConnected = false;
try {
let doc = node.ownerDocument;
isConnected = (doc && doc.defaultView && doc.documentElement.contains(node));
} catch (e) {
// "can't access dead object" error
}
// Is it an element node
let isElementNode = node.nodeType === Ci.nsIDOMNode.ELEMENT_NODE;
return isNotDead && isConnected && isElementNode;
},
/**
* Hide the box model highlighting if it was shown before
*/
@ -258,12 +252,190 @@ let HighlighterActor = protocol.ActorClass({
})
});
exports.HighlighterActor = HighlighterActor;
let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
/**
* The HighlighterFront class
* A generic highlighter actor class that instantiate a highlighter given its
* type name and allows to show/hide it.
*/
let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
let CustomHighlighterActor = exports.CustomHighlighterActor = protocol.ActorClass({
typeName: "customhighlighter",
/**
* Create a highlighter instance given its typename
* The typename must be one of HIGHLIGHTER_CLASSES and the class must
* implement constructor(tab, inspector), show(node), hide(), destroy()
*/
initialize: function(inspector, typeName) {
protocol.Actor.prototype.initialize.call(this, null);
this._inspector = inspector;
let constructor = HIGHLIGHTER_CLASSES[typeName];
if (!constructor) {
throw new Error(typeName + " isn't a valid highlighter class (" +
Object.keys(HIGHLIGHTER_CLASSES) + ")");
return;
}
// The assumption is that all custom highlighters need a XUL parent in the
// browser to append their elements
if (supportXULBasedHighlighter(inspector.tabActor)) {
this._highlighter = new constructor(inspector.tabActor, inspector);
}
},
get conn() this._inspector && this._inspector.conn,
destroy: function() {
protocol.Actor.prototype.destroy.call(this);
this.finalize();
},
/**
* Display the highlighter on a given NodeActor.
* @param NodeActor The node to be highlighted
*/
show: method(function(node) {
if (!node || !isNodeValid(node.rawNode) || !this._highlighter) {
return;
}
this._highlighter.show(node.rawNode);
}, {
request: {
node: Arg(0, "domnode")
}
}),
/**
* Hide the highlighter if it was shown before
*/
hide: method(function() {
if (this._highlighter) {
this._highlighter.hide();
}
}, {
request: {}
}),
/**
* Kill this actor. This method is called automatically just before the actor
* is destroyed.
*/
finalize: method(function() {
if (this._highlighter) {
this._highlighter.destroy();
this._highlighter = null;
}
}, {
oneway: true
})
});
let CustomHighlighterFront = protocol.FrontClass(CustomHighlighterActor, {});
/**
* Parent class for XUL-based complex highlighter that are inserted in the
* parent browser structure
*/
function XULBasedHighlighter(tabActor, inspector) {
this._inspector = inspector;
this.browser = tabActor.browser;
this.win = tabActor.window;
this.chromeDoc = this.browser.ownerDocument;
this.currentNode = null;
this.update = this.update.bind(this);
}
XULBasedHighlighter.prototype = {
/**
* Show the highlighter on a given node
* @param {DOMNode} node
*/
show: function(node) {
if (!isNodeValid(node) || node === this.currentNode) {
return;
}
this._detachPageListeners();
this.currentNode = node;
this._attachPageListeners();
this._show();
},
/**
* Hide the highlighter
*/
hide: function() {
if (!isNodeValid(this.currentNode)) {
return;
}
this._hide();
this._detachPageListeners();
this.currentNode = null;
},
/**
* Update the highlighter while shown
*/
update: function() {
if (isNodeValid(this.currentNode)) {
this._update();
}
},
_show: function() {
// To be implemented by sub classes
// When called, sub classes should actually show the highlighter for
// this.currentNode
},
_update: function() {
// To be implemented by sub classes
// When called, sub classes should update the highlighter shown for
// this.currentNode
// This is called as a result of a page scroll, zoom or repaint
},
_hide: function() {
// To be implemented by sub classes
// When called, sub classes should actually hide the highlighter
},
/**
* Listen to changes on the content page to update the highlighter
*/
_attachPageListeners: function() {
if (isNodeValid(this.currentNode)) {
let win = this.currentNode.ownerDocument.defaultView;
this.browser.addEventListener("MozAfterPaint", this.update);
}
},
/**
* Stop listening to page changes
*/
_detachPageListeners: function() {
if (isNodeValid(this.currentNode)) {
let win = this.currentNode.ownerDocument.defaultView;
this.browser.removeEventListener("MozAfterPaint", this.update);
}
},
destroy: function() {
this.hide();
this.win = null;
this.browser = null;
this.chromeDoc = null;
this._inspector = null;
this.currentNode = null;
}
};
/**
* The BoxModelHighlighter is the class that actually draws the the box model
@ -308,26 +480,13 @@ let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
* </stack>
*/
function BoxModelHighlighter(tabActor, inspector) {
this.browser = tabActor.browser;
this.win = tabActor.window;
this.chromeDoc = this.browser.ownerDocument;
this.chromeWin = this.chromeDoc.defaultView;
this._inspector = inspector;
XULBasedHighlighter.call(this, tabActor, inspector);
this.layoutHelpers = new LayoutHelpers(this.win);
this.chromeLayoutHelper = new LayoutHelpers(this.chromeWin);
this.transitionDisabler = null;
this.pageEventsMuter = null;
this._update = this._update.bind(this);
this.handleEvent = this.handleEvent.bind(this);
this.currentNode = null;
EventEmitter.decorate(this);
this._initMarkup();
EventEmitter.decorate(this);
}
BoxModelHighlighter.prototype = {
BoxModelHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
get zoom() {
return this.win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils).fullZoom;
@ -450,61 +609,42 @@ BoxModelHighlighter.prototype = {
* Destroy the nodes. Remove listeners.
*/
destroy: function() {
this.hide();
this.chromeWin.clearTimeout(this.transitionDisabler);
this.chromeWin.clearTimeout(this.pageEventsMuter);
this.nodeInfo = null;
XULBasedHighlighter.prototype.destroy.call(this);
this._highlighterContainer.remove();
this._highlighterContainer = null;
this.nodeInfo = null;
this.rect = null;
this.win = null;
this.browser = null;
this.chromeDoc = null;
this.chromeWin = null;
this.currentNode = null;
},
/**
* Show the highlighter on a given node
*
* @param {DOMNode} node
* @param {Object} options
* Object used for passing options
*/
show: function(node, options={}) {
this.currentNode = node;
this._showInfobar();
this._detachPageListeners();
this._attachPageListeners();
_show: function(options={}) {
this._update();
this._trackMutations();
this.emit("ready");
},
/**
* Track the current node markup mutations so that the node info bar can be
* updated to reflects the node's attributes
*/
_trackMutations: function() {
if (this.currentNode) {
if (isNodeValid(this.currentNode)) {
let win = this.currentNode.ownerDocument.defaultView;
this.currentNodeObserver = new win.MutationObserver(() => {
this._update();
});
this.currentNodeObserver = new win.MutationObserver(this.update);
this.currentNodeObserver.observe(this.currentNode, {attributes: true});
}
},
_untrackMutations: function() {
if (this.currentNode) {
if (this.currentNodeObserver) {
// The following may fail with a "can't access dead object" exception
// when the actor is being destroyed
try {
this.currentNodeObserver.disconnect();
} catch (e) {}
this.currentNodeObserver = null;
}
if (isNodeValid(this.currentNode) && this.currentNodeObserver) {
this.currentNodeObserver.disconnect();
this.currentNodeObserver = null;
}
},
@ -518,29 +658,22 @@ BoxModelHighlighter.prototype = {
* the box that the guides should outline. Default is content.
*/
_update: function(options={}) {
if (this.currentNode) {
if (this._highlightBoxModel(options)) {
this._showInfobar();
} else {
// Nothing to highlight (0px rectangle like a <script> tag for instance)
this.hide();
}
this.emit("ready");
if (this._updateBoxModel(options)) {
this._showInfobar();
this._showBoxModel();
} else {
// Nothing to highlight (0px rectangle like a <script> tag for instance)
this._hide();
}
},
/**
* Hide the highlighter, the outline and the infobar.
*/
hide: function() {
if (this.currentNode) {
this._untrackMutations();
this.currentNode = null;
this._hideBoxModel();
this._hideInfobar();
this._detachPageListeners();
}
this.emit("hide");
_hide: function() {
this._untrackMutations();
this._hideBoxModel();
this._hideInfobar();
},
/**
@ -573,55 +706,40 @@ BoxModelHighlighter.prototype = {
},
/**
* Highlight the box model.
* Update the box model as per the current node.
*
* @param {Object} options
* Object used for passing options. Valid options are:
* - region: "content", "padding", "border" or "margin." This specifies
* the region that the guides should outline. Default is content.
* @return {boolean}
* True if the rectangle was highlighted, false otherwise.
* True if the current node has a box model to be highlighted
*/
_highlightBoxModel: function(options) {
let isShown = false;
_updateBoxModel: function(options) {
options.region = options.region || "content";
this.rect = this.layoutHelpers.getAdjustedQuads(this.currentNode, "margin");
if (!this.rect) {
return null;
if (!this.rect || (this.rect.bounds.width <= 0 && this.rect.bounds.height <= 0)) {
return false;
}
if (this.rect.bounds.width > 0 && this.rect.bounds.height > 0) {
for (let boxType in this._boxModelNodes) {
let {p1, p2, p3, p4} = boxType === "margin" ? this.rect :
this.layoutHelpers.getAdjustedQuads(this.currentNode, boxType);
for (let boxType in this._boxModelNodes) {
let {p1, p2, p3, p4} = boxType === "margin" ? this.rect :
this.layoutHelpers.getAdjustedQuads(this.currentNode, boxType);
let boxNode = this._boxModelNodes[boxType];
boxNode.setAttribute("points",
p1.x + "," + p1.y + " " +
p2.x + "," + p2.y + " " +
p3.x + "," + p3.y + " " +
p4.x + "," + p4.y);
let boxNode = this._boxModelNodes[boxType];
boxNode.setAttribute("points",
p1.x + "," + p1.y + " " +
p2.x + "," + p2.y + " " +
p3.x + "," + p3.y + " " +
p4.x + "," + p4.y);
if (boxType === options.region) {
this._showGuides(p1, p2, p3, p4);
}
}
isShown = true;
this._showBoxModel();
} else {
// Only return false if the element really is invisible.
// A height of 0 and a non-0 width corresponds to a visible element that
// is below the fold for instance
if (this.rect.width > 0 || this.rect.height > 0) {
isShown = true;
this._hideBoxModel();
if (boxType === options.region) {
this._showGuides(p1, p2, p3, p4);
}
}
return isShown;
return true;
},
/**
@ -711,31 +829,40 @@ BoxModelHighlighter.prototype = {
return;
}
// Tag name
this.nodeInfo.tagNameLabel.textContent = this.currentNode.tagName;
let node = this.currentNode;
let info = this.nodeInfo;
// ID
this.nodeInfo.idLabel.textContent = this.currentNode.id ? "#" + this.currentNode.id : "";
// Update the tag, id, classes, pseudo-classes and dimensions only if they
// changed to avoid triggering paint events
// Classes
let classes = this.nodeInfo.classesBox;
let tagName = node.tagName;
if (info.tagNameLabel.textContent !== tagName) {
info.tagNameLabel.textContent = tagName;
}
classes.textContent = this.currentNode.classList.length ?
"." + Array.join(this.currentNode.classList, ".") : "";
let id = node.id ? "#" + node.id : "";
if (info.idLabel.textContent !== id) {
info.idLabel.textContent = id;
}
let classList = node.classList.length ? "." + [...node.classList].join(".") : "";
if (info.classesBox.textContent !== classList) {
info.classesBox.textContent = classList;
}
// Pseudo-classes
let pseudos = PSEUDO_CLASSES.filter(pseudo => {
return DOMUtils.hasPseudoClassLock(this.currentNode, pseudo);
}, this);
return DOMUtils.hasPseudoClassLock(node, pseudo);
}, this).join("");
if (info.pseudoClassesBox.textContent !== pseudos) {
info.pseudoClassesBox.textContent = pseudos;
}
let pseudoBox = this.nodeInfo.pseudoClassesBox;
pseudoBox.textContent = pseudos.join("");
let rect = node.getBoundingClientRect();
let dim = Math.ceil(rect.width) + " x " + Math.ceil(rect.height);
if (info.dimensionBox.textContent !== dim) {
info.dimensionBox.textContent = dim;
}
// Dimensions
let dimensionBox = this.nodeInfo.dimensionBox;
let rect = this.currentNode.getBoundingClientRect();
dimensionBox.textContent = Math.ceil(rect.width) + " x " +
Math.ceil(rect.height);
this._moveInfobar();
},
@ -790,44 +917,173 @@ BoxModelHighlighter.prototype = {
this.nodeInfo.positioner.setAttribute("position", "top");
this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
}
},
}
});
_attachPageListeners: function() {
if (this.currentNode) {
let win = this.currentNode.ownerGlobal;
/**
* The CssTransformHighlighter is the class that draws an outline around a
* transformed element and an outline around where it would be if untransformed
* as well as arrows connecting the 2 outlines' corners.
*/
function CssTransformHighlighter(tabActor, inspector) {
XULBasedHighlighter.call(this, tabActor, inspector);
win.addEventListener("scroll", this, false);
win.addEventListener("resize", this, false);
win.addEventListener("MozAfterPaint", this, false);
this.layoutHelpers = new LayoutHelpers(tabActor.window);
this._initMarkup();
}
let MARKER_COUNTER = 1;
CssTransformHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
_initMarkup: function() {
let stack = this.browser.parentNode;
this._container = this.chromeDoc.createElement("stack");
this._container.className = "highlighter-container";
this._svgRoot = this._createSVGNode("root", "svg", this._container);
this._svgRoot.setAttribute("hidden", "true");
// Add a marker tag to the svg root for the arrow tip
let marker = this.chromeDoc.createElementNS(SVG_NS, "marker");
this.markerId = "css-transform-arrow-marker-" + MARKER_COUNTER;
MARKER_COUNTER ++;
marker.setAttribute("id", this.markerId);
marker.setAttribute("markerWidth", "10");
marker.setAttribute("markerHeight", "5");
marker.setAttribute("orient", "auto");
marker.setAttribute("markerUnits", "strokeWidth");
marker.setAttribute("refX", "10");
marker.setAttribute("refY", "5");
marker.setAttribute("viewBox", "0 0 10 10");
let path = this.chromeDoc.createElementNS(SVG_NS, "path");
path.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
path.setAttribute("fill", "#08C");
marker.appendChild(path);
this._svgRoot.appendChild(marker);
// Create the 2 polygons (transformed and untransformed)
let shapesGroup = this._createSVGNode("container", "g", this._svgRoot);
this._shapes = {
untransformed: this._createSVGNode("untransformed", "polygon", shapesGroup),
transformed: this._createSVGNode("transformed", "polygon", shapesGroup)
};
// Create the arrows
for (let nb of ["1", "2", "3", "4"]) {
let line = this._createSVGNode("line", "line", shapesGroup);
line.setAttribute("marker-end", "url(#" + this.markerId + ")");
this._shapes["line" + nb] = line;
}
this._container.appendChild(this._svgRoot);
// Insert the highlighter right after the browser
stack.insertBefore(this._container, stack.childNodes[1]);
},
_detachPageListeners: function() {
if (this.currentNode) {
let win = this.currentNode.ownerGlobal;
_createSVGNode: function(classPostfix, nodeType, parent) {
let node = this.chromeDoc.createElementNS(SVG_NS, nodeType);
node.setAttribute("class", "css-transform-" + classPostfix);
win.removeEventListener("scroll", this, false);
win.removeEventListener("resize", this, false);
win.removeEventListener("MozAfterPaint", this, false);
parent.appendChild(node);
return node;
},
/**
* Destroy the nodes. Remove listeners.
*/
destroy: function() {
XULBasedHighlighter.prototype.destroy.call(this);
this._container.remove();
this._container = null;
},
/**
* Show the highlighter on a given node
* @param {DOMNode} node
*/
_show: function() {
if (!this._isTransformed(this.currentNode)) {
this.hide();
return;
}
this._update();
},
/**
* Checks if the supplied node is transformed and not inline
*/
_isTransformed: function(node) {
let style = node.ownerDocument.defaultView.getComputedStyle(node);
return style.transform !== "none" && style.display !== "inline";
},
_setPolygonPoints: function(quad, poly) {
let points = [];
for (let point of ["p1","p2", "p3", "p4"]) {
points.push(quad[point].x + "," + quad[point].y);
}
poly.setAttribute("points", points.join(" "));
},
_setLinePoints: function(p1, p2, line) {
line.setAttribute("x1", p1.x);
line.setAttribute("y1", p1.y);
line.setAttribute("x2", p2.x);
line.setAttribute("y2", p2.y);
let dist = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
if (dist < ARROW_LINE_MIN_DISTANCE) {
line.removeAttribute("marker-end");
} else {
line.setAttribute("marker-end", "url(#" + this.markerId + ")");
}
},
/**
* Generic event handler.
*
* @param nsIDOMEvent aEvent
* The DOM event object.
* Update the highlighter on the current highlighted node (the one that was
* passed as an argument to show(node)).
* Should be called whenever node size or attributes change
*/
handleEvent: function(event) {
switch (event.type) {
case "resize":
case "MozAfterPaint":
case "scroll":
this._update();
break;
_update: function() {
// Getting the points for the transformed shape
let quad = this.layoutHelpers.getAdjustedQuads(this.currentNode, "border");
if (!quad || quad.bounds.width <= 0 || quad.bounds.height <= 0) {
this._hideShapes();
return null;
}
// Getting the points for the untransformed shape
let untransformedQuad = this.layoutHelpers.getNodeBounds(this.currentNode);
this._setPolygonPoints(quad, this._shapes.transformed);
this._setPolygonPoints(untransformedQuad, this._shapes.untransformed);
for (let nb of ["1", "2", "3", "4"]) {
this._setLinePoints(untransformedQuad["p" + nb], quad["p" + nb],
this._shapes["line" + nb]);
}
this._showShapes();
},
};
/**
* Hide the highlighter, the outline and the infobar.
*/
_hide: function() {
this._hideShapes();
},
_hideShapes: function() {
this._svgRoot.setAttribute("hidden", "true");
},
_showShapes: function() {
this._svgRoot.removeAttribute("hidden");
}
});
/**
* The SimpleOutlineHighlighter is a class that has the same API than the
@ -891,6 +1147,40 @@ SimpleOutlineHighlighter.prototype = {
}
};
/**
* Can the host support the XUL-based highlighters which require a parent
* XUL node to get attached.
* @param {TabActor}
* @return {Boolean}
*/
function supportXULBasedHighlighter(tabActor) {
// Note that <browser>s on Fennec also have a XUL parentNode but the box
// model highlighter doesn't display correctly on Fennec (bug 993190)
return tabActor.browser &&
!!tabActor.browser.parentNode &&
Services.appinfo.ID !== "{aa3c5121-dab2-40e2-81ca-7ea25febc110}";
}
function isNodeValid(node) {
// Is it null or dead?
if(!node || Cu.isDeadWrapper(node)) {
return false;
}
// Is it an element node
if (node.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
return false;
}
// Is it connected to the document?
let doc = node.ownerDocument;
if (!doc || !doc.defaultView || !doc.documentElement.contains(node)) {
return false;
}
return true;
}
XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
});

View File

@ -61,7 +61,11 @@ const events = require("sdk/event/core");
const {Unknown} = require("sdk/platform/xpcom");
const {Class} = require("sdk/core/heritage");
const {PageStyleActor} = require("devtools/server/actors/styles");
const {HighlighterActor} = require("devtools/server/actors/highlighter");
const {
HighlighterActor,
CustomHighlighterActor,
HIGHLIGHTER_CLASSES
} = require("devtools/server/actors/highlighter");
const {getLayoutChangesObserver, releaseLayoutChangesObserver} =
require("devtools/server/actors/layout");
@ -2601,6 +2605,19 @@ var InspectorActor = protocol.ActorClass({
}
}),
/**
* The most used highlighter actor is the HighlighterActor which can be
* conveniently retrieved by this method.
* The same instance will always be returned by this method when called
* several times.
* The highlighter actor returned here is used to highlighter elements's
* box-models from the markup-view, layout-view, console, debugger, ... as
* well as select elements with the pointer (pick).
*
* @param {Boolean} autohide Optionally autohide the highlighter after an
* element has been picked
* @return {HighlighterActor}
*/
getHighlighter: method(function (autohide) {
if (this._highlighterPromise) {
return this._highlighterPromise;
@ -2611,12 +2628,40 @@ var InspectorActor = protocol.ActorClass({
});
return this._highlighterPromise;
}, {
request: { autohide: Arg(0, "boolean") },
request: {
autohide: Arg(0, "boolean")
},
response: {
highligter: RetVal("highlighter")
}
}),
/**
* If consumers need to display several highlighters at the same time or
* different types of highlighters, then this method should be used, passing
* the type name of the highlighter needed as argument.
* A new instance will be created everytime the method is called, so it's up
* to the consumer to release it when it is not needed anymore
*
* @param {String} type The type of highlighter to create
* @return {Highlighter} The highlighter actor instance or null if the
* typeName passed doesn't match any available highlighter
*/
getHighlighterByType: method(function (typeName) {
if (HIGHLIGHTER_CLASSES[typeName]) {
return CustomHighlighterActor(this, typeName);
} else {
return null;
}
}, {
request: {
typeName: Arg(0)
},
response: {
highlighter: RetVal("nullable:customhighlighter")
}
}),
/**
* Get the node's image data if any (for canvas and img nodes).
* Returns an imageData object with the actual data being a LongStringActor

View File

@ -104,6 +104,12 @@ RootActor.prototype = {
// Whether the server-side highlighter actor exists and can be used to
// remotely highlight nodes (see server/actors/highlighter.js)
highlightable: true,
// Which custom highlighter does the server-side highlighter actor supports?
// (see server/actors/highlighter.js)
customHighlighters: [
"BoxModelHighlighter",
"CssTransformHighlighter"
],
// Whether the inspector actor implements the getImageDataFromURL
// method that returns data-uris for image URLs. This is used for image
// tooltips for instance

View File

@ -23,6 +23,9 @@ support-files =
[test_framerate_02.html]
[test_framerate_03.html]
[test_framerate_04.html]
[test_highlighter-csstransform_01.html]
[test_highlighter-csstransform_02.html]
[test_highlighter-csstransform_03.html]
[test_inspector-changeattrs.html]
[test_inspector-changevalue.html]
[test_inspector-hide.html]

View File

@ -0,0 +1,113 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 1014547 - CSS transforms highlighter
Test the high level API of the highlighters
-->
<head>
<meta charset="utf-8">
<title>Framerate actor test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script type="application/javascript;version=1.8">
window.onload = function() {
var Cu = Components.utils;
var Cc = Components.classes;
var Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/devtools/Loader.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
Cu.import("resource://gre/modules/Task.jsm");
SimpleTest.waitForExplicitFinish();
var {InspectorFront} = devtools.require("devtools/server/actors/inspector");
DebuggerServer.init(() => true);
DebuggerServer.addBrowserActors();
var client = new DebuggerClient(DebuggerServer.connectPipe());
client.connect(() => {
client.listTabs(response => {
var form = response.tabs[response.selected];
var front = InspectorFront(client, form);
Task.spawn(function*() {
yield onlyOneInstanceOfMainHighlighter(front);
yield manyInstancesOfCustomHighlighters(front);
yield showHideMethodsAreAvailable(front);
yield unknownHighlighterTypeShouldntBeAccepted(front);
yield rootActorTraitsShouldContainKnownTypes(client);
}).then(null, ok.bind(null, false)).then(() => {
client.close(() => {
DebuggerServer.destroy();
SimpleTest.finish();
});
});
});
});
function* onlyOneInstanceOfMainHighlighter(inspectorFront) {
info("Check that the inspector always sends back the same main highlighter");
let h1 = yield inspectorFront.getHighlighter(false);
let h2 = yield inspectorFront.getHighlighter(false);
is(h1, h2, "The same highlighter front was returned");
is(h1.typeName, "highlighter", "The right front type was returned");
}
function* manyInstancesOfCustomHighlighters(inspectorFront) {
let h1 = yield inspectorFront.getHighlighterByType("BoxModelHighlighter");
let h2 = yield inspectorFront.getHighlighterByType("BoxModelHighlighter");
ok(h1 !== h2, "getHighlighterByType returns new instances every time (1)");
let h3 = yield inspectorFront.getHighlighterByType("CssTransformHighlighter");
let h4 = yield inspectorFront.getHighlighterByType("CssTransformHighlighter");
ok(h3 !== h4, "getHighlighterByType returns new instances every time (2)");
ok(h3 !== h1 && h3 !== h2,
"getHighlighterByType returns new instances every time (3)");
ok(h4 !== h1 && h4 !== h2,
"getHighlighterByType returns new instances every time (4)");
yield h1.finalize();
yield h2.finalize();
yield h3.finalize();
yield h4.finalize();
}
function* showHideMethodsAreAvailable(inspectorFront) {
let h1 = yield inspectorFront.getHighlighterByType("BoxModelHighlighter");
let h2 = yield inspectorFront.getHighlighterByType("CssTransformHighlighter");
ok("show" in h1, "Show method is present on the front API");
ok("show" in h2, "Show method is present on the front API");
ok("hide" in h1, "Hide method is present on the front API");
ok("hide" in h2, "Hide method is present on the front API");
yield h1.finalize();
yield h2.finalize();
}
function* unknownHighlighterTypeShouldntBeAccepted(inspectorFront) {
let h = yield inspectorFront.getHighlighterByType("whatever");
ok(!h, "No highlighter was returned for the invalid type");
}
function* rootActorTraitsShouldContainKnownTypes(client) {
ok(client.traits.customHighlighters.indexOf("BoxModelHighlighter") !== -1,
"The root actor's trait contains BoxModelHighlighter as a known type");
ok(client.traits.customHighlighters.indexOf("CssTransformHighlighter") !== -1,
"The root actor's trait contains CssTransformHighlighter as a known type");
}
}
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,156 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 1014547 - CSS transforms highlighter
Test the creation of the SVG highlighter elements in the browser
-->
<head>
<meta charset="utf-8">
<title>Framerate actor test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<div id="transformed" style="border:1px solid red;width:100px;height:100px;transform:skew(13deg);"></div>
<div id="untransformed" style="border:1px solid blue;width:100px;height:100px;"></div>
<span id="inline" style="transform:rotate(90deg);">this is an inline transformed element</span>
<pre id="test">
<script type="application/javascript;version=1.8">
window.onload = function() {
var Cu = Components.utils;
var Cc = Components.classes;
var Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/devtools/Loader.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
Cu.import("resource://gre/modules/Task.jsm");
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
SimpleTest.waitForExplicitFinish();
var {InspectorFront} = devtools.require("devtools/server/actors/inspector");
DebuggerServer.init(() => true);
DebuggerServer.addBrowserActors();
var client = new DebuggerClient(DebuggerServer.connectPipe());
client.connect(() => {
client.listTabs(response => {
var form = response.tabs[response.selected];
var front = InspectorFront(client, form);
Task.spawn(function*() {
let walkerFront = yield front.getWalker();
let highlighterFront = yield front.getHighlighterByType(
"CssTransformHighlighter");
let gBrowser = Services.wm.getMostRecentWindow("navigator:browser").gBrowser;
let container =
gBrowser.selectedBrowser.parentNode.querySelector(".highlighter-container");
ok(container, "The highlighter container was found");
yield isHiddenByDefault(container);
yield has2PolygonsAnd4Lines(container);
yield isNotShownForUntransformed(highlighterFront, walkerFront, container);
yield isNotShownForInline(highlighterFront, walkerFront, container);
yield isVisibleWhenShown(highlighterFront, walkerFront, container);
yield linesLinkThePolygons(highlighterFront, walkerFront, container);
yield highlighterFront.finalize();
}).then(null, ok.bind(null, false)).then(() => {
client.close(() => {
DebuggerServer.destroy();
SimpleTest.finish();
});
});
});
});
function* isHiddenByDefault(container) {
let svg = container.querySelector("svg");
ok(svg.hasAttribute("hidden"), "The highlighter is hidden by default");
}
function* has2PolygonsAnd4Lines(container) {
is(container.querySelectorAll("polygon").length, 2, "Found 2 polygons");
is(container.querySelectorAll("line").length, 4, "Found 4 lines");
}
function* isNotShownForUntransformed(highlighterFront, walkerFront, container) {
let rawNode = document.getElementById("untransformed");
let node = walkerFront.frontForRawNode(rawNode);
info("Asking to show the highlighter on the untransformed test node");
yield highlighterFront.show(node);
let svg = container.querySelector("svg");
ok(svg.hasAttribute("hidden"), "The highlighter is still hidden");
}
function* isNotShownForInline(highlighterFront, walkerFront, container) {
let rawNode = document.getElementById("inline");
let node = walkerFront.frontForRawNode(rawNode);
info("Asking to show the highlighter on the inline test node");
yield highlighterFront.show(node);
let svg = container.querySelector("svg");
ok(svg.hasAttribute("hidden"), "The highlighter is still hidden");
}
function* isVisibleWhenShown(highlighterFront, walkerFront, container) {
let rawNode = document.getElementById("transformed");
let node = walkerFront.frontForRawNode(rawNode);
info("Asking to show the highlighter on the test node");
yield highlighterFront.show(node);
let svg = container.querySelector("svg");
ok(!svg.hasAttribute("hidden"), "The highlighter is visible");
info("Hiding the highlighter");
yield highlighterFront.hide();
ok(svg.hasAttribute("hidden"), "The highlighter is hidden");
}
function* linesLinkThePolygons(highlighterFront, walkerFront, container) {
let rawNode = document.getElementById("transformed");
let node = walkerFront.frontForRawNode(rawNode);
info("Showing the highlighter on the transformed node");
yield highlighterFront.show(node);
info("Checking that the 4 lines do link the 2 shape's corners");
let lines = [...container.querySelectorAll("line")];
let polygon1 = container.querySelector(".css-transform-untransformed");
let points1 = polygon1.getAttribute("points").split(" ");
let polygon2 = container.querySelector(".css-transform-transformed");
let points2 = polygon2.getAttribute("points").split(" ");
for (let i = 0; i < lines.length; i++) {
info("Checking line nb " + i);
let line = lines[i];
let p1 = points1[i].split(",");
let x1 = line.getAttribute("x1");
let y1 = line.getAttribute("y1");
is(p1[0], x1, "line " + i + "'s first point matches the untransformed x coordinate");
is(p1[1], y1, "line " + i + "'s first point matches the untransformed y coordinate");
let p2 = points2[i].split(",");
let x2 = line.getAttribute("x2");
let y2 = line.getAttribute("y2");
is(p2[0], x2, "line " + i + "'s first point matches the transformed x coordinate");
is(p2[1], y2, "line " + i + "'s first point matches the transformed y coordinate");
}
yield highlighterFront.hide();
}
}
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,119 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 1014547 - CSS transforms highlighter
Test that the highlighter elements created have the right size and coordinates.
Note that instead of hard-coding values here, the assertions are made by
comparing with the result of LayoutHelpers.getAdjustedQuads.
There's a separate test for checking that getAdjustedQuads actually returns
sensible values
(browser/devtools/shared/test/browser_layoutHelpers-getBoxQuads.js),
so the present test doesn't care about that, it just verifies that the css
transform highlighter applies those values correctly to the SVG elements
-->
<head>
<meta charset="utf-8">
<title>Framerate actor test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<style type="text/css">
#test-node {
position: absolute;
top: 0;
left: 0;
width: 300px;
height: 300px;
transform: rotate(90deg) skew(13deg) scale(.8) translateX(50px);
transform-origin: 50%;
background: linear-gradient(green, yellow);
}
</style>
</head>
<body>
<div id="test-node"></div>
<pre id="test">
<script type="application/javascript;version=1.8">
window.onload = function() {
var Cu = Components.utils;
var Cc = Components.classes;
var Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/devtools/Loader.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
Cu.import("resource://gre/modules/Task.jsm");
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
SimpleTest.waitForExplicitFinish();
var {InspectorFront} = devtools.require("devtools/server/actors/inspector");
DebuggerServer.init(() => true);
DebuggerServer.addBrowserActors();
var client = new DebuggerClient(DebuggerServer.connectPipe());
client.connect(() => {
client.listTabs(response => {
var form = response.tabs[response.selected];
var front = InspectorFront(client, form);
Task.spawn(function*() {
let walker = yield front.getWalker();
let highlighter = yield front.getHighlighterByType(
"CssTransformHighlighter");
let browser = Services.wm.getMostRecentWindow("navigator:browser")
.gBrowser.selectedBrowser;
let container = browser.parentNode.querySelector(".highlighter-container");
let node = document.querySelector("#test-node");
let helper = new LayoutHelpers(browser.docShell
.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow));
info("Displaying the transform highlighter on test node " +
node.tagName);
yield highlighter.show(walker.frontForRawNode(node));
let expected = helper.getAdjustedQuads(node, "border");
let polygon = container.querySelector(".css-transform-transformed");
let polygonPoints = polygon.getAttribute("points").split(" ").map(p => {
return {
x: +p.substring(0, p.indexOf(",")),
y: +p.substring(p.indexOf(",")+1)
};
});
for (let i = 1; i < 5; i ++) {
is(polygonPoints[i - 1].x, expected["p" + i].x,
"p" + i + " x coordinate is correct");
is(polygonPoints[i - 1].y, expected["p" + i].y,
"p" + i + " y coordinate is correct");
}
info("Hiding the transform highlighter");
yield highlighter.hide();
yield highlighter.finalize();
}).then(null, ok.bind(null, false)).then(() => {
client.close(() => {
DebuggerServer.destroy();
SimpleTest.finish();
});
});
});
});
}
</script>
</pre>
</body>
</html>