tenfourfox/devtools/client/styleinspector/style-inspector-overlays.js
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

440 lines
12 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// The style-inspector overlays are:
// - tooltips that appear when hovering over property values
// - editor tooltips that appear when clicking color swatches, etc.
// - in-content highlighters that appear when hovering over property values
// - etc.
const {Cc, Ci, Cu} = require("chrome");
const {
Tooltip,
SwatchColorPickerTooltip,
SwatchCubicBezierTooltip,
CssDocsTooltip,
SwatchFilterTooltip
} = require("devtools/client/shared/widgets/Tooltip");
const EventEmitter = require("devtools/shared/event-emitter");
const promise = require("promise");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Services.jsm");
const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize";
// Types of existing tooltips
const TOOLTIP_IMAGE_TYPE = "image";
const TOOLTIP_FONTFAMILY_TYPE = "font-family";
// Types of nodes in the rule/computed-view
const VIEW_NODE_SELECTOR_TYPE = exports.VIEW_NODE_SELECTOR_TYPE = 1;
const VIEW_NODE_PROPERTY_TYPE = exports.VIEW_NODE_PROPERTY_TYPE = 2;
const VIEW_NODE_VALUE_TYPE = exports.VIEW_NODE_VALUE_TYPE = 3;
const VIEW_NODE_IMAGE_URL_TYPE = exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
const VIEW_NODE_LOCATION_TYPE = exports.VIEW_NODE_LOCATION_TYPE = 5;
/**
* Manages all highlighters in the style-inspector.
*
* @param {CssRuleView|CssComputedView} view
* Either the rule-view or computed-view panel
*/
function HighlightersOverlay(view) {
this.view = view;
let {CssRuleView} = require("devtools/client/styleinspector/rule-view");
this.isRuleView = view instanceof CssRuleView;
this.highlighterUtils = this.view.inspector.toolbox.highlighterUtils;
this._onMouseMove = this._onMouseMove.bind(this);
this._onMouseLeave = this._onMouseLeave.bind(this);
this.highlighters = {};
// Only initialize the overlay if at least one of the highlighter types is
// supported
this.supportsHighlighters =
this.highlighterUtils.supportsCustomHighlighters();
EventEmitter.decorate(this);
}
exports.HighlightersOverlay = HighlightersOverlay;
HighlightersOverlay.prototype = {
/**
* Add the highlighters overlay to the view. This will start tracking mouse
* movements and display highlighters when needed
*/
addToView: function() {
if (!this.supportsHighlighters || this._isStarted || this._isDestroyed) {
return;
}
let el = this.view.element;
el.addEventListener("mousemove", this._onMouseMove, false);
el.addEventListener("mouseleave", this._onMouseLeave, false);
this._isStarted = true;
},
/**
* Remove the overlay from the current view. This will stop tracking mouse
* movement and showing highlighters
*/
removeFromView: function() {
if (!this.supportsHighlighters || !this._isStarted || this._isDestroyed) {
return;
}
this._hideCurrent();
let el = this.view.element;
el.removeEventListener("mousemove", this._onMouseMove, false);
el.removeEventListener("mouseleave", this._onMouseLeave, false);
this._isStarted = false;
},
_onMouseMove: function(event) {
// Bail out if the target is the same as for the last mousemove
if (event.target === this._lastHovered) {
return;
}
// Only one highlighter can be displayed at a time, hide the currently shown
this._hideCurrent();
this._lastHovered = event.target;
let nodeInfo = this.view.getNodeInfo(event.target);
if (!nodeInfo) {
return;
}
// Choose the type of highlighter required for the hovered node
let type;
if (this._isRuleViewTransform(nodeInfo) ||
this._isComputedViewTransform(nodeInfo)) {
type = "CssTransformHighlighter";
}
if (type) {
this.highlighterShown = type;
let node = this.view.inspector.selection.nodeFront;
this._getHighlighter(type)
.then(highlighter => highlighter.show(node))
.then(shown => {
if (shown) {
this.emit("highlighter-shown");
}
});
}
},
_onMouseLeave: function() {
this._lastHovered = null;
this._hideCurrent();
},
/**
* Is the current hovered node a css transform property value in the rule-view
*
* @param {Object} nodeInfo
* @return {Boolean}
*/
_isRuleViewTransform: function(nodeInfo) {
let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
nodeInfo.value.property === "transform";
let isEnabled = nodeInfo.value.enabled &&
!nodeInfo.value.overridden &&
!nodeInfo.value.pseudoElement;
return this.isRuleView && isTransform && isEnabled;
},
/**
* Is the current hovered node a css transform property value in the
* computed-view
*
* @param {Object} nodeInfo
* @return {Boolean}
*/
_isComputedViewTransform: function(nodeInfo) {
let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
nodeInfo.value.property === "transform";
return !this.isRuleView && isTransform;
},
/**
* Hide the currently shown highlighter
*/
_hideCurrent: function() {
if (!this.highlighterShown || !this.highlighters[this.highlighterShown]) {
return;
}
// For some reason, the call to highlighter.hide doesn't always return a
// promise. This causes some tests to fail when trying to install a
// rejection handler on the result of the call. To avoid this, check
// whether the result is truthy before installing the handler.
let promise = this.highlighters[this.highlighterShown].hide();
if (promise) {
promise.then(null, e => console.error(e));
}
this.highlighterShown = null;
this.emit("highlighter-hidden");
},
/**
* Get a highlighter front given a type. It will only be initialized once
* @param {String} type The highlighter type. One of this.highlighters
* @return a promise that resolves to the highlighter
*/
_getHighlighter: function(type) {
let utils = this.highlighterUtils;
if (this.highlighters[type]) {
return promise.resolve(this.highlighters[type]);
}
return utils.getHighlighterByType(type).then(highlighter => {
this.highlighters[type] = highlighter;
return highlighter;
});
},
/**
* Destroy this overlay instance, removing it from the view and destroying
* all initialized highlighters
*/
destroy: function() {
this.removeFromView();
for (let type in this.highlighters) {
if (this.highlighters[type]) {
this.highlighters[type].finalize();
this.highlighters[type] = null;
}
}
this.view = null;
this.highlighterUtils = null;
this._isDestroyed = true;
}
};
/**
* Manages all tooltips in the style-inspector.
*
* @param {CssRuleView|CssComputedView} view
* Either the rule-view or computed-view panel
*/
function TooltipsOverlay(view) {
this.view = view;
let {CssRuleView} = require("devtools/client/styleinspector/rule-view");
this.isRuleView = view instanceof CssRuleView;
this._onNewSelection = this._onNewSelection.bind(this);
this.view.inspector.selection.on("new-node-front", this._onNewSelection);
}
exports.TooltipsOverlay = TooltipsOverlay;
TooltipsOverlay.prototype = {
get isEditing() {
return this.colorPicker.tooltip.isShown() ||
this.colorPicker.eyedropperOpen ||
this.cubicBezier.tooltip.isShown() ||
this.filterEditor.tooltip.isShown();
},
/**
* Add the tooltips overlay to the view. This will start tracking mouse
* movements and display tooltips when needed
*/
addToView: function() {
if (this._isStarted || this._isDestroyed) {
return;
}
let panelDoc = this.view.inspector.panelDoc;
// Image, fonts, ... preview tooltip
this.previewTooltip = new Tooltip(panelDoc);
this.previewTooltip.startTogglingOnHover(this.view.element,
this._onPreviewTooltipTargetHover.bind(this));
// MDN CSS help tooltip
this.cssDocs = new CssDocsTooltip(panelDoc);
if (this.isRuleView) {
// Color picker tooltip
this.colorPicker = new SwatchColorPickerTooltip(panelDoc);
// Cubic bezier tooltip
this.cubicBezier = new SwatchCubicBezierTooltip(panelDoc);
// Filter editor tooltip
this.filterEditor = new SwatchFilterTooltip(panelDoc);
}
this._isStarted = true;
},
/**
* Remove the tooltips overlay from the view. This will stop tracking mouse
* movements and displaying tooltips
*/
removeFromView: function() {
if (!this._isStarted || this._isDestroyed) {
return;
}
this.previewTooltip.stopTogglingOnHover(this.view.element);
this.previewTooltip.destroy();
if (this.colorPicker) {
this.colorPicker.destroy();
}
if (this.cubicBezier) {
this.cubicBezier.destroy();
}
if (this.cssDocs) {
this.cssDocs.destroy();
}
if (this.filterEditor) {
this.filterEditor.destroy();
}
this._isStarted = false;
},
/**
* Given a hovered node info, find out which type of tooltip should be shown,
* if any
*
* @param {Object} nodeInfo
* @return {String} The tooltip type to be shown, or null
*/
_getTooltipType: function({type, value: prop}) {
let tooltipType = null;
let inspector = this.view.inspector;
// Image preview tooltip
if (type === VIEW_NODE_IMAGE_URL_TYPE &&
inspector.hasUrlToImageDataResolver) {
tooltipType = TOOLTIP_IMAGE_TYPE;
}
// Font preview tooltip
if (type === VIEW_NODE_VALUE_TYPE && prop.property === "font-family") {
let value = prop.value.toLowerCase();
if (value !== "inherit" && value !== "unset" && value !== "initial") {
tooltipType = TOOLTIP_FONTFAMILY_TYPE;
}
}
return tooltipType;
},
/**
* Executed by the tooltip when the pointer hovers over an element of the
* view. Used to decide whether the tooltip should be shown or not and to
* actually put content in it.
* Checks if the hovered target is a css value we support tooltips for.
*
* @param {DOMNode} target The currently hovered node
*/
_onPreviewTooltipTargetHover: function(target) {
let nodeInfo = this.view.getNodeInfo(target);
if (!nodeInfo) {
// The hovered node isn't something we care about
return promise.reject(false);
}
let type = this._getTooltipType(nodeInfo);
if (!type) {
// There is no tooltip type defined for the hovered node
return promise.reject(false);
}
if (this.isRuleView && this.colorPicker.tooltip.isShown()) {
this.colorPicker.revert();
this.colorPicker.hide();
}
if (this.isRuleView && this.cubicBezier.tooltip.isShown()) {
this.cubicBezier.revert();
this.cubicBezier.hide();
}
if (this.isRuleView && this.cssDocs.tooltip.isShown()) {
this.cssDocs.hide();
}
if (this.isRuleView && this.filterEditor.tooltip.isShown()) {
this.filterEditor.revert();
this.filterEdtior.hide();
}
let inspector = this.view.inspector;
if (type === TOOLTIP_IMAGE_TYPE) {
let dim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE);
// nodeInfo contains an absolute uri
let uri = nodeInfo.value.url;
return this.previewTooltip.setRelativeImageContent(uri,
inspector.inspector, dim);
}
if (type === TOOLTIP_FONTFAMILY_TYPE) {
return this.previewTooltip.setFontFamilyContent(nodeInfo.value.value,
inspector.selection.nodeFront);
}
},
_onNewSelection: function() {
if (this.previewTooltip) {
this.previewTooltip.hide();
}
if (this.colorPicker) {
this.colorPicker.hide();
}
if (this.cubicBezier) {
this.cubicBezier.hide();
}
if (this.cssDocs) {
this.cssDocs.hide();
}
if (this.filterEditor) {
this.filterEditor.hide();
}
},
/**
* Destroy this overlay instance, removing it from the view
*/
destroy: function() {
this.removeFromView();
this.view.inspector.selection.off("new-node-front", this._onNewSelection);
this.view = null;
this._isDestroyed = true;
}
};