michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {Cc, Cu, Ci} = require("chrome"); michael@0: const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: const IOService = Cc["@mozilla.org/network/io-service;1"] michael@0: .getService(Ci.nsIIOService); michael@0: const {Spectrum} = require("devtools/shared/widgets/Spectrum"); michael@0: const EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: const {colorUtils} = require("devtools/css-color"); michael@0: const Heritage = require("sdk/core/heritage"); michael@0: const {CSSTransformPreviewer} = require("devtools/shared/widgets/CSSTransformPreviewer"); michael@0: const {Eyedropper} = require("devtools/eyedropper/eyedropper"); michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "setNamedTimeout", michael@0: "resource:///modules/devtools/ViewHelpers.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "clearNamedTimeout", michael@0: "resource:///modules/devtools/ViewHelpers.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "VariablesView", michael@0: "resource:///modules/devtools/VariablesView.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController", michael@0: "resource:///modules/devtools/VariablesViewController.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: michael@0: const GRADIENT_RE = /\b(repeating-)?(linear|radial)-gradient\(((rgb|hsl)a?\(.+?\)|[^\)])+\)/gi; michael@0: const BORDERCOLOR_RE = /^border-[-a-z]*color$/ig; michael@0: const BORDER_RE = /^border(-(top|bottom|left|right))?$/ig; michael@0: const XHTML_NS = "http://www.w3.org/1999/xhtml"; michael@0: const SPECTRUM_FRAME = "chrome://browser/content/devtools/spectrum-frame.xhtml"; michael@0: const ESCAPE_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE; michael@0: const RETURN_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_RETURN; michael@0: const POPUP_EVENTS = ["shown", "hidden", "showing", "hiding"]; michael@0: const FONT_FAMILY_PREVIEW_TEXT = "(ABCabc123&@%)"; michael@0: michael@0: /** michael@0: * Tooltip widget. michael@0: * michael@0: * This widget is intended at any tool that may need to show rich content in the michael@0: * form of floating panels. michael@0: * A common use case is image previewing in the CSS rule view, but more complex michael@0: * use cases may include color pickers, object inspection, etc... michael@0: * michael@0: * Tooltips are based on XUL (namely XUL arrow-type s), and therefore michael@0: * need a XUL Document to live in. michael@0: * This is pretty much the only requirement they have on their environment. michael@0: * michael@0: * The way to use a tooltip is simply by instantiating a tooltip yourself and michael@0: * attaching some content in it, or using one of the ready-made content types. michael@0: * michael@0: * A convenient `startTogglingOnHover` method may avoid having to register event michael@0: * handlers yourself if the tooltip has to be shown when hovering over a michael@0: * specific element or group of elements (which is usually the most common case) michael@0: */ michael@0: michael@0: /** michael@0: * Container used for dealing with optional parameters. michael@0: * michael@0: * @param {Object} defaults michael@0: * An object with all default options {p1: v1, p2: v2, ...} michael@0: * @param {Object} options michael@0: * The actual values. michael@0: */ michael@0: function OptionsStore(defaults, options) { michael@0: this.defaults = defaults || {}; michael@0: this.options = options || {}; michael@0: } michael@0: michael@0: OptionsStore.prototype = { michael@0: /** michael@0: * Get the value for a given option name. michael@0: * @return {Object} Returns the value for that option, coming either for the michael@0: * actual values that have been set in the constructor, or from the michael@0: * defaults if that options was not specified. michael@0: */ michael@0: get: function(name) { michael@0: if (typeof this.options[name] !== "undefined") { michael@0: return this.options[name]; michael@0: } else { michael@0: return this.defaults[name]; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * The low level structure of a tooltip is a XUL element (a ). michael@0: */ michael@0: let PanelFactory = { michael@0: /** michael@0: * Get a new XUL panel instance. michael@0: * @param {XULDocument} doc michael@0: * The XUL document to put that panel into michael@0: * @param {OptionsStore} options michael@0: * An options store to get some configuration from michael@0: */ michael@0: get: function(doc, options) { michael@0: // Create the tooltip michael@0: let panel = doc.createElement("panel"); michael@0: panel.setAttribute("hidden", true); michael@0: panel.setAttribute("ignorekeys", true); michael@0: panel.setAttribute("animate", false); michael@0: michael@0: panel.setAttribute("consumeoutsideclicks", options.get("consumeOutsideClick")); michael@0: panel.setAttribute("noautofocus", options.get("noAutoFocus")); michael@0: panel.setAttribute("type", "arrow"); michael@0: panel.setAttribute("level", "top"); michael@0: michael@0: panel.setAttribute("class", "devtools-tooltip theme-tooltip-panel"); michael@0: doc.querySelector("window").appendChild(panel); michael@0: michael@0: return panel; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Tooltip class. michael@0: * michael@0: * Basic usage: michael@0: * let t = new Tooltip(xulDoc); michael@0: * t.content = someXulContent; michael@0: * t.show(); michael@0: * t.hide(); michael@0: * t.destroy(); michael@0: * michael@0: * Better usage: michael@0: * let t = new Tooltip(xulDoc); michael@0: * t.startTogglingOnHover(container, target => { michael@0: * if () { michael@0: * t.setImageContent("http://image.png"); michael@0: * return true; michael@0: * } michael@0: * }); michael@0: * t.destroy(); michael@0: * michael@0: * @param {XULDocument} doc michael@0: * The XUL document hosting this tooltip michael@0: * @param {Object} options michael@0: * Optional options that give options to consumers: michael@0: * - consumeOutsideClick {Boolean} Wether the first click outside of the michael@0: * tooltip should close the tooltip and be consumed or not. michael@0: * Defaults to false. michael@0: * - closeOnKeys {Array} An array of key codes that should close the michael@0: * tooltip. Defaults to [27] (escape key). michael@0: * - closeOnEvents [{emitter: {Object}, event: {String}, useCapture: {Boolean}}] michael@0: * Provide an optional list of emitter objects and event names here to michael@0: * trigger the closing of the tooltip when these events are fired by the michael@0: * emitters. The emitter objects should either implement on/off(event, cb) michael@0: * or addEventListener/removeEventListener(event, cb). Defaults to []. michael@0: * For instance, the following would close the tooltip whenever the michael@0: * toolbox selects a new tool and when a DOM node gets scrolled: michael@0: * new Tooltip(doc, { michael@0: * closeOnEvents: [ michael@0: * {emitter: toolbox, event: "select"}, michael@0: * {emitter: myContainer, event: "scroll", useCapture: true} michael@0: * ] michael@0: * }); michael@0: * - noAutoFocus {Boolean} Should the focus automatically go to the panel michael@0: * when it opens. Defaults to true. michael@0: * michael@0: * Fires these events: michael@0: * - showing : just before the tooltip shows michael@0: * - shown : when the tooltip is shown michael@0: * - hiding : just before the tooltip closes michael@0: * - hidden : when the tooltip gets hidden michael@0: * - keypress : when any key gets pressed, with keyCode michael@0: */ michael@0: function Tooltip(doc, options) { michael@0: EventEmitter.decorate(this); michael@0: michael@0: this.doc = doc; michael@0: this.options = new OptionsStore({ michael@0: consumeOutsideClick: false, michael@0: closeOnKeys: [ESCAPE_KEYCODE], michael@0: noAutoFocus: true, michael@0: closeOnEvents: [] michael@0: }, options); michael@0: this.panel = PanelFactory.get(doc, this.options); michael@0: michael@0: // Used for namedTimeouts in the mouseover handling michael@0: this.uid = "tooltip-" + Date.now(); michael@0: michael@0: // Emit show/hide events michael@0: for (let event of POPUP_EVENTS) { michael@0: this["_onPopup" + event] = ((e) => { michael@0: return () => this.emit(e); michael@0: })(event); michael@0: this.panel.addEventListener("popup" + event, michael@0: this["_onPopup" + event], false); michael@0: } michael@0: michael@0: // Listen to keypress events to close the tooltip if configured to do so michael@0: let win = this.doc.querySelector("window"); michael@0: this._onKeyPress = event => { michael@0: this.emit("keypress", event.keyCode); michael@0: if (this.options.get("closeOnKeys").indexOf(event.keyCode) !== -1) { michael@0: if (!this.panel.hidden) { michael@0: event.stopPropagation(); michael@0: } michael@0: this.hide(); michael@0: } michael@0: }; michael@0: win.addEventListener("keypress", this._onKeyPress, false); michael@0: michael@0: // Listen to custom emitters' events to close the tooltip michael@0: this.hide = this.hide.bind(this); michael@0: let closeOnEvents = this.options.get("closeOnEvents"); michael@0: for (let {emitter, event, useCapture} of closeOnEvents) { michael@0: for (let add of ["addEventListener", "on"]) { michael@0: if (add in emitter) { michael@0: emitter[add](event, this.hide, useCapture); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: module.exports.Tooltip = Tooltip; michael@0: michael@0: Tooltip.prototype = { michael@0: defaultPosition: "before_start", michael@0: defaultOffsetX: 0, // px michael@0: defaultOffsetY: 0, // px michael@0: defaultShowDelay: 50, // ms michael@0: michael@0: /** michael@0: * Show the tooltip. It might be wise to append some content first if you michael@0: * don't want the tooltip to be empty. You may access the content of the michael@0: * tooltip by setting a XUL node to t.content. michael@0: * @param {node} anchor michael@0: * Which node should the tooltip be shown on michael@0: * @param {string} position [optional] michael@0: * Optional tooltip position. Defaults to before_start michael@0: * https://developer.mozilla.org/en-US/docs/XUL/PopupGuide/Positioning michael@0: * @param {number} x, y [optional] michael@0: * The left and top offset coordinates, in pixels. michael@0: */ michael@0: show: function(anchor, michael@0: position = this.defaultPosition, michael@0: x = this.defaultOffsetX, michael@0: y = this.defaultOffsetY) { michael@0: this.panel.hidden = false; michael@0: this.panel.openPopup(anchor, position, x, y); michael@0: }, michael@0: michael@0: /** michael@0: * Hide the tooltip michael@0: */ michael@0: hide: function() { michael@0: this.panel.hidden = true; michael@0: this.panel.hidePopup(); michael@0: }, michael@0: michael@0: isShown: function() { michael@0: return this.panel.state !== "closed" && this.panel.state !== "hiding"; michael@0: }, michael@0: michael@0: setSize: function(width, height) { michael@0: this.panel.sizeTo(width, height); michael@0: }, michael@0: michael@0: /** michael@0: * Empty the tooltip's content michael@0: */ michael@0: empty: function() { michael@0: while (this.panel.hasChildNodes()) { michael@0: this.panel.removeChild(this.panel.firstChild); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Gets this panel's visibility state. michael@0: * @return boolean michael@0: */ michael@0: isHidden: function() { michael@0: return this.panel.state == "closed" || this.panel.state == "hiding"; michael@0: }, michael@0: michael@0: /** michael@0: * Gets if this panel has any child nodes. michael@0: * @return boolean michael@0: */ michael@0: isEmpty: function() { michael@0: return !this.panel.hasChildNodes(); michael@0: }, michael@0: michael@0: /** michael@0: * Get rid of references and event listeners michael@0: */ michael@0: destroy: function () { michael@0: this.hide(); michael@0: michael@0: for (let event of POPUP_EVENTS) { michael@0: this.panel.removeEventListener("popup" + event, michael@0: this["_onPopup" + event], false); michael@0: } michael@0: michael@0: let win = this.doc.querySelector("window"); michael@0: win.removeEventListener("keypress", this._onKeyPress, false); michael@0: michael@0: let closeOnEvents = this.options.get("closeOnEvents"); michael@0: for (let {emitter, event, useCapture} of closeOnEvents) { michael@0: for (let remove of ["removeEventListener", "off"]) { michael@0: if (remove in emitter) { michael@0: emitter[remove](event, this.hide, useCapture); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: this.content = null; michael@0: michael@0: if (this._basedNode) { michael@0: this.stopTogglingOnHover(); michael@0: } michael@0: michael@0: this.doc = null; michael@0: michael@0: this.panel.remove(); michael@0: this.panel = null; michael@0: }, michael@0: michael@0: /** michael@0: * Show/hide the tooltip when the mouse hovers over particular nodes. michael@0: * michael@0: * 2 Ways to make this work: michael@0: * - Provide a single node to attach the tooltip to, as the baseNode, and michael@0: * omit the second targetNodeCb argument michael@0: * - Provide a baseNode that is the container of possibly numerous children michael@0: * elements that may receive a tooltip. In this case, provide the second michael@0: * targetNodeCb argument to decide wether or not a child should receive michael@0: * a tooltip. michael@0: * michael@0: * This works by tracking mouse movements on a base container node (baseNode) michael@0: * and showing the tooltip when the mouse stops moving. The targetNodeCb michael@0: * callback is used to know whether or not the particular element being michael@0: * hovered over should indeed receive the tooltip. If you don't provide it michael@0: * it's equivalent to a function that always returns true. michael@0: * michael@0: * Note that if you call this function a second time, it will itself call michael@0: * stopTogglingOnHover before adding mouse tracking listeners again. michael@0: * michael@0: * @param {node} baseNode michael@0: * The container for all target nodes michael@0: * @param {Function} targetNodeCb michael@0: * A function that accepts a node argument and returns true or false michael@0: * (or a promise that resolves or rejects) to signify if the tooltip michael@0: * should be shown on that node or not. michael@0: * Additionally, the function receives a second argument which is the michael@0: * tooltip instance itself, to be used to add/modify the content of the michael@0: * tooltip if needed. If omitted, the tooltip will be shown everytime. michael@0: * @param {Number} showDelay michael@0: * An optional delay that will be observed before showing the tooltip. michael@0: * Defaults to this.defaultShowDelay. michael@0: */ michael@0: startTogglingOnHover: function(baseNode, targetNodeCb, showDelay=this.defaultShowDelay) { michael@0: if (this._basedNode) { michael@0: this.stopTogglingOnHover(); michael@0: } michael@0: michael@0: this._basedNode = baseNode; michael@0: this._showDelay = showDelay; michael@0: this._targetNodeCb = targetNodeCb || (() => true); michael@0: michael@0: this._onBaseNodeMouseMove = this._onBaseNodeMouseMove.bind(this); michael@0: this._onBaseNodeMouseLeave = this._onBaseNodeMouseLeave.bind(this); michael@0: michael@0: baseNode.addEventListener("mousemove", this._onBaseNodeMouseMove, false); michael@0: baseNode.addEventListener("mouseleave", this._onBaseNodeMouseLeave, false); michael@0: }, michael@0: michael@0: /** michael@0: * If the startTogglingOnHover function has been used previously, and you want michael@0: * to get rid of this behavior, then call this function to remove the mouse michael@0: * movement tracking michael@0: */ michael@0: stopTogglingOnHover: function() { michael@0: clearNamedTimeout(this.uid); michael@0: michael@0: this._basedNode.removeEventListener("mousemove", michael@0: this._onBaseNodeMouseMove, false); michael@0: this._basedNode.removeEventListener("mouseleave", michael@0: this._onBaseNodeMouseLeave, false); michael@0: michael@0: this._basedNode = null; michael@0: this._targetNodeCb = null; michael@0: this._lastHovered = null; michael@0: }, michael@0: michael@0: _onBaseNodeMouseMove: function(event) { michael@0: if (event.target !== this._lastHovered) { michael@0: this.hide(); michael@0: this._lastHovered = event.target; michael@0: setNamedTimeout(this.uid, this._showDelay, () => { michael@0: this.isValidHoverTarget(event.target).then(target => { michael@0: this.show(target); michael@0: }); michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Is the given target DOMNode a valid node for toggling the tooltip on hover. michael@0: * This delegates to the user-defined _targetNodeCb callback. michael@0: * @return a promise that resolves or rejects depending if the tooltip should michael@0: * be shown or not. If it resolves, it does to the actual anchor to be used michael@0: */ michael@0: isValidHoverTarget: function(target) { michael@0: // Execute the user-defined callback which should return either true/false michael@0: // or a promise that resolves or rejects michael@0: let res = this._targetNodeCb(target, this); michael@0: michael@0: // The callback can additionally return a DOMNode to replace the anchor of michael@0: // the tooltip when shown michael@0: if (res && res.then) { michael@0: return res.then(arg => { michael@0: return arg instanceof Ci.nsIDOMNode ? arg : target; michael@0: }, () => { michael@0: return false; michael@0: }); michael@0: } else { michael@0: let newTarget = res instanceof Ci.nsIDOMNode ? res : target; michael@0: return res ? promise.resolve(newTarget) : promise.reject(false); michael@0: } michael@0: }, michael@0: michael@0: _onBaseNodeMouseLeave: function() { michael@0: clearNamedTimeout(this.uid); michael@0: this._lastHovered = null; michael@0: this.hide(); michael@0: }, michael@0: michael@0: /** michael@0: * Set the content of this tooltip. Will first empty the tooltip and then michael@0: * append the new content element. michael@0: * Consider using one of the setContent() functions instead. michael@0: * @param {node} content michael@0: * A node that can be appended in the tooltip XUL element michael@0: */ michael@0: set content(content) { michael@0: if (this.content == content) { michael@0: return; michael@0: } michael@0: michael@0: this.empty(); michael@0: this.panel.removeAttribute("clamped-dimensions"); michael@0: michael@0: if (content) { michael@0: this.panel.appendChild(content); michael@0: } michael@0: }, michael@0: michael@0: get content() { michael@0: return this.panel.firstChild; michael@0: }, michael@0: michael@0: /** michael@0: * Sets some text as the content of this tooltip. michael@0: * michael@0: * @param {array} messages michael@0: * A list of text messages. michael@0: * @param {string} messagesClass [optional] michael@0: * A style class for the text messages. michael@0: * @param {string} containerClass [optional] michael@0: * A style class for the text messages container. michael@0: * @param {boolean} isAlertTooltip [optional] michael@0: * Pass true to add an alert image for your tooltip. michael@0: */ michael@0: setTextContent: function( michael@0: { michael@0: messages, michael@0: messagesClass, michael@0: containerClass, michael@0: isAlertTooltip michael@0: }, michael@0: extraButtons = []) { michael@0: messagesClass = messagesClass || "default-tooltip-simple-text-colors"; michael@0: containerClass = containerClass || "default-tooltip-simple-text-colors"; michael@0: michael@0: let vbox = this.doc.createElement("vbox"); michael@0: vbox.className = "devtools-tooltip-simple-text-container " + containerClass; michael@0: vbox.setAttribute("flex", "1"); michael@0: michael@0: for (let text of messages) { michael@0: let description = this.doc.createElement("description"); michael@0: description.setAttribute("flex", "1"); michael@0: description.className = "devtools-tooltip-simple-text " + messagesClass; michael@0: description.textContent = text; michael@0: vbox.appendChild(description); michael@0: } michael@0: michael@0: for (let { label, className, command } of extraButtons) { michael@0: let button = this.doc.createElement("button"); michael@0: button.className = className; michael@0: button.setAttribute("label", label); michael@0: button.addEventListener("command", command); michael@0: vbox.appendChild(button); michael@0: } michael@0: michael@0: if (isAlertTooltip) { michael@0: let hbox = this.doc.createElement("hbox"); michael@0: hbox.setAttribute("align", "start"); michael@0: michael@0: let alertImg = this.doc.createElement("image"); michael@0: alertImg.className = "devtools-tooltip-alert-icon"; michael@0: hbox.appendChild(alertImg); michael@0: hbox.appendChild(vbox); michael@0: this.content = hbox; michael@0: } else { michael@0: this.content = vbox; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Fill the tooltip with a variables view, inspecting an object via its michael@0: * corresponding object actor, as specified in the remote debugging protocol. michael@0: * michael@0: * @param {object} objectActor michael@0: * The value grip for the object actor. michael@0: * @param {object} viewOptions [optional] michael@0: * Options for the variables view visualization. michael@0: * @param {object} controllerOptions [optional] michael@0: * Options for the variables view controller. michael@0: * @param {object} relayEvents [optional] michael@0: * A collection of events to listen on the variables view widget. michael@0: * For example, { fetched: () => ... } michael@0: * @param {boolean} reuseCachedWidget [optional] michael@0: * Pass false to instantiate a brand new widget for this variable. michael@0: * Otherwise, if a variable was previously inspected, its widget michael@0: * will be reused. michael@0: * @param {Toolbox} toolbox [optional] michael@0: * Pass the instance of the current toolbox if you want the variables michael@0: * view widget to allow highlighting and selection of DOM nodes michael@0: */ michael@0: setVariableContent: function( michael@0: objectActor, michael@0: viewOptions = {}, michael@0: controllerOptions = {}, michael@0: relayEvents = {}, michael@0: extraButtons = [], michael@0: toolbox = null) { michael@0: michael@0: let vbox = this.doc.createElement("vbox"); michael@0: vbox.className = "devtools-tooltip-variables-view-box"; michael@0: vbox.setAttribute("flex", "1"); michael@0: michael@0: let innerbox = this.doc.createElement("vbox"); michael@0: innerbox.className = "devtools-tooltip-variables-view-innerbox"; michael@0: innerbox.setAttribute("flex", "1"); michael@0: vbox.appendChild(innerbox); michael@0: michael@0: for (let { label, className, command } of extraButtons) { michael@0: let button = this.doc.createElement("button"); michael@0: button.className = className; michael@0: button.setAttribute("label", label); michael@0: button.addEventListener("command", command); michael@0: vbox.appendChild(button); michael@0: } michael@0: michael@0: let widget = new VariablesView(innerbox, viewOptions); michael@0: michael@0: // If a toolbox was provided, link it to the vview michael@0: if (toolbox) { michael@0: widget.toolbox = toolbox; michael@0: } michael@0: michael@0: // Analyzing state history isn't useful with transient object inspectors. michael@0: widget.commitHierarchy = () => {}; michael@0: michael@0: for (let e in relayEvents) widget.on(e, relayEvents[e]); michael@0: VariablesViewController.attach(widget, controllerOptions); michael@0: michael@0: // Some of the view options are allowed to change between uses. michael@0: widget.searchPlaceholder = viewOptions.searchPlaceholder; michael@0: widget.searchEnabled = viewOptions.searchEnabled; michael@0: michael@0: // Use the object actor's grip to display it as a variable in the widget. michael@0: // The controller options are allowed to change between uses. michael@0: widget.controller.setSingleVariable( michael@0: { objectActor: objectActor }, controllerOptions); michael@0: michael@0: this.content = vbox; michael@0: this.panel.setAttribute("clamped-dimensions", ""); michael@0: }, michael@0: michael@0: /** michael@0: * Uses the provided inspectorFront's getImageDataFromURL method to resolve michael@0: * the relative URL on the server-side, in the page context, and then sets the michael@0: * tooltip content with the resulting image just like |setImageContent| does. michael@0: * @return a promise that resolves when the image is shown in the tooltip or michael@0: * resolves when the broken image tooltip content is ready, but never rejects. michael@0: */ michael@0: setRelativeImageContent: Task.async(function*(imageUrl, inspectorFront, maxDim) { michael@0: if (imageUrl.startsWith("data:")) { michael@0: // If the imageUrl already is a data-url, save ourselves a round-trip michael@0: this.setImageContent(imageUrl, {maxDim: maxDim}); michael@0: } else if (inspectorFront) { michael@0: try { michael@0: let {data, size} = yield inspectorFront.getImageDataFromURL(imageUrl, maxDim); michael@0: size.maxDim = maxDim; michael@0: let str = yield data.string(); michael@0: this.setImageContent(str, size); michael@0: } catch (e) { michael@0: this.setBrokenImageContent(); michael@0: } michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Fill the tooltip with a message explaining the the image is missing michael@0: */ michael@0: setBrokenImageContent: function() { michael@0: this.setTextContent({ michael@0: messages: [l10n.strings.GetStringFromName("previewTooltip.image.brokenImage")] michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Fill the tooltip with an image and add the image dimension at the bottom. michael@0: * michael@0: * Only use this for absolute URLs that can be queried from the devtools michael@0: * client-side. For relative URLs, use |setRelativeImageContent|. michael@0: * michael@0: * @param {string} imageUrl michael@0: * The url to load the image from michael@0: * @param {Object} options michael@0: * The following options are supported: michael@0: * - resized : whether or not the image identified by imageUrl has been michael@0: * resized before this function was called. michael@0: * - naturalWidth/naturalHeight : the original size of the image before michael@0: * it was resized, if if was resized before this function was called. michael@0: * If not provided, will be measured on the loaded image. michael@0: * - maxDim : if the image should be resized before being shown, pass michael@0: * a number here michael@0: */ michael@0: setImageContent: function(imageUrl, options={}) { michael@0: if (!imageUrl) { michael@0: return; michael@0: } michael@0: michael@0: // Main container michael@0: let vbox = this.doc.createElement("vbox"); michael@0: vbox.setAttribute("align", "center"); michael@0: michael@0: // Display the image michael@0: let image = this.doc.createElement("image"); michael@0: image.setAttribute("src", imageUrl); michael@0: if (options.maxDim) { michael@0: image.style.maxWidth = options.maxDim + "px"; michael@0: image.style.maxHeight = options.maxDim + "px"; michael@0: } michael@0: vbox.appendChild(image); michael@0: michael@0: // Dimension label michael@0: let label = this.doc.createElement("label"); michael@0: label.classList.add("devtools-tooltip-caption"); michael@0: label.classList.add("theme-comment"); michael@0: if (options.naturalWidth && options.naturalHeight) { michael@0: label.textContent = this._getImageDimensionLabel(options.naturalWidth, michael@0: options.naturalHeight); michael@0: } else { michael@0: // If no dimensions were provided, load the image to get them michael@0: label.textContent = l10n.strings.GetStringFromName("previewTooltip.image.brokenImage"); michael@0: let imgObj = new this.doc.defaultView.Image(); michael@0: imgObj.src = imageUrl; michael@0: imgObj.onload = () => { michael@0: imgObj.onload = null; michael@0: label.textContent = this._getImageDimensionLabel(imgObj.naturalWidth, michael@0: imgObj.naturalHeight); michael@0: } michael@0: } michael@0: vbox.appendChild(label); michael@0: michael@0: this.content = vbox; michael@0: }, michael@0: michael@0: _getImageDimensionLabel: (w, h) => w + " x " + h, michael@0: michael@0: /** michael@0: * Fill the tooltip with a new instance of the spectrum color picker widget michael@0: * initialized with the given color, and return a promise that resolves to michael@0: * the instance of spectrum michael@0: */ michael@0: setColorPickerContent: function(color) { michael@0: let def = promise.defer(); michael@0: michael@0: // Create an iframe to contain spectrum michael@0: let iframe = this.doc.createElementNS(XHTML_NS, "iframe"); michael@0: iframe.setAttribute("transparent", true); michael@0: iframe.setAttribute("width", "210"); michael@0: iframe.setAttribute("height", "216"); michael@0: iframe.setAttribute("flex", "1"); michael@0: iframe.setAttribute("class", "devtools-tooltip-iframe"); michael@0: michael@0: let panel = this.panel; michael@0: let xulWin = this.doc.ownerGlobal; michael@0: michael@0: // Wait for the load to initialize spectrum michael@0: function onLoad() { michael@0: iframe.removeEventListener("load", onLoad, true); michael@0: let win = iframe.contentWindow.wrappedJSObject; michael@0: michael@0: let container = win.document.getElementById("spectrum"); michael@0: let spectrum = new Spectrum(container, color); michael@0: michael@0: function finalizeSpectrum() { michael@0: spectrum.show(); michael@0: def.resolve(spectrum); michael@0: } michael@0: michael@0: // Finalize spectrum's init when the tooltip becomes visible michael@0: if (panel.state == "open") { michael@0: finalizeSpectrum(); michael@0: } michael@0: else { michael@0: panel.addEventListener("popupshown", function shown() { michael@0: panel.removeEventListener("popupshown", shown, true); michael@0: finalizeSpectrum(); michael@0: }, true); michael@0: } michael@0: } michael@0: iframe.addEventListener("load", onLoad, true); michael@0: iframe.setAttribute("src", SPECTRUM_FRAME); michael@0: michael@0: // Put the iframe in the tooltip michael@0: this.content = iframe; michael@0: michael@0: return def.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Set the content of the tooltip to be the result of CSSTransformPreviewer. michael@0: * Meaning a canvas previewing a css transformation. michael@0: * michael@0: * @param {String} transform michael@0: * The CSS transform value (e.g. "rotate(45deg) translateX(50px)") michael@0: * @param {PageStyleActor} pageStyle michael@0: * An instance of the PageStyleActor that will be used to retrieve michael@0: * computed styles michael@0: * @param {NodeActor} node michael@0: * The NodeActor for the currently selected node michael@0: * @return A promise that resolves when the tooltip content is ready, or michael@0: * rejects if no transform is provided or the transform is invalid michael@0: */ michael@0: setCssTransformContent: Task.async(function*(transform, pageStyle, node) { michael@0: if (!transform) { michael@0: throw "Missing transform"; michael@0: } michael@0: michael@0: // Look into the computed styles to find the width and height and possibly michael@0: // the origin if it hadn't been provided michael@0: let styles = yield pageStyle.getComputed(node, { michael@0: filter: "user", michael@0: markMatched: false, michael@0: onlyMatched: false michael@0: }); michael@0: michael@0: let origin = styles["transform-origin"].value; michael@0: let width = parseInt(styles["width"].value); michael@0: let height = parseInt(styles["height"].value); michael@0: michael@0: let root = this.doc.createElementNS(XHTML_NS, "div"); michael@0: let previewer = new CSSTransformPreviewer(root); michael@0: this.content = root; michael@0: if (!previewer.preview(transform, origin, width, height)) { michael@0: throw "Invalid transform"; michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Set the content of the tooltip to display a font family preview. michael@0: * This is based on Lea Verou's Dablet. See https://github.com/LeaVerou/dabblet michael@0: * for more info. michael@0: * @param {String} font The font family value. michael@0: */ michael@0: setFontFamilyContent: function(font) { michael@0: if (!font) { michael@0: return; michael@0: } michael@0: michael@0: // Main container michael@0: let vbox = this.doc.createElement("vbox"); michael@0: vbox.setAttribute("flex", "1"); michael@0: michael@0: // Display the font family previewer michael@0: let previewer = this.doc.createElement("description"); michael@0: previewer.setAttribute("flex", "1"); michael@0: previewer.style.fontFamily = font; michael@0: previewer.classList.add("devtools-tooltip-font-previewer-text"); michael@0: previewer.textContent = FONT_FAMILY_PREVIEW_TEXT; michael@0: vbox.appendChild(previewer); michael@0: michael@0: this.content = vbox; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Base class for all (color, gradient, ...)-swatch based value editors inside michael@0: * tooltips michael@0: * michael@0: * @param {XULDocument} doc michael@0: */ michael@0: function SwatchBasedEditorTooltip(doc) { michael@0: // Creating a tooltip instance michael@0: // This one will consume outside clicks as it makes more sense to let the user michael@0: // close the tooltip by clicking out michael@0: // It will also close on and michael@0: this.tooltip = new Tooltip(doc, { michael@0: consumeOutsideClick: true, michael@0: closeOnKeys: [ESCAPE_KEYCODE, RETURN_KEYCODE], michael@0: noAutoFocus: false michael@0: }); michael@0: michael@0: // By default, swatch-based editor tooltips revert value change on and michael@0: // commit value change on michael@0: this._onTooltipKeypress = (event, code) => { michael@0: if (code === ESCAPE_KEYCODE) { michael@0: this.revert(); michael@0: } else if (code === RETURN_KEYCODE) { michael@0: this.commit(); michael@0: } michael@0: }; michael@0: this.tooltip.on("keypress", this._onTooltipKeypress); michael@0: michael@0: // All target swatches are kept in a map, indexed by swatch DOM elements michael@0: this.swatches = new Map(); michael@0: michael@0: // When a swatch is clicked, and for as long as the tooltip is shown, the michael@0: // activeSwatch property will hold the reference to the swatch DOM element michael@0: // that was clicked michael@0: this.activeSwatch = null; michael@0: michael@0: this._onSwatchClick = this._onSwatchClick.bind(this); michael@0: } michael@0: michael@0: SwatchBasedEditorTooltip.prototype = { michael@0: show: function() { michael@0: if (this.activeSwatch) { michael@0: this.tooltip.show(this.activeSwatch, "topcenter bottomleft"); michael@0: this.tooltip.once("hidden", () => { michael@0: if (!this.eyedropperOpen) { michael@0: this.activeSwatch = null; michael@0: } michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: hide: function() { michael@0: this.tooltip.hide(); michael@0: }, michael@0: michael@0: /** michael@0: * Add a new swatch DOM element to the list of swatch elements this editor michael@0: * tooltip knows about. That means from now on, clicking on that swatch will michael@0: * toggle the editor. michael@0: * michael@0: * @param {node} swatchEl michael@0: * The element to add michael@0: * @param {object} callbacks michael@0: * Callbacks that will be executed when the editor wants to preview a michael@0: * value change, or revert a change, or commit a change. michael@0: * - onPreview: will be called when one of the sub-classes calls preview michael@0: * - onRevert: will be called when the user ESCapes out of the tooltip michael@0: * - onCommit: will be called when the user presses ENTER or clicks michael@0: * outside the tooltip. If the user-defined onCommit returns a value, michael@0: * it will be used to replace originalValue, so that the swatch-based michael@0: * tooltip always knows what is the current originalValue and can use michael@0: * it when reverting michael@0: * @param {object} originalValue michael@0: * The original value before the editor in the tooltip makes changes michael@0: * This can be of any type, and will be passed, as is, in the revert michael@0: * callback michael@0: */ michael@0: addSwatch: function(swatchEl, callbacks={}, originalValue) { michael@0: if (!callbacks.onPreview) callbacks.onPreview = function() {}; michael@0: if (!callbacks.onRevert) callbacks.onRevert = function() {}; michael@0: if (!callbacks.onCommit) callbacks.onCommit = function() {}; michael@0: michael@0: this.swatches.set(swatchEl, { michael@0: callbacks: callbacks, michael@0: originalValue: originalValue michael@0: }); michael@0: swatchEl.addEventListener("click", this._onSwatchClick, false); michael@0: }, michael@0: michael@0: removeSwatch: function(swatchEl) { michael@0: if (this.swatches.has(swatchEl)) { michael@0: if (this.activeSwatch === swatchEl) { michael@0: this.hide(); michael@0: this.activeSwatch = null; michael@0: } michael@0: swatchEl.removeEventListener("click", this._onSwatchClick, false); michael@0: this.swatches.delete(swatchEl); michael@0: } michael@0: }, michael@0: michael@0: _onSwatchClick: function(event) { michael@0: let swatch = this.swatches.get(event.target); michael@0: if (swatch) { michael@0: this.activeSwatch = event.target; michael@0: this.show(); michael@0: event.stopPropagation(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Not called by this parent class, needs to be taken care of by sub-classes michael@0: */ michael@0: preview: function(value) { michael@0: if (this.activeSwatch) { michael@0: let swatch = this.swatches.get(this.activeSwatch); michael@0: swatch.callbacks.onPreview(value); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * This parent class only calls this on keypress michael@0: */ michael@0: revert: function() { michael@0: if (this.activeSwatch) { michael@0: let swatch = this.swatches.get(this.activeSwatch); michael@0: swatch.callbacks.onRevert(swatch.originalValue); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * This parent class only calls this on keypress michael@0: */ michael@0: commit: function() { michael@0: if (this.activeSwatch) { michael@0: let swatch = this.swatches.get(this.activeSwatch); michael@0: let newValue = swatch.callbacks.onCommit(); michael@0: if (typeof newValue !== "undefined") { michael@0: swatch.originalValue = newValue; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: destroy: function() { michael@0: this.swatches.clear(); michael@0: this.activeSwatch = null; michael@0: this.tooltip.off("keypress", this._onTooltipKeypress); michael@0: this.tooltip.destroy(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * The swatch color picker tooltip class is a specific class meant to be used michael@0: * along with output-parser's generated color swatches. michael@0: * It extends the parent SwatchBasedEditorTooltip class. michael@0: * It just wraps a standard Tooltip and sets its content with an instance of a michael@0: * color picker. michael@0: * michael@0: * @param {XULDocument} doc michael@0: */ michael@0: function SwatchColorPickerTooltip(doc) { michael@0: SwatchBasedEditorTooltip.call(this, doc); michael@0: michael@0: // Creating a spectrum instance. this.spectrum will always be a promise that michael@0: // resolves to the spectrum instance michael@0: this.spectrum = this.tooltip.setColorPickerContent([0, 0, 0, 1]); michael@0: this._onSpectrumColorChange = this._onSpectrumColorChange.bind(this); michael@0: this._openEyeDropper = this._openEyeDropper.bind(this); michael@0: } michael@0: michael@0: module.exports.SwatchColorPickerTooltip = SwatchColorPickerTooltip; michael@0: michael@0: SwatchColorPickerTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, { michael@0: /** michael@0: * Overriding the SwatchBasedEditorTooltip.show function to set spectrum's michael@0: * color. michael@0: */ michael@0: show: function() { michael@0: // Call then parent class' show function michael@0: SwatchBasedEditorTooltip.prototype.show.call(this); michael@0: // Then set spectrum's color and listen to color changes to preview them michael@0: if (this.activeSwatch) { michael@0: this.currentSwatchColor = this.activeSwatch.nextSibling; michael@0: let swatch = this.swatches.get(this.activeSwatch); michael@0: let color = this.activeSwatch.style.backgroundColor; michael@0: this.spectrum.then(spectrum => { michael@0: spectrum.off("changed", this._onSpectrumColorChange); michael@0: spectrum.rgb = this._colorToRgba(color); michael@0: spectrum.on("changed", this._onSpectrumColorChange); michael@0: spectrum.updateUI(); michael@0: }); michael@0: } michael@0: michael@0: let tooltipDoc = this.tooltip.content.contentDocument; michael@0: let eyeButton = tooltipDoc.querySelector("#eyedropper-button"); michael@0: eyeButton.addEventListener("click", this._openEyeDropper); michael@0: }, michael@0: michael@0: _onSpectrumColorChange: function(event, rgba, cssColor) { michael@0: this._selectColor(cssColor); michael@0: }, michael@0: michael@0: _selectColor: function(color) { michael@0: if (this.activeSwatch) { michael@0: this.activeSwatch.style.backgroundColor = color; michael@0: this.currentSwatchColor.textContent = color; michael@0: this.preview(color); michael@0: } michael@0: }, michael@0: michael@0: _openEyeDropper: function() { michael@0: let chromeWindow = this.tooltip.doc.defaultView.top; michael@0: let windowType = chromeWindow.document.documentElement michael@0: .getAttribute("windowtype"); michael@0: let toolboxWindow; michael@0: if (windowType != "navigator:browser") { michael@0: // this means the toolbox is in a seperate window. We need to make michael@0: // sure we'll be inspecting the browser window instead michael@0: toolboxWindow = chromeWindow; michael@0: chromeWindow = Services.wm.getMostRecentWindow("navigator:browser"); michael@0: chromeWindow.focus(); michael@0: } michael@0: let dropper = new Eyedropper(chromeWindow, { copyOnSelect: false }); michael@0: michael@0: dropper.once("select", (event, color) => { michael@0: if (toolboxWindow) { michael@0: toolboxWindow.focus(); michael@0: } michael@0: this._selectColor(color); michael@0: }); michael@0: michael@0: dropper.once("destroy", () => { michael@0: this.eyedropperOpen = false; michael@0: this.activeSwatch = null; michael@0: }) michael@0: michael@0: dropper.open(); michael@0: this.eyedropperOpen = true; michael@0: michael@0: // close the colorpicker tooltip so that only the eyedropper is open. michael@0: this.hide(); michael@0: michael@0: this.tooltip.emit("eyedropper-opened", dropper); michael@0: }, michael@0: michael@0: _colorToRgba: function(color) { michael@0: color = new colorUtils.CssColor(color); michael@0: let rgba = color._getRGBATuple(); michael@0: return [rgba.r, rgba.g, rgba.b, rgba.a]; michael@0: }, michael@0: michael@0: destroy: function() { michael@0: SwatchBasedEditorTooltip.prototype.destroy.call(this); michael@0: this.currentSwatchColor = null; michael@0: this.spectrum.then(spectrum => { michael@0: spectrum.off("changed", this._onSpectrumColorChange); michael@0: spectrum.destroy(); michael@0: }); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Internal util, checks whether a css declaration is a gradient michael@0: */ michael@0: function isGradientRule(property, value) { michael@0: return (property === "background" || property === "background-image") && michael@0: value.match(GRADIENT_RE); michael@0: } michael@0: michael@0: /** michael@0: * Internal util, checks whether a css declaration is a color michael@0: */ michael@0: function isColorOnly(property, value) { michael@0: return property === "background-color" || michael@0: property === "color" || michael@0: property.match(BORDERCOLOR_RE); michael@0: } michael@0: michael@0: /** michael@0: * L10N utility class michael@0: */ michael@0: function L10N() {} michael@0: L10N.prototype = {}; michael@0: michael@0: let l10n = new L10N(); michael@0: michael@0: loader.lazyGetter(L10N.prototype, "strings", () => { michael@0: return Services.strings.createBundle( michael@0: "chrome://browser/locale/devtools/inspector.properties"); michael@0: });