Bug 765105 - Tooltip shared component, showing image previews in markup, css rules and computed views, r=miker,harth

This commit is contained in:
Patrick Brosset 2013-10-26 00:51:01 +05:30
parent 7ff1ca9b78
commit 1b7307bf0b
14 changed files with 932 additions and 14 deletions

View File

@ -21,6 +21,7 @@ const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
const {HTMLEditor} = require("devtools/markupview/html-editor");
const {OutputParser} = require("devtools/output-parser");
const promise = require("sdk/core/promise");
const {Tooltip} = require("devtools/shared/widgets/Tooltip");
Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
Cu.import("resource://gre/modules/devtools/Templater.jsm");
@ -374,7 +375,7 @@ MarkupView.prototype = {
this._elt.appendChild(container.elt);
this._rootNode = aNode;
} else {
var container = new MarkupContainer(this, aNode);
var container = new MarkupContainer(this, aNode, this._inspector);
if (aFlashNode) {
container.flashMutation();
}
@ -1046,12 +1047,15 @@ MarkupView.prototype = {
* The markup view that owns this container.
* @param DOMNode aNode
* The node to display.
* @param Inspector aInspector
* The inspector tool container the markup-view
*/
function MarkupContainer(aMarkupView, aNode) {
function MarkupContainer(aMarkupView, aNode, aInspector) {
this.markup = aMarkupView;
this.doc = this.markup.doc;
this.undo = this.markup.undo;
this.node = aNode;
this._inspector = aInspector;
if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
this.editor = new TextEditor(this, aNode, "text");
@ -1094,6 +1098,9 @@ function MarkupContainer(aMarkupView, aNode) {
this._onMouseDown = this._onMouseDown.bind(this);
this.elt.addEventListener("mousedown", this._onMouseDown, false);
this.tooltip = null;
this._attachTooltipIfNeeded();
}
MarkupContainer.prototype = {
@ -1101,6 +1108,39 @@ MarkupContainer.prototype = {
return "[MarkupContainer for " + this.node + "]";
},
_attachTooltipIfNeeded: function() {
if (this.node.tagName) {
let tagName = this.node.tagName.toLowerCase();
let isImage = tagName === "img" &&
this.editor.getAttributeElement("src");
let isCanvas = tagName && tagName === "canvas";
// Get the image data for later so that when the user actually hovers over
// the element, the tooltip does contain the image
if (isImage || isCanvas) {
this.tooltip = new Tooltip(this._inspector.panelDoc);
this.node.getImageData().then(data => {
if (data) {
data.string().then(str => {
this.tooltip.setImageContent(str);
});
}
});
}
// If it's an image, show the tooltip on the src attribute
if (isImage) {
this.tooltip.startTogglingOnHover(this.editor.getAttributeElement("src"));
}
// If it's a canvas, show it on the tag
if (isCanvas) {
this.tooltip.startTogglingOnHover(this.editor.tag);
}
}
},
/**
* True if the current node has children. The MarkupView
* will set this attribute for the MarkupContainer.
@ -1335,6 +1375,12 @@ MarkupContainer.prototype = {
// Destroy my editor
this.editor.destroy();
// Destroy the tooltip if any
if (this.tooltip) {
this.tooltip.destroy();
this.tooltip = null;
}
}
};
@ -1348,7 +1394,7 @@ function RootContainer(aMarkupView, aNode) {
this.elt.container = this;
this.children = this.elt;
this.node = aNode;
this.toString = function() { return "[root container]"}
this.toString = () => "[root container]";
}
RootContainer.prototype = {
@ -1578,6 +1624,16 @@ ElementEditor.prototype = {
return this.node.startModifyingAttributes();
},
/**
* Get the element used for one of the attributes of this element
* @param string attrName The name of the attribute to get the element for
* @return DOMElement
*/
getAttributeElement: function(attrName) {
return this.attrList.querySelector(
".attreditor[data-attr=" + attrName + "] .attr-value");
},
_createAttribute: function(aAttr, aBefore = null) {
// Create the template editor, which will save some variables here.
let data = {

View File

@ -15,3 +15,5 @@ skip-if = true
[browser_inspector_markup_navigation.js]
[browser_inspector_markup_subset.html]
[browser_inspector_markup_subset.js]
[browser_inspector_markup_765105_tooltip.js]
[browser_inspector_markup_765105_tooltip.png]

View File

@ -0,0 +1,137 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let {PanelFactory} = devtools.require("devtools/shared/widgets/Tooltip");
let contentDoc;
let inspector;
let markup;
const PAGE_CONTENT = [
'<img class="local" src="chrome://branding/content/about-logo.png" />',
'<img class="data" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAADI5JREFUeNrsWwuQFNUVPf1m5z87szv7HWSWj8CigBFMEFZKiQsB1PgJwUAZg1HBpIQsKmokEhNjWUnFVPnDWBT+KolJYbRMoqUVq0yCClpqiX8sCchPWFwVlt2db7+X93pez7zu6Vn2NxsVWh8987p7pu+9555z7+tZjTGGY3kjOMa34w447oBjfKsY7i/UNM3Y8eFSAkD50Plgw03K5P9gvGv7U5ieeR3PszeREiPNX3/0DL4hjslzhm8THh+OITfXk3dhiv4GDtGPVzCaeJmPLYzuu5qJuWfuw2QTlcN1X9pwQU7LhdZ/ZAseD45cOh9hHvDkc/yAF/DNhdb5Mrr3PvBMaAYW8fMSIi2G497IMEK/YutGtAYr6+ej+nxu/NN8Ks3N7AR6HgcLz0Eg1Ljg1UcxZzi5qewIkMYLRweTr2Kzp+nmyXAd5pS3XQDd+N/4h4zgu9FI7brlXf90nMEnuwQxlvv+hosE3TuexmWeysmT4W+WxkMaLzf9Y8ATgjcUn7T9H1gqrpFq8eV1gMn6t16NhngjfoX6q4DUP032Rd4LJgpSLwJ1yzFqBG69eRkah0MVyo0Acfe+yy9AG4nMiYCkeM53KKFXncBLAXqEm+wCqZwaueq7WCmuLTcKSJmj737ol2hurA9eq9VdyiO8yWa3NNyog+SB5CZodSsQq/dfu34tJpYbBaTMzvVddDZu16q5smXf4G8zEvqm4cyaAmJPuTJk3oJWdS4WzcVtfMZbThSQckb/pYfRGgo3zNOqZnEHbJPGK4abaDCQIIsT8V/qTaBqHkLh6LzXH8XZQhbLhYKyyCC/WeHYcNdmvOgfe8skzbWL270/T3wf7tSx/lGCbTu8xlzzmCSWLc5iwmgikcCHi3Mga0Ry913vBFvQwg90l6M4ImWKfsWOp7DSWxmfpPlCFuPFfsNfKrCnPYpQKIRgqBK7D0SxYaNHwkEiJMtl0ReDp3Lc5D3PGoTo/sKngCl7a5chFqvBatKwjBd7WwqIlzB/78NcoUcp5VSgGxm+7b8eqQRGnHMO634epO4S1EZww09/iFg5UmGoESDuznP1xVhTUX1WWHPzjpd25wyH0hRxI3LGM75nxmuNEEUVpAN0XgxmPoKralakbQnWlIMQyVBD/w+3orkq4lvualjKyWwzt4MaxqspQHVhPOWG64bxYuhZXSFGWhipbSDVragOu5Y9eAsmDDUKyBA703vemVhHoueD6e9wAzJK1WfmN0Umk5GGM4kEMZcuIECqgjm0nldAqmbjwtm4VxZH5AvlADP6mx9Eqy9Q0+KqW8Ch+47FaMMYmnNGfY1iPMshoC6qFxme4wQ+0p+ARE6H3+9veWEDWgUhDhUKyFARn4jM5BNxT0XsMg7bfymGK1ov3wtjDfhL4w0HVGUVBEjDaaE+QNdrcNWch1PG4W6xrjBUXECGivg++Cva3JUT4iQUz3V2RsSVaKLwOuDT89A3HdBQoxhNC+fnVm74ual2EG893P6G+PuP4SfiO4cCBWQooL9qCWKNXPbcI37Aa/lnlZxXRt4RFONGwSDCPAHqOuqjWct1QiEMw5mChM5X4K47FyNqcd3aK9AwFH0CGYLoe1ctxk2eWi57rg5JfGp9rzC6ggCdFlAgHBDw5Yxlcg6G8SyHCjMlsgmDD9zhSeHlF+JnAgWDTQUy2NxfdwOao1UVV3pi3+bE97YSbWpLAbn6zefHNQkp1PMpIBwwvslKgIYTKM2nEpNzrGcH3FXTEal0L38kJ4uDQgEZbO4vnI173LXf5NHZaiUxtaCxyZuo/rK6LpUg54yg3zTWRAArvDcRIPZ6BqzrQ1REpmL+DNw32OKIDCb3X1qPVn8wNNMT4w2bvs+q4bAZrqBh2skaL3yyhhIIZ4i6oHkUK0RckcB8GigEyRIH4A6Mgc8fatl0/+BkkQxC9gIT4ljna1rIZW9rEdNbjJcNjsnoYj7LHWCUwpITzEgzRQKZ3XAFHbTzA3hrz8TEUUZxFBhoKpABQt/97p+w0hMZG68I8R6FtlsJT3FELndZntjM+VMnylKYq8GJI3UZaRMpquGSGFVOEfv0YZBMNzz+uvjbfzS6xQERIhlI9FcvQWNdFVb7x1zCb+QNK8vb9NsiifmI5hBgVoOCBC1sb0ab5RomqENxLO3eA1/0NDRU47q2RQNbRCUDIb7lF2CNL3ZGxEV4n08TVvZWYG4pZyV0zUdS45tyCBByOHWiyvZmxFXDCyRo1ge5+Sy0TA+8lWMiP/6O0S32exGV9Jf4fr8azdUR3zL/CZz4MtvzdX5uOYs6NDOmpkuj5Huh+7qUQSYl0ThHzw0YQzcGo6bhzEqoYq5rN3yRiYiG3Vfe2Ybm/qKA9NNZ3nNm4F7/yDkg9AN+U1mHiBcXP8zuDN76jj8hg1QyiWQigalj02BJPhK8I0zxijAjhp5zhlpLUDvS+BCy2HMAvvB4XDgL9/SXC0g/ou/5+6/xLX8w0uJrOIkXfPvyhY0F6gr7M8H0KWFYikcqAXakB+xwD9CdREBLoau7Gz3cAdSIdLFxFtJTCqRChSjnutvhDcREtzjz2Tswtz+yeNRFUeXZXtWux7C1fuoVcbd3J//ipDX3uZZDLGrwweS+UBLL5TDliVBnF8P7H+XI8aRRGsIBJg/Zlslt1+W+D1JWoSyi+kD9jfhs78t7mhZhSl+fLfY1Bdyv3I8V/qpY3B1McgN7ZFT5/vNO0I5DPLLdPBIJA8qc4h2I0QplYfDpJwHT+aj0246r5S8rToG8OjCle8wk4OLvvYGa+Ovr84uo2qBSwJS9G5egoZFLTfiEqWDtbwGfHgKOdPHcS+ai7XDzMPW/FJRLGGcxnBbK4YJC2K+h+T6Bdu5CqHqCWERd3bawb7JI+iJ735+LNaHaprBLLHBm08U3XxShEsdt+f3eTh3v7aC95Dct4RCWL5OZWh/oXBZThxAIxyOXLzBk8aiEWJID8rK3CpPOmeHaGpvCS+7EHv5FujVHUSJPLXvIFeHcNc+9xrB2gws9KZdxuLFax/WLM5gzzSm/lTXF/OdAcapyvjxPqxqHjr2v4ckX2bS2dRBrc5lSdpKjEJ9/9tdwX2WMd53ZQ2IVo3RES+UwVSpCPvYepNx4gmTGDUKIMQ4eduPnD7mx9xOn/KZKOlFbStjONxHTtR+BYAPmnoZ1Zp8wkBRwP/EL3u0F/C2hGl7vpz7vW37T3vP7if8wroKuoh8ribknX9BK5rcF+mo1qKaKyRPJTgTDjbzY8szcuLb3bpH00u35T47j7prRpwDJTxzyG0dHgxPp5bPG8VdkpfPbUg3SgoOo2mwVukb98D5EqpswZTTulCggTk4gpYhv0++wIhCJxr0+Hq1sondis0SE2oxQe3qWXwWyO4DSQg9gJ8Iiw1VFcGqXxet0N9xE4ygIxv/9W6wo9WyROEX/R+eiobYSq2vHTOR631Eiv2lRfh9dvxkumkXh92Qsx8XrAJ+7YGbWuhxOi/U+31NQmzyqNYG8N/3wfo6CRtRHcN01FzkvojohwLu0VVvDa56IS/xcj2b7nN+O+m0jqpE1wMPXZxAN9iCVThtDvH7gmiRGRpU8Lspv1Uhq4wIVdQoyuGSLNYPKUCS8+CzNURbzMmjK3i8u0U793lmuV0ef9nWQ5MGC/DiUqEUSaCtXna9RJEspZS1lrXINK/pcq+SpT50t98QKMq1FRmDfx3vxty102k0PM4ssEnvuz5+G26Ij4yDpz6z9fV8bkyIkqBFkhej0Ib+ZQ34XJK9AfozaiimqIoX3Jp3tiISrcfYpuN2+iFph/02P36PNC9fVcCnp6H9jYouKyfaWufz5Tp9tVxcUniw7IohZv4dZz81/ns67z3AYPrc2n0+Ix2q8k0PWjgBy88XaibnfK9A+5LdDY2Ivhy36fbT8Zv3Lb1U1qLqUxorXEEXIs0mjjrtxoTZWtdvigNs2sgPiujTv6DIZLld6b/V5742JZV3fUsUVFy5gdsNtKWFzUCEVbNepD1MkSMVbsb6SZm7jI3/zODtQKgUMsOw8wDZ63t5xcV1TnaEAxoc6wrqY+Fj+N4DsqOnhOIdicrQSm1MPYCPlIqHn5bbHg8/bj2D3QfZnCX3mpAICDZV8jH5kpbZqTD0W+DxaA74CWzLN2nd14OlL72J38Lf7+TjC7dadZFDoZJQPrtaIKL/G0L6ktptPZVJ8fMqHYPZOKYPMyQGadIJfDvdXwAFiZOTvDBPydf5vk4rWA+RfdhBlaF/yDDBRoMu9pfnSjv/p7DG+HXfAcQcc49v/BBgAcFAO4DmB2GQAAAAASUVORK5CYII=" />',
'<img class="remote" src="http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_765105_tooltip.png" />',
'<canvas class="canvas" width="600" height="600"></canvas>'
].join("\n");
const TEST_NODES = [
"img.local",
"img.data",
"img.remote",
".canvas"
];
function test() {
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
contentDoc = content.document;
waitForFocus(createDocument, content);
}, true);
content.location = "data:text/html,markup view tooltip test";
}
function createDocument() {
contentDoc.body.innerHTML = PAGE_CONTENT;
var target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
inspector = toolbox.getCurrentPanel();
markup = inspector.markup;
startTests();
});
}
function startTests() {
// Draw something in the canvas :)
let doc = content.document;
let context = doc.querySelector(".canvas").getContext("2d");
context.beginPath();
context.moveTo(300, 0);
context.lineTo(600, 600);
context.lineTo(0, 600);
context.closePath();
context.fillStyle = "#ffc821";
context.fill();
// Actually start testing
inspector.selection.setNode(contentDoc.querySelector("img"));
inspector.once("inspector-updated", () => {
testImageTooltip(0);
});
}
function endTests() {
contentDoc = inspector = markup = null;
gBrowser.removeCurrentTab();
finish();
}
function testImageTooltip(index) {
if (index === TEST_NODES.length) {
return endTests();
}
let node = contentDoc.querySelector(TEST_NODES[index]);
ok(node, "We have the [" + TEST_NODES[index] + "] image node to test for tooltip");
let isImg = node.tagName.toLowerCase() === "img";
let container = getContainerForRawNode(markup, node);
let target = container.editor.tag;
if (isImg) {
target = container.editor.getAttributeElement("src");
}
assertTooltipShownOn(container.tooltip, target, () => {
let images = container.tooltip.panel.getElementsByTagName("image");
is(images.length, 1, "Tooltip for [" + TEST_NODES[index] + "] contains an image");
if (isImg) {
compareImageData(node, images[0].src);
}
container.tooltip.hide();
testImageTooltip(index + 1);
});
}
function compareImageData(img, imgData) {
let canvas = content.document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
let ctx = canvas.getContext("2d");
let data = "";
try {
ctx.drawImage(img, 0, 0);
data = canvas.toDataURL("image/png");
} catch (e) {}
is(data, imgData, "Tooltip image has the right content");
}
function assertTooltipShownOn(tooltip, element, cb) {
// If there is indeed a show-on-hover on element, the xul panel will be shown
tooltip.panel.addEventListener("popupshown", function shown() {
tooltip.panel.removeEventListener("popupshown", shown, true);
// Poll until the image gets loaded in the tooltip. This is required because
// markup containers only load images in their associated tooltips when
// the image data comes back from the server. However, this test is executed
// synchronously as soon as "inspector-updated" is fired, which is before
// the data for images is known.
let hasImage = () => tooltip.panel.getElementsByTagName("image").length;
let poll = setInterval(() => {
if (hasImage()) {
clearInterval(poll);
cb();
}
}, 200);
}, true);
tooltip._showOnHover(element);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -9,3 +9,4 @@ libs::
$(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
$(NSINSTALL) $(srcdir)/widgets/*.jsm $(FINAL_TARGET)/modules/devtools
$(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/shared
$(NSINSTALL) $(srcdir)/widgets/*.js $(FINAL_TARGET)/modules/devtools/shared/widgets

View File

@ -0,0 +1,420 @@
/* 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 {Cc, Cu, Ci} = require("chrome");
const promise = require("sdk/core/promise");
const IOService = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService);
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
const GRADIENT_RE = /\b(repeating-)?(linear|radial)-gradient\(((rgb|hsl)a?\(.+?\)|[^\)])+\)/gi;
const BORDERCOLOR_RE = /^border-[-a-z]*color$/ig;
const BORDER_RE = /^border(-(top|bottom|left|right))?$/ig;
const BACKGROUND_IMAGE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/;
/**
* Tooltip widget.
*
* This widget is intended at any tool that may need to show rich content in the
* form of floating panels.
* A common use case is image previewing in the CSS rule view, but more complex
* use cases may include color pickers, object inspection, etc...
*
* Tooltips are based on XUL (namely XUL arrow-type <panel>s), and therefore
* need a XUL Document to live in.
* This is pretty much the only requirement they have on their environment.
*
* The way to use a tooltip is simply by instantiating a tooltip yourself and
* attaching some content in it, or using one of the ready-made content types.
*
* A convenient `startTogglingOnHover` method may avoid having to register event
* handlers yourself if the tooltip has to be shown when hovering over a
* specific element or group of elements (which is usually the most common case)
*/
/**
* The low level structure of a tooltip is a XUL element (a <panel>, although
* <tooltip> is supported too, it won't have the nice arrow shape).
*/
let PanelFactory = {
get: function(doc, xulTag="panel") {
// Create the tooltip
let panel = doc.createElement(xulTag);
panel.setAttribute("hidden", true);
if (xulTag === "panel") {
// Prevent the click used to close the panel from being consumed
panel.setAttribute("consumeoutsideclicks", false);
panel.setAttribute("type", "arrow");
panel.setAttribute("level", "top");
}
panel.setAttribute("class", "devtools-tooltip devtools-tooltip-" + xulTag);
doc.querySelector("window").appendChild(panel);
return panel;
}
};
/**
* Tooltip class.
*
* Basic usage:
* let t = new Tooltip(xulDoc);
* t.content = someXulContent;
* t.show();
* t.hide();
* t.destroy();
*
* Better usage:
* let t = new Tooltip(xulDoc);
* t.startTogglingOnHover(container, target => {
* if (<condition based on target>) {
* t.setImageContent("http://image.png");
* return true;
* }
* });
* t.destroy();
*
* @param XULDocument doc
* The XUL document hosting this tooltip
*/
function Tooltip(doc) {
this.doc = doc;
this.panel = PanelFactory.get(doc);
// Used for namedTimeouts in the mouseover handling
this.uid = "tooltip-" + Date.now();
}
module.exports.Tooltip = Tooltip;
Tooltip.prototype = {
/**
* Show the tooltip. It might be wise to append some content first if you
* don't want the tooltip to be empty. You may access the content of the
* tooltip by setting a XUL node to t.tooltip.content.
* @param {node} anchor
* Which node should the tooltip be shown on
* @param {string} position
* https://developer.mozilla.org/en-US/docs/XUL/PopupGuide/Positioning
* Defaults to before_start
*/
show: function(anchor, position="before_start") {
this.panel.hidden = false;
this.panel.openPopup(anchor, position);
},
/**
* Hide the tooltip
*/
hide: function() {
this.panel.hidden = true;
this.panel.hidePopup();
},
/**
* Empty the tooltip's content
*/
empty: function() {
while (this.panel.hasChildNodes()) {
this.panel.removeChild(this.panel.firstChild);
}
},
/**
* Get rid of references and event listeners
*/
destroy: function () {
this.hide();
this.content = null;
this.doc = null;
this.panel.parentNode.removeChild(this.panel);
this.panel = null;
if (this._basedNode) {
this.stopTogglingOnHover();
}
},
/**
* Show/hide the tooltip when the mouse hovers over particular nodes.
*
* 2 Ways to make this work:
* - Provide a single node to attach the tooltip to, as the baseNode, and
* omit the second targetNodeCb argument
* - Provide a baseNode that is the container of possibly numerous children
* elements that may receive a tooltip. In this case, provide the second
* targetNodeCb argument to decide wether or not a child should receive
* a tooltip.
*
* This works by tracking mouse movements on a base container node (baseNode)
* and showing the tooltip when the mouse stops moving. The targetNodeCb
* callback is used to know whether or not the particular element being
* hovered over should indeed receive the tooltip. If you don't provide it
* it's equivalent to a function that always returns true.
*
* Note that if you call this function a second time, it will itself call
* stopTogglingOnHover before adding mouse tracking listeners again.
*
* @param {node} baseNode
* The container for all target nodes
* @param {Function} targetNodeCb
* A function that accepts a node argument and returns true or false
* to signify if the tooltip should be shown on that node or not.
* Additionally, the function receives a second argument which is the
* tooltip instance itself, to be used to add/modify the content of the
* tooltip if needed. If omitted, the tooltip will be shown everytime.
* @param {Number} showDelay
* An optional delay that will be observed before showing the tooltip.
* Defaults to 750ms
*/
startTogglingOnHover: function(baseNode, targetNodeCb, showDelay = 750) {
if (this._basedNode) {
this.stopTogglingOnHover();
}
this._basedNode = baseNode;
this._showDelay = showDelay;
this._targetNodeCb = targetNodeCb || (() => true);
this._onBaseNodeMouseMove = this._onBaseNodeMouseMove.bind(this);
this._onBaseNodeMouseLeave = this._onBaseNodeMouseLeave.bind(this);
baseNode.addEventListener("mousemove", this._onBaseNodeMouseMove, false);
baseNode.addEventListener("mouseleave", this._onBaseNodeMouseLeave, false);
},
/**
* If the startTogglingOnHover function has been used previously, and you want
* to get rid of this behavior, then call this function to remove the mouse
* movement tracking
*/
stopTogglingOnHover: function() {
clearNamedTimeout(this.uid);
this._basedNode.removeEventListener("mousemove",
this._onBaseNodeMouseMove, false);
this._basedNode.removeEventListener("mouseleave",
this._onBaseNodeMouseLeave, false);
this._basedNode = null;
this._targetNodeCb = null;
this._lastHovered = null;
},
_onBaseNodeMouseMove: function(event) {
if (event.target !== this._lastHovered) {
this.hide();
this._lastHovered = null;
setNamedTimeout(this.uid, this._showDelay, () => {
this._showOnHover(event.target);
});
}
},
_showOnHover: function(target) {
if (this._targetNodeCb && this._targetNodeCb(target, this)) {
this.show(target);
this._lastHovered = target;
}
},
_onBaseNodeMouseLeave: function() {
clearNamedTimeout(this.uid);
this._lastHovered = null;
},
/**
* Set the content of this tooltip. Will first empty the tooltip and then
* append the new content element.
* Consider using one of the set<type>Content() functions instead.
* @param {node} content
* A node that can be appended in the tooltip XUL element
*/
set content(content) {
this.empty();
if (content) {
this.panel.appendChild(content);
}
},
get content() {
return this.panel.firstChild;
},
/**
* Fill the tooltip with an image, displayed over a tiled background useful
* for transparent images.
* Also adds the image dimension as a label at the bottom.
*/
setImageContent: function(imageUrl, maxDim=400) {
// Main container
let vbox = this.doc.createElement("vbox");
vbox.setAttribute("align", "center")
// Transparency tiles (image will go in there)
let tiles = createTransparencyTiles(this.doc, vbox);
// Temporary label during image load
let label = this.doc.createElement("label");
label.classList.add("devtools-tooltip-caption");
label.textContent = l10n.strings.GetStringFromName("previewTooltip.image.brokenImage");
vbox.appendChild(label);
// Display the image
let image = this.doc.createElement("image");
image.setAttribute("src", imageUrl);
if (maxDim) {
image.style.maxWidth = maxDim + "px";
image.style.maxHeight = maxDim + "px";
}
tiles.appendChild(image);
this.content = vbox;
// Load the image to get dimensions and display it when done
let imgObj = new this.doc.defaultView.Image();
imgObj.src = imageUrl;
imgObj.onload = () => {
imgObj.onload = null;
// Display dimensions
label.textContent = imgObj.naturalWidth + " x " + imgObj.naturalHeight;
if (imgObj.naturalWidth > maxDim ||
imgObj.naturalHeight > maxDim) {
label.textContent += " *";
}
}
},
/**
* Exactly the same as the `image` function but takes a css background image
* value instead : url(....)
*/
setCssBackgroundImageContent: function(cssBackground, sheetHref, maxDim=400) {
let uri = getBackgroundImageUri(cssBackground, sheetHref);
if (uri) {
this.setImageContent(uri, maxDim);
}
},
setCssGradientContent: function(cssGradient) {
let tiles = createTransparencyTiles(this.doc);
let gradientBox = this.doc.createElement("box");
gradientBox.width = "100";
gradientBox.height = "100";
gradientBox.style.background = this.cssGradient;
gradientBox.style.borderRadius = "2px";
gradientBox.style.boxShadow = "inset 0 0 4px #333";
tiles.appendChild(gradientBox)
this.content = tiles;
},
_setSimpleCssPropertiesContent: function(properties, width, height) {
let tiles = createTransparencyTiles(this.doc);
let box = this.doc.createElement("box");
box.width = width + "";
box.height = height + "";
properties.forEach(({name, value}) => {
box.style[name] = value;
});
tiles.appendChild(box);
this.content = tiles;
},
setCssColorContent: function(cssColor) {
this._setSimpleCssPropertiesContent([
{name: "background", value: cssColor},
{name: "borderRadius", value: "2px"},
{name: "boxShadow", value: "inset 0 0 4px #333"},
], 50, 50);
},
setCssBoxShadowContent: function(cssBoxShadow) {
this._setSimpleCssPropertiesContent([
{name: "background", value: "white"},
{name: "boxShadow", value: cssBoxShadow}
], 80, 80);
},
setCssBorderContent: function(cssBorder) {
this._setSimpleCssPropertiesContent([
{name: "background", value: "white"},
{name: "border", value: cssBorder}
], 80, 80);
}
};
/**
* Internal utility function that creates a tiled background useful for
* displaying semi-transparent images
*/
function createTransparencyTiles(doc, parentEl) {
let tiles = doc.createElement("box");
tiles.classList.add("devtools-tooltip-tiles");
if (parentEl) {
parentEl.appendChild(tiles);
}
return tiles;
}
/**
* Internal util, checks whether a css declaration is a gradient
*/
function isGradientRule(property, value) {
return (property === "background" || property === "background-image") &&
value.match(GRADIENT_RE);
}
/**
* Internal util, checks whether a css declaration is a color
*/
function isColorOnly(property, value) {
return property === "background-color" ||
property === "color" ||
property.match(BORDERCOLOR_RE);
}
/**
* Internal util, returns the background image uri if any
*/
function getBackgroundImageUri(value, sheetHref) {
let uriMatch = BACKGROUND_IMAGE_RE.exec(value);
let uri = null;
if (uriMatch && uriMatch[1]) {
uri = uriMatch[1];
if (sheetHref) {
let sheetUri = IOService.newURI(sheetHref, null, null);
uri = sheetUri.resolve(uri);
}
}
return uri;
}
/**
* L10N utility class
*/
function L10N() {}
L10N.prototype = {};
let l10n = new L10N();
loader.lazyGetter(L10N.prototype, "strings", () => {
return Services.strings.createBundle(
"chrome://browser/locale/devtools/inspector.properties");
});

View File

@ -11,8 +11,8 @@ let {CssLogic} = require("devtools/styleinspector/css-logic");
let {ELEMENT_STYLE} = require("devtools/server/actors/styles");
let promise = require("sdk/core/promise");
let {EventEmitter} = require("devtools/shared/event-emitter");
const {OutputParser} = require("devtools/output-parser");
const {Tooltip} = require("devtools/shared/widgets/Tooltip");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm");
@ -169,6 +169,11 @@ function CssHtmlTree(aStyleInspector, aPageStyle)
// The element that we're inspecting, and the document that it comes from.
this.viewedElement = null;
// Properties preview tooltip
this.tooltip = new Tooltip(this.styleInspector.inspector.panelDoc);
this.tooltip.startTogglingOnHover(this.propertyContainer,
this._buildTooltipContent.bind(this));
this._buildContextMenu();
this.createStyleViews();
}
@ -490,6 +495,29 @@ CssHtmlTree.prototype = {
win.focus();
},
/**
* Verify that target is indeed a css value we want a tooltip on, and if yes
* prepare some content for the tooltip
*/
_buildTooltipContent: function(target)
{
// If the hovered element is not a property view and is not a background
// image, then don't show a tooltip
let isPropertyValue = target.classList.contains("property-value");
if (!isPropertyValue) {
return false;
}
let propName = target.parentNode.querySelector(".property-name");
let isBackgroundImage = propName.textContent === "background-image";
if (!isBackgroundImage) {
return false;
}
// Fill some content
this.tooltip.setCssBackgroundImageContent(target.textContent);
return true;
},
/**
* Create a context menu.
*/
@ -648,6 +676,9 @@ CssHtmlTree.prototype = {
this._contextmenu = null;
}
this.tooltip.stopTogglingOnHover(this.propertyContainer);
this.tooltip.destroy();
// Remove bound listeners
this.styleDocument.removeEventListener("contextmenu", this._onContextMenu);
this.styleDocument.removeEventListener("copy", this._onCopy);
@ -831,9 +862,8 @@ PropertyView.prototype = {
{
let doc = this.tree.styleDocument;
this.onMatchedToggle = this.onMatchedToggle.bind(this);
// Build the container element
this.onMatchedToggle = this.onMatchedToggle.bind(this);
this.element = doc.createElementNS(HTML_NS, "div");
this.element.setAttribute("class", this.propertyHeaderClassName);
this.element.addEventListener("dblclick", this.onMatchedToggle, false);
@ -858,6 +888,8 @@ PropertyView.prototype = {
this.matchedExpander.addEventListener("click", this.onMatchedToggle, false);
this.element.appendChild(this.matchedExpander);
this.focusElement = () => this.element.focus();
// Build the style name element
this.nameNode = doc.createElementNS(HTML_NS, "div");
this.nameNode.setAttribute("class", "property-name theme-fg-color5");
@ -1034,7 +1066,7 @@ PropertyView.prototype = {
};
/**
* A container to view us easy access to display data from a CssRule
* A container to give us easy access to display data from a CssRule
* @param CssHtmlTree aTree, the owning CssHtmlTree
* @param aSelectorInfo
*/

View File

@ -13,6 +13,7 @@ let {CssLogic} = require("devtools/styleinspector/css-logic");
let {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
let {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
let {Tooltip} = require("devtools/shared/widgets/Tooltip");
const {OutputParser} = require("devtools/output-parser");
@ -1029,6 +1030,7 @@ TextProperty.prototype = {
* apply to a given element. After construction, the 'element'
* property will be available with the user interface.
*
* @param {Inspector} aInspector
* @param {Document} aDoc
* The document that will contain the rule view.
* @param {object} aStore
@ -1039,8 +1041,9 @@ TextProperty.prototype = {
* The PageStyleFront for communicating with the remote server.
* @constructor
*/
function CssRuleView(aDoc, aStore, aPageStyle)
function CssRuleView(aInspector, aDoc, aStore, aPageStyle)
{
this.inspector = aInspector;
this.doc = aDoc;
this.store = aStore || {};
this.pageStyle = aPageStyle;
@ -1067,6 +1070,9 @@ function CssRuleView(aDoc, aStore, aPageStyle)
};
this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options);
this.tooltip = new Tooltip(this.inspector.panelDoc);
this.tooltip.startTogglingOnHover(this.element, this._buildTooltipContent.bind(this));
this._buildContextMenu();
this._showEmpty();
}
@ -1107,6 +1113,37 @@ CssRuleView.prototype = {
popupset.appendChild(this._contextmenu);
},
/**
* Verify that target is indeed a css value we want a tooltip on, and if yes
* prepare some content for the tooltip
*/
_buildTooltipContent: function(target) {
let isValueWithImage = target.classList.contains("ruleview-propertyvalue") &&
target.querySelector(".theme-link");
let isImageHref = target.classList.contains("theme-link") &&
target.parentNode.classList.contains("ruleview-propertyvalue");
if (isImageHref) {
target = target.parentNode;
}
let isEditing = this.isEditing;
// If the inplace-editor is visible or if this is not a background image
// don't show the tooltip
if (this.isEditing || (!isImageHref && !isValueWithImage)) {
return false;
}
// Retrieve the TextProperty for the hovered element
let property = target.textProperty;
let href = property.rule.domRule.href;
// Fill some content
this.tooltip.setCssBackgroundImageContent(property.value, href);
return true;
},
/**
* Update the context menu. This means enabling or disabling menuitems as
* appropriate.
@ -1240,6 +1277,9 @@ CssRuleView.prototype = {
// We manage the popupNode ourselves so we also need to destroy it.
this.doc.popupNode = null;
this.tooltip.stopTogglingOnHover(this.element);
this.tooltip.destroy();
if (this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
@ -1307,7 +1347,6 @@ CssRuleView.prototype = {
}
this._createEditors();
// Notify anyone that cares that we refreshed.
var evt = this.doc.createEvent("Events");
evt.initEvent("CssRuleViewRefreshed", true, false);
@ -1853,6 +1892,10 @@ TextPropertyEditor.prototype = {
tabindex: "0",
});
// Storing the TextProperty on the valuespan for easy access
// (for instance by the tooltip)
this.valueSpan.textProperty = this.prop;
// Save the initial value as the last committed value,
// for restoring after pressing escape.
this.committed = { name: this.prop.name,
@ -1971,7 +2014,6 @@ TextPropertyEditor.prototype = {
});
a.addEventListener("click", (aEvent) => {
// Clicks within the link shouldn't trigger editing.
aEvent.stopPropagation();
aEvent.preventDefault();
@ -2133,6 +2175,7 @@ TextPropertyEditor.prototype = {
{
this.element.parentNode.removeChild(this.element);
this.ruleEditor.rule.editClosestTextProperty(this.prop);
this.valueSpan.textProperty = null;
this.prop.remove();
},

View File

@ -25,7 +25,7 @@ function RuleViewTool(aInspector, aWindow, aIFrame)
this.doc = aWindow.document;
this.outerIFrame = aIFrame;
this.view = new RuleView.CssRuleView(this.doc);
this.view = new RuleView.CssRuleView(aInspector, this.doc);
this.doc.documentElement.appendChild(this.view.element);
this._changeHandler = () => {

View File

@ -38,6 +38,7 @@ MOCHITEST_BROWSER_FILES = \
browser_ruleview_pseudoelement.js \
browser_computedview_bug835808_keyboard_nav.js \
browser_bug913014_matched_expand.js \
browser_bug765105_background_image_tooltip.js \
head.js \
$(NULL)

View File

@ -0,0 +1,162 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let contentDoc;
let inspector;
let ruleView;
let computedView;
const PAGE_CONTENT = [
'<style type="text/css">',
' body {',
' padding: 1em;',
' background-image: url(data:image/png;base64,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);',
' background-repeat: repeat-y;',
' background-position: right top;',
' }',
' .test-element {',
' font-family: verdana;',
' color: #333;',
' background: url(chrome://global/skin/icons/warning-64.png) no-repeat left center;',
' padding-left: 70px;',
' }',
'</style>',
'<div class="test-element">test element</div>',
'<div class="test-element-2">test element 2</div>'
].join("\n");
function test() {
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
contentDoc = content.document;
waitForFocus(createDocument, content);
}, true);
content.location = "data:text/html,rule view tooltip test";
}
function createDocument() {
contentDoc.body.innerHTML = PAGE_CONTENT;
openRuleView((aInspector, aRuleView) => {
inspector = aInspector;
ruleView = aRuleView;
startTests();
});
}
function startTests() {
// let testElement = contentDoc.querySelector(".test-element");
inspector.selection.setNode(contentDoc.body);
inspector.once("inspector-updated", testBodyRuleView);
}
function endTests() {
contentDoc = inspector = ruleView = computedView = null;
gBrowser.removeCurrentTab();
finish();
}
function assertTooltipShownOn(tooltip, element, cb) {
// If there is indeed a show-on-hover on element, the xul panel will be shown
tooltip.panel.addEventListener("popupshown", function shown() {
tooltip.panel.removeEventListener("popupshown", shown, true);
cb();
}, true);
tooltip._showOnHover(element);
}
function testBodyRuleView() {
info("Testing tooltips in the rule view");
let panel = ruleView.tooltip.panel;
// Check that the rule view has a tooltip and that a XUL panel has been created
ok(ruleView.tooltip, "Tooltip instance exists");
ok(panel, "XUL panel exists");
// Get the background-image property inside the rule view
let {nameSpan, valueSpan} = getRuleViewProperty("background-image");
// And verify that the tooltip gets shown on this property
assertTooltipShownOn(ruleView.tooltip, valueSpan, () => {
let images = panel.getElementsByTagName("image");
is(images.length, 1, "Tooltip contains an image");
ok(images[0].src.indexOf("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHe") !== -1, "The image URL seems fine");
ruleView.tooltip.hide();
inspector.selection.setNode(contentDoc.querySelector(".test-element"));
inspector.once("inspector-updated", testDivRuleView);
});
}
function testDivRuleView() {
let panel = ruleView.tooltip.panel;
// Get the background property inside the rule view
let {nameSpan, valueSpan} = getRuleViewProperty("background");
let uriSpan = valueSpan.querySelector(".theme-link");
// And verify that the tooltip gets shown on this property
assertTooltipShownOn(ruleView.tooltip, uriSpan, () => {
let images = panel.getElementsByTagName("image");
is(images.length, 1, "Tooltip contains an image");
ok(images[0].src === "chrome://global/skin/icons/warning-64.png");
ruleView.tooltip.hide();
testComputedView();
});
}
function testComputedView() {
info("Testing tooltips in the computed view");
inspector.sidebar.select("computedview");
computedView = inspector.sidebar.getWindowForTab("computedview").computedview.view;
let doc = computedView.styleDocument;
let panel = computedView.tooltip.panel;
let {nameSpan, valueSpan} = getComputedViewProperty("background-image");
assertTooltipShownOn(computedView.tooltip, valueSpan, () => {
let images = panel.getElementsByTagName("image");
is(images.length, 1, "Tooltip contains an image");
ok(images[0].src === "chrome://global/skin/icons/warning-64.png");
computedView.tooltip.hide();
endTests();
});
}
function getRuleViewProperty(name) {
let prop = null;
[].forEach.call(ruleView.doc.querySelectorAll(".ruleview-property"), property => {
let nameSpan = property.querySelector(".ruleview-propertyname");
let valueSpan = property.querySelector(".ruleview-propertyvalue");
if (nameSpan.textContent === name) {
prop = {nameSpan: nameSpan, valueSpan: valueSpan};
}
});
return prop;
}
function getComputedViewProperty(name) {
let prop = null;
[].forEach.call(computedView.styleDocument.querySelectorAll(".property-view"), property => {
let nameSpan = property.querySelector(".property-name");
let valueSpan = property.querySelector(".property-value");
if (nameSpan.textContent === name) {
prop = {nameSpan: nameSpan, valueSpan: valueSpan};
}
});
return prop;
}

View File

@ -31,7 +31,6 @@ debuggerPausedWarning.message=Debugger is paused. Some features like mouse selec
# the node is selected.
nodeMenu.tooltiptext=Node operations
# LOCALIZATION NOTE (inspector.*)
# Used for the menuitem in the tool menu
inspector.label=Inspector
@ -44,3 +43,6 @@ inspector.accesskey=I
markupView.more.showing=Some nodes were hidden.
markupView.more.showAll=Show All %S Nodes
inspector.tooltip=DOM and Style Inspector
#LOCALIZATION NOTE: Used in the image preview tooltip when the image could not be loaded
previewTooltip.image.brokenImage=Could not load the image

View File

@ -111,3 +111,25 @@
max-height: 75vh;
}
}
/* Tooltip widget (see browser/devtools/shared/widgets/Tooltip.js) */
.devtools-tooltip.devtools-tooltip-tooltip {
/* If the tooltip uses a <tooltip> XUL element */
-moz-appearance: none;
padding: 4px;
background: #eee;
border-radius: 3px;
}
.devtools-tooltip.devtools-tooltip-panel .panel-arrowcontent {
/* If the tooltip uses a <panel> XUL element instead */
padding: 4px;
}
.devtools-tooltip-tiles {
background-color: #eee;
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
}

View File

@ -248,6 +248,47 @@ var NodeActor = protocol.ActorClass({
response: {}
}),
/**
* Get the node's image data if any (for canvas and img nodes).
* Returns a LongStringActor with the image or canvas' image data as png
* a data:image/png;base64,.... string
* A null return value means the node isn't an image
* An empty string return value means the node is an image but image data
* could not be retrieved (missing/broken image).
*/
getImageData: method(function() {
let isImg = this.rawNode.tagName.toLowerCase() === "img";
let isCanvas = this.rawNode.tagName.toLowerCase() === "canvas";
if (!isImg && !isCanvas) {
return null;
}
let imageData;
if (isImg) {
let canvas = this.rawNode.ownerDocument.createElement("canvas");
canvas.width = this.rawNode.naturalWidth;
canvas.height = this.rawNode.naturalHeight;
let ctx = canvas.getContext("2d");
try {
// This will fail if the image is missing
ctx.drawImage(this.rawNode, 0, 0);
imageData = canvas.toDataURL("image/png");
} catch (e) {
imageData = "";
}
} else if (isCanvas) {
imageData = this.rawNode.toDataURL("image/png");
}
return LongStringActor(this.conn, imageData);
}, {
request: {},
response: {
data: RetVal("nullable:longstring")
}
}),
/**
* Modify a node's attributes. Passed an array of modifications
* similar in format to "attributes" mutations.
@ -283,8 +324,7 @@ var NodeActor = protocol.ActorClass({
modifications: Arg(0, "array:json")
},
response: {}
}),
})
});
/**