--- a/devtools/client/inspector/boxmodel/actions/box-model.js
+++ b/devtools/client/inspector/boxmodel/actions/box-model.js
@@ -1,21 +1,35 @@
/* 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 {
+ UPDATE_GEOMETRY_EDITOR_ENABLED,
UPDATE_LAYOUT,
} = require("./index");
module.exports = {
/**
+ * Update the geometry editor's enabled state.
+ *
+ * @param {Boolean} enabled
+ * Whether or not the geometry editor is enabled or not.
+ */
+ updateGeometryEditorEnabled(enabled) {
+ return {
+ type: UPDATE_GEOMETRY_EDITOR_ENABLED,
+ enabled,
+ };
+ },
+
+ /**
* Update the layout state with the new layout properties.
*/
updateLayout(layout) {
return {
type: UPDATE_LAYOUT,
layout,
};
},
--- a/devtools/client/inspector/boxmodel/actions/index.js
+++ b/devtools/client/inspector/boxmodel/actions/index.js
@@ -3,12 +3,15 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { createEnum } = require("devtools/client/shared/enum");
createEnum([
+ // Update the geometry editor's enabled state.
+ "UPDATE_GEOMETRY_EDITOR_ENABLED",
+
// Update the layout state with the latest layout properties.
"UPDATE_LAYOUT",
], module.exports);
--- a/devtools/client/inspector/boxmodel/box-model.js
+++ b/devtools/client/inspector/boxmodel/box-model.js
@@ -5,42 +5,50 @@
"use strict";
const { Task } = require("devtools/shared/task");
const { getCssProperties } = require("devtools/shared/fronts/css-properties");
const { ReflowFront } = require("devtools/shared/fronts/reflow");
const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
-const { updateLayout } = require("./actions/box-model");
+const {
+ updateGeometryEditorEnabled,
+ updateLayout,
+} = require("./actions/box-model");
const EditingSession = require("./utils/editing-session");
const NUMERIC = /^-?[\d\.]+$/;
/**
* A singleton instance of the box model controllers.
*
* @param {Inspector} inspector
* An instance of the Inspector currently loaded in the toolbox.
* @param {Window} window
* The document window of the toolbox.
*/
function BoxModel(inspector, window) {
this.document = window.document;
+ this.highlighters = inspector.highlighters;
this.inspector = inspector;
this.store = inspector.store;
this.updateBoxModel = this.updateBoxModel.bind(this);
this.onHideBoxModelHighlighter = this.onHideBoxModelHighlighter.bind(this);
+ this.onHideGeometryEditor = this.onHideGeometryEditor.bind(this);
+ this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this);
+ this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this);
this.onNewSelection = this.onNewSelection.bind(this);
this.onShowBoxModelEditor = this.onShowBoxModelEditor.bind(this);
this.onShowBoxModelHighlighter = this.onShowBoxModelHighlighter.bind(this);
this.onSidebarSelect = this.onSidebarSelect.bind(this);
+ this.onToggleGeometryEditor = this.onToggleGeometryEditor.bind(this);
this.inspector.selection.on("new-node-front", this.onNewSelection);
this.inspector.sidebar.on("select", this.onSidebarSelect);
}
BoxModel.prototype = {
/**
@@ -53,44 +61,46 @@ BoxModel.prototype = {
if (this.reflowFront) {
this.untrackReflows();
this.reflowFront.destroy();
this.reflowFront = null;
}
this.document = null;
+ this.highlighters = null;
this.inspector = null;
this.walker = null;
},
/**
* Returns an object containing the box model's handler functions used in the box
* model's React component props.
*/
getComponentProps() {
return {
onHideBoxModelHighlighter: this.onHideBoxModelHighlighter,
onShowBoxModelEditor: this.onShowBoxModelEditor,
onShowBoxModelHighlighter: this.onShowBoxModelHighlighter,
+ onToggleGeometryEditor: this.onToggleGeometryEditor,
};
},
/**
* Returns true if the computed or layout panel is visible, and false otherwise.
*/
isPanelVisible() {
return this.inspector.toolbox && this.inspector.sidebar &&
this.inspector.toolbox.currentToolId === "inspector" &&
(this.inspector.sidebar.getCurrentTabID() === "layoutview" ||
this.inspector.sidebar.getCurrentTabID() === "computedview");
},
/**
- * Returns true if the layout panel is visible and the current node is valid to
+ * Returns true if the layout panel is visible and the current element is valid to
* be displayed in the view.
*/
isPanelVisibleAndNodeValid() {
return this.isPanelVisible() &&
this.inspector.selection.isConnected() &&
this.inspector.selection.isElementNode();
},
@@ -145,16 +155,23 @@ BoxModel.prototype = {
let node = this.inspector.selection.nodeFront;
let layout = yield this.inspector.pageStyle.getLayout(node, {
autoMargins: true,
});
let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
this.elementRules = styleEntries.map(e => e.rule);
+ // Update the layout properties with whether or not the element's position is
+ // editable with the geometry editor.
+ let isPositionEditable = yield this.inspector.pageStyle.isPositionEditable(node);
+ layout = Object.assign({}, layout, {
+ isPositionEditable,
+ });
+
// Update the redux store with the latest layout properties and update the box
// model view.
this.store.dispatch(updateLayout(layout));
// If a subsequent request has been made, wait for that one instead.
if (this._lastRequest != lastRequest) {
return this._lastRequest;
}
@@ -166,40 +183,81 @@ BoxModel.prototype = {
return null;
}).bind(this)).catch(console.error);
this._lastRequest = lastRequest;
},
/**
+ * Hides the box-model highlighter on the currently selected element.
+ */
+ onHideBoxModelHighlighter() {
+ let toolbox = this.inspector.toolbox;
+ toolbox.highlighterUtils.unhighlight();
+ },
+
+ /**
+ * Hides the geometry editor and updates the box moodel store with the new
+ * geometry editor enabled state.
+ */
+ onHideGeometryEditor() {
+ let { markup, selection, toolbox } = this.inspector;
+
+ this.highlighters.hideGeometryEditor();
+ this.store.dispatch(updateGeometryEditorEnabled(false));
+
+ toolbox.off("picker-started", this.onHideGeometryEditor);
+ selection.off("new-node-front", this.onHideGeometryEditor);
+ markup.off("leave", this.onMarkupViewLeave);
+ markup.off("node-hover", this.onMarkupViewNodeHover);
+ },
+
+ /**
+ * Handler function that re-shows the geometry editor for an element that already
+ * had the geometry editor enabled. This handler function is called on a "leave" event
+ * on the markup view.
+ */
+ onMarkupViewLeave() {
+ let state = this.store.getState();
+ let enabled = state.boxModel.geometryEditorEnabled;
+
+ if (!enabled) {
+ return;
+ }
+
+ let nodeFront = this.inspector.selection.nodeFront;
+ this.highlighters.showGeometryEditor(nodeFront);
+ },
+
+ /**
+ * Handler function that temporarily hides the geomery editor when the
+ * markup view has a "node-hover" event.
+ */
+ onMarkupViewNodeHover() {
+ this.highlighters.hideGeometryEditor();
+ },
+
+ /**
* Selection 'new-node-front' event handler.
*/
- onNewSelection: function () {
+ onNewSelection() {
if (!this.isPanelVisibleAndNodeValid()) {
return;
}
if (this.inspector.selection.isConnected() &&
this.inspector.selection.isElementNode()) {
this.trackReflows();
}
this.updateBoxModel("new-selection");
},
/**
- * Hides the box-model highlighter on the currently selected element.
- */
- onHideBoxModelHighlighter() {
- let toolbox = this.inspector.toolbox;
- toolbox.highlighterUtils.unhighlight();
- },
-
- /**
* Shows the inplace editor when a box model editable value is clicked on the
* box model panel.
*
* @param {DOMNode} element
* The element that was clicked.
* @param {Event} event
* The event object.
* @param {String} property
@@ -291,11 +349,39 @@ BoxModel.prototype = {
if (this.inspector.selection.isConnected() &&
this.inspector.selection.isElementNode()) {
this.trackReflows();
}
this.updateBoxModel();
},
+ /**
+ * Toggles on/off the geometry editor for the current element when the geometry editor
+ * toggle button is clicked.
+ */
+ onToggleGeometryEditor() {
+ let { markup, selection, toolbox } = this.inspector;
+ let nodeFront = this.inspector.selection.nodeFront;
+ let state = this.store.getState();
+ let enabled = !state.boxModel.geometryEditorEnabled;
+
+ this.highlighters.toggleGeometryHighlighter(nodeFront);
+ this.store.dispatch(updateGeometryEditorEnabled(enabled));
+
+ if (enabled) {
+ // Hide completely the geometry editor if the picker is clicked or a new node front
+ toolbox.on("picker-started", this.onHideGeometryEditor);
+ selection.on("new-node-front", this.onHideGeometryEditor);
+ // Temporary hide the geometry editor
+ markup.on("leave", this.onMarkupViewLeave);
+ markup.on("node-hover", this.onMarkupViewNodeHover);
+ } else {
+ toolbox.off("picker-started", this.onHideGeometryEditor);
+ selection.off("new-node-front", this.onHideGeometryEditor);
+ markup.off("leave", this.onMarkupViewLeave);
+ markup.off("node-hover", this.onMarkupViewNodeHover);
+ }
+ },
+
};
module.exports = BoxModel;
--- a/devtools/client/inspector/boxmodel/components/BoxModel.js
+++ b/devtools/client/inspector/boxmodel/components/BoxModel.js
@@ -18,41 +18,44 @@ module.exports = createClass({
displayName: "BoxModel",
propTypes: {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
showBoxModelProperties: PropTypes.bool.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
onShowBoxModelHighlighter: PropTypes.func.isRequired,
+ onToggleGeometryEditor: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
render() {
let {
boxModel,
showBoxModelProperties,
onHideBoxModelHighlighter,
onShowBoxModelEditor,
onShowBoxModelHighlighter,
+ onToggleGeometryEditor,
} = this.props;
return dom.div(
{
className: "boxmodel-container",
},
BoxModelMain({
boxModel,
onHideBoxModelHighlighter,
onShowBoxModelEditor,
onShowBoxModelHighlighter,
}),
BoxModelInfo({
boxModel,
+ onToggleGeometryEditor,
}),
showBoxModelProperties ?
BoxModelProperties({
boxModel,
})
:
null
);
--- a/devtools/client/inspector/boxmodel/components/BoxModelApp.js
+++ b/devtools/client/inspector/boxmodel/components/BoxModelApp.js
@@ -24,16 +24,17 @@ const BoxModelApp = createClass({
displayName: "BoxModelApp",
propTypes: {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
showBoxModelProperties: PropTypes.bool.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
onShowBoxModelHighlighter: PropTypes.func.isRequired,
+ onToggleGeometryEditor: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
render() {
return Accordion({
items: [
{
--- a/devtools/client/inspector/boxmodel/components/BoxModelInfo.js
+++ b/devtools/client/inspector/boxmodel/components/BoxModelInfo.js
@@ -17,43 +17,62 @@ const SHARED_STRINGS_URI = "devtools/cli
const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
module.exports = createClass({
displayName: "BoxModelInfo",
propTypes: {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
+ onToggleGeometryEditor: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
+ onToggleGeometryEditor(e) {
+ this.props.onToggleGeometryEditor();
+ },
+
render() {
let { boxModel } = this.props;
- let { layout } = boxModel;
- let { width, height, position } = layout;
+ let { geometryEditorEnabled, layout } = boxModel;
+ let {
+ height,
+ isPositionEditable,
+ position,
+ width,
+ } = layout;
+
+ let buttonClass = "layout-geometry-editor devtools-button";
+ if (geometryEditorEnabled) {
+ buttonClass += " checked";
+ }
return dom.div(
{
className: "boxmodel-info",
},
dom.span(
{
className: "boxmodel-element-size",
},
SHARED_L10N.getFormatStr("dimensions", width, height)
),
dom.section(
{
className: "boxmodel-position-group",
},
- dom.button({
- className: "layout-geometry-editor devtools-button",
- title: BOXMODEL_L10N.getStr("boxmodel.geometryButton.tooltip"),
- }),
+ isPositionEditable ?
+ dom.button({
+ className: buttonClass,
+ title: BOXMODEL_L10N.getStr("boxmodel.geometryButton.tooltip"),
+ onClick: this.onToggleGeometryEditor,
+ })
+ :
+ null,
dom.span(
{
className: "boxmodel-element-position",
},
position
)
)
);
--- a/devtools/client/inspector/boxmodel/reducers/box-model.js
+++ b/devtools/client/inspector/boxmodel/reducers/box-model.js
@@ -1,24 +1,32 @@
/* 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 {
+ UPDATE_GEOMETRY_EDITOR_ENABLED,
UPDATE_LAYOUT,
} = require("../actions/index");
const INITIAL_BOX_MODEL = {
+ geometryEditorEnabled: false,
layout: {},
};
let reducers = {
+ [UPDATE_GEOMETRY_EDITOR_ENABLED](boxModel, { enabled }) {
+ return Object.assign({}, boxModel, {
+ geometryEditorEnabled: enabled,
+ });
+ },
+
[UPDATE_LAYOUT](boxModel, { layout }) {
return Object.assign({}, boxModel, {
layout,
});
},
};
--- a/devtools/client/inspector/boxmodel/types.js
+++ b/devtools/client/inspector/boxmodel/types.js
@@ -6,12 +6,15 @@
const { PropTypes } = require("devtools/client/shared/vendor/react");
/**
* The box model data for the current selected node.
*/
exports.boxModel = {
+ // Whether or not the geometry editor is enabled
+ geometryEditorEnabled: PropTypes.boolean,
+
// The layout information of the current selected node
layout: PropTypes.object,
};
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -616,26 +616,28 @@ CssComputedView.prototype = {
/**
* Render the box model view.
*/
createBoxModelView: function () {
let {
onHideBoxModelHighlighter,
onShowBoxModelEditor,
onShowBoxModelHighlighter,
+ onToggleGeometryEditor,
} = this.inspector.boxmodel.getComponentProps();
let provider = createElement(
Provider,
{ store: this.store },
BoxModelApp({
showBoxModelProperties: false,
onHideBoxModelHighlighter,
onShowBoxModelEditor,
onShowBoxModelHighlighter,
+ onToggleGeometryEditor,
})
);
ReactDOM.render(provider, this.boxModelWrapper);
},
/**
* The CSS as displayed by the UI.
*/
--- a/devtools/client/inspector/layout/layout.js
+++ b/devtools/client/inspector/layout/layout.js
@@ -31,16 +31,17 @@ LayoutView.prototype = {
if (!this.inspector) {
return;
}
let {
onHideBoxModelHighlighter,
onShowBoxModelEditor,
onShowBoxModelHighlighter,
+ onToggleGeometryEditor,
} = this.inspector.boxmodel.getComponentProps();
let {
getSwatchColorPickerTooltip,
setSelectedNode,
onSetGridOverlayColor,
onShowBoxModelHighlighterForNode,
onShowGridAreaHighlight,
@@ -67,16 +68,17 @@ LayoutView.prototype = {
onHideBoxModelHighlighter,
onSetGridOverlayColor,
onShowBoxModelEditor,
onShowBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
onShowGridAreaHighlight,
onShowGridCellHighlight,
+ onToggleGeometryEditor,
onToggleGridHighlighter,
onToggleShowGridLineNumbers,
onToggleShowInfiniteLines,
});
let provider = createElement(Provider, {
store: this.store,
id: "layoutview",
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -22,16 +22,18 @@ const DEFAULT_GRID_COLOR = "#4B0082";
function HighlightersOverlay(inspector) {
this.inspector = inspector;
this.highlighters = {};
this.highlighterUtils = this.inspector.toolbox.highlighterUtils;
// Only initialize the overlay if at least one of the highlighter types is supported.
this.supportsHighlighters = this.highlighterUtils.supportsCustomHighlighters();
+ // NodeFront of element that is highlighted by the geometry editor.
+ this.geometryEditorHighlighterShown = null;
// NodeFront of the grid container that is highlighted.
this.gridHighlighterShown = null;
// Name of the highlighter shown on mouse hover.
this.hoveredHighlighterShown = null;
// Name of the selector highlighter shown.
this.selectorHighlighterShown = null;
// Saved state to be restore on page navigation.
this.state = {
@@ -171,16 +173,67 @@ HighlightersOverlay.prototype = {
this.state.grid.options);
this.gridHighlighterShown = null;
// Erase grid highlighter state.
this.state.grid = {};
}),
/**
+ * Toggle the geometry editor highlighter for the given element.
+ *
+ * @param {NodeFront} node
+ * The NodeFront of the element to highlight.
+ */
+ toggleGeometryHighlighter: Task.async(function* (node) {
+ if (node == this.geometryEditorHighlighterShown) {
+ yield this.hideGeometryEditor();
+ return;
+ }
+
+ yield this.showGeometryEditor(node);
+ }),
+
+ /**
+ * Show the geometry editor highlightor for the given element.
+ *
+ * @param {NodeFront} node
+ * THe NodeFront of the element to highlight.
+ */
+ showGeometryEditor: Task.async(function* (node) {
+ let highlighter = yield this._getHighlighter("GeometryEditorHighlighter");
+ if (!highlighter) {
+ return;
+ }
+
+ let isShown = yield highlighter.show(node);
+ if (!isShown) {
+ return;
+ }
+
+ this.emit("geometry-editor-highlighter-shown");
+ this.geometryEditorHighlighterShown = node;
+ }),
+
+ /**
+ * Hide the geometry editor highlighter.
+ */
+ hideGeometryEditor: Task.async(function* () {
+ if (!this.geometryEditorHighlighterShown ||
+ !this.highlighters.GeometryEditorHighlighter) {
+ return;
+ }
+
+ yield this.highlighters.GeometryEditorHighlighter.hide();
+
+ this.emit("geometry-editor-highlighter-hidden");
+ this.geometryEditorHighlighterShown = null;
+ }),
+
+ /**
* Restore the saved highlighter states.
*
* @return {Promise} that resolves when the highlighter state was restored, and the
* expected highlighters are displayed.
*/
restoreState: Task.async(function* () {
let { selector, options, url } = this.state.grid;
@@ -385,16 +438,17 @@ HighlightersOverlay.prototype = {
this._handleRejection(e);
}
}),
/**
* Clear saved highlighter shown properties on will-navigate.
*/
onWillNavigate: function () {
+ this.geometryEditorHighlighterShown = null;
this.gridHighlighterShown = null;
this.hoveredHighlighterShown = null;
this.selectorHighlighterShown = null;
// The inspector panel should emit the new-root event when it is ready after navigate.
this.onInspectorNewRoot = this.inspector.once("new-root");
},
@@ -415,16 +469,19 @@ HighlightersOverlay.prototype = {
this.inspector.target.off("will-navigate", this.onWillNavigate);
this._lastHovered = null;
this.inspector = null;
this.highlighters = null;
this.highlighterUtils = null;
this.supportsHighlighters = null;
+
+ this.geometryEditorHighlighterShown = null;
this.gridHighlighterShown = null;
this.hoveredHighlighterShown = null;
this.selectorHighlighterShown = null;
+
this.destroyed = true;
}
};
module.exports = HighlightersOverlay;
--- a/devtools/client/themes/boxmodel.css
+++ b/devtools/client/themes/boxmodel.css
@@ -17,20 +17,16 @@
.boxmodel-header,
.boxmodel-info {
display: flex;
align-items: center;
padding: 4px 17px;
}
-.layout-geometry-editor {
- visibility: hidden;
-}
-
.layout-geometry-editor::before {
background: url(images/geometry-editor.svg) no-repeat center center / 16px 16px;
}
/* Main: contains the box-model regions */
.boxmodel-main {
position: relative;
--- a/devtools/server/actors/highlighters/geometry-editor.js
+++ b/devtools/server/actors/highlighters/geometry-editor.js
@@ -219,16 +219,20 @@ function GeometryEditorHighlighter(highl
// Register the mousedown event for each Geometry Editor's handler.
// Those events are automatically removed when the markup is destroyed.
let onMouseDown = this.handleEvent.bind(this);
for (let side of GeoProp.SIDES) {
this.getElement("handler-" + side)
.addEventListener("mousedown", onMouseDown);
}
+
+ this.onWillNavigate = this.onWillNavigate.bind(this);
+
+ this.highlighterEnv.on("will-navigate", this.onWillNavigate);
}
GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
typeName: "GeometryEditorHighlighter",
ID_CLASS_PREFIX: "geometry-editor-",
_buildMarkup: function () {
@@ -694,11 +698,17 @@ GeometryEditorHighlighter.prototype = ex
}
}
let labelCross = crossPos;
labelEl.setAttribute("transform", GeoProp.isHorizontal(side)
? "translate(" + labelMain + " " + labelCross + ")"
: "translate(" + labelCross + " " + labelMain + ")");
labelEl.removeAttribute("hidden");
labelTextEl.setTextContent(labelValue);
- }
+ },
+
+ onWillNavigate({ isTopLevel }) {
+ if (isTopLevel) {
+ this.hide();
+ }
+ },
});
exports.GeometryEditorHighlighter = GeometryEditorHighlighter;