1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/shared/widgets/Tooltip.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1090 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const {Cc, Cu, Ci} = require("chrome"); 1.11 +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.12 +const IOService = Cc["@mozilla.org/network/io-service;1"] 1.13 + .getService(Ci.nsIIOService); 1.14 +const {Spectrum} = require("devtools/shared/widgets/Spectrum"); 1.15 +const EventEmitter = require("devtools/toolkit/event-emitter"); 1.16 +const {colorUtils} = require("devtools/css-color"); 1.17 +const Heritage = require("sdk/core/heritage"); 1.18 +const {CSSTransformPreviewer} = require("devtools/shared/widgets/CSSTransformPreviewer"); 1.19 +const {Eyedropper} = require("devtools/eyedropper/eyedropper"); 1.20 + 1.21 +Cu.import("resource://gre/modules/Services.jsm"); 1.22 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.23 + 1.24 +XPCOMUtils.defineLazyModuleGetter(this, "setNamedTimeout", 1.25 + "resource:///modules/devtools/ViewHelpers.jsm"); 1.26 +XPCOMUtils.defineLazyModuleGetter(this, "clearNamedTimeout", 1.27 + "resource:///modules/devtools/ViewHelpers.jsm"); 1.28 +XPCOMUtils.defineLazyModuleGetter(this, "VariablesView", 1.29 + "resource:///modules/devtools/VariablesView.jsm"); 1.30 +XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController", 1.31 + "resource:///modules/devtools/VariablesViewController.jsm"); 1.32 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.33 + "resource://gre/modules/Task.jsm"); 1.34 + 1.35 +const GRADIENT_RE = /\b(repeating-)?(linear|radial)-gradient\(((rgb|hsl)a?\(.+?\)|[^\)])+\)/gi; 1.36 +const BORDERCOLOR_RE = /^border-[-a-z]*color$/ig; 1.37 +const BORDER_RE = /^border(-(top|bottom|left|right))?$/ig; 1.38 +const XHTML_NS = "http://www.w3.org/1999/xhtml"; 1.39 +const SPECTRUM_FRAME = "chrome://browser/content/devtools/spectrum-frame.xhtml"; 1.40 +const ESCAPE_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE; 1.41 +const RETURN_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_RETURN; 1.42 +const POPUP_EVENTS = ["shown", "hidden", "showing", "hiding"]; 1.43 +const FONT_FAMILY_PREVIEW_TEXT = "(ABCabc123&@%)"; 1.44 + 1.45 +/** 1.46 + * Tooltip widget. 1.47 + * 1.48 + * This widget is intended at any tool that may need to show rich content in the 1.49 + * form of floating panels. 1.50 + * A common use case is image previewing in the CSS rule view, but more complex 1.51 + * use cases may include color pickers, object inspection, etc... 1.52 + * 1.53 + * Tooltips are based on XUL (namely XUL arrow-type <panel>s), and therefore 1.54 + * need a XUL Document to live in. 1.55 + * This is pretty much the only requirement they have on their environment. 1.56 + * 1.57 + * The way to use a tooltip is simply by instantiating a tooltip yourself and 1.58 + * attaching some content in it, or using one of the ready-made content types. 1.59 + * 1.60 + * A convenient `startTogglingOnHover` method may avoid having to register event 1.61 + * handlers yourself if the tooltip has to be shown when hovering over a 1.62 + * specific element or group of elements (which is usually the most common case) 1.63 + */ 1.64 + 1.65 +/** 1.66 + * Container used for dealing with optional parameters. 1.67 + * 1.68 + * @param {Object} defaults 1.69 + * An object with all default options {p1: v1, p2: v2, ...} 1.70 + * @param {Object} options 1.71 + * The actual values. 1.72 + */ 1.73 +function OptionsStore(defaults, options) { 1.74 + this.defaults = defaults || {}; 1.75 + this.options = options || {}; 1.76 +} 1.77 + 1.78 +OptionsStore.prototype = { 1.79 + /** 1.80 + * Get the value for a given option name. 1.81 + * @return {Object} Returns the value for that option, coming either for the 1.82 + * actual values that have been set in the constructor, or from the 1.83 + * defaults if that options was not specified. 1.84 + */ 1.85 + get: function(name) { 1.86 + if (typeof this.options[name] !== "undefined") { 1.87 + return this.options[name]; 1.88 + } else { 1.89 + return this.defaults[name]; 1.90 + } 1.91 + } 1.92 +}; 1.93 + 1.94 +/** 1.95 + * The low level structure of a tooltip is a XUL element (a <panel>). 1.96 + */ 1.97 +let PanelFactory = { 1.98 + /** 1.99 + * Get a new XUL panel instance. 1.100 + * @param {XULDocument} doc 1.101 + * The XUL document to put that panel into 1.102 + * @param {OptionsStore} options 1.103 + * An options store to get some configuration from 1.104 + */ 1.105 + get: function(doc, options) { 1.106 + // Create the tooltip 1.107 + let panel = doc.createElement("panel"); 1.108 + panel.setAttribute("hidden", true); 1.109 + panel.setAttribute("ignorekeys", true); 1.110 + panel.setAttribute("animate", false); 1.111 + 1.112 + panel.setAttribute("consumeoutsideclicks", options.get("consumeOutsideClick")); 1.113 + panel.setAttribute("noautofocus", options.get("noAutoFocus")); 1.114 + panel.setAttribute("type", "arrow"); 1.115 + panel.setAttribute("level", "top"); 1.116 + 1.117 + panel.setAttribute("class", "devtools-tooltip theme-tooltip-panel"); 1.118 + doc.querySelector("window").appendChild(panel); 1.119 + 1.120 + return panel; 1.121 + } 1.122 +}; 1.123 + 1.124 +/** 1.125 + * Tooltip class. 1.126 + * 1.127 + * Basic usage: 1.128 + * let t = new Tooltip(xulDoc); 1.129 + * t.content = someXulContent; 1.130 + * t.show(); 1.131 + * t.hide(); 1.132 + * t.destroy(); 1.133 + * 1.134 + * Better usage: 1.135 + * let t = new Tooltip(xulDoc); 1.136 + * t.startTogglingOnHover(container, target => { 1.137 + * if (<condition based on target>) { 1.138 + * t.setImageContent("http://image.png"); 1.139 + * return true; 1.140 + * } 1.141 + * }); 1.142 + * t.destroy(); 1.143 + * 1.144 + * @param {XULDocument} doc 1.145 + * The XUL document hosting this tooltip 1.146 + * @param {Object} options 1.147 + * Optional options that give options to consumers: 1.148 + * - consumeOutsideClick {Boolean} Wether the first click outside of the 1.149 + * tooltip should close the tooltip and be consumed or not. 1.150 + * Defaults to false. 1.151 + * - closeOnKeys {Array} An array of key codes that should close the 1.152 + * tooltip. Defaults to [27] (escape key). 1.153 + * - closeOnEvents [{emitter: {Object}, event: {String}, useCapture: {Boolean}}] 1.154 + * Provide an optional list of emitter objects and event names here to 1.155 + * trigger the closing of the tooltip when these events are fired by the 1.156 + * emitters. The emitter objects should either implement on/off(event, cb) 1.157 + * or addEventListener/removeEventListener(event, cb). Defaults to []. 1.158 + * For instance, the following would close the tooltip whenever the 1.159 + * toolbox selects a new tool and when a DOM node gets scrolled: 1.160 + * new Tooltip(doc, { 1.161 + * closeOnEvents: [ 1.162 + * {emitter: toolbox, event: "select"}, 1.163 + * {emitter: myContainer, event: "scroll", useCapture: true} 1.164 + * ] 1.165 + * }); 1.166 + * - noAutoFocus {Boolean} Should the focus automatically go to the panel 1.167 + * when it opens. Defaults to true. 1.168 + * 1.169 + * Fires these events: 1.170 + * - showing : just before the tooltip shows 1.171 + * - shown : when the tooltip is shown 1.172 + * - hiding : just before the tooltip closes 1.173 + * - hidden : when the tooltip gets hidden 1.174 + * - keypress : when any key gets pressed, with keyCode 1.175 + */ 1.176 +function Tooltip(doc, options) { 1.177 + EventEmitter.decorate(this); 1.178 + 1.179 + this.doc = doc; 1.180 + this.options = new OptionsStore({ 1.181 + consumeOutsideClick: false, 1.182 + closeOnKeys: [ESCAPE_KEYCODE], 1.183 + noAutoFocus: true, 1.184 + closeOnEvents: [] 1.185 + }, options); 1.186 + this.panel = PanelFactory.get(doc, this.options); 1.187 + 1.188 + // Used for namedTimeouts in the mouseover handling 1.189 + this.uid = "tooltip-" + Date.now(); 1.190 + 1.191 + // Emit show/hide events 1.192 + for (let event of POPUP_EVENTS) { 1.193 + this["_onPopup" + event] = ((e) => { 1.194 + return () => this.emit(e); 1.195 + })(event); 1.196 + this.panel.addEventListener("popup" + event, 1.197 + this["_onPopup" + event], false); 1.198 + } 1.199 + 1.200 + // Listen to keypress events to close the tooltip if configured to do so 1.201 + let win = this.doc.querySelector("window"); 1.202 + this._onKeyPress = event => { 1.203 + this.emit("keypress", event.keyCode); 1.204 + if (this.options.get("closeOnKeys").indexOf(event.keyCode) !== -1) { 1.205 + if (!this.panel.hidden) { 1.206 + event.stopPropagation(); 1.207 + } 1.208 + this.hide(); 1.209 + } 1.210 + }; 1.211 + win.addEventListener("keypress", this._onKeyPress, false); 1.212 + 1.213 + // Listen to custom emitters' events to close the tooltip 1.214 + this.hide = this.hide.bind(this); 1.215 + let closeOnEvents = this.options.get("closeOnEvents"); 1.216 + for (let {emitter, event, useCapture} of closeOnEvents) { 1.217 + for (let add of ["addEventListener", "on"]) { 1.218 + if (add in emitter) { 1.219 + emitter[add](event, this.hide, useCapture); 1.220 + break; 1.221 + } 1.222 + } 1.223 + } 1.224 +} 1.225 + 1.226 +module.exports.Tooltip = Tooltip; 1.227 + 1.228 +Tooltip.prototype = { 1.229 + defaultPosition: "before_start", 1.230 + defaultOffsetX: 0, // px 1.231 + defaultOffsetY: 0, // px 1.232 + defaultShowDelay: 50, // ms 1.233 + 1.234 + /** 1.235 + * Show the tooltip. It might be wise to append some content first if you 1.236 + * don't want the tooltip to be empty. You may access the content of the 1.237 + * tooltip by setting a XUL node to t.content. 1.238 + * @param {node} anchor 1.239 + * Which node should the tooltip be shown on 1.240 + * @param {string} position [optional] 1.241 + * Optional tooltip position. Defaults to before_start 1.242 + * https://developer.mozilla.org/en-US/docs/XUL/PopupGuide/Positioning 1.243 + * @param {number} x, y [optional] 1.244 + * The left and top offset coordinates, in pixels. 1.245 + */ 1.246 + show: function(anchor, 1.247 + position = this.defaultPosition, 1.248 + x = this.defaultOffsetX, 1.249 + y = this.defaultOffsetY) { 1.250 + this.panel.hidden = false; 1.251 + this.panel.openPopup(anchor, position, x, y); 1.252 + }, 1.253 + 1.254 + /** 1.255 + * Hide the tooltip 1.256 + */ 1.257 + hide: function() { 1.258 + this.panel.hidden = true; 1.259 + this.panel.hidePopup(); 1.260 + }, 1.261 + 1.262 + isShown: function() { 1.263 + return this.panel.state !== "closed" && this.panel.state !== "hiding"; 1.264 + }, 1.265 + 1.266 + setSize: function(width, height) { 1.267 + this.panel.sizeTo(width, height); 1.268 + }, 1.269 + 1.270 + /** 1.271 + * Empty the tooltip's content 1.272 + */ 1.273 + empty: function() { 1.274 + while (this.panel.hasChildNodes()) { 1.275 + this.panel.removeChild(this.panel.firstChild); 1.276 + } 1.277 + }, 1.278 + 1.279 + /** 1.280 + * Gets this panel's visibility state. 1.281 + * @return boolean 1.282 + */ 1.283 + isHidden: function() { 1.284 + return this.panel.state == "closed" || this.panel.state == "hiding"; 1.285 + }, 1.286 + 1.287 + /** 1.288 + * Gets if this panel has any child nodes. 1.289 + * @return boolean 1.290 + */ 1.291 + isEmpty: function() { 1.292 + return !this.panel.hasChildNodes(); 1.293 + }, 1.294 + 1.295 + /** 1.296 + * Get rid of references and event listeners 1.297 + */ 1.298 + destroy: function () { 1.299 + this.hide(); 1.300 + 1.301 + for (let event of POPUP_EVENTS) { 1.302 + this.panel.removeEventListener("popup" + event, 1.303 + this["_onPopup" + event], false); 1.304 + } 1.305 + 1.306 + let win = this.doc.querySelector("window"); 1.307 + win.removeEventListener("keypress", this._onKeyPress, false); 1.308 + 1.309 + let closeOnEvents = this.options.get("closeOnEvents"); 1.310 + for (let {emitter, event, useCapture} of closeOnEvents) { 1.311 + for (let remove of ["removeEventListener", "off"]) { 1.312 + if (remove in emitter) { 1.313 + emitter[remove](event, this.hide, useCapture); 1.314 + break; 1.315 + } 1.316 + } 1.317 + } 1.318 + 1.319 + this.content = null; 1.320 + 1.321 + if (this._basedNode) { 1.322 + this.stopTogglingOnHover(); 1.323 + } 1.324 + 1.325 + this.doc = null; 1.326 + 1.327 + this.panel.remove(); 1.328 + this.panel = null; 1.329 + }, 1.330 + 1.331 + /** 1.332 + * Show/hide the tooltip when the mouse hovers over particular nodes. 1.333 + * 1.334 + * 2 Ways to make this work: 1.335 + * - Provide a single node to attach the tooltip to, as the baseNode, and 1.336 + * omit the second targetNodeCb argument 1.337 + * - Provide a baseNode that is the container of possibly numerous children 1.338 + * elements that may receive a tooltip. In this case, provide the second 1.339 + * targetNodeCb argument to decide wether or not a child should receive 1.340 + * a tooltip. 1.341 + * 1.342 + * This works by tracking mouse movements on a base container node (baseNode) 1.343 + * and showing the tooltip when the mouse stops moving. The targetNodeCb 1.344 + * callback is used to know whether or not the particular element being 1.345 + * hovered over should indeed receive the tooltip. If you don't provide it 1.346 + * it's equivalent to a function that always returns true. 1.347 + * 1.348 + * Note that if you call this function a second time, it will itself call 1.349 + * stopTogglingOnHover before adding mouse tracking listeners again. 1.350 + * 1.351 + * @param {node} baseNode 1.352 + * The container for all target nodes 1.353 + * @param {Function} targetNodeCb 1.354 + * A function that accepts a node argument and returns true or false 1.355 + * (or a promise that resolves or rejects) to signify if the tooltip 1.356 + * should be shown on that node or not. 1.357 + * Additionally, the function receives a second argument which is the 1.358 + * tooltip instance itself, to be used to add/modify the content of the 1.359 + * tooltip if needed. If omitted, the tooltip will be shown everytime. 1.360 + * @param {Number} showDelay 1.361 + * An optional delay that will be observed before showing the tooltip. 1.362 + * Defaults to this.defaultShowDelay. 1.363 + */ 1.364 + startTogglingOnHover: function(baseNode, targetNodeCb, showDelay=this.defaultShowDelay) { 1.365 + if (this._basedNode) { 1.366 + this.stopTogglingOnHover(); 1.367 + } 1.368 + 1.369 + this._basedNode = baseNode; 1.370 + this._showDelay = showDelay; 1.371 + this._targetNodeCb = targetNodeCb || (() => true); 1.372 + 1.373 + this._onBaseNodeMouseMove = this._onBaseNodeMouseMove.bind(this); 1.374 + this._onBaseNodeMouseLeave = this._onBaseNodeMouseLeave.bind(this); 1.375 + 1.376 + baseNode.addEventListener("mousemove", this._onBaseNodeMouseMove, false); 1.377 + baseNode.addEventListener("mouseleave", this._onBaseNodeMouseLeave, false); 1.378 + }, 1.379 + 1.380 + /** 1.381 + * If the startTogglingOnHover function has been used previously, and you want 1.382 + * to get rid of this behavior, then call this function to remove the mouse 1.383 + * movement tracking 1.384 + */ 1.385 + stopTogglingOnHover: function() { 1.386 + clearNamedTimeout(this.uid); 1.387 + 1.388 + this._basedNode.removeEventListener("mousemove", 1.389 + this._onBaseNodeMouseMove, false); 1.390 + this._basedNode.removeEventListener("mouseleave", 1.391 + this._onBaseNodeMouseLeave, false); 1.392 + 1.393 + this._basedNode = null; 1.394 + this._targetNodeCb = null; 1.395 + this._lastHovered = null; 1.396 + }, 1.397 + 1.398 + _onBaseNodeMouseMove: function(event) { 1.399 + if (event.target !== this._lastHovered) { 1.400 + this.hide(); 1.401 + this._lastHovered = event.target; 1.402 + setNamedTimeout(this.uid, this._showDelay, () => { 1.403 + this.isValidHoverTarget(event.target).then(target => { 1.404 + this.show(target); 1.405 + }); 1.406 + }); 1.407 + } 1.408 + }, 1.409 + 1.410 + /** 1.411 + * Is the given target DOMNode a valid node for toggling the tooltip on hover. 1.412 + * This delegates to the user-defined _targetNodeCb callback. 1.413 + * @return a promise that resolves or rejects depending if the tooltip should 1.414 + * be shown or not. If it resolves, it does to the actual anchor to be used 1.415 + */ 1.416 + isValidHoverTarget: function(target) { 1.417 + // Execute the user-defined callback which should return either true/false 1.418 + // or a promise that resolves or rejects 1.419 + let res = this._targetNodeCb(target, this); 1.420 + 1.421 + // The callback can additionally return a DOMNode to replace the anchor of 1.422 + // the tooltip when shown 1.423 + if (res && res.then) { 1.424 + return res.then(arg => { 1.425 + return arg instanceof Ci.nsIDOMNode ? arg : target; 1.426 + }, () => { 1.427 + return false; 1.428 + }); 1.429 + } else { 1.430 + let newTarget = res instanceof Ci.nsIDOMNode ? res : target; 1.431 + return res ? promise.resolve(newTarget) : promise.reject(false); 1.432 + } 1.433 + }, 1.434 + 1.435 + _onBaseNodeMouseLeave: function() { 1.436 + clearNamedTimeout(this.uid); 1.437 + this._lastHovered = null; 1.438 + this.hide(); 1.439 + }, 1.440 + 1.441 + /** 1.442 + * Set the content of this tooltip. Will first empty the tooltip and then 1.443 + * append the new content element. 1.444 + * Consider using one of the set<type>Content() functions instead. 1.445 + * @param {node} content 1.446 + * A node that can be appended in the tooltip XUL element 1.447 + */ 1.448 + set content(content) { 1.449 + if (this.content == content) { 1.450 + return; 1.451 + } 1.452 + 1.453 + this.empty(); 1.454 + this.panel.removeAttribute("clamped-dimensions"); 1.455 + 1.456 + if (content) { 1.457 + this.panel.appendChild(content); 1.458 + } 1.459 + }, 1.460 + 1.461 + get content() { 1.462 + return this.panel.firstChild; 1.463 + }, 1.464 + 1.465 + /** 1.466 + * Sets some text as the content of this tooltip. 1.467 + * 1.468 + * @param {array} messages 1.469 + * A list of text messages. 1.470 + * @param {string} messagesClass [optional] 1.471 + * A style class for the text messages. 1.472 + * @param {string} containerClass [optional] 1.473 + * A style class for the text messages container. 1.474 + * @param {boolean} isAlertTooltip [optional] 1.475 + * Pass true to add an alert image for your tooltip. 1.476 + */ 1.477 + setTextContent: function( 1.478 + { 1.479 + messages, 1.480 + messagesClass, 1.481 + containerClass, 1.482 + isAlertTooltip 1.483 + }, 1.484 + extraButtons = []) { 1.485 + messagesClass = messagesClass || "default-tooltip-simple-text-colors"; 1.486 + containerClass = containerClass || "default-tooltip-simple-text-colors"; 1.487 + 1.488 + let vbox = this.doc.createElement("vbox"); 1.489 + vbox.className = "devtools-tooltip-simple-text-container " + containerClass; 1.490 + vbox.setAttribute("flex", "1"); 1.491 + 1.492 + for (let text of messages) { 1.493 + let description = this.doc.createElement("description"); 1.494 + description.setAttribute("flex", "1"); 1.495 + description.className = "devtools-tooltip-simple-text " + messagesClass; 1.496 + description.textContent = text; 1.497 + vbox.appendChild(description); 1.498 + } 1.499 + 1.500 + for (let { label, className, command } of extraButtons) { 1.501 + let button = this.doc.createElement("button"); 1.502 + button.className = className; 1.503 + button.setAttribute("label", label); 1.504 + button.addEventListener("command", command); 1.505 + vbox.appendChild(button); 1.506 + } 1.507 + 1.508 + if (isAlertTooltip) { 1.509 + let hbox = this.doc.createElement("hbox"); 1.510 + hbox.setAttribute("align", "start"); 1.511 + 1.512 + let alertImg = this.doc.createElement("image"); 1.513 + alertImg.className = "devtools-tooltip-alert-icon"; 1.514 + hbox.appendChild(alertImg); 1.515 + hbox.appendChild(vbox); 1.516 + this.content = hbox; 1.517 + } else { 1.518 + this.content = vbox; 1.519 + } 1.520 + }, 1.521 + 1.522 + /** 1.523 + * Fill the tooltip with a variables view, inspecting an object via its 1.524 + * corresponding object actor, as specified in the remote debugging protocol. 1.525 + * 1.526 + * @param {object} objectActor 1.527 + * The value grip for the object actor. 1.528 + * @param {object} viewOptions [optional] 1.529 + * Options for the variables view visualization. 1.530 + * @param {object} controllerOptions [optional] 1.531 + * Options for the variables view controller. 1.532 + * @param {object} relayEvents [optional] 1.533 + * A collection of events to listen on the variables view widget. 1.534 + * For example, { fetched: () => ... } 1.535 + * @param {boolean} reuseCachedWidget [optional] 1.536 + * Pass false to instantiate a brand new widget for this variable. 1.537 + * Otherwise, if a variable was previously inspected, its widget 1.538 + * will be reused. 1.539 + * @param {Toolbox} toolbox [optional] 1.540 + * Pass the instance of the current toolbox if you want the variables 1.541 + * view widget to allow highlighting and selection of DOM nodes 1.542 + */ 1.543 + setVariableContent: function( 1.544 + objectActor, 1.545 + viewOptions = {}, 1.546 + controllerOptions = {}, 1.547 + relayEvents = {}, 1.548 + extraButtons = [], 1.549 + toolbox = null) { 1.550 + 1.551 + let vbox = this.doc.createElement("vbox"); 1.552 + vbox.className = "devtools-tooltip-variables-view-box"; 1.553 + vbox.setAttribute("flex", "1"); 1.554 + 1.555 + let innerbox = this.doc.createElement("vbox"); 1.556 + innerbox.className = "devtools-tooltip-variables-view-innerbox"; 1.557 + innerbox.setAttribute("flex", "1"); 1.558 + vbox.appendChild(innerbox); 1.559 + 1.560 + for (let { label, className, command } of extraButtons) { 1.561 + let button = this.doc.createElement("button"); 1.562 + button.className = className; 1.563 + button.setAttribute("label", label); 1.564 + button.addEventListener("command", command); 1.565 + vbox.appendChild(button); 1.566 + } 1.567 + 1.568 + let widget = new VariablesView(innerbox, viewOptions); 1.569 + 1.570 + // If a toolbox was provided, link it to the vview 1.571 + if (toolbox) { 1.572 + widget.toolbox = toolbox; 1.573 + } 1.574 + 1.575 + // Analyzing state history isn't useful with transient object inspectors. 1.576 + widget.commitHierarchy = () => {}; 1.577 + 1.578 + for (let e in relayEvents) widget.on(e, relayEvents[e]); 1.579 + VariablesViewController.attach(widget, controllerOptions); 1.580 + 1.581 + // Some of the view options are allowed to change between uses. 1.582 + widget.searchPlaceholder = viewOptions.searchPlaceholder; 1.583 + widget.searchEnabled = viewOptions.searchEnabled; 1.584 + 1.585 + // Use the object actor's grip to display it as a variable in the widget. 1.586 + // The controller options are allowed to change between uses. 1.587 + widget.controller.setSingleVariable( 1.588 + { objectActor: objectActor }, controllerOptions); 1.589 + 1.590 + this.content = vbox; 1.591 + this.panel.setAttribute("clamped-dimensions", ""); 1.592 + }, 1.593 + 1.594 + /** 1.595 + * Uses the provided inspectorFront's getImageDataFromURL method to resolve 1.596 + * the relative URL on the server-side, in the page context, and then sets the 1.597 + * tooltip content with the resulting image just like |setImageContent| does. 1.598 + * @return a promise that resolves when the image is shown in the tooltip or 1.599 + * resolves when the broken image tooltip content is ready, but never rejects. 1.600 + */ 1.601 + setRelativeImageContent: Task.async(function*(imageUrl, inspectorFront, maxDim) { 1.602 + if (imageUrl.startsWith("data:")) { 1.603 + // If the imageUrl already is a data-url, save ourselves a round-trip 1.604 + this.setImageContent(imageUrl, {maxDim: maxDim}); 1.605 + } else if (inspectorFront) { 1.606 + try { 1.607 + let {data, size} = yield inspectorFront.getImageDataFromURL(imageUrl, maxDim); 1.608 + size.maxDim = maxDim; 1.609 + let str = yield data.string(); 1.610 + this.setImageContent(str, size); 1.611 + } catch (e) { 1.612 + this.setBrokenImageContent(); 1.613 + } 1.614 + } 1.615 + }), 1.616 + 1.617 + /** 1.618 + * Fill the tooltip with a message explaining the the image is missing 1.619 + */ 1.620 + setBrokenImageContent: function() { 1.621 + this.setTextContent({ 1.622 + messages: [l10n.strings.GetStringFromName("previewTooltip.image.brokenImage")] 1.623 + }); 1.624 + }, 1.625 + 1.626 + /** 1.627 + * Fill the tooltip with an image and add the image dimension at the bottom. 1.628 + * 1.629 + * Only use this for absolute URLs that can be queried from the devtools 1.630 + * client-side. For relative URLs, use |setRelativeImageContent|. 1.631 + * 1.632 + * @param {string} imageUrl 1.633 + * The url to load the image from 1.634 + * @param {Object} options 1.635 + * The following options are supported: 1.636 + * - resized : whether or not the image identified by imageUrl has been 1.637 + * resized before this function was called. 1.638 + * - naturalWidth/naturalHeight : the original size of the image before 1.639 + * it was resized, if if was resized before this function was called. 1.640 + * If not provided, will be measured on the loaded image. 1.641 + * - maxDim : if the image should be resized before being shown, pass 1.642 + * a number here 1.643 + */ 1.644 + setImageContent: function(imageUrl, options={}) { 1.645 + if (!imageUrl) { 1.646 + return; 1.647 + } 1.648 + 1.649 + // Main container 1.650 + let vbox = this.doc.createElement("vbox"); 1.651 + vbox.setAttribute("align", "center"); 1.652 + 1.653 + // Display the image 1.654 + let image = this.doc.createElement("image"); 1.655 + image.setAttribute("src", imageUrl); 1.656 + if (options.maxDim) { 1.657 + image.style.maxWidth = options.maxDim + "px"; 1.658 + image.style.maxHeight = options.maxDim + "px"; 1.659 + } 1.660 + vbox.appendChild(image); 1.661 + 1.662 + // Dimension label 1.663 + let label = this.doc.createElement("label"); 1.664 + label.classList.add("devtools-tooltip-caption"); 1.665 + label.classList.add("theme-comment"); 1.666 + if (options.naturalWidth && options.naturalHeight) { 1.667 + label.textContent = this._getImageDimensionLabel(options.naturalWidth, 1.668 + options.naturalHeight); 1.669 + } else { 1.670 + // If no dimensions were provided, load the image to get them 1.671 + label.textContent = l10n.strings.GetStringFromName("previewTooltip.image.brokenImage"); 1.672 + let imgObj = new this.doc.defaultView.Image(); 1.673 + imgObj.src = imageUrl; 1.674 + imgObj.onload = () => { 1.675 + imgObj.onload = null; 1.676 + label.textContent = this._getImageDimensionLabel(imgObj.naturalWidth, 1.677 + imgObj.naturalHeight); 1.678 + } 1.679 + } 1.680 + vbox.appendChild(label); 1.681 + 1.682 + this.content = vbox; 1.683 + }, 1.684 + 1.685 + _getImageDimensionLabel: (w, h) => w + " x " + h, 1.686 + 1.687 + /** 1.688 + * Fill the tooltip with a new instance of the spectrum color picker widget 1.689 + * initialized with the given color, and return a promise that resolves to 1.690 + * the instance of spectrum 1.691 + */ 1.692 + setColorPickerContent: function(color) { 1.693 + let def = promise.defer(); 1.694 + 1.695 + // Create an iframe to contain spectrum 1.696 + let iframe = this.doc.createElementNS(XHTML_NS, "iframe"); 1.697 + iframe.setAttribute("transparent", true); 1.698 + iframe.setAttribute("width", "210"); 1.699 + iframe.setAttribute("height", "216"); 1.700 + iframe.setAttribute("flex", "1"); 1.701 + iframe.setAttribute("class", "devtools-tooltip-iframe"); 1.702 + 1.703 + let panel = this.panel; 1.704 + let xulWin = this.doc.ownerGlobal; 1.705 + 1.706 + // Wait for the load to initialize spectrum 1.707 + function onLoad() { 1.708 + iframe.removeEventListener("load", onLoad, true); 1.709 + let win = iframe.contentWindow.wrappedJSObject; 1.710 + 1.711 + let container = win.document.getElementById("spectrum"); 1.712 + let spectrum = new Spectrum(container, color); 1.713 + 1.714 + function finalizeSpectrum() { 1.715 + spectrum.show(); 1.716 + def.resolve(spectrum); 1.717 + } 1.718 + 1.719 + // Finalize spectrum's init when the tooltip becomes visible 1.720 + if (panel.state == "open") { 1.721 + finalizeSpectrum(); 1.722 + } 1.723 + else { 1.724 + panel.addEventListener("popupshown", function shown() { 1.725 + panel.removeEventListener("popupshown", shown, true); 1.726 + finalizeSpectrum(); 1.727 + }, true); 1.728 + } 1.729 + } 1.730 + iframe.addEventListener("load", onLoad, true); 1.731 + iframe.setAttribute("src", SPECTRUM_FRAME); 1.732 + 1.733 + // Put the iframe in the tooltip 1.734 + this.content = iframe; 1.735 + 1.736 + return def.promise; 1.737 + }, 1.738 + 1.739 + /** 1.740 + * Set the content of the tooltip to be the result of CSSTransformPreviewer. 1.741 + * Meaning a canvas previewing a css transformation. 1.742 + * 1.743 + * @param {String} transform 1.744 + * The CSS transform value (e.g. "rotate(45deg) translateX(50px)") 1.745 + * @param {PageStyleActor} pageStyle 1.746 + * An instance of the PageStyleActor that will be used to retrieve 1.747 + * computed styles 1.748 + * @param {NodeActor} node 1.749 + * The NodeActor for the currently selected node 1.750 + * @return A promise that resolves when the tooltip content is ready, or 1.751 + * rejects if no transform is provided or the transform is invalid 1.752 + */ 1.753 + setCssTransformContent: Task.async(function*(transform, pageStyle, node) { 1.754 + if (!transform) { 1.755 + throw "Missing transform"; 1.756 + } 1.757 + 1.758 + // Look into the computed styles to find the width and height and possibly 1.759 + // the origin if it hadn't been provided 1.760 + let styles = yield pageStyle.getComputed(node, { 1.761 + filter: "user", 1.762 + markMatched: false, 1.763 + onlyMatched: false 1.764 + }); 1.765 + 1.766 + let origin = styles["transform-origin"].value; 1.767 + let width = parseInt(styles["width"].value); 1.768 + let height = parseInt(styles["height"].value); 1.769 + 1.770 + let root = this.doc.createElementNS(XHTML_NS, "div"); 1.771 + let previewer = new CSSTransformPreviewer(root); 1.772 + this.content = root; 1.773 + if (!previewer.preview(transform, origin, width, height)) { 1.774 + throw "Invalid transform"; 1.775 + } 1.776 + }), 1.777 + 1.778 + /** 1.779 + * Set the content of the tooltip to display a font family preview. 1.780 + * This is based on Lea Verou's Dablet. See https://github.com/LeaVerou/dabblet 1.781 + * for more info. 1.782 + * @param {String} font The font family value. 1.783 + */ 1.784 + setFontFamilyContent: function(font) { 1.785 + if (!font) { 1.786 + return; 1.787 + } 1.788 + 1.789 + // Main container 1.790 + let vbox = this.doc.createElement("vbox"); 1.791 + vbox.setAttribute("flex", "1"); 1.792 + 1.793 + // Display the font family previewer 1.794 + let previewer = this.doc.createElement("description"); 1.795 + previewer.setAttribute("flex", "1"); 1.796 + previewer.style.fontFamily = font; 1.797 + previewer.classList.add("devtools-tooltip-font-previewer-text"); 1.798 + previewer.textContent = FONT_FAMILY_PREVIEW_TEXT; 1.799 + vbox.appendChild(previewer); 1.800 + 1.801 + this.content = vbox; 1.802 + } 1.803 +}; 1.804 + 1.805 +/** 1.806 + * Base class for all (color, gradient, ...)-swatch based value editors inside 1.807 + * tooltips 1.808 + * 1.809 + * @param {XULDocument} doc 1.810 + */ 1.811 +function SwatchBasedEditorTooltip(doc) { 1.812 + // Creating a tooltip instance 1.813 + // This one will consume outside clicks as it makes more sense to let the user 1.814 + // close the tooltip by clicking out 1.815 + // It will also close on <escape> and <enter> 1.816 + this.tooltip = new Tooltip(doc, { 1.817 + consumeOutsideClick: true, 1.818 + closeOnKeys: [ESCAPE_KEYCODE, RETURN_KEYCODE], 1.819 + noAutoFocus: false 1.820 + }); 1.821 + 1.822 + // By default, swatch-based editor tooltips revert value change on <esc> and 1.823 + // commit value change on <enter> 1.824 + this._onTooltipKeypress = (event, code) => { 1.825 + if (code === ESCAPE_KEYCODE) { 1.826 + this.revert(); 1.827 + } else if (code === RETURN_KEYCODE) { 1.828 + this.commit(); 1.829 + } 1.830 + }; 1.831 + this.tooltip.on("keypress", this._onTooltipKeypress); 1.832 + 1.833 + // All target swatches are kept in a map, indexed by swatch DOM elements 1.834 + this.swatches = new Map(); 1.835 + 1.836 + // When a swatch is clicked, and for as long as the tooltip is shown, the 1.837 + // activeSwatch property will hold the reference to the swatch DOM element 1.838 + // that was clicked 1.839 + this.activeSwatch = null; 1.840 + 1.841 + this._onSwatchClick = this._onSwatchClick.bind(this); 1.842 +} 1.843 + 1.844 +SwatchBasedEditorTooltip.prototype = { 1.845 + show: function() { 1.846 + if (this.activeSwatch) { 1.847 + this.tooltip.show(this.activeSwatch, "topcenter bottomleft"); 1.848 + this.tooltip.once("hidden", () => { 1.849 + if (!this.eyedropperOpen) { 1.850 + this.activeSwatch = null; 1.851 + } 1.852 + }); 1.853 + } 1.854 + }, 1.855 + 1.856 + hide: function() { 1.857 + this.tooltip.hide(); 1.858 + }, 1.859 + 1.860 + /** 1.861 + * Add a new swatch DOM element to the list of swatch elements this editor 1.862 + * tooltip knows about. That means from now on, clicking on that swatch will 1.863 + * toggle the editor. 1.864 + * 1.865 + * @param {node} swatchEl 1.866 + * The element to add 1.867 + * @param {object} callbacks 1.868 + * Callbacks that will be executed when the editor wants to preview a 1.869 + * value change, or revert a change, or commit a change. 1.870 + * - onPreview: will be called when one of the sub-classes calls preview 1.871 + * - onRevert: will be called when the user ESCapes out of the tooltip 1.872 + * - onCommit: will be called when the user presses ENTER or clicks 1.873 + * outside the tooltip. If the user-defined onCommit returns a value, 1.874 + * it will be used to replace originalValue, so that the swatch-based 1.875 + * tooltip always knows what is the current originalValue and can use 1.876 + * it when reverting 1.877 + * @param {object} originalValue 1.878 + * The original value before the editor in the tooltip makes changes 1.879 + * This can be of any type, and will be passed, as is, in the revert 1.880 + * callback 1.881 + */ 1.882 + addSwatch: function(swatchEl, callbacks={}, originalValue) { 1.883 + if (!callbacks.onPreview) callbacks.onPreview = function() {}; 1.884 + if (!callbacks.onRevert) callbacks.onRevert = function() {}; 1.885 + if (!callbacks.onCommit) callbacks.onCommit = function() {}; 1.886 + 1.887 + this.swatches.set(swatchEl, { 1.888 + callbacks: callbacks, 1.889 + originalValue: originalValue 1.890 + }); 1.891 + swatchEl.addEventListener("click", this._onSwatchClick, false); 1.892 + }, 1.893 + 1.894 + removeSwatch: function(swatchEl) { 1.895 + if (this.swatches.has(swatchEl)) { 1.896 + if (this.activeSwatch === swatchEl) { 1.897 + this.hide(); 1.898 + this.activeSwatch = null; 1.899 + } 1.900 + swatchEl.removeEventListener("click", this._onSwatchClick, false); 1.901 + this.swatches.delete(swatchEl); 1.902 + } 1.903 + }, 1.904 + 1.905 + _onSwatchClick: function(event) { 1.906 + let swatch = this.swatches.get(event.target); 1.907 + if (swatch) { 1.908 + this.activeSwatch = event.target; 1.909 + this.show(); 1.910 + event.stopPropagation(); 1.911 + } 1.912 + }, 1.913 + 1.914 + /** 1.915 + * Not called by this parent class, needs to be taken care of by sub-classes 1.916 + */ 1.917 + preview: function(value) { 1.918 + if (this.activeSwatch) { 1.919 + let swatch = this.swatches.get(this.activeSwatch); 1.920 + swatch.callbacks.onPreview(value); 1.921 + } 1.922 + }, 1.923 + 1.924 + /** 1.925 + * This parent class only calls this on <esc> keypress 1.926 + */ 1.927 + revert: function() { 1.928 + if (this.activeSwatch) { 1.929 + let swatch = this.swatches.get(this.activeSwatch); 1.930 + swatch.callbacks.onRevert(swatch.originalValue); 1.931 + } 1.932 + }, 1.933 + 1.934 + /** 1.935 + * This parent class only calls this on <enter> keypress 1.936 + */ 1.937 + commit: function() { 1.938 + if (this.activeSwatch) { 1.939 + let swatch = this.swatches.get(this.activeSwatch); 1.940 + let newValue = swatch.callbacks.onCommit(); 1.941 + if (typeof newValue !== "undefined") { 1.942 + swatch.originalValue = newValue; 1.943 + } 1.944 + } 1.945 + }, 1.946 + 1.947 + destroy: function() { 1.948 + this.swatches.clear(); 1.949 + this.activeSwatch = null; 1.950 + this.tooltip.off("keypress", this._onTooltipKeypress); 1.951 + this.tooltip.destroy(); 1.952 + } 1.953 +}; 1.954 + 1.955 +/** 1.956 + * The swatch color picker tooltip class is a specific class meant to be used 1.957 + * along with output-parser's generated color swatches. 1.958 + * It extends the parent SwatchBasedEditorTooltip class. 1.959 + * It just wraps a standard Tooltip and sets its content with an instance of a 1.960 + * color picker. 1.961 + * 1.962 + * @param {XULDocument} doc 1.963 + */ 1.964 +function SwatchColorPickerTooltip(doc) { 1.965 + SwatchBasedEditorTooltip.call(this, doc); 1.966 + 1.967 + // Creating a spectrum instance. this.spectrum will always be a promise that 1.968 + // resolves to the spectrum instance 1.969 + this.spectrum = this.tooltip.setColorPickerContent([0, 0, 0, 1]); 1.970 + this._onSpectrumColorChange = this._onSpectrumColorChange.bind(this); 1.971 + this._openEyeDropper = this._openEyeDropper.bind(this); 1.972 +} 1.973 + 1.974 +module.exports.SwatchColorPickerTooltip = SwatchColorPickerTooltip; 1.975 + 1.976 +SwatchColorPickerTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, { 1.977 + /** 1.978 + * Overriding the SwatchBasedEditorTooltip.show function to set spectrum's 1.979 + * color. 1.980 + */ 1.981 + show: function() { 1.982 + // Call then parent class' show function 1.983 + SwatchBasedEditorTooltip.prototype.show.call(this); 1.984 + // Then set spectrum's color and listen to color changes to preview them 1.985 + if (this.activeSwatch) { 1.986 + this.currentSwatchColor = this.activeSwatch.nextSibling; 1.987 + let swatch = this.swatches.get(this.activeSwatch); 1.988 + let color = this.activeSwatch.style.backgroundColor; 1.989 + this.spectrum.then(spectrum => { 1.990 + spectrum.off("changed", this._onSpectrumColorChange); 1.991 + spectrum.rgb = this._colorToRgba(color); 1.992 + spectrum.on("changed", this._onSpectrumColorChange); 1.993 + spectrum.updateUI(); 1.994 + }); 1.995 + } 1.996 + 1.997 + let tooltipDoc = this.tooltip.content.contentDocument; 1.998 + let eyeButton = tooltipDoc.querySelector("#eyedropper-button"); 1.999 + eyeButton.addEventListener("click", this._openEyeDropper); 1.1000 + }, 1.1001 + 1.1002 + _onSpectrumColorChange: function(event, rgba, cssColor) { 1.1003 + this._selectColor(cssColor); 1.1004 + }, 1.1005 + 1.1006 + _selectColor: function(color) { 1.1007 + if (this.activeSwatch) { 1.1008 + this.activeSwatch.style.backgroundColor = color; 1.1009 + this.currentSwatchColor.textContent = color; 1.1010 + this.preview(color); 1.1011 + } 1.1012 + }, 1.1013 + 1.1014 + _openEyeDropper: function() { 1.1015 + let chromeWindow = this.tooltip.doc.defaultView.top; 1.1016 + let windowType = chromeWindow.document.documentElement 1.1017 + .getAttribute("windowtype"); 1.1018 + let toolboxWindow; 1.1019 + if (windowType != "navigator:browser") { 1.1020 + // this means the toolbox is in a seperate window. We need to make 1.1021 + // sure we'll be inspecting the browser window instead 1.1022 + toolboxWindow = chromeWindow; 1.1023 + chromeWindow = Services.wm.getMostRecentWindow("navigator:browser"); 1.1024 + chromeWindow.focus(); 1.1025 + } 1.1026 + let dropper = new Eyedropper(chromeWindow, { copyOnSelect: false }); 1.1027 + 1.1028 + dropper.once("select", (event, color) => { 1.1029 + if (toolboxWindow) { 1.1030 + toolboxWindow.focus(); 1.1031 + } 1.1032 + this._selectColor(color); 1.1033 + }); 1.1034 + 1.1035 + dropper.once("destroy", () => { 1.1036 + this.eyedropperOpen = false; 1.1037 + this.activeSwatch = null; 1.1038 + }) 1.1039 + 1.1040 + dropper.open(); 1.1041 + this.eyedropperOpen = true; 1.1042 + 1.1043 + // close the colorpicker tooltip so that only the eyedropper is open. 1.1044 + this.hide(); 1.1045 + 1.1046 + this.tooltip.emit("eyedropper-opened", dropper); 1.1047 + }, 1.1048 + 1.1049 + _colorToRgba: function(color) { 1.1050 + color = new colorUtils.CssColor(color); 1.1051 + let rgba = color._getRGBATuple(); 1.1052 + return [rgba.r, rgba.g, rgba.b, rgba.a]; 1.1053 + }, 1.1054 + 1.1055 + destroy: function() { 1.1056 + SwatchBasedEditorTooltip.prototype.destroy.call(this); 1.1057 + this.currentSwatchColor = null; 1.1058 + this.spectrum.then(spectrum => { 1.1059 + spectrum.off("changed", this._onSpectrumColorChange); 1.1060 + spectrum.destroy(); 1.1061 + }); 1.1062 + } 1.1063 +}); 1.1064 + 1.1065 +/** 1.1066 + * Internal util, checks whether a css declaration is a gradient 1.1067 + */ 1.1068 +function isGradientRule(property, value) { 1.1069 + return (property === "background" || property === "background-image") && 1.1070 + value.match(GRADIENT_RE); 1.1071 +} 1.1072 + 1.1073 +/** 1.1074 + * Internal util, checks whether a css declaration is a color 1.1075 + */ 1.1076 +function isColorOnly(property, value) { 1.1077 + return property === "background-color" || 1.1078 + property === "color" || 1.1079 + property.match(BORDERCOLOR_RE); 1.1080 +} 1.1081 + 1.1082 +/** 1.1083 + * L10N utility class 1.1084 + */ 1.1085 +function L10N() {} 1.1086 +L10N.prototype = {}; 1.1087 + 1.1088 +let l10n = new L10N(); 1.1089 + 1.1090 +loader.lazyGetter(L10N.prototype, "strings", () => { 1.1091 + return Services.strings.createBundle( 1.1092 + "chrome://browser/locale/devtools/inspector.properties"); 1.1093 +});