mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-12 04:45:45 +00:00
Bug 1089240 - Add a measurement tool. r=pbrosset
This commit is contained in:
parent
66ccd684c3
commit
2a149c16f4
@ -1344,7 +1344,7 @@ pref("devtools.toolbox.sidebar.width", 500);
|
||||
pref("devtools.toolbox.host", "bottom");
|
||||
pref("devtools.toolbox.previousHost", "side");
|
||||
pref("devtools.toolbox.selectedTool", "webconsole");
|
||||
pref("devtools.toolbox.toolbarSpec", '["splitconsole", "paintflashing toggle","tilt toggle","scratchpad","resize toggle","eyedropper","screenshot --fullpage", "rulers"]');
|
||||
pref("devtools.toolbox.toolbarSpec", '["splitconsole", "paintflashing toggle","tilt toggle","scratchpad","resize toggle","eyedropper","screenshot --fullpage", "rulers", "measure"]');
|
||||
pref("devtools.toolbox.sideEnabled", true);
|
||||
pref("devtools.toolbox.zoomValue", "1");
|
||||
pref("devtools.toolbox.splitconsoleEnabled", false);
|
||||
@ -1361,6 +1361,7 @@ pref("devtools.command-button-responsive.enabled", true);
|
||||
pref("devtools.command-button-eyedropper.enabled", false);
|
||||
pref("devtools.command-button-screenshot.enabled", false);
|
||||
pref("devtools.command-button-rulers.enabled", false);
|
||||
pref("devtools.command-button-measure.enabled", false);
|
||||
|
||||
// Inspector preferences
|
||||
// Enable the Inspector
|
||||
|
@ -64,6 +64,7 @@ support-files =
|
||||
support-files =
|
||||
browser_cmd_jsb_script.jsi
|
||||
[browser_cmd_listen.js]
|
||||
[browser_cmd_measure.js]
|
||||
[browser_cmd_media.js]
|
||||
support-files =
|
||||
browser_cmd_media.html
|
||||
|
53
devtools/client/commandline/test/browser_cmd_measure.js
Normal file
53
devtools/client/commandline/test/browser_cmd_measure.js
Normal file
@ -0,0 +1,53 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests the highlight command, ensure no invalid arguments are given
|
||||
|
||||
const TEST_PAGE = "data:text/html;charset=utf-8,foo";
|
||||
|
||||
function test() {
|
||||
return Task.spawn(spawnTest).then(finish, helpers.handleError);
|
||||
}
|
||||
|
||||
function* spawnTest() {
|
||||
let options = yield helpers.openTab(TEST_PAGE);
|
||||
yield helpers.openToolbar(options);
|
||||
|
||||
yield helpers.audit(options, [
|
||||
{
|
||||
setup: "measure",
|
||||
check: {
|
||||
input: "measure",
|
||||
markup: "VVVVVVV",
|
||||
status: "VALID"
|
||||
}
|
||||
},
|
||||
{
|
||||
setup: "measure on",
|
||||
check: {
|
||||
input: "measure on",
|
||||
markup: "VVVVVVVVEE",
|
||||
status: "ERROR"
|
||||
},
|
||||
exec: {
|
||||
output: "Error: Too many arguments"
|
||||
}
|
||||
},
|
||||
{
|
||||
setup: "measure --visible",
|
||||
check: {
|
||||
input: "measure --visible",
|
||||
markup: "VVVVVVVVEEEEEEEEE",
|
||||
status: "ERROR"
|
||||
},
|
||||
exec: {
|
||||
output: "Error: Too many arguments"
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
yield helpers.closeToolbar(options);
|
||||
yield helpers.closeTab(options);
|
||||
}
|
@ -92,7 +92,8 @@ const ToolboxButtons = exports.ToolboxButtons = [
|
||||
{ id: "command-button-scratchpad" },
|
||||
{ id: "command-button-eyedropper" },
|
||||
{ id: "command-button-screenshot" },
|
||||
{ id: "command-button-rulers"}
|
||||
{ id: "command-button-rulers" },
|
||||
{ id: "command-button-measure" }
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -63,6 +63,8 @@ skip-if = e10s # GCLI isn't e10s compatible. See bug 1128988.
|
||||
[browser_inspector_highlighter-keybinding_02.js]
|
||||
[browser_inspector_highlighter-keybinding_03.js]
|
||||
[browser_inspector_highlighter-keybinding_04.js]
|
||||
[browser_inspector_highlighter-measure_01.js]
|
||||
[browser_inspector_highlighter-measure_02.js]
|
||||
[browser_inspector_highlighter-options.js]
|
||||
[browser_inspector_highlighter-rect_01.js]
|
||||
[browser_inspector_highlighter-rect_02.js]
|
||||
|
@ -0,0 +1,88 @@
|
||||
/* 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 TEST_URL = `data:text/html;charset=utf-8,
|
||||
<div style='
|
||||
position:absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 40000px;
|
||||
height: 8000px'>
|
||||
</div>`;
|
||||
|
||||
const PREFIX = "measuring-tool-highlighter-";
|
||||
const HIGHLIGHTER_TYPE = "MeasuringToolHighlighter";
|
||||
|
||||
const X = 32;
|
||||
const Y = 20;
|
||||
|
||||
add_task(function*() {
|
||||
let helper = yield openInspectorForURL(TEST_URL)
|
||||
.then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
|
||||
|
||||
let { finalize } = helper;
|
||||
|
||||
helper.prefix = PREFIX;
|
||||
|
||||
yield isHiddenByDefault(helper);
|
||||
yield areLabelsHiddenByDefaultWhenShows(helper);
|
||||
yield areLabelsProperlyDisplayedWhenMouseMoved(helper);
|
||||
|
||||
yield finalize();
|
||||
});
|
||||
|
||||
function* isHiddenByDefault({isElementHidden}) {
|
||||
info("Checking the highlighter is hidden by default");
|
||||
|
||||
let hidden = yield isElementHidden("elements");
|
||||
ok(hidden, "highlighter's root is hidden by default");
|
||||
|
||||
hidden = yield isElementHidden("label-size");
|
||||
ok(hidden, "highlighter's label size is hidden by default");
|
||||
|
||||
hidden = yield isElementHidden("label-position");
|
||||
ok(hidden, "highlighter's label position is hidden by default");
|
||||
}
|
||||
|
||||
function* areLabelsHiddenByDefaultWhenShows({isElementHidden, show}) {
|
||||
info("Checking the highlighter is displayed when asked");
|
||||
|
||||
yield show();
|
||||
|
||||
let hidden = yield isElementHidden("elements");
|
||||
is(hidden, false, "highlighter is visible after show");
|
||||
|
||||
hidden = yield isElementHidden("label-size");
|
||||
ok(hidden, "label's size still hidden");
|
||||
|
||||
hidden = yield isElementHidden("label-position");
|
||||
ok(hidden, "label's position still hidden");
|
||||
}
|
||||
|
||||
function* areLabelsProperlyDisplayedWhenMouseMoved({isElementHidden,
|
||||
synthesizeMouse, getElementTextContent}) {
|
||||
info("Checking labels are properly displayed when mouse moved");
|
||||
|
||||
yield synthesizeMouse({
|
||||
selector: ":root",
|
||||
options: {type: "mousemove"},
|
||||
x: X,
|
||||
y: Y
|
||||
});
|
||||
|
||||
let hidden = yield isElementHidden("label-position");
|
||||
is(hidden, false, "label's position is displayed after the mouse is moved");
|
||||
|
||||
hidden = yield isElementHidden("label-size");
|
||||
ok(hidden, "label's size still hidden");
|
||||
|
||||
let text = yield getElementTextContent("label-position");
|
||||
|
||||
let [x, y] = text.replace(/ /g, "").split(/\n/);
|
||||
|
||||
is(+x, X, "label's position shows the proper X coord");
|
||||
is(+y, Y, "label's position shows the proper Y coord");
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
/* 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 TEST_URL = `data:text/html;charset=utf-8,
|
||||
<div style='
|
||||
position:absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 40000px;
|
||||
height: 8000px'>
|
||||
</div>`;
|
||||
|
||||
const PREFIX = "measuring-tool-highlighter-";
|
||||
const HIGHLIGHTER_TYPE = "MeasuringToolHighlighter";
|
||||
|
||||
const SIDES = ["top", "right", "bottom", "left"];
|
||||
|
||||
const X = 32;
|
||||
const Y = 20;
|
||||
const WIDTH = 160;
|
||||
const HEIGHT = 100;
|
||||
const HYPOTENUSE = Math.hypot(WIDTH, HEIGHT).toFixed(2);
|
||||
|
||||
add_task(function*() {
|
||||
let helper = yield openInspectorForURL(TEST_URL)
|
||||
.then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
|
||||
|
||||
let { show, finalize } = helper;
|
||||
|
||||
helper.prefix = PREFIX;
|
||||
|
||||
yield show();
|
||||
|
||||
yield hasNoLabelsWhenStarts(helper);
|
||||
yield hasSizeLabelWhenMoved(helper);
|
||||
yield hasCorrectSizeLabelValue(helper);
|
||||
yield hasSizeLabelAndGuidesWhenStops(helper);
|
||||
yield hasCorrectSizeLabelValue(helper);
|
||||
|
||||
yield finalize();
|
||||
});
|
||||
|
||||
function* hasNoLabelsWhenStarts({isElementHidden, synthesizeMouse}) {
|
||||
info("Checking highlighter has no labels when we start to select");
|
||||
|
||||
yield synthesizeMouse({
|
||||
selector: ":root",
|
||||
options: {type: "mousedown"},
|
||||
x: X,
|
||||
y: Y
|
||||
});
|
||||
|
||||
let hidden = yield isElementHidden("label-size");
|
||||
ok(hidden, "label's size still hidden");
|
||||
|
||||
hidden = yield isElementHidden("label-position");
|
||||
ok(hidden, "label's position still hidden");
|
||||
|
||||
info("Checking highlighter has no guides when we start to select");
|
||||
|
||||
let guidesHidden = true;
|
||||
for (let side of SIDES) {
|
||||
guidesHidden = guidesHidden && (yield isElementHidden("guide-" + side));
|
||||
}
|
||||
|
||||
ok(guidesHidden, "guides are hidden during dragging");
|
||||
}
|
||||
|
||||
function* hasSizeLabelWhenMoved({isElementHidden, synthesizeMouse}) {
|
||||
info("Checking highlighter has size label when we select the area");
|
||||
|
||||
yield synthesizeMouse({
|
||||
selector: ":root",
|
||||
options: {type: "mousemove"},
|
||||
x: X + WIDTH,
|
||||
y: Y + HEIGHT
|
||||
});
|
||||
|
||||
let hidden = yield isElementHidden("label-size");
|
||||
is(hidden, false, "label's size is visible during selection");
|
||||
|
||||
hidden = yield isElementHidden("label-position");
|
||||
ok(hidden, "label's position still hidden");
|
||||
|
||||
info("Checking highlighter has no guides when we select the area");
|
||||
|
||||
let guidesHidden = true;
|
||||
for (let side of SIDES) {
|
||||
guidesHidden = guidesHidden && (yield isElementHidden("guide-" + side));
|
||||
}
|
||||
|
||||
ok(guidesHidden, "guides are hidden during selection");
|
||||
}
|
||||
|
||||
function* hasSizeLabelAndGuidesWhenStops({isElementHidden, synthesizeMouse}) {
|
||||
info("Checking highlighter has size label and guides when we stop");
|
||||
|
||||
yield synthesizeMouse({
|
||||
selector: ":root",
|
||||
options: {type: "mouseup"},
|
||||
x: X + WIDTH,
|
||||
y: Y + HEIGHT
|
||||
});
|
||||
|
||||
let hidden = yield isElementHidden("label-size");
|
||||
is(hidden, false, "label's size is visible when the selection is done");
|
||||
|
||||
hidden = yield isElementHidden("label-position");
|
||||
ok(hidden, "label's position still hidden");
|
||||
|
||||
let guidesVisible = true;
|
||||
for (let side of SIDES) {
|
||||
guidesVisible = guidesVisible && !(yield isElementHidden("guide-" + side));
|
||||
}
|
||||
|
||||
ok(guidesVisible, "guides are visible when the selection is done");
|
||||
}
|
||||
|
||||
function* hasCorrectSizeLabelValue({getElementTextContent}) {
|
||||
let text = yield getElementTextContent("label-size");
|
||||
|
||||
let [width, height, hypot] = text.match(/\d.*px/g);
|
||||
|
||||
is(parseFloat(width), WIDTH, "width on label's size is correct");
|
||||
is(parseFloat(height), HEIGHT, "height on label's size is correct");
|
||||
is(parseFloat(hypot), HYPOTENUSE, "hypotenuse on label's size is correct");
|
||||
}
|
@ -480,3 +480,57 @@ function dispatchCommandEvent(node) {
|
||||
false, false, null);
|
||||
node.dispatchEvent(commandEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulate some common operations for highlighter's tests, to have
|
||||
* the tests cleaner, without exposing directly `inspector`, `highlighter`, and
|
||||
* `testActor` if not needed.
|
||||
*
|
||||
* @param {String}
|
||||
* The highlighter's type
|
||||
* @return
|
||||
* A generator function that takes an object with `inspector` and `testActor`
|
||||
* properties. (see `openInspector`)
|
||||
*/
|
||||
const getHighlighterHelperFor = (type) => Task.async(
|
||||
function*({inspector, testActor}) {
|
||||
let front = inspector.inspector;
|
||||
let highlighter = yield front.getHighlighterByType(type);
|
||||
|
||||
let prefix = "";
|
||||
|
||||
return {
|
||||
set prefix(value) {
|
||||
prefix = value;
|
||||
},
|
||||
|
||||
show: function*(selector = ":root") {
|
||||
let node = yield getNodeFront(selector, inspector);
|
||||
yield highlighter.show(node);
|
||||
},
|
||||
|
||||
isElementHidden: function*(id) {
|
||||
return (yield testActor.getHighlighterNodeAttribute(
|
||||
prefix + id, "hidden", highlighter)) === "true";
|
||||
},
|
||||
|
||||
getElementTextContent: function*(id) {
|
||||
return yield testActor.getHighlighterNodeTextContent(
|
||||
prefix + id, highlighter);
|
||||
},
|
||||
|
||||
getElementAttribute: function*(id, name) {
|
||||
return yield testActor.getHighlighterNodeAttribute(
|
||||
prefix + id, name, highlighter);
|
||||
},
|
||||
|
||||
synthesizeMouse: function*(options) {
|
||||
yield testActor.synthesizeMouse(options);
|
||||
},
|
||||
|
||||
finalize: function*() {
|
||||
yield highlighter.finalize();
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -205,6 +205,8 @@ devtools.jar:
|
||||
skin/themes/images/command-eyedropper@2x.png (themes/images/command-eyedropper@2x.png)
|
||||
skin/themes/images/command-rulers.png (themes/images/command-rulers.png)
|
||||
skin/themes/images/command-rulers@2x.png (themes/images/command-rulers@2x.png)
|
||||
skin/themes/images/command-measure.png (themes/images/command-measure.png)
|
||||
skin/themes/images/command-measure@2x.png (themes/images/command-measure@2x.png)
|
||||
skin/themes/markup-view.css (themes/markup-view.css)
|
||||
skin/themes/images/editor-error.png (themes/images/editor-error.png)
|
||||
skin/themes/images/editor-breakpoint.png (themes/images/editor-breakpoint.png)
|
||||
|
BIN
devtools/client/themes/images/command-measure.png
Normal file
BIN
devtools/client/themes/images/command-measure.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 207 B |
BIN
devtools/client/themes/images/command-measure@2x.png
Normal file
BIN
devtools/client/themes/images/command-measure@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 179 B |
@ -768,6 +768,10 @@
|
||||
background-image: url("chrome://devtools/skin/themes/images/command-rulers.png");
|
||||
}
|
||||
|
||||
#command-button-measure > image {
|
||||
background-image: url("chrome://devtools/skin/themes/images/command-measure.png");
|
||||
}
|
||||
|
||||
@media (min-resolution: 1.1dppx) {
|
||||
#command-button-paintflashing > image {
|
||||
background-image: url("chrome://devtools/skin/themes/images/command-paintflashing@2x.png");
|
||||
@ -808,6 +812,10 @@
|
||||
#command-button-rulers > image {
|
||||
background-image: url("chrome://devtools/skin/themes/images/command-rulers@2x.png");
|
||||
}
|
||||
|
||||
#command-button-measure > image {
|
||||
background-image: url("chrome://devtools/skin/themes/images/command-measure@2x.png");
|
||||
}
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
|
@ -273,3 +273,62 @@
|
||||
transform: rotate(-90deg);
|
||||
text-anchor: end;
|
||||
}
|
||||
|
||||
/* Measuring Tool highlighter */
|
||||
|
||||
:-moz-native-anonymous .measuring-tool-highlighter-root {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: auto;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .measuring-tool-highlighter-root path {
|
||||
shape-rendering: crispEdges;
|
||||
fill: rgba(135, 206, 235, 0.6);
|
||||
stroke: #08c;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .dragging path {
|
||||
fill: rgba(135, 206, 235, 0.6);
|
||||
stroke: #08c;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .measuring-tool-highlighter-label-size,
|
||||
:-moz-native-anonymous .measuring-tool-highlighter-label-position {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
white-space: pre-line;
|
||||
font: message-box;
|
||||
font-size: 10px;
|
||||
pointer-events: none;
|
||||
-moz-user-select: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .measuring-tool-highlighter-label-position {
|
||||
color: #fff;
|
||||
background: hsla(214, 13%, 24%, 0.8);
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .measuring-tool-highlighter-label-size {
|
||||
color: hsl(216, 33%, 97%);
|
||||
background: hsl(214, 13%, 24%);
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .measuring-tool-highlighter-guide-top,
|
||||
:-moz-native-anonymous .measuring-tool-highlighter-guide-right,
|
||||
:-moz-native-anonymous .measuring-tool-highlighter-guide-bottom,
|
||||
:-moz-native-anonymous .measuring-tool-highlighter-guide-left {
|
||||
stroke: #08c;
|
||||
stroke-dasharray: 5 3;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
@ -703,3 +703,7 @@ exports.GeometryEditorHighlighter = GeometryEditorHighlighter;
|
||||
const { RulersHighlighter } = require("./highlighters/rulers");
|
||||
register(RulersHighlighter);
|
||||
exports.RulersHighlighter = RulersHighlighter;
|
||||
|
||||
const { MeasuringToolHighlighter } = require("./highlighters/measuring-tool");
|
||||
register(MeasuringToolHighlighter);
|
||||
exports.MeasuringToolHighlighter = MeasuringToolHighlighter;
|
||||
|
562
devtools/server/actors/highlighters/measuring-tool.js
Normal file
562
devtools/server/actors/highlighters/measuring-tool.js
Normal file
@ -0,0 +1,562 @@
|
||||
/* 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 events = require("sdk/event/core");
|
||||
const { getCurrentZoom,
|
||||
setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
|
||||
const {
|
||||
CanvasFrameAnonymousContentHelper,
|
||||
createSVGNode, createNode } = require("./utils/markup");
|
||||
|
||||
// Hard coded value about the size of measuring tool label, in order to
|
||||
// position and flip it when is needed.
|
||||
const LABEL_SIZE_MARGIN = 8;
|
||||
const LABEL_SIZE_WIDTH = 80;
|
||||
const LABEL_SIZE_HEIGHT = 52;
|
||||
const LABEL_POS_MARGIN = 4;
|
||||
const LABEL_POS_WIDTH = 40;
|
||||
const LABEL_POS_HEIGHT = 34;
|
||||
|
||||
const SIDES = ["top", "right", "bottom", "left"];
|
||||
|
||||
/**
|
||||
* The MeasuringToolHighlighter is used to measure distances in a content page.
|
||||
* It allows users to click and drag with their mouse to draw an area whose
|
||||
* dimensions will be displayed in a tooltip next to it.
|
||||
* This allows users to measure distances between elements on a page.
|
||||
*/
|
||||
function MeasuringToolHighlighter(highlighterEnv) {
|
||||
this.env = highlighterEnv;
|
||||
this.markup = new CanvasFrameAnonymousContentHelper(highlighterEnv,
|
||||
this._buildMarkup.bind(this));
|
||||
|
||||
this.coords = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
|
||||
let { pageListenerTarget } = highlighterEnv;
|
||||
|
||||
pageListenerTarget.addEventListener("mousedown", this);
|
||||
pageListenerTarget.addEventListener("mousemove", this);
|
||||
pageListenerTarget.addEventListener("mouseleave", this);
|
||||
pageListenerTarget.addEventListener("scroll", this);
|
||||
pageListenerTarget.addEventListener("pagehide", this);
|
||||
}
|
||||
|
||||
MeasuringToolHighlighter.prototype = {
|
||||
typeName: "MeasuringToolHighlighter",
|
||||
|
||||
ID_CLASS_PREFIX: "measuring-tool-highlighter-",
|
||||
|
||||
_buildMarkup() {
|
||||
let prefix = this.ID_CLASS_PREFIX;
|
||||
let { window } = this.env;
|
||||
|
||||
let container = createNode(window, {
|
||||
attributes: {"class": "highlighter-container"}
|
||||
});
|
||||
|
||||
let root = createNode(window, {
|
||||
parent: container,
|
||||
attributes: {
|
||||
"id": "root",
|
||||
"class": "root",
|
||||
},
|
||||
prefix
|
||||
});
|
||||
|
||||
let svg = createSVGNode(window, {
|
||||
nodeType: "svg",
|
||||
parent: root,
|
||||
attributes: {
|
||||
id: "elements",
|
||||
"class": "elements",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
hidden: "true"
|
||||
},
|
||||
prefix
|
||||
});
|
||||
|
||||
createNode(window, {
|
||||
nodeType: "label",
|
||||
attributes: {
|
||||
id: "label-size",
|
||||
"class": "label-size",
|
||||
"hidden": "true"
|
||||
},
|
||||
parent: root,
|
||||
prefix
|
||||
});
|
||||
|
||||
createNode(window, {
|
||||
nodeType: "label",
|
||||
attributes: {
|
||||
id: "label-position",
|
||||
"class": "label-position",
|
||||
"hidden": "true"
|
||||
},
|
||||
parent: root,
|
||||
prefix
|
||||
});
|
||||
|
||||
// Creating a <g> element in order to group all the paths below, that
|
||||
// together represent the measuring tool; so that would be easier move them
|
||||
// around
|
||||
let g = createSVGNode(window, {
|
||||
nodeType: "g",
|
||||
attributes: {
|
||||
id: "tool",
|
||||
},
|
||||
parent: svg,
|
||||
prefix
|
||||
});
|
||||
|
||||
createSVGNode(window, {
|
||||
nodeType: "path",
|
||||
attributes: {
|
||||
id: "box-path"
|
||||
},
|
||||
parent: g,
|
||||
prefix
|
||||
});
|
||||
|
||||
createSVGNode(window, {
|
||||
nodeType: "path",
|
||||
attributes: {
|
||||
id: "diagonal-path"
|
||||
},
|
||||
parent: g,
|
||||
prefix
|
||||
});
|
||||
|
||||
for (let side of SIDES) {
|
||||
createSVGNode(window, {
|
||||
nodeType: "line",
|
||||
parent: svg,
|
||||
attributes: {
|
||||
"class": `guide-${side}`,
|
||||
id: `guide-${side}`,
|
||||
hidden: "true"
|
||||
},
|
||||
prefix
|
||||
});
|
||||
}
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
_update() {
|
||||
let { window } = this.env;
|
||||
|
||||
setIgnoreLayoutChanges(true);
|
||||
|
||||
let zoom = getCurrentZoom(window);
|
||||
|
||||
let { documentElement } = window.document;
|
||||
|
||||
let width = Math.max(documentElement.clientWidth,
|
||||
documentElement.scrollWidth,
|
||||
documentElement.offsetWidth);
|
||||
|
||||
let height = Math.max(documentElement.clientHeight,
|
||||
documentElement.scrollHeight,
|
||||
documentElement.offsetHeight);
|
||||
|
||||
let { body } = window.document;
|
||||
|
||||
// get the size of the content document despite the compatMode
|
||||
if (body) {
|
||||
width = Math.max(width, body.scrollWidth, body.offsetWidth);
|
||||
height = Math.max(height, body.scrollHeight, body.offsetHeight);
|
||||
}
|
||||
|
||||
let { coords } = this;
|
||||
|
||||
let isZoomChanged = zoom !== coords.zoom;
|
||||
|
||||
if (isZoomChanged) {
|
||||
coords.zoom = zoom;
|
||||
this.updateLabel();
|
||||
}
|
||||
|
||||
let isDocumentSizeChanged = width !== coords.documentWidth ||
|
||||
height !== coords.documentHeight;
|
||||
|
||||
if (isDocumentSizeChanged) {
|
||||
coords.documentWidth = width;
|
||||
coords.documentHeight = height;
|
||||
}
|
||||
|
||||
// If either the document's size or the zoom is changed since the last
|
||||
// repaint, we update the tool's size as well.
|
||||
if (isZoomChanged || isDocumentSizeChanged) {
|
||||
this.updateViewport();
|
||||
}
|
||||
|
||||
setIgnoreLayoutChanges(false, documentElement);
|
||||
|
||||
this._rafID = window.requestAnimationFrame(() => this._update());
|
||||
},
|
||||
|
||||
_cancelUpdate() {
|
||||
if (this._rafID) {
|
||||
this.env.window.cancelAnimationFrame(this._rafID);
|
||||
this._rafID = 0;
|
||||
}
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this.hide();
|
||||
|
||||
this._cancelUpdate();
|
||||
|
||||
let { pageListenerTarget } = this.env;
|
||||
|
||||
pageListenerTarget.removeEventListener("mousedown", this);
|
||||
pageListenerTarget.removeEventListener("mousemove", this);
|
||||
pageListenerTarget.removeEventListener("mouseup", this);
|
||||
pageListenerTarget.removeEventListener("scroll", this);
|
||||
pageListenerTarget.removeEventListener("pagehide", this);
|
||||
|
||||
this.markup.destroy();
|
||||
|
||||
events.emit(this, "destroy");
|
||||
},
|
||||
|
||||
show() {
|
||||
setIgnoreLayoutChanges(true);
|
||||
|
||||
this.getElement("elements").removeAttribute("hidden");
|
||||
|
||||
this._update();
|
||||
|
||||
setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
|
||||
},
|
||||
|
||||
hide() {
|
||||
setIgnoreLayoutChanges(true);
|
||||
|
||||
this.hideLabel("size");
|
||||
this.hideLabel("position");
|
||||
|
||||
this.getElement("elements").setAttribute("hidden", "true");
|
||||
|
||||
this._cancelUpdate();
|
||||
|
||||
setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
|
||||
},
|
||||
|
||||
getElement(id) {
|
||||
return this.markup.getElement(this.ID_CLASS_PREFIX + id);
|
||||
},
|
||||
|
||||
setSize(w, h) {
|
||||
this.setCoords(undefined, undefined, w, h);
|
||||
},
|
||||
|
||||
setCoords(x, y, w, h) {
|
||||
let { coords } = this;
|
||||
|
||||
if (typeof x !== "undefined") {
|
||||
coords.x = x;
|
||||
}
|
||||
|
||||
if (typeof y !== "undefined") {
|
||||
coords.y = y;
|
||||
}
|
||||
|
||||
if (typeof w !== "undefined") {
|
||||
coords.w = w;
|
||||
}
|
||||
|
||||
if (typeof h !== "undefined") {
|
||||
coords.h = h;
|
||||
}
|
||||
|
||||
setIgnoreLayoutChanges(true);
|
||||
|
||||
if (this._isDragging) {
|
||||
this.updatePaths();
|
||||
}
|
||||
|
||||
this.updateLabel();
|
||||
|
||||
setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
|
||||
},
|
||||
|
||||
updatePaths() {
|
||||
let { x, y, w, h } = this.coords;
|
||||
let dir = `M0 0 L${w} 0 L${w} ${h} L0 ${h}z`;
|
||||
|
||||
// Adding correction to the line path, otherwise some pixels are drawn
|
||||
// outside the main rectangle area.
|
||||
let x1 = w > 0 ? 0.5 : 0;
|
||||
let y1 = w < 0 && h < 0 ? -0.5 : 0;
|
||||
let w1 = w + (h < 0 && w < 0 ? 0.5 : 0);
|
||||
let h1 = h + (h > 0 && w > 0 ? -0.5 : 0);
|
||||
|
||||
let linedir = `M${x1} ${y1} L${w1} ${h1}`;
|
||||
|
||||
this.getElement("box-path").setAttribute("d", dir);
|
||||
this.getElement("diagonal-path").setAttribute("d", linedir);
|
||||
this.getElement("tool").setAttribute("transform", `translate(${x},${y})`);
|
||||
},
|
||||
|
||||
updateLabel(type) {
|
||||
type = type || this._isDragging ? "size" : "position";
|
||||
|
||||
let isSizeLabel = type === "size";
|
||||
|
||||
let label = this.getElement(`label-${type}`);
|
||||
|
||||
let origin = "top left";
|
||||
|
||||
let { innerWidth, innerHeight, scrollX, scrollY } = this.env.window;
|
||||
let { x, y, w, h, zoom } = this.coords;
|
||||
let scale = 1 / zoom;
|
||||
|
||||
w = w || 0;
|
||||
h = h || 0;
|
||||
x = (x || 0) + w;
|
||||
y = (y || 0) + h;
|
||||
|
||||
let labelMargin, labelHeight, labelWidth;
|
||||
|
||||
if (isSizeLabel) {
|
||||
labelMargin = LABEL_SIZE_MARGIN;
|
||||
labelWidth = LABEL_SIZE_WIDTH;
|
||||
labelHeight = LABEL_SIZE_HEIGHT;
|
||||
|
||||
let d = Math.hypot(w, h).toFixed(2);
|
||||
|
||||
label.setTextContent(`W: ${Math.abs(w)} px
|
||||
H: ${Math.abs(h)} px
|
||||
↘: ${d}px`);
|
||||
} else {
|
||||
labelMargin = LABEL_POS_MARGIN;
|
||||
labelWidth = LABEL_POS_WIDTH;
|
||||
labelHeight = LABEL_POS_HEIGHT;
|
||||
|
||||
label.setTextContent(`${x}
|
||||
${y}`);
|
||||
}
|
||||
|
||||
// Size used to position properly the label
|
||||
let labelBoxWidth = (labelWidth + labelMargin) * scale;
|
||||
let labelBoxHeight = (labelHeight + labelMargin) * scale;
|
||||
|
||||
let isGoingLeft = w < scrollX;
|
||||
let isSizeGoingLeft = isSizeLabel && isGoingLeft;
|
||||
let isExceedingLeftMargin = x - labelBoxWidth < scrollX;
|
||||
let isExceedingRightMargin = x + labelBoxWidth > innerWidth + scrollX;
|
||||
let isExceedingTopMargin = y - labelBoxHeight < scrollY;
|
||||
let isExceedingBottomMargin = y + labelBoxHeight > innerHeight + scrollY;
|
||||
|
||||
if ((isSizeGoingLeft && !isExceedingLeftMargin) || isExceedingRightMargin) {
|
||||
x -= labelBoxWidth;
|
||||
origin = "top right";
|
||||
} else {
|
||||
x += labelMargin * scale;
|
||||
}
|
||||
|
||||
if (isSizeLabel) {
|
||||
y += isExceedingTopMargin ? labelMargin * scale : -labelBoxHeight;
|
||||
} else {
|
||||
y += isExceedingBottomMargin ? -labelBoxHeight : labelMargin * scale;
|
||||
}
|
||||
|
||||
label.setAttribute("style", `
|
||||
width: ${labelWidth}px;
|
||||
height: ${labelHeight}px;
|
||||
transform-origin: ${origin};
|
||||
transform: translate(${x}px,${y}px) scale(${scale})
|
||||
`);
|
||||
|
||||
if (!isSizeLabel) {
|
||||
let labelSize = this.getElement("label-size");
|
||||
let style = labelSize.getAttribute("style");
|
||||
|
||||
if (style) {
|
||||
labelSize.setAttribute("style",
|
||||
style.replace(/scale[^)]+\)/, `scale(${scale})`));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateViewport() {
|
||||
let { scrollX, scrollY, devicePixelRatio } = this.env.window;
|
||||
let { documentWidth, documentHeight, zoom } = this.coords;
|
||||
|
||||
// Because `devicePixelRatio` is affected by zoom (see bug 809788),
|
||||
// in order to get the "real" device pixel ratio, we need divide by `zoom`
|
||||
let pixelRatio = devicePixelRatio / zoom;
|
||||
|
||||
// The "real" device pixel ratio is used to calculate the max stroke
|
||||
// width we can actually assign: on retina, for instance, it would be 0.5,
|
||||
// where on non high dpi monitor would be 1.
|
||||
let minWidth = 1 / pixelRatio;
|
||||
let strokeWidth = Math.min(minWidth, minWidth / zoom);
|
||||
|
||||
this.getElement("root").setAttribute("style",
|
||||
`stroke-width:${strokeWidth};
|
||||
width:${documentWidth}px;
|
||||
height:${documentHeight}px;
|
||||
transform: translate(${-scrollX}px,${-scrollY}px)`);
|
||||
},
|
||||
|
||||
updateGuides() {
|
||||
let { x, y, w, h } = this.coords;
|
||||
|
||||
let guide = this.getElement("guide-top");
|
||||
|
||||
guide.setAttribute("x1", "0");
|
||||
guide.setAttribute("y1", y);
|
||||
guide.setAttribute("x2", "100%");
|
||||
guide.setAttribute("y2", y);
|
||||
|
||||
guide = this.getElement("guide-right");
|
||||
|
||||
guide.setAttribute("x1", x + w);
|
||||
guide.setAttribute("y1", 0);
|
||||
guide.setAttribute("x2", x + w);
|
||||
guide.setAttribute("y2", "100%");
|
||||
|
||||
guide = this.getElement("guide-bottom");
|
||||
|
||||
guide.setAttribute("x1", "0");
|
||||
guide.setAttribute("y1", y + h);
|
||||
guide.setAttribute("x2", "100%");
|
||||
guide.setAttribute("y2", y + h);
|
||||
|
||||
guide = this.getElement("guide-left");
|
||||
|
||||
guide.setAttribute("x1", x);
|
||||
guide.setAttribute("y1", 0);
|
||||
guide.setAttribute("x2", x);
|
||||
guide.setAttribute("y2", "100%");
|
||||
},
|
||||
|
||||
showLabel(type) {
|
||||
setIgnoreLayoutChanges(true);
|
||||
|
||||
this.getElement(`label-${type}`).removeAttribute("hidden");
|
||||
|
||||
setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
|
||||
},
|
||||
|
||||
hideLabel(type) {
|
||||
setIgnoreLayoutChanges(true);
|
||||
|
||||
this.getElement(`label-${type}`).setAttribute("hidden", "true");
|
||||
|
||||
setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
|
||||
},
|
||||
|
||||
showGuides() {
|
||||
let prefix = this.ID_CLASS_PREFIX + "guide-";
|
||||
|
||||
for (let side of SIDES) {
|
||||
this.markup.removeAttributeForElement(`${prefix + side}`, "hidden");
|
||||
}
|
||||
},
|
||||
|
||||
hideGuides() {
|
||||
let prefix = this.ID_CLASS_PREFIX + "guide-";
|
||||
|
||||
for (let side of SIDES) {
|
||||
this.markup.setAttributeForElement(`${prefix + side}`, "hidden", "true");
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
let scrollX, scrollY, innerWidth, innerHeight;
|
||||
let x, y;
|
||||
|
||||
let { pageListenerTarget } = this.env;
|
||||
|
||||
switch (event.type) {
|
||||
case "mousedown":
|
||||
if (event.button) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isDragging = true;
|
||||
|
||||
let { window } = this.env;
|
||||
|
||||
({ scrollX, scrollY } = window);
|
||||
x = event.clientX + scrollX;
|
||||
y = event.clientY + scrollY;
|
||||
|
||||
pageListenerTarget.addEventListener("mouseup", this);
|
||||
|
||||
setIgnoreLayoutChanges(true);
|
||||
|
||||
this.getElement("tool").setAttribute("class", "dragging");
|
||||
|
||||
this.hideLabel("size");
|
||||
this.hideLabel("position");
|
||||
|
||||
this.hideGuides();
|
||||
this.setCoords(x, y, 0, 0);
|
||||
|
||||
setIgnoreLayoutChanges(false, window.document.documentElement);
|
||||
|
||||
break;
|
||||
case "mouseup":
|
||||
this._isDragging = false;
|
||||
|
||||
pageListenerTarget.removeEventListener("mouseup", this);
|
||||
|
||||
setIgnoreLayoutChanges(true);
|
||||
|
||||
this.getElement("tool").removeAttribute("class", "");
|
||||
|
||||
// Shows the guides only if an actual area is selected
|
||||
if (this.coords.w !== 0 && this.coords.h !== 0) {
|
||||
this.updateGuides();
|
||||
this.showGuides();
|
||||
}
|
||||
|
||||
setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
|
||||
|
||||
break;
|
||||
case "mousemove":
|
||||
({ scrollX, scrollY, innerWidth, innerHeight } = this.env.window);
|
||||
x = event.clientX + scrollX;
|
||||
y = event.clientY + scrollY;
|
||||
|
||||
let { coords } = this;
|
||||
|
||||
x = Math.min(innerWidth + scrollX - 1, Math.max(0 + scrollX, x));
|
||||
y = Math.min(innerHeight + scrollY, Math.max(1 + scrollY, y));
|
||||
|
||||
this.setSize(x - coords.x, y - coords.y);
|
||||
|
||||
let type = this._isDragging ? "size" : "position";
|
||||
|
||||
this.showLabel(type);
|
||||
break;
|
||||
case "mouseleave":
|
||||
if (!this._isDragging) {
|
||||
this.hideLabel("position");
|
||||
}
|
||||
break;
|
||||
case "scroll":
|
||||
setIgnoreLayoutChanges(true);
|
||||
this.updateViewport();
|
||||
setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
|
||||
|
||||
break;
|
||||
case "pagehide":
|
||||
this.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
exports.MeasuringToolHighlighter = MeasuringToolHighlighter;
|
@ -13,6 +13,7 @@ DevToolsModules(
|
||||
'box-model.js',
|
||||
'css-transform.js',
|
||||
'geometry-editor.js',
|
||||
'measuring-tool.js',
|
||||
'rect.js',
|
||||
'rulers.js',
|
||||
'selector.js',
|
||||
|
@ -65,6 +65,7 @@ exports.devtoolsModules = [
|
||||
"devtools/shared/gcli/commands/inject",
|
||||
"devtools/shared/gcli/commands/jsb",
|
||||
"devtools/shared/gcli/commands/listen",
|
||||
"devtools/shared/gcli/commands/measure",
|
||||
"devtools/shared/gcli/commands/media",
|
||||
"devtools/shared/gcli/commands/pagemod",
|
||||
"devtools/shared/gcli/commands/paintflashing",
|
||||
|
112
devtools/shared/gcli/commands/measure.js
Normal file
112
devtools/shared/gcli/commands/measure.js
Normal file
@ -0,0 +1,112 @@
|
||||
/* 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/. */
|
||||
/* globals getOuterId, getBrowserForTab */
|
||||
|
||||
"use strict";
|
||||
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const eventEmitter = new EventEmitter();
|
||||
const events = require("sdk/event/core");
|
||||
|
||||
loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
|
||||
loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
|
||||
|
||||
const l10n = require("gcli/l10n");
|
||||
require("devtools/server/actors/inspector");
|
||||
const { MeasuringToolHighlighter, HighlighterEnvironment } =
|
||||
require("devtools/server/actors/highlighters");
|
||||
|
||||
const highlighters = new WeakMap();
|
||||
const visibleHighlighters = new Set();
|
||||
|
||||
const isCheckedFor = (tab) =>
|
||||
tab ? visibleHighlighters.has(getBrowserForTab(tab).outerWindowID) : false;
|
||||
|
||||
exports.items = [
|
||||
// The client measure command is used to maintain the toolbar button state
|
||||
// only and redirects to the server command to actually toggle the measuring
|
||||
// tool (see `measure_server` below).
|
||||
{
|
||||
name: "measure",
|
||||
runAt: "client",
|
||||
description: l10n.lookup("measureDesc"),
|
||||
manual: l10n.lookup("measureManual"),
|
||||
buttonId: "command-button-measure",
|
||||
buttonClass: "command-button command-button-invertable",
|
||||
tooltipText: l10n.lookup("measureTooltip"),
|
||||
state: {
|
||||
isChecked: ({_tab}) => isCheckedFor(_tab),
|
||||
onChange: (target, handler) => eventEmitter.on("changed", handler),
|
||||
offChange: (target, handler) => eventEmitter.off("changed", handler)
|
||||
},
|
||||
exec: function*(args, context) {
|
||||
let { target } = context.environment;
|
||||
|
||||
// Pipe the call to the server command.
|
||||
let response = yield context.updateExec("measure_server");
|
||||
let { visible, id } = response.data;
|
||||
|
||||
if (visible) {
|
||||
visibleHighlighters.add(id);
|
||||
} else {
|
||||
visibleHighlighters.delete(id);
|
||||
}
|
||||
|
||||
eventEmitter.emit("changed", { target });
|
||||
|
||||
// Toggle off the button when the page navigates because the measuring
|
||||
// tool is removed automatically by the MeasuringToolHighlighter on the
|
||||
// server then.
|
||||
let onNavigate = () => {
|
||||
visibleHighlighters.delete(id);
|
||||
eventEmitter.emit("changed", { target });
|
||||
};
|
||||
target.off("will-navigate", onNavigate);
|
||||
target.once("will-navigate", onNavigate);
|
||||
}
|
||||
},
|
||||
// The server measure command is hidden by default, it's just used by the
|
||||
// client command.
|
||||
{
|
||||
name: "measure_server",
|
||||
runAt: "server",
|
||||
hidden: true,
|
||||
returnType: "highlighterVisibility",
|
||||
exec: function(args, context) {
|
||||
let env = context.environment;
|
||||
let { document } = env;
|
||||
let id = getOuterId(env.window);
|
||||
|
||||
// Calling the command again after the measuring tool has been shown once,
|
||||
// hides it.
|
||||
if (highlighters.has(document)) {
|
||||
let { highlighter } = highlighters.get(document);
|
||||
highlighter.destroy();
|
||||
return {visible: false, id};
|
||||
}
|
||||
|
||||
// Otherwise, display the measuring tool.
|
||||
let environment = new HighlighterEnvironment();
|
||||
environment.initFromWindow(env.window);
|
||||
let highlighter = new MeasuringToolHighlighter(environment);
|
||||
|
||||
// Store the instance of the measuring tool highlighter for this document
|
||||
// so we can hide it later.
|
||||
highlighters.set(document, { highlighter, environment });
|
||||
|
||||
// Listen to the highlighter's destroy event which may happen if the
|
||||
// window is refreshed or closed with the measuring tool shown.
|
||||
events.once(highlighter, "destroy", () => {
|
||||
if (highlighters.has(document)) {
|
||||
let { environment } = highlighters.get(document);
|
||||
environment.destroy();
|
||||
highlighters.delete(document);
|
||||
}
|
||||
});
|
||||
|
||||
highlighter.show();
|
||||
return {visible: true, id};
|
||||
}
|
||||
}
|
||||
];
|
@ -17,6 +17,7 @@ DevToolsModules(
|
||||
'inject.js',
|
||||
'jsb.js',
|
||||
'listen.js',
|
||||
'measure.js',
|
||||
'media.js',
|
||||
'pagemod.js',
|
||||
'paintflashing.js',
|
||||
|
@ -1618,3 +1618,17 @@ rulersManual=Toggle the horizontal and vertical rulers for the current page
|
||||
# LOCALIZATION NOTE (rulersTooltip) A string displayed as the
|
||||
# tooltip of button in devtools toolbox which toggles the rulers.
|
||||
rulersTooltip=Toggle rulers for the page
|
||||
|
||||
# LOCALIZATION NOTE (measureDesc) A very short description of the
|
||||
# 'measure' command. See measureManual for a fuller description of what
|
||||
# it does. This string is designed to be shown in a menu alongside the
|
||||
# command name, which is why it should be as short as possible.
|
||||
measureDesc=Measure a portion of the page
|
||||
|
||||
# LOCALIZATION NOTE (measureManual) A fuller description of the 'measure'
|
||||
# command, displayed when the user asks for help on what it does.
|
||||
measureManual=Activate the measuring tool to measure an arbitrary area of the page
|
||||
|
||||
# LOCALIZATION NOTE (measureTooltip) A string displayed as the
|
||||
# tooltip of button in devtools toolbox which toggles the measuring tool.
|
||||
measureTooltip=Measure a portion of the page
|
||||
|
Loading…
Reference in New Issue
Block a user