Bug 663778 - Box Model Highlighter r=jwalker

This commit is contained in:
Michael Ratcliffe 2014-03-13 21:36:48 +00:00
parent 3c5af4257d
commit 4ba79c2f4a
12 changed files with 587 additions and 316 deletions

View File

@ -4,6 +4,7 @@
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
@namespace html url("http://www.w3.org/1999/xhtml");
@namespace svg url("http://www.w3.org/2000/svg");
#main-window:not([chromehidden~="toolbar"]) {
%ifdef XP_MACOSX

View File

@ -6,25 +6,15 @@
pointer-events: none;
}
.highlighter-outline-container {
overflow: hidden;
position: relative;
}
.highlighter-outline {
position: absolute;
}
.highlighter-outline[hidden] {
opacity: 0;
pointer-events: none;
display: -moz-box;
}
.highlighter-outline:not([disable-transitions]) {
transition-property: opacity, top, left, width, height;
transition-duration: 0.1s;
transition-timing-function: linear;
/*
* Box model highlighter
*/
svg|svg.box-model-root[hidden],
svg|line.box-model-guide-top[hidden],
svg|line.box-model-guide-right[hidden],
svg|line.box-model-guide-left[hidden],
svg|line.box-model-guide-bottom[hidden] {
display: none;
}
/*
@ -45,13 +35,6 @@
display: -moz-box;
}
.highlighter-nodeinfobar-positioner:not([disable-transitions]),
.highlighter-nodeinfobar-positioner[disable-transitions][force-transitions] {
transition-property: transform, opacity, top, left;
transition-duration: 0.1s;
transition-timing-function: linear;
}
.highlighter-nodeinfobar-text {
overflow: hidden;
white-space: nowrap;

View File

@ -160,20 +160,22 @@ Selection.prototype = {
setNodeFront: function(value, reason="unknown") {
this.reason = reason;
if (value !== this._nodeFront) {
let rawValue = null;
if (value && value.isLocal_toBeDeprecated()) {
rawValue = value.rawNode();
}
this.emit("before-new-node", rawValue, reason);
this.emit("before-new-node-front", value, reason);
let previousNode = this._node;
let previousFront = this._nodeFront;
this._node = rawValue;
this._nodeFront = value;
this.emit("new-node", previousNode, this.reason);
this.emit("new-node-front", value, this.reason);
// We used to return here if the node had not changed but we now need to
// set the node even if it is already set otherwise it is not possible to
// e.g. highlight the same node twice.
let rawValue = null;
if (value && value.isLocal_toBeDeprecated()) {
rawValue = value.rawNode();
}
this.emit("before-new-node", rawValue, reason);
this.emit("before-new-node-front", value, reason);
let previousNode = this._node;
let previousFront = this._nodeFront;
this._node = rawValue;
this._nodeFront = value;
this.emit("new-node", previousNode, this.reason);
this.emit("new-node-front", value, this.reason);
},
get documentFront() {

View File

@ -73,6 +73,8 @@ function Toolbox(target, selectedTool, hostType, hostOptions) {
this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this)
this.destroy = this.destroy.bind(this);
this.highlighterUtils = new ToolboxHighlighterUtils(this);
this._highlighterReady = this._highlighterReady.bind(this);
this._highlighterHidden = this._highlighterHidden.bind(this);
this._target.on("close", this.destroy);
@ -1097,8 +1099,14 @@ Toolbox.prototype = {
this._inspector = InspectorFront(this._target.client, this._target.form);
this._walker = yield this._inspector.getWalker();
this._selection = new Selection(this._walker);
if (this.highlighterUtils.isRemoteHighlightable) {
this._highlighter = yield this._inspector.getHighlighter();
let autohide = !gDevTools.testing;
this.walker.on("highlighter-ready", this._highlighterReady);
this.walker.on("highlighter-hide", this._highlighterHidden);
this._highlighter = yield this._inspector.getHighlighter(autohide);
}
}.bind(this));
}
@ -1110,6 +1118,10 @@ Toolbox.prototype = {
* Returns a promise that resolves when the fronts are destroyed
*/
destroyInspector: function() {
if (this._destroying) {
return this._destroying;
}
if (!this._inspector) {
return promise.resolve();
}
@ -1125,6 +1137,9 @@ Toolbox.prototype = {
this._selection.destroy();
}
this.walker.off("highlighter-ready", this._highlighterReady);
this.walker.off("highlighter-hide", this._highlighterHidden);
this._inspector = null;
this._highlighter = null;
this._selection = null;
@ -1135,7 +1150,9 @@ Toolbox.prototype = {
// Releasing the walker (if it has been created)
// This can fail, but in any case, we want to continue destroying the
// inspector/highlighter/selection
let walker = this._walker ? this._walker.release() : promise.resolve();
let walker = (this._destroying = this._walker) ?
this._walker.release() :
promise.resolve();
return walker.then(outstanding, outstanding);
},
@ -1224,7 +1241,15 @@ Toolbox.prototype = {
this._host = null;
this._toolPanels.clear();
}).then(null, console.error);
}
},
_highlighterReady: function() {
this.emit("highlighter-ready");
},
_highlighterHidden: function() {
this.emit("highlighter-hide");
},
};
/**
@ -1284,6 +1309,7 @@ ToolboxHighlighterUtils.prototype = {
let deferred = promise.defer();
let done = () => {
this._isPicking = true;
this.toolbox.emit("picker-started");
this.toolbox.on("select", this.stopPicker);
deferred.resolve();
@ -1293,20 +1319,20 @@ ToolboxHighlighterUtils.prototype = {
this.toolbox.initInspector(),
this.toolbox.selectTool("inspector")
]).then(() => {
this._isPicking = true;
this.toolbox._pickerButton.setAttribute("checked", "true");
if (this.isRemoteHighlightable) {
this.toolbox.highlighter.pick().then(done);
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 {
this.toolbox.walker.pick().then(node => {
this.toolbox.selection.setNodeFront(node, "picker-node-picked");
this.stopPicker();
return this.toolbox.walker.pick().then(node => {
this.toolbox.selection.setNodeFront(node, "picker-node-picked").then(() => {
this.stopPicker();
done();
});
});
done();
}
});

View File

@ -165,7 +165,7 @@ MarkupView.prototype = {
},
_onMouseLeave: function() {
this._hideBoxModel();
this._hideBoxModel(true);
if (this._hoveredNode) {
this._containers.get(this._hoveredNode).hovered = false;
}
@ -176,8 +176,8 @@ MarkupView.prototype = {
this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
},
_hideBoxModel: function() {
this._inspector.toolbox.highlighterUtils.unhighlight();
_hideBoxModel: function(forceHide) {
this._inspector.toolbox.highlighterUtils.unhighlight(forceHide);
},
_briefBoxModelTimer: null,

View File

@ -8,6 +8,7 @@
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
@namespace html url("http://www.w3.org/1999/xhtml");
@namespace svg url("http://www.w3.org/2000/svg");
%include ../shared/browser.inc
%include linuxShared.inc

View File

@ -14,6 +14,7 @@
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
@namespace html url("http://www.w3.org/1999/xhtml");
@namespace svg url("http://www.w3.org/2000/svg");
#urlbar:-moz-lwtheme:not([focused="true"]),
.searchbar-textbox:-moz-lwtheme:not([focused="true"]) {

View File

@ -4,12 +4,40 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
%endif
/* Highlighter */
/* Box model highlighter */
svg|g.box-model-container {
opacity: 0.4;
}
.highlighter-outline {
box-shadow: 0 0 0 1px black;
outline: 1px dashed white;
outline-offset: -1px;
svg|polygon.box-model-content {
fill: #80d4ff;
}
svg|polygon.box-model-padding {
fill: #66cc52;
}
svg|polygon.box-model-border {
fill: #ffe431;
}
svg|polygon.box-model-margin {
fill: #d89b28;
}
svg|polygon.box-model-content,
svg|polygon.box-model-padding,
svg|polygon.box-model-border,
svg|polygon.box-model-margin {
stroke: none;
}
svg|line.box-model-guide-top,
svg|line.box-model-guide-right,
svg|line.box-model-guide-bottom,
svg|line.box-model-guide-left {
stroke: #08C;
stroke-dasharray: 5 3;
}
/* Highlighter - Node Infobar */

View File

@ -6,6 +6,7 @@
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
@namespace html url("http://www.w3.org/1999/xhtml");
@namespace svg url("http://www.w3.org/2000/svg");
%include ../shared/browser.inc
%include windowsShared.inc

View File

@ -24,77 +24,73 @@ this.LayoutHelpers = LayoutHelpers = function(aTopLevelWindow) {
LayoutHelpers.prototype = {
/**
* Compute the position and the dimensions for the visible portion
* of a node, relativalely to the root window.
* Get box quads adjusted for iframes and zoom level.
*
* @param nsIDOMNode aNode
* a DOM element to be highlighted
* @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"
*/
getDirtyRect: function LH_getDirectyRect(aNode) {
let frameWin = aNode.ownerDocument.defaultView;
let clientRect = aNode.getBoundingClientRect();
// Go up in the tree of frames to determine the correct rectangle.
// clientRect is read-only, we need to be able to change properties.
rect = {top: clientRect.top,
left: clientRect.left,
width: clientRect.width,
height: clientRect.height};
// We iterate through all the parent windows.
while (true) {
// Does the selection overflow on the right of its window?
let diffx = frameWin.innerWidth - (rect.left + rect.width);
if (diffx < 0) {
rect.width += diffx;
}
// Does the selection overflow on the bottom of its window?
let diffy = frameWin.innerHeight - (rect.top + rect.height);
if (diffy < 0) {
rect.height += diffy;
}
// Does the selection overflow on the left of its window?
if (rect.left < 0) {
rect.width += rect.left;
rect.left = 0;
}
// Does the selection overflow on the top of its window?
if (rect.top < 0) {
rect.height += rect.top;
rect.top = 0;
}
// Selection has been clipped to fit in its own window.
// Are we in the top-level window?
if (this.isTopLevelWindow(frameWin)) {
break;
}
let frameElement = this.getFrameElement(frameWin);
if (!frameElement) {
break;
}
// We are in an iframe.
// We take into account the parent iframe position and its
// offset (borders and padding).
let frameRect = frameElement.getBoundingClientRect();
let [offsetTop, offsetLeft] =
this.getIframeContentOffset(frameElement);
rect.top += frameRect.top + offsetTop;
rect.left += frameRect.left + offsetLeft;
frameWin = this.getParentWindow(frameWin);
getAdjustedQuads: function(node, region) {
if (!node) {
return;
}
return rect;
let [quads] = node.getBoxQuads({
box: region
});
if (!quads) {
return;
}
let [xOffset, yOffset] = this._getNodeOffsets(node);
let scale = this.calculateScale(node);
return {
p1: {
w: quads.p1.w * scale,
x: quads.p1.x * scale + xOffset,
y: quads.p1.y * scale + yOffset,
z: quads.p1.z * scale
},
p2: {
w: quads.p2.w * scale,
x: quads.p2.x * scale + xOffset,
y: quads.p2.y * scale + yOffset,
z: quads.p2.z * scale
},
p3: {
w: quads.p3.w * scale,
x: quads.p3.x * scale + xOffset,
y: quads.p3.y * scale + yOffset,
z: quads.p3.z * scale
},
p4: {
w: quads.p4.w * scale,
x: quads.p4.x * scale + xOffset,
y: quads.p4.y * scale + yOffset,
z: quads.p4.z * scale
},
bounds: {
bottom: quads.bounds.bottom * scale + yOffset,
height: quads.bounds.height * scale,
left: quads.bounds.left * scale + xOffset,
right: quads.bounds.right * scale + xOffset,
top: quads.bounds.top * scale + yOffset,
width: quads.bounds.width * scale,
x: quads.bounds.x * scale + xOffset,
y: quads.bounds.y * scale + yOffset
}
};
},
calculateScale: function(node) {
let win = node.ownerDocument.defaultView;
let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
return winUtils.fullZoom;
},
/**
@ -112,7 +108,7 @@ LayoutHelpers.prototype = {
// Go up in the tree of frames to determine the correct rectangle.
// clientRect is read-only, we need to be able to change properties.
rect = {top: clientRect.top + aContentWindow.pageYOffset,
let rect = {top: clientRect.top + aContentWindow.pageYOffset,
left: clientRect.left + aContentWindow.pageXOffset,
width: clientRect.width,
height: clientRect.height};
@ -178,26 +174,6 @@ LayoutHelpers.prototype = {
return [borderTop + paddingTop, borderLeft + paddingLeft];
},
/**
* Apply the page zoom factor.
*/
getZoomedRect: function LH_getZoomedRect(aWin, aRect) {
// get page zoom factor, if any
let zoom =
aWin.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils)
.fullZoom;
// adjust rect for zoom scaling
let aRectScaled = {};
for (let prop in aRect) {
aRectScaled[prop] = aRect[prop] * zoom;
}
return aRectScaled;
},
/**
* Find an element from the given coordinates. This method descends through
* frames to find the element the user clicked inside frames.
@ -243,8 +219,7 @@ LayoutHelpers.prototype = {
* appear on the top of the viewport. It is true by default, and that is
* usually what you want.
*/
scrollIntoViewIfNeeded:
function LH_scrollIntoViewIfNeeded(elem, centered) {
scrollIntoViewIfNeeded: function(elem, centered) {
// We want to default to centering the element in the page,
// so as to keep the context of the element.
centered = centered === undefined? true: !!centered;
@ -397,4 +372,138 @@ LayoutHelpers.prototype = {
return winUtils.containerElement;
},
/**
* Get the x and y offsets for a node taking iframes into account.
*
* @param {DOMNode} node
* The node for which we are to get the offset
*/
_getNodeOffsets: function(node) {
let xOffset = 0;
let yOffset = 0;
let frameWin = node.ownerDocument.defaultView;
let scale = this.calculateScale(node);
while (true) {
// Are we in the top-level window?
if (this.isTopLevelWindow(frameWin)) {
break;
}
let frameElement = this.getFrameElement(frameWin);
if (!frameElement) {
break;
}
// We are in an iframe.
// We take into account the parent iframe position and its
// offset (borders and padding).
let frameRect = frameElement.getBoundingClientRect();
let [offsetTop, offsetLeft] =
this.getIframeContentOffset(frameElement);
xOffset += frameRect.left + offsetLeft;
yOffset += frameRect.top + offsetTop;
frameWin = this.getParentWindow(frameWin);
}
return [xOffset * scale, yOffset * scale];
},
/********************************************************************
* GetBoxQuads POLYFILL START TODO: Remove this when bug 917755 is fixed.
********************************************************************/
_getBoxQuadsFromRect: function(rect, node) {
let scale = this.calculateScale(node);
let [xOffset, yOffset] = this._getNodeOffsets(node);
let out = {
p1: {
x: rect.left * scale + xOffset,
y: rect.top * scale + yOffset
},
p2: {
x: (rect.left + rect.width) * scale + xOffset,
y: rect.top * scale + yOffset
},
p3: {
x: (rect.left + rect.width) * scale + xOffset,
y: (rect.top + rect.height) * scale + yOffset
},
p4: {
x: rect.left * scale + xOffset,
y: (rect.top + rect.height) * scale + yOffset
}
};
out.bounds = {
bottom: out.p4.y,
height: out.p4.y - out.p1.y,
left: out.p1.x,
right: out.p2.x,
top: out.p1.y,
width: out.p2.x - out.p1.x,
x: out.p1.x,
y: out.p1.y
};
return out;
},
_parseNb: function(distance) {
let nb = parseFloat(distance, 10);
return isNaN(nb) ? 0 : nb;
},
getAdjustedQuadsPolyfill: function(node, region) {
// Get the border-box rect
// Note that this is relative to the node's viewport, so before we can use
// it, will need to go back up the frames like getRect
let borderRect = node.getBoundingClientRect();
// If the boxType is border, no need to go any further, we're done
if (region === "border") {
return this._getBoxQuadsFromRect(borderRect, node);
}
// Else, need to get margin/padding/border distances
let style = node.ownerDocument.defaultView.getComputedStyle(node);
let camel = s => s.substring(0, 1).toUpperCase() + s.substring(1);
let distances = {border:{}, padding:{}, margin: {}};
for (let side of ["top", "right", "bottom", "left"]) {
distances.border[side] = this._parseNb(style["border" + camel(side) + "Width"]);
distances.padding[side] = this._parseNb(style["padding" + camel(side)]);
distances.margin[side] = this._parseNb(style["margin" + camel(side)]);
}
// From the border-box rect, calculate the content-box, padding-box and
// margin-box rects
function offsetRect(rect, offsetType, dir=1) {
return {
top: rect.top + (dir * distances[offsetType].top),
left: rect.left + (dir * distances[offsetType].left),
width: rect.width - (dir * (distances[offsetType].left + distances[offsetType].right)),
height: rect.height - (dir * (distances[offsetType].top + distances[offsetType].bottom))
};
}
if (region === "margin") {
return this._getBoxQuadsFromRect(offsetRect(borderRect, "margin", -1), node);
} else if (region === "padding") {
return this._getBoxQuadsFromRect(offsetRect(borderRect, "border"), node);
} else if (region === "content") {
let paddingRect = offsetRect(borderRect, "border");
return this._getBoxQuadsFromRect(offsetRect(paddingRect, "padding"), node);
}
},
/********************************************************************
* GetBoxQuads POLYFILL END
********************************************************************/
};

View File

@ -9,6 +9,10 @@ const Services = require("Services");
const protocol = require("devtools/server/protocol");
const {Arg, Option, method} = protocol;
const events = require("sdk/event/core");
const EventEmitter = require("devtools/toolkit/event-emitter");
const GUIDE_STROKE_WIDTH = 1;
// Make sure the domnode type is known here
require("devtools/server/actors/inspector");
@ -21,7 +25,9 @@ const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } ";
HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } ";
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 HighlighterActor is the server-side entry points for any tool that wishes
@ -36,15 +42,23 @@ const HIGHLIGHTER_PICKED_TIMER = 1000;
let HighlighterActor = protocol.ActorClass({
typeName: "highlighter",
initialize: function(inspector) {
initialize: function(inspector, autohide) {
protocol.Actor.prototype.initialize.call(this, null);
this._autohide = autohide;
this._inspector = inspector;
this._walker = this._inspector.walker;
this._tabActor = this._inspector.tabActor;
this._highlighterReady = this._highlighterReady.bind(this);
this._highlighterHidden = this._highlighterHidden.bind(this);
if (this._supportsBoxModelHighlighter()) {
this._boxModelHighlighter = new BoxModelHighlighter(this._tabActor);
this._boxModelHighlighter =
new BoxModelHighlighter(this._tabActor, this._inspector);
this._boxModelHighlighter.on("ready", this._highlighterReady);
this._boxModelHighlighter.on("hide", this._highlighterHidden);
} else {
this._boxModelHighlighter = new SimpleOutlineHighlighter(this._tabActor);
}
@ -63,9 +77,12 @@ let HighlighterActor = protocol.ActorClass({
destroy: function() {
protocol.Actor.prototype.destroy.call(this);
if (this._boxModelHighlighter) {
this._boxModelHighlighter.off("ready", this._highlighterReady);
this._boxModelHighlighter.off("hide", this._highlighterHidden);
this._boxModelHighlighter.destroy();
this._boxModelHighlighter = null;
}
this._autohide = null;
this._inspector = null;
this._walker = null;
this._tabActor = null;
@ -79,8 +96,7 @@ let HighlighterActor = protocol.ActorClass({
*
* @param NodeActor The node to be highlighted
* @param Options See the request part for existing options. Note that not
* all options may be supported by all types of highlighters. The simple
* outline highlighter for instance does not scrollIntoView
* all options may be supported by all types of highlighters.
*/
showBoxModel: method(function(node, options={}) {
if (node && this._isNodeValidForHighlighting(node.rawNode)) {
@ -91,7 +107,8 @@ let HighlighterActor = protocol.ActorClass({
}, {
request: {
node: Arg(0, "domnode"),
scrollIntoView: Option(1)
scrollIntoView: Option(1),
region: Option(1)
}
}),
@ -135,6 +152,7 @@ let HighlighterActor = protocol.ActorClass({
*/
_isPicking: false,
_hoveredNode: null,
pick: method(function() {
if (this._isPicking) {
return null;
@ -150,9 +168,11 @@ let HighlighterActor = protocol.ActorClass({
this._preventContentEvent(event);
this._stopPickerListeners();
this._isPicking = false;
this._tabActor.window.setTimeout(() => {
this._boxModelHighlighter.hide();
}, HIGHLIGHTER_PICKED_TIMER);
if (this._autohide) {
this._tabActor.window.setTimeout(() => {
this._boxModelHighlighter.hide();
}, HIGHLIGHTER_PICKED_TIMER);
}
events.emit(this._walker, "picker-node-picked", this._findAndAttachElement(event));
};
@ -217,6 +237,14 @@ let HighlighterActor = protocol.ActorClass({
target.removeEventListener("dblclick", this._preventContentEvent, true);
},
_highlighterReady: function() {
events.emit(this._inspector.walker, "highlighter-ready");
},
_highlighterHidden: function() {
events.emit(this._inspector.walker, "highlighter-hide");
},
cancelPick: method(function() {
if (this._isPicking) {
this._boxModelHighlighter.hide();
@ -247,26 +275,41 @@ let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
* h.destroy();
*
* Structure:
* <stack class="highlighter-container">
* <box class="highlighter-outline-container">
* <box class="highlighter-outline" />
* </box>
* <box class="highlighter-nodeinfobar-container">
* <box class="highlighter-nodeinfobar-positioner" position="top/bottom">
* <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top"/>
* <hbox class="highlighter-nodeinfobar">
* <hbox class="highlighter-nodeinfobar-text">tagname#id.class1.class2</hbox>
* </hbox>
* <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/>
* </box>
* </box>
* </stack>
* <stack class="highlighter-container">
* <svg class="box-model-root" hidden="true">
* <g class="box-model-container">
* <polygon class="box-model-margin" points="317,122 747,36 747,181 317,267" />
* <polygon class="box-model-border" points="317,128 747,42 747,161 317,247" />
* <polygon class="box-model-padding" points="323,127 747,42 747,161 323,246" />
* <polygon class="box-model-content" points="335,137 735,57 735,152 335,232" />
* </g>
* <line class="box-model-guide-top" x1="0" y1="592" x2="99999" y2="592" />
* <line class="box-model-guide-right" x1="735" y1="0" x2="735" y2="99999" />
* <line class="box-model-guide-bottom" x1="0" y1="612" x2="99999" y2="612" />
* <line class="box-model-guide-left" x1="334" y1="0" x2="334" y2="99999" />
* </svg>
* <box class="highlighter-nodeinfobar-container">
* <box class="highlighter-nodeinfobar-positioner" position="top" />
* <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top" />
* <hbox class="highlighter-nodeinfobar">
* <hbox class="highlighter-nodeinfobar-text" align="center" flex="1">
* <span class="highlighter-nodeinfobar-tagname">Node name</span>
* <span class="highlighter-nodeinfobar-id">Node id</span>
* <span class="highlighter-nodeinfobar-classes">.someClass</span>
* <span class="highlighter-nodeinfobar-pseudo-classes">:hover</span>
* </hbox>
* </hbox>
* <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/>
* </box>
* </box>
* </stack>
*/
function BoxModelHighlighter(tabActor) {
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;
this.layoutHelpers = new LayoutHelpers(this.win);
this.chromeLayoutHelper = new LayoutHelpers(this.chromeWin);
@ -274,32 +317,56 @@ function BoxModelHighlighter(tabActor) {
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();
}
BoxModelHighlighter.prototype = {
get zoom() {
return this.win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils).fullZoom;
},
_initMarkup: function() {
let stack = this.browser.parentNode;
this.highlighterContainer = this.chromeDoc.createElement("stack");
this.highlighterContainer.className = "highlighter-container";
this._highlighterContainer = this.chromeDoc.createElement("stack");
this._highlighterContainer.className = "highlighter-container";
this.outline = this.chromeDoc.createElement("box");
this.outline.className = "highlighter-outline";
this._svgRoot = this._createSVGNode("root", "svg", this._highlighterContainer);
let outlineContainer = this.chromeDoc.createElement("box");
outlineContainer.appendChild(this.outline);
outlineContainer.className = "highlighter-outline-container";
this.highlighterContainer.appendChild(outlineContainer);
this._boxModelContainer = this._createSVGNode("container", "g", this._svgRoot);
this._boxModelNodes = {
margin: this._createSVGNode("margin", "polygon", this._boxModelContainer),
border: this._createSVGNode("border", "polygon", this._boxModelContainer),
padding: this._createSVGNode("padding", "polygon", this._boxModelContainer),
content: this._createSVGNode("content", "polygon", this._boxModelContainer)
};
this._guideNodes = {
top: this._createSVGNode("guide-top", "line", this._svgRoot),
right: this._createSVGNode("guide-right", "line", this._svgRoot),
bottom: this._createSVGNode("guide-bottom", "line", this._svgRoot),
left: this._createSVGNode("guide-left", "line", this._svgRoot)
};
this._guideNodes.top.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
this._guideNodes.right.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
this._guideNodes.bottom.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
this._guideNodes.left.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
this._highlighterContainer.appendChild(this._svgRoot);
let infobarContainer = this.chromeDoc.createElement("box");
infobarContainer.className = "highlighter-nodeinfobar-container";
this.highlighterContainer.appendChild(infobarContainer);
this._highlighterContainer.appendChild(infobarContainer);
// Insert the highlighter right after the browser
stack.insertBefore(this.highlighterContainer, stack.childNodes[1]);
stack.insertBefore(this._highlighterContainer, stack.childNodes[1]);
// Building the infobar
let infobarPositioner = this.chromeDoc.createElement("box");
@ -362,6 +429,15 @@ BoxModelHighlighter.prototype = {
};
},
_createSVGNode: function(classPostfix, nodeType, parent) {
let node = this.chromeDoc.createElementNS(SVG_NS, nodeType);
node.setAttribute("class", "box-model-" + classPostfix);
parent.appendChild(node);
return node;
},
/**
* Destroy the nodes. Remove listeners.
*/
@ -371,15 +447,13 @@ BoxModelHighlighter.prototype = {
this.chromeWin.clearTimeout(this.transitionDisabler);
this.chromeWin.clearTimeout(this.pageEventsMuter);
this._contentRect = null;
this._highlightRect = null;
this.outline = null;
this.nodeInfo = null;
this.highlighterContainer.remove();
this.highlighterContainer = null;
this._highlighterContainer.remove();
this._highlighterContainer = null;
this.win = null
this.rect = null;
this.win = null;
this.browser = null;
this.chromeDoc = null;
this.chromeWin = null;
@ -390,28 +464,29 @@ BoxModelHighlighter.prototype = {
* Show the highlighter on a given node
*
* @param {DOMNode} node
* @param {Object} options
* Object used for passing options
*/
show: function(node, options={}) {
if (!this.currentNode || node !== this.currentNode) {
this.currentNode = node;
this.currentNode = node;
this._showInfobar();
this._computeZoomFactor();
this._detachPageListeners();
this._attachPageListeners();
this._update();
this._trackMutations();
this._showInfobar();
this._detachPageListeners();
this._attachPageListeners();
this._update();
this._trackMutations();
if (options.scrollIntoView) {
this.chromeLayoutHelper.scrollIntoViewIfNeeded(node);
}
if (options.scrollIntoView) {
this.chromeLayoutHelper.scrollIntoViewIfNeeded(node);
}
},
_trackMutations: function() {
if (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});
}
},
@ -433,21 +508,20 @@ BoxModelHighlighter.prototype = {
* 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
* @param {Boolean} brieflyDisableTransitions
* In case _update is called during scrolling or repaint, set this
* to true to avoid transitions
* @param {Object} options
* Object used for passing options. Valid options are:
* - box: "content", "padding", "border" or "margin." This specifies
* the box that the guides should outline. Default is content.
*/
_update: function(brieflyDisableTransitions) {
_update: function(options={}) {
if (this.currentNode) {
let rect = this.layoutHelpers.getDirtyRect(this.currentNode);
if (this._highlightRectangle(rect, brieflyDisableTransitions)) {
this._moveInfobar();
this._updateInfobar();
if (this._highlightBoxModel(options)) {
this._showInfobar();
} else {
// Nothing to highlight (0px rectangle like a <script> tag for instance)
this.hide();
}
this.emit("ready");
}
},
@ -458,17 +532,17 @@ BoxModelHighlighter.prototype = {
if (this.currentNode) {
this._untrackMutations();
this.currentNode = null;
this._hideOutline();
this._hideBoxModel();
this._hideInfobar();
this._detachPageListeners();
}
this.emit("hide");
},
/**
* Hide the infobar
*/
_hideInfobar: function() {
this.nodeInfo.positioner.setAttribute("force-transitions", "true");
this.nodeInfo.positioner.setAttribute("hidden", "true");
},
@ -477,82 +551,155 @@ BoxModelHighlighter.prototype = {
*/
_showInfobar: function() {
this.nodeInfo.positioner.removeAttribute("hidden");
this._moveInfobar();
this.nodeInfo.positioner.removeAttribute("force-transitions");
this._updateInfobar();
},
/**
* Hide the outline
* Hide the box model
*/
_hideOutline: function() {
this.outline.setAttribute("hidden", "true");
_hideBoxModel: function() {
this._svgRoot.setAttribute("hidden", "true");
},
/**
* Show the outline
* Show the box model
*/
_showOutline: function() {
this.outline.removeAttribute("hidden");
_showBoxModel: function() {
this._svgRoot.removeAttribute("hidden");
},
/**
* Highlight a rectangular region.
* Highlight the box model.
*
* @param {object} aRect
* The rectangle region to highlight.
* @param {boolean} brieflyDisableTransitions
* Set to true to avoid transitions during the highlighting
* @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.
*/
_highlightRectangle: function(aRect, brieflyDisableTransitions) {
if (!aRect) {
return false;
}
let oldRect = this._contentRect;
if (oldRect && aRect.top == oldRect.top && aRect.left == oldRect.left &&
aRect.width == oldRect.width && aRect.height == oldRect.height) {
this._showOutline();
return true; // same rectangle
}
let aRectScaled = this.layoutHelpers.getZoomedRect(this.win, aRect);
_highlightBoxModel: function(options) {
let isShown = false;
if (aRectScaled.left >= 0 && aRectScaled.top >= 0 &&
aRectScaled.width > 0 && aRectScaled.height > 0) {
options.region = options.region || "content";
// The bottom div and the right div are flexibles (flex=1).
// We don't need to resize them.
let top = "top:" + aRectScaled.top + "px;";
let left = "left:" + aRectScaled.left + "px;";
let width = "width:" + aRectScaled.width + "px;";
let height = "height:" + aRectScaled.height + "px;";
// TODO: Remove this polyfill
this.rect =
this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, "margin");
if (brieflyDisableTransitions) {
this._brieflyDisableTransitions();
if (!this.rect) {
return;
}
if (this.rect.bounds.width > 0 && this.rect.bounds.height > 0) {
for (let boxType in this._boxModelNodes) {
// TODO: Remove this polyfill
let {p1, p2, p3, p4} = boxType === "margin" ? this.rect :
this.layoutHelpers.getAdjustedQuadsPolyfill(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);
if (boxType === options.region) {
this._showGuides(p1, p2, p3, p4);
}
}
this.outline.setAttribute("style", top + left + width + height);
isShown = true;
this._showOutline();
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 (aRectScaled.width > 0 || aRectScaled.height > 0) {
if (this.rect.width > 0 || this.rect.height > 0) {
isShown = true;
this._hideOutline();
this._hideBoxModel();
}
}
return isShown;
},
/**
* We only want to show guides for horizontal and vertical edges as this helps
* to line them up. This method finds these edges and displays a guide there.
*
* @param {DOMPoint} p1
* Point 1
* @param {DOMPoint} p2
* Point 2
* @param {DOMPoint} p3 [description]
* Point 3
* @param {DOMPoint} p4 [description]
* Point 4
*/
_showGuides: function(p1, p2, p3, p4) {
let allX = [p1.x, p2.x, p3.x, p4.x].sort();
let allY = [p1.y, p2.y, p3.y, p4.y].sort();
let toShowX = [];
let toShowY = [];
for (let arr of [allX, allY]) {
for (let i = 0; i < arr.length; i++) {
let val = arr[i];
if (i !== arr.lastIndexOf(val)) {
if (arr === allX) {
toShowX.push(val);
} else {
toShowY.push(val);
}
arr.splice(arr.lastIndexOf(val), 1);
}
}
}
this._contentRect = aRect; // save orig (non-scaled) rect
this._highlightRect = aRectScaled; // and save the scaled rect.
// Move guide into place or hide it if no valid co-ordinate was found.
this._updateGuide(this._guideNodes.top, toShowY[0]);
this._updateGuide(this._guideNodes.right, toShowX[1]);
this._updateGuide(this._guideNodes.bottom, toShowY[1]);
this._updateGuide(this._guideNodes.left, toShowX[0]);
},
return isShown;
/**
* Move a guide to the appropriate position and display it. If no point is
* passed then the guide is hidden.
*
* @param {SVGLine} guide
* The guide to update
* @param {Integer} point
* x or y co-ordinate. If this is undefined we hide the guide.
*/
_updateGuide: function(guide, point=-1) {
if (point > 0) {
let offset = GUIDE_STROKE_WIDTH / 2;
if (guide === this._guideNodes.top || guide === this._guideNodes.left) {
point -= offset;
} else {
point += offset;
}
if (guide === this._guideNodes.top || guide === this._guideNodes.bottom) {
guide.setAttribute("x1", 0);
guide.setAttribute("y1", point);
guide.setAttribute("x2", "100%");
guide.setAttribute("y2", point);
} else {
guide.setAttribute("x1", point);
guide.setAttribute("y1", 0);
guide.setAttribute("x2", point);
guide.setAttribute("y2", "100%");
}
guide.removeAttribute("hidden");
return true;
} else {
guide.setAttribute("hidden", "true");
return false;
}
},
/**
@ -579,6 +726,8 @@ BoxModelHighlighter.prototype = {
let pseudoBox = this.nodeInfo.pseudoClassesBox;
pseudoBox.textContent = pseudos.join("");
this._moveInfobar();
}
},
@ -586,46 +735,33 @@ BoxModelHighlighter.prototype = {
* Move the Infobar to the right place in the highlighter.
*/
_moveInfobar: function() {
if (this._highlightRect) {
if (this.rect) {
let bounds = this.rect.bounds;
let winHeight = this.win.innerHeight * this.zoom;
let winWidth = this.win.innerWidth * this.zoom;
let rect = {top: this._highlightRect.top,
left: this._highlightRect.left,
width: this._highlightRect.width,
height: this._highlightRect.height};
rect.top = Math.max(rect.top, 0);
rect.left = Math.max(rect.left, 0);
rect.width = Math.max(rect.width, 0);
rect.height = Math.max(rect.height, 0);
rect.top = Math.min(rect.top, winHeight);
rect.left = Math.min(rect.left, winWidth);
this.nodeInfo.positioner.removeAttribute("disabled");
// Can the bar be above the node?
if (rect.top < this.nodeInfo.barHeight) {
if (bounds.top < this.nodeInfo.barHeight) {
// No. Can we move the toolbar under the node?
if (rect.top + rect.height +
this.nodeInfo.barHeight > winHeight) {
if (bounds.bottom + this.nodeInfo.barHeight > winHeight) {
// No. Let's move it inside.
this.nodeInfo.positioner.style.top = rect.top + "px";
this.nodeInfo.positioner.style.top = bounds.top + "px";
this.nodeInfo.positioner.setAttribute("position", "overlap");
} else {
// Yes. Let's move it under the node.
this.nodeInfo.positioner.style.top = rect.top + rect.height + "px";
this.nodeInfo.positioner.style.top = bounds.bottom - INFO_BAR_OFFSET + "px";
this.nodeInfo.positioner.setAttribute("position", "bottom");
}
} else {
// Yes. Let's move it on top of the node.
this.nodeInfo.positioner.style.top =
rect.top - this.nodeInfo.barHeight + "px";
bounds.top + INFO_BAR_OFFSET - this.nodeInfo.barHeight + "px";
this.nodeInfo.positioner.setAttribute("position", "top");
}
let barWidth = this.nodeInfo.positioner.getBoundingClientRect().width;
let left = rect.left + rect.width / 2 - barWidth / 2;
let left = bounds.right - bounds.width / 2 - barWidth / 2;
// Make sure the whole infobar is visible
if (left < 0) {
@ -648,26 +784,24 @@ BoxModelHighlighter.prototype = {
}
},
/**
* Store page zoom factor.
*/
_computeZoomFactor: function() {
this.zoom =
this.win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.fullZoom;
},
_attachPageListeners: function() {
this.browser.addEventListener("resize", this, true);
this.browser.addEventListener("scroll", this, true);
this.browser.addEventListener("MozAfterPaint", this, true);
if (this.currentNode) {
let win = this.currentNode.ownerGlobal;
win.addEventListener("scroll", this, false);
win.addEventListener("resize", this, false);
win.addEventListener("MozAfterPaint", this, false);
}
},
_detachPageListeners: function() {
this.browser.removeEventListener("resize", this, true);
this.browser.removeEventListener("scroll", this, true);
this.browser.removeEventListener("MozAfterPaint", this, true);
if (this.currentNode) {
let win = this.currentNode.ownerGlobal;
win.removeEventListener("scroll", this, false);
win.removeEventListener("resize", this, false);
win.removeEventListener("MozAfterPaint", this, false);
}
},
/**
@ -679,33 +813,12 @@ BoxModelHighlighter.prototype = {
handleEvent: function(event) {
switch (event.type) {
case "resize":
this._computeZoomFactor();
break;
case "MozAfterPaint":
case "scroll":
this._update(true);
this._update();
break;
}
},
/**
* Disable the CSS transitions for a short time to avoid laggy animations
* during scrolling or resizing.
*/
_brieflyDisableTransitions: function() {
if (this.transitionDisabler) {
this.chromeWin.clearTimeout(this.transitionDisabler);
} else {
this.outline.setAttribute("disable-transitions", "true");
this.nodeInfo.positioner.setAttribute("disable-transitions", "true");
}
this.transitionDisabler =
this.chromeWin.setTimeout(() => {
this.outline.removeAttribute("disable-transitions");
this.nodeInfo.positioner.removeAttribute("disable-transitions");
this.transitionDisabler = null;
}, 500);
}
};
/**

View File

@ -848,6 +848,12 @@ var WalkerActor = protocol.ActorClass({
"picker-node-hovered" : {
type: "pickerNodeHovered",
node: Arg(0, "disconnectedNode")
},
"highlighter-ready" : {
type: "highlighter-ready"
},
"highlighter-hide" : {
type: "highlighter-hide"
}
},
@ -2545,17 +2551,17 @@ var InspectorActor = protocol.ActorClass({
}
}),
getHighlighter: method(function () {
getHighlighter: method(function (autohide) {
if (this._highlighterPromise) {
return this._highlighterPromise;
}
this._highlighterPromise = this.getWalker().then(walker => {
return HighlighterActor(this);
return HighlighterActor(this, autohide);
});
return this._highlighterPromise;
}, {
request: {},
request: { autohide: Arg(0, "boolean") },
response: {
highligter: RetVal("highlighter")
}