diff options
Diffstat (limited to 'devtools/client/inspector/shared/tooltips-overlay.js')
-rw-r--r-- | devtools/client/inspector/shared/tooltips-overlay.js | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/devtools/client/inspector/shared/tooltips-overlay.js b/devtools/client/inspector/shared/tooltips-overlay.js new file mode 100644 index 000000000..8a02d7e3d --- /dev/null +++ b/devtools/client/inspector/shared/tooltips-overlay.js @@ -0,0 +1,319 @@ +/* -*- 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 tooltip overlays are tooltips that appear when hovering over property values and + * editor tooltips that appear when clicking swatch based editors. + */ + +const { Task } = require("devtools/shared/task"); +const Services = require("Services"); +const { + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, +} = require("devtools/client/inspector/shared/node-types"); +const { getColor } = require("devtools/client/shared/theme"); +const { getCssProperties } = require("devtools/shared/fronts/css-properties"); +const CssDocsTooltip = require("devtools/client/shared/widgets/tooltip/CssDocsTooltip"); +const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip"); +const { + getImageDimensions, + setImageTooltip, + setBrokenImageTooltip, +} = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper"); +const SwatchColorPickerTooltip = require("devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip"); +const SwatchCubicBezierTooltip = require("devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip"); +const SwatchFilterTooltip = require("devtools/client/shared/widgets/tooltip/SwatchFilterTooltip"); + +const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize"; + +// Types of existing tooltips +const TOOLTIP_IMAGE_TYPE = "image"; +const TOOLTIP_FONTFAMILY_TYPE = "font-family"; + +/** + * 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/inspector/rules/rules"); + this.isRuleView = view instanceof CssRuleView; + this._cssProperties = getCssProperties(this.view.inspector.toolbox); + + this._onNewSelection = this._onNewSelection.bind(this); + this.view.inspector.selection.on("new-node-front", this._onNewSelection); +} + +TooltipsOverlay.prototype = { + get isEditing() { + return this.colorPicker.tooltip.isVisible() || + this.colorPicker.eyedropperOpen || + this.cubicBezier.tooltip.isVisible() || + this.filterEditor.tooltip.isVisible(); + }, + + /** + * 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 { toolbox } = this.view.inspector; + + // Initializing the different tooltips that are used in the inspector. + // These tooltips are attached to the toolbox document if they require a popup panel. + // Otherwise, it is attached to the inspector panel document if it is an inline + // editor. + this.previewTooltip = new HTMLTooltip(toolbox.doc, { + type: "arrow", + useXulWrapper: true + }); + this.previewTooltip.startTogglingOnHover(this.view.element, + this._onPreviewTooltipTargetHover.bind(this)); + + // MDN CSS help tooltip + this.cssDocs = new CssDocsTooltip(toolbox.doc); + + if (this.isRuleView) { + // Color picker tooltip + this.colorPicker = new SwatchColorPickerTooltip(toolbox.doc, this.view.inspector); + // Cubic bezier tooltip + this.cubicBezier = new SwatchCubicBezierTooltip(toolbox.doc); + // Filter editor tooltip + this.filterEditor = new SwatchFilterTooltip(toolbox.doc, + this._cssProperties.getValidityChecker(this.view.inspector.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 + * @return {Promise} + */ + _onPreviewTooltipTargetHover: Task.async(function* (target) { + let nodeInfo = this.view.getNodeInfo(target); + if (!nodeInfo) { + // The hovered node isn't something we care about + return false; + } + + let type = this._getTooltipType(nodeInfo); + if (!type) { + // There is no tooltip type defined for the hovered node + return false; + } + + if (this.isRuleView && this.colorPicker.tooltip.isVisible()) { + this.colorPicker.revert(); + this.colorPicker.hide(); + } + + if (this.isRuleView && this.cubicBezier.tooltip.isVisible()) { + this.cubicBezier.revert(); + this.cubicBezier.hide(); + } + + if (this.isRuleView && this.cssDocs.tooltip.isVisible()) { + this.cssDocs.hide(); + } + + if (this.isRuleView && this.filterEditor.tooltip.isVisible()) { + this.filterEditor.revert(); + this.filterEdtior.hide(); + } + + let inspector = this.view.inspector; + + if (type === TOOLTIP_IMAGE_TYPE) { + try { + yield this._setImagePreviewTooltip(nodeInfo.value.url); + } catch (e) { + yield setBrokenImageTooltip(this.previewTooltip, this.view.inspector.panelDoc); + } + return true; + } + + if (type === TOOLTIP_FONTFAMILY_TYPE) { + let font = nodeInfo.value.value; + let nodeFront = inspector.selection.nodeFront; + yield this._setFontPreviewTooltip(font, nodeFront); + return true; + } + + return false; + }), + + /** + * Set the content of the preview tooltip to display an image preview. The image URL can + * be relative, a call will be made to the debuggee to retrieve the image content as an + * imageData URI. + * + * @param {String} imageUrl + * The image url value (may be relative or absolute). + * @return {Promise} A promise that resolves when the preview tooltip content is ready + */ + _setImagePreviewTooltip: Task.async(function* (imageUrl) { + let doc = this.view.inspector.panelDoc; + let maxDim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE); + + let naturalWidth, naturalHeight; + if (imageUrl.startsWith("data:")) { + // If the imageUrl already is a data-url, save ourselves a round-trip + let size = yield getImageDimensions(doc, imageUrl); + naturalWidth = size.naturalWidth; + naturalHeight = size.naturalHeight; + } else { + let inspectorFront = this.view.inspector.inspector; + let {data, size} = yield inspectorFront.getImageDataFromURL(imageUrl, maxDim); + imageUrl = yield data.string(); + naturalWidth = size.naturalWidth; + naturalHeight = size.naturalHeight; + } + + yield setImageTooltip(this.previewTooltip, doc, imageUrl, + {maxDim, naturalWidth, naturalHeight}); + }), + + /** + * Set the content of the preview tooltip to display a font family preview. + * + * @param {String} font + * The font family value. + * @param {object} nodeFront + * The NodeActor that will used to retrieve the dataURL for the font + * family tooltip contents. + * @return {Promise} A promise that resolves when the preview tooltip content is ready + */ + _setFontPreviewTooltip: Task.async(function* (font, nodeFront) { + if (!font || !nodeFront || typeof nodeFront.getFontFamilyDataURL !== "function") { + throw new Error("Unable to create font preview tooltip content."); + } + + font = font.replace(/"/g, "'"); + font = font.replace("!important", ""); + font = font.trim(); + + let fillStyle = getColor("body-color"); + let {data, size: maxDim} = yield nodeFront.getFontFamilyDataURL(font, fillStyle); + + let imageUrl = yield data.string(); + let doc = this.view.inspector.panelDoc; + let {naturalWidth, naturalHeight} = yield getImageDimensions(doc, imageUrl); + + yield setImageTooltip(this.previewTooltip, doc, imageUrl, + {hideDimensionLabel: true, maxDim, naturalWidth, naturalHeight}); + }), + + _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; + } +}; + +module.exports = TooltipsOverlay; |