browser/devtools/shared/widgets/Tooltip.js

Thu, 15 Jan 2015 21:13:52 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 15 Jan 2015 21:13:52 +0100
branch
TOR_BUG_9701
changeset 12
7540298fafa1
permissions
-rw-r--r--

Remove forgotten relic of ABI crash risk averse overloaded method change.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 const {Cc, Cu, Ci} = require("chrome");
michael@0 8 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
michael@0 9 const IOService = Cc["@mozilla.org/network/io-service;1"]
michael@0 10 .getService(Ci.nsIIOService);
michael@0 11 const {Spectrum} = require("devtools/shared/widgets/Spectrum");
michael@0 12 const EventEmitter = require("devtools/toolkit/event-emitter");
michael@0 13 const {colorUtils} = require("devtools/css-color");
michael@0 14 const Heritage = require("sdk/core/heritage");
michael@0 15 const {CSSTransformPreviewer} = require("devtools/shared/widgets/CSSTransformPreviewer");
michael@0 16 const {Eyedropper} = require("devtools/eyedropper/eyedropper");
michael@0 17
michael@0 18 Cu.import("resource://gre/modules/Services.jsm");
michael@0 19 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 20
michael@0 21 XPCOMUtils.defineLazyModuleGetter(this, "setNamedTimeout",
michael@0 22 "resource:///modules/devtools/ViewHelpers.jsm");
michael@0 23 XPCOMUtils.defineLazyModuleGetter(this, "clearNamedTimeout",
michael@0 24 "resource:///modules/devtools/ViewHelpers.jsm");
michael@0 25 XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
michael@0 26 "resource:///modules/devtools/VariablesView.jsm");
michael@0 27 XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
michael@0 28 "resource:///modules/devtools/VariablesViewController.jsm");
michael@0 29 XPCOMUtils.defineLazyModuleGetter(this, "Task",
michael@0 30 "resource://gre/modules/Task.jsm");
michael@0 31
michael@0 32 const GRADIENT_RE = /\b(repeating-)?(linear|radial)-gradient\(((rgb|hsl)a?\(.+?\)|[^\)])+\)/gi;
michael@0 33 const BORDERCOLOR_RE = /^border-[-a-z]*color$/ig;
michael@0 34 const BORDER_RE = /^border(-(top|bottom|left|right))?$/ig;
michael@0 35 const XHTML_NS = "http://www.w3.org/1999/xhtml";
michael@0 36 const SPECTRUM_FRAME = "chrome://browser/content/devtools/spectrum-frame.xhtml";
michael@0 37 const ESCAPE_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE;
michael@0 38 const RETURN_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_RETURN;
michael@0 39 const POPUP_EVENTS = ["shown", "hidden", "showing", "hiding"];
michael@0 40 const FONT_FAMILY_PREVIEW_TEXT = "(ABCabc123&@%)";
michael@0 41
michael@0 42 /**
michael@0 43 * Tooltip widget.
michael@0 44 *
michael@0 45 * This widget is intended at any tool that may need to show rich content in the
michael@0 46 * form of floating panels.
michael@0 47 * A common use case is image previewing in the CSS rule view, but more complex
michael@0 48 * use cases may include color pickers, object inspection, etc...
michael@0 49 *
michael@0 50 * Tooltips are based on XUL (namely XUL arrow-type <panel>s), and therefore
michael@0 51 * need a XUL Document to live in.
michael@0 52 * This is pretty much the only requirement they have on their environment.
michael@0 53 *
michael@0 54 * The way to use a tooltip is simply by instantiating a tooltip yourself and
michael@0 55 * attaching some content in it, or using one of the ready-made content types.
michael@0 56 *
michael@0 57 * A convenient `startTogglingOnHover` method may avoid having to register event
michael@0 58 * handlers yourself if the tooltip has to be shown when hovering over a
michael@0 59 * specific element or group of elements (which is usually the most common case)
michael@0 60 */
michael@0 61
michael@0 62 /**
michael@0 63 * Container used for dealing with optional parameters.
michael@0 64 *
michael@0 65 * @param {Object} defaults
michael@0 66 * An object with all default options {p1: v1, p2: v2, ...}
michael@0 67 * @param {Object} options
michael@0 68 * The actual values.
michael@0 69 */
michael@0 70 function OptionsStore(defaults, options) {
michael@0 71 this.defaults = defaults || {};
michael@0 72 this.options = options || {};
michael@0 73 }
michael@0 74
michael@0 75 OptionsStore.prototype = {
michael@0 76 /**
michael@0 77 * Get the value for a given option name.
michael@0 78 * @return {Object} Returns the value for that option, coming either for the
michael@0 79 * actual values that have been set in the constructor, or from the
michael@0 80 * defaults if that options was not specified.
michael@0 81 */
michael@0 82 get: function(name) {
michael@0 83 if (typeof this.options[name] !== "undefined") {
michael@0 84 return this.options[name];
michael@0 85 } else {
michael@0 86 return this.defaults[name];
michael@0 87 }
michael@0 88 }
michael@0 89 };
michael@0 90
michael@0 91 /**
michael@0 92 * The low level structure of a tooltip is a XUL element (a <panel>).
michael@0 93 */
michael@0 94 let PanelFactory = {
michael@0 95 /**
michael@0 96 * Get a new XUL panel instance.
michael@0 97 * @param {XULDocument} doc
michael@0 98 * The XUL document to put that panel into
michael@0 99 * @param {OptionsStore} options
michael@0 100 * An options store to get some configuration from
michael@0 101 */
michael@0 102 get: function(doc, options) {
michael@0 103 // Create the tooltip
michael@0 104 let panel = doc.createElement("panel");
michael@0 105 panel.setAttribute("hidden", true);
michael@0 106 panel.setAttribute("ignorekeys", true);
michael@0 107 panel.setAttribute("animate", false);
michael@0 108
michael@0 109 panel.setAttribute("consumeoutsideclicks", options.get("consumeOutsideClick"));
michael@0 110 panel.setAttribute("noautofocus", options.get("noAutoFocus"));
michael@0 111 panel.setAttribute("type", "arrow");
michael@0 112 panel.setAttribute("level", "top");
michael@0 113
michael@0 114 panel.setAttribute("class", "devtools-tooltip theme-tooltip-panel");
michael@0 115 doc.querySelector("window").appendChild(panel);
michael@0 116
michael@0 117 return panel;
michael@0 118 }
michael@0 119 };
michael@0 120
michael@0 121 /**
michael@0 122 * Tooltip class.
michael@0 123 *
michael@0 124 * Basic usage:
michael@0 125 * let t = new Tooltip(xulDoc);
michael@0 126 * t.content = someXulContent;
michael@0 127 * t.show();
michael@0 128 * t.hide();
michael@0 129 * t.destroy();
michael@0 130 *
michael@0 131 * Better usage:
michael@0 132 * let t = new Tooltip(xulDoc);
michael@0 133 * t.startTogglingOnHover(container, target => {
michael@0 134 * if (<condition based on target>) {
michael@0 135 * t.setImageContent("http://image.png");
michael@0 136 * return true;
michael@0 137 * }
michael@0 138 * });
michael@0 139 * t.destroy();
michael@0 140 *
michael@0 141 * @param {XULDocument} doc
michael@0 142 * The XUL document hosting this tooltip
michael@0 143 * @param {Object} options
michael@0 144 * Optional options that give options to consumers:
michael@0 145 * - consumeOutsideClick {Boolean} Wether the first click outside of the
michael@0 146 * tooltip should close the tooltip and be consumed or not.
michael@0 147 * Defaults to false.
michael@0 148 * - closeOnKeys {Array} An array of key codes that should close the
michael@0 149 * tooltip. Defaults to [27] (escape key).
michael@0 150 * - closeOnEvents [{emitter: {Object}, event: {String}, useCapture: {Boolean}}]
michael@0 151 * Provide an optional list of emitter objects and event names here to
michael@0 152 * trigger the closing of the tooltip when these events are fired by the
michael@0 153 * emitters. The emitter objects should either implement on/off(event, cb)
michael@0 154 * or addEventListener/removeEventListener(event, cb). Defaults to [].
michael@0 155 * For instance, the following would close the tooltip whenever the
michael@0 156 * toolbox selects a new tool and when a DOM node gets scrolled:
michael@0 157 * new Tooltip(doc, {
michael@0 158 * closeOnEvents: [
michael@0 159 * {emitter: toolbox, event: "select"},
michael@0 160 * {emitter: myContainer, event: "scroll", useCapture: true}
michael@0 161 * ]
michael@0 162 * });
michael@0 163 * - noAutoFocus {Boolean} Should the focus automatically go to the panel
michael@0 164 * when it opens. Defaults to true.
michael@0 165 *
michael@0 166 * Fires these events:
michael@0 167 * - showing : just before the tooltip shows
michael@0 168 * - shown : when the tooltip is shown
michael@0 169 * - hiding : just before the tooltip closes
michael@0 170 * - hidden : when the tooltip gets hidden
michael@0 171 * - keypress : when any key gets pressed, with keyCode
michael@0 172 */
michael@0 173 function Tooltip(doc, options) {
michael@0 174 EventEmitter.decorate(this);
michael@0 175
michael@0 176 this.doc = doc;
michael@0 177 this.options = new OptionsStore({
michael@0 178 consumeOutsideClick: false,
michael@0 179 closeOnKeys: [ESCAPE_KEYCODE],
michael@0 180 noAutoFocus: true,
michael@0 181 closeOnEvents: []
michael@0 182 }, options);
michael@0 183 this.panel = PanelFactory.get(doc, this.options);
michael@0 184
michael@0 185 // Used for namedTimeouts in the mouseover handling
michael@0 186 this.uid = "tooltip-" + Date.now();
michael@0 187
michael@0 188 // Emit show/hide events
michael@0 189 for (let event of POPUP_EVENTS) {
michael@0 190 this["_onPopup" + event] = ((e) => {
michael@0 191 return () => this.emit(e);
michael@0 192 })(event);
michael@0 193 this.panel.addEventListener("popup" + event,
michael@0 194 this["_onPopup" + event], false);
michael@0 195 }
michael@0 196
michael@0 197 // Listen to keypress events to close the tooltip if configured to do so
michael@0 198 let win = this.doc.querySelector("window");
michael@0 199 this._onKeyPress = event => {
michael@0 200 this.emit("keypress", event.keyCode);
michael@0 201 if (this.options.get("closeOnKeys").indexOf(event.keyCode) !== -1) {
michael@0 202 if (!this.panel.hidden) {
michael@0 203 event.stopPropagation();
michael@0 204 }
michael@0 205 this.hide();
michael@0 206 }
michael@0 207 };
michael@0 208 win.addEventListener("keypress", this._onKeyPress, false);
michael@0 209
michael@0 210 // Listen to custom emitters' events to close the tooltip
michael@0 211 this.hide = this.hide.bind(this);
michael@0 212 let closeOnEvents = this.options.get("closeOnEvents");
michael@0 213 for (let {emitter, event, useCapture} of closeOnEvents) {
michael@0 214 for (let add of ["addEventListener", "on"]) {
michael@0 215 if (add in emitter) {
michael@0 216 emitter[add](event, this.hide, useCapture);
michael@0 217 break;
michael@0 218 }
michael@0 219 }
michael@0 220 }
michael@0 221 }
michael@0 222
michael@0 223 module.exports.Tooltip = Tooltip;
michael@0 224
michael@0 225 Tooltip.prototype = {
michael@0 226 defaultPosition: "before_start",
michael@0 227 defaultOffsetX: 0, // px
michael@0 228 defaultOffsetY: 0, // px
michael@0 229 defaultShowDelay: 50, // ms
michael@0 230
michael@0 231 /**
michael@0 232 * Show the tooltip. It might be wise to append some content first if you
michael@0 233 * don't want the tooltip to be empty. You may access the content of the
michael@0 234 * tooltip by setting a XUL node to t.content.
michael@0 235 * @param {node} anchor
michael@0 236 * Which node should the tooltip be shown on
michael@0 237 * @param {string} position [optional]
michael@0 238 * Optional tooltip position. Defaults to before_start
michael@0 239 * https://developer.mozilla.org/en-US/docs/XUL/PopupGuide/Positioning
michael@0 240 * @param {number} x, y [optional]
michael@0 241 * The left and top offset coordinates, in pixels.
michael@0 242 */
michael@0 243 show: function(anchor,
michael@0 244 position = this.defaultPosition,
michael@0 245 x = this.defaultOffsetX,
michael@0 246 y = this.defaultOffsetY) {
michael@0 247 this.panel.hidden = false;
michael@0 248 this.panel.openPopup(anchor, position, x, y);
michael@0 249 },
michael@0 250
michael@0 251 /**
michael@0 252 * Hide the tooltip
michael@0 253 */
michael@0 254 hide: function() {
michael@0 255 this.panel.hidden = true;
michael@0 256 this.panel.hidePopup();
michael@0 257 },
michael@0 258
michael@0 259 isShown: function() {
michael@0 260 return this.panel.state !== "closed" && this.panel.state !== "hiding";
michael@0 261 },
michael@0 262
michael@0 263 setSize: function(width, height) {
michael@0 264 this.panel.sizeTo(width, height);
michael@0 265 },
michael@0 266
michael@0 267 /**
michael@0 268 * Empty the tooltip's content
michael@0 269 */
michael@0 270 empty: function() {
michael@0 271 while (this.panel.hasChildNodes()) {
michael@0 272 this.panel.removeChild(this.panel.firstChild);
michael@0 273 }
michael@0 274 },
michael@0 275
michael@0 276 /**
michael@0 277 * Gets this panel's visibility state.
michael@0 278 * @return boolean
michael@0 279 */
michael@0 280 isHidden: function() {
michael@0 281 return this.panel.state == "closed" || this.panel.state == "hiding";
michael@0 282 },
michael@0 283
michael@0 284 /**
michael@0 285 * Gets if this panel has any child nodes.
michael@0 286 * @return boolean
michael@0 287 */
michael@0 288 isEmpty: function() {
michael@0 289 return !this.panel.hasChildNodes();
michael@0 290 },
michael@0 291
michael@0 292 /**
michael@0 293 * Get rid of references and event listeners
michael@0 294 */
michael@0 295 destroy: function () {
michael@0 296 this.hide();
michael@0 297
michael@0 298 for (let event of POPUP_EVENTS) {
michael@0 299 this.panel.removeEventListener("popup" + event,
michael@0 300 this["_onPopup" + event], false);
michael@0 301 }
michael@0 302
michael@0 303 let win = this.doc.querySelector("window");
michael@0 304 win.removeEventListener("keypress", this._onKeyPress, false);
michael@0 305
michael@0 306 let closeOnEvents = this.options.get("closeOnEvents");
michael@0 307 for (let {emitter, event, useCapture} of closeOnEvents) {
michael@0 308 for (let remove of ["removeEventListener", "off"]) {
michael@0 309 if (remove in emitter) {
michael@0 310 emitter[remove](event, this.hide, useCapture);
michael@0 311 break;
michael@0 312 }
michael@0 313 }
michael@0 314 }
michael@0 315
michael@0 316 this.content = null;
michael@0 317
michael@0 318 if (this._basedNode) {
michael@0 319 this.stopTogglingOnHover();
michael@0 320 }
michael@0 321
michael@0 322 this.doc = null;
michael@0 323
michael@0 324 this.panel.remove();
michael@0 325 this.panel = null;
michael@0 326 },
michael@0 327
michael@0 328 /**
michael@0 329 * Show/hide the tooltip when the mouse hovers over particular nodes.
michael@0 330 *
michael@0 331 * 2 Ways to make this work:
michael@0 332 * - Provide a single node to attach the tooltip to, as the baseNode, and
michael@0 333 * omit the second targetNodeCb argument
michael@0 334 * - Provide a baseNode that is the container of possibly numerous children
michael@0 335 * elements that may receive a tooltip. In this case, provide the second
michael@0 336 * targetNodeCb argument to decide wether or not a child should receive
michael@0 337 * a tooltip.
michael@0 338 *
michael@0 339 * This works by tracking mouse movements on a base container node (baseNode)
michael@0 340 * and showing the tooltip when the mouse stops moving. The targetNodeCb
michael@0 341 * callback is used to know whether or not the particular element being
michael@0 342 * hovered over should indeed receive the tooltip. If you don't provide it
michael@0 343 * it's equivalent to a function that always returns true.
michael@0 344 *
michael@0 345 * Note that if you call this function a second time, it will itself call
michael@0 346 * stopTogglingOnHover before adding mouse tracking listeners again.
michael@0 347 *
michael@0 348 * @param {node} baseNode
michael@0 349 * The container for all target nodes
michael@0 350 * @param {Function} targetNodeCb
michael@0 351 * A function that accepts a node argument and returns true or false
michael@0 352 * (or a promise that resolves or rejects) to signify if the tooltip
michael@0 353 * should be shown on that node or not.
michael@0 354 * Additionally, the function receives a second argument which is the
michael@0 355 * tooltip instance itself, to be used to add/modify the content of the
michael@0 356 * tooltip if needed. If omitted, the tooltip will be shown everytime.
michael@0 357 * @param {Number} showDelay
michael@0 358 * An optional delay that will be observed before showing the tooltip.
michael@0 359 * Defaults to this.defaultShowDelay.
michael@0 360 */
michael@0 361 startTogglingOnHover: function(baseNode, targetNodeCb, showDelay=this.defaultShowDelay) {
michael@0 362 if (this._basedNode) {
michael@0 363 this.stopTogglingOnHover();
michael@0 364 }
michael@0 365
michael@0 366 this._basedNode = baseNode;
michael@0 367 this._showDelay = showDelay;
michael@0 368 this._targetNodeCb = targetNodeCb || (() => true);
michael@0 369
michael@0 370 this._onBaseNodeMouseMove = this._onBaseNodeMouseMove.bind(this);
michael@0 371 this._onBaseNodeMouseLeave = this._onBaseNodeMouseLeave.bind(this);
michael@0 372
michael@0 373 baseNode.addEventListener("mousemove", this._onBaseNodeMouseMove, false);
michael@0 374 baseNode.addEventListener("mouseleave", this._onBaseNodeMouseLeave, false);
michael@0 375 },
michael@0 376
michael@0 377 /**
michael@0 378 * If the startTogglingOnHover function has been used previously, and you want
michael@0 379 * to get rid of this behavior, then call this function to remove the mouse
michael@0 380 * movement tracking
michael@0 381 */
michael@0 382 stopTogglingOnHover: function() {
michael@0 383 clearNamedTimeout(this.uid);
michael@0 384
michael@0 385 this._basedNode.removeEventListener("mousemove",
michael@0 386 this._onBaseNodeMouseMove, false);
michael@0 387 this._basedNode.removeEventListener("mouseleave",
michael@0 388 this._onBaseNodeMouseLeave, false);
michael@0 389
michael@0 390 this._basedNode = null;
michael@0 391 this._targetNodeCb = null;
michael@0 392 this._lastHovered = null;
michael@0 393 },
michael@0 394
michael@0 395 _onBaseNodeMouseMove: function(event) {
michael@0 396 if (event.target !== this._lastHovered) {
michael@0 397 this.hide();
michael@0 398 this._lastHovered = event.target;
michael@0 399 setNamedTimeout(this.uid, this._showDelay, () => {
michael@0 400 this.isValidHoverTarget(event.target).then(target => {
michael@0 401 this.show(target);
michael@0 402 });
michael@0 403 });
michael@0 404 }
michael@0 405 },
michael@0 406
michael@0 407 /**
michael@0 408 * Is the given target DOMNode a valid node for toggling the tooltip on hover.
michael@0 409 * This delegates to the user-defined _targetNodeCb callback.
michael@0 410 * @return a promise that resolves or rejects depending if the tooltip should
michael@0 411 * be shown or not. If it resolves, it does to the actual anchor to be used
michael@0 412 */
michael@0 413 isValidHoverTarget: function(target) {
michael@0 414 // Execute the user-defined callback which should return either true/false
michael@0 415 // or a promise that resolves or rejects
michael@0 416 let res = this._targetNodeCb(target, this);
michael@0 417
michael@0 418 // The callback can additionally return a DOMNode to replace the anchor of
michael@0 419 // the tooltip when shown
michael@0 420 if (res && res.then) {
michael@0 421 return res.then(arg => {
michael@0 422 return arg instanceof Ci.nsIDOMNode ? arg : target;
michael@0 423 }, () => {
michael@0 424 return false;
michael@0 425 });
michael@0 426 } else {
michael@0 427 let newTarget = res instanceof Ci.nsIDOMNode ? res : target;
michael@0 428 return res ? promise.resolve(newTarget) : promise.reject(false);
michael@0 429 }
michael@0 430 },
michael@0 431
michael@0 432 _onBaseNodeMouseLeave: function() {
michael@0 433 clearNamedTimeout(this.uid);
michael@0 434 this._lastHovered = null;
michael@0 435 this.hide();
michael@0 436 },
michael@0 437
michael@0 438 /**
michael@0 439 * Set the content of this tooltip. Will first empty the tooltip and then
michael@0 440 * append the new content element.
michael@0 441 * Consider using one of the set<type>Content() functions instead.
michael@0 442 * @param {node} content
michael@0 443 * A node that can be appended in the tooltip XUL element
michael@0 444 */
michael@0 445 set content(content) {
michael@0 446 if (this.content == content) {
michael@0 447 return;
michael@0 448 }
michael@0 449
michael@0 450 this.empty();
michael@0 451 this.panel.removeAttribute("clamped-dimensions");
michael@0 452
michael@0 453 if (content) {
michael@0 454 this.panel.appendChild(content);
michael@0 455 }
michael@0 456 },
michael@0 457
michael@0 458 get content() {
michael@0 459 return this.panel.firstChild;
michael@0 460 },
michael@0 461
michael@0 462 /**
michael@0 463 * Sets some text as the content of this tooltip.
michael@0 464 *
michael@0 465 * @param {array} messages
michael@0 466 * A list of text messages.
michael@0 467 * @param {string} messagesClass [optional]
michael@0 468 * A style class for the text messages.
michael@0 469 * @param {string} containerClass [optional]
michael@0 470 * A style class for the text messages container.
michael@0 471 * @param {boolean} isAlertTooltip [optional]
michael@0 472 * Pass true to add an alert image for your tooltip.
michael@0 473 */
michael@0 474 setTextContent: function(
michael@0 475 {
michael@0 476 messages,
michael@0 477 messagesClass,
michael@0 478 containerClass,
michael@0 479 isAlertTooltip
michael@0 480 },
michael@0 481 extraButtons = []) {
michael@0 482 messagesClass = messagesClass || "default-tooltip-simple-text-colors";
michael@0 483 containerClass = containerClass || "default-tooltip-simple-text-colors";
michael@0 484
michael@0 485 let vbox = this.doc.createElement("vbox");
michael@0 486 vbox.className = "devtools-tooltip-simple-text-container " + containerClass;
michael@0 487 vbox.setAttribute("flex", "1");
michael@0 488
michael@0 489 for (let text of messages) {
michael@0 490 let description = this.doc.createElement("description");
michael@0 491 description.setAttribute("flex", "1");
michael@0 492 description.className = "devtools-tooltip-simple-text " + messagesClass;
michael@0 493 description.textContent = text;
michael@0 494 vbox.appendChild(description);
michael@0 495 }
michael@0 496
michael@0 497 for (let { label, className, command } of extraButtons) {
michael@0 498 let button = this.doc.createElement("button");
michael@0 499 button.className = className;
michael@0 500 button.setAttribute("label", label);
michael@0 501 button.addEventListener("command", command);
michael@0 502 vbox.appendChild(button);
michael@0 503 }
michael@0 504
michael@0 505 if (isAlertTooltip) {
michael@0 506 let hbox = this.doc.createElement("hbox");
michael@0 507 hbox.setAttribute("align", "start");
michael@0 508
michael@0 509 let alertImg = this.doc.createElement("image");
michael@0 510 alertImg.className = "devtools-tooltip-alert-icon";
michael@0 511 hbox.appendChild(alertImg);
michael@0 512 hbox.appendChild(vbox);
michael@0 513 this.content = hbox;
michael@0 514 } else {
michael@0 515 this.content = vbox;
michael@0 516 }
michael@0 517 },
michael@0 518
michael@0 519 /**
michael@0 520 * Fill the tooltip with a variables view, inspecting an object via its
michael@0 521 * corresponding object actor, as specified in the remote debugging protocol.
michael@0 522 *
michael@0 523 * @param {object} objectActor
michael@0 524 * The value grip for the object actor.
michael@0 525 * @param {object} viewOptions [optional]
michael@0 526 * Options for the variables view visualization.
michael@0 527 * @param {object} controllerOptions [optional]
michael@0 528 * Options for the variables view controller.
michael@0 529 * @param {object} relayEvents [optional]
michael@0 530 * A collection of events to listen on the variables view widget.
michael@0 531 * For example, { fetched: () => ... }
michael@0 532 * @param {boolean} reuseCachedWidget [optional]
michael@0 533 * Pass false to instantiate a brand new widget for this variable.
michael@0 534 * Otherwise, if a variable was previously inspected, its widget
michael@0 535 * will be reused.
michael@0 536 * @param {Toolbox} toolbox [optional]
michael@0 537 * Pass the instance of the current toolbox if you want the variables
michael@0 538 * view widget to allow highlighting and selection of DOM nodes
michael@0 539 */
michael@0 540 setVariableContent: function(
michael@0 541 objectActor,
michael@0 542 viewOptions = {},
michael@0 543 controllerOptions = {},
michael@0 544 relayEvents = {},
michael@0 545 extraButtons = [],
michael@0 546 toolbox = null) {
michael@0 547
michael@0 548 let vbox = this.doc.createElement("vbox");
michael@0 549 vbox.className = "devtools-tooltip-variables-view-box";
michael@0 550 vbox.setAttribute("flex", "1");
michael@0 551
michael@0 552 let innerbox = this.doc.createElement("vbox");
michael@0 553 innerbox.className = "devtools-tooltip-variables-view-innerbox";
michael@0 554 innerbox.setAttribute("flex", "1");
michael@0 555 vbox.appendChild(innerbox);
michael@0 556
michael@0 557 for (let { label, className, command } of extraButtons) {
michael@0 558 let button = this.doc.createElement("button");
michael@0 559 button.className = className;
michael@0 560 button.setAttribute("label", label);
michael@0 561 button.addEventListener("command", command);
michael@0 562 vbox.appendChild(button);
michael@0 563 }
michael@0 564
michael@0 565 let widget = new VariablesView(innerbox, viewOptions);
michael@0 566
michael@0 567 // If a toolbox was provided, link it to the vview
michael@0 568 if (toolbox) {
michael@0 569 widget.toolbox = toolbox;
michael@0 570 }
michael@0 571
michael@0 572 // Analyzing state history isn't useful with transient object inspectors.
michael@0 573 widget.commitHierarchy = () => {};
michael@0 574
michael@0 575 for (let e in relayEvents) widget.on(e, relayEvents[e]);
michael@0 576 VariablesViewController.attach(widget, controllerOptions);
michael@0 577
michael@0 578 // Some of the view options are allowed to change between uses.
michael@0 579 widget.searchPlaceholder = viewOptions.searchPlaceholder;
michael@0 580 widget.searchEnabled = viewOptions.searchEnabled;
michael@0 581
michael@0 582 // Use the object actor's grip to display it as a variable in the widget.
michael@0 583 // The controller options are allowed to change between uses.
michael@0 584 widget.controller.setSingleVariable(
michael@0 585 { objectActor: objectActor }, controllerOptions);
michael@0 586
michael@0 587 this.content = vbox;
michael@0 588 this.panel.setAttribute("clamped-dimensions", "");
michael@0 589 },
michael@0 590
michael@0 591 /**
michael@0 592 * Uses the provided inspectorFront's getImageDataFromURL method to resolve
michael@0 593 * the relative URL on the server-side, in the page context, and then sets the
michael@0 594 * tooltip content with the resulting image just like |setImageContent| does.
michael@0 595 * @return a promise that resolves when the image is shown in the tooltip or
michael@0 596 * resolves when the broken image tooltip content is ready, but never rejects.
michael@0 597 */
michael@0 598 setRelativeImageContent: Task.async(function*(imageUrl, inspectorFront, maxDim) {
michael@0 599 if (imageUrl.startsWith("data:")) {
michael@0 600 // If the imageUrl already is a data-url, save ourselves a round-trip
michael@0 601 this.setImageContent(imageUrl, {maxDim: maxDim});
michael@0 602 } else if (inspectorFront) {
michael@0 603 try {
michael@0 604 let {data, size} = yield inspectorFront.getImageDataFromURL(imageUrl, maxDim);
michael@0 605 size.maxDim = maxDim;
michael@0 606 let str = yield data.string();
michael@0 607 this.setImageContent(str, size);
michael@0 608 } catch (e) {
michael@0 609 this.setBrokenImageContent();
michael@0 610 }
michael@0 611 }
michael@0 612 }),
michael@0 613
michael@0 614 /**
michael@0 615 * Fill the tooltip with a message explaining the the image is missing
michael@0 616 */
michael@0 617 setBrokenImageContent: function() {
michael@0 618 this.setTextContent({
michael@0 619 messages: [l10n.strings.GetStringFromName("previewTooltip.image.brokenImage")]
michael@0 620 });
michael@0 621 },
michael@0 622
michael@0 623 /**
michael@0 624 * Fill the tooltip with an image and add the image dimension at the bottom.
michael@0 625 *
michael@0 626 * Only use this for absolute URLs that can be queried from the devtools
michael@0 627 * client-side. For relative URLs, use |setRelativeImageContent|.
michael@0 628 *
michael@0 629 * @param {string} imageUrl
michael@0 630 * The url to load the image from
michael@0 631 * @param {Object} options
michael@0 632 * The following options are supported:
michael@0 633 * - resized : whether or not the image identified by imageUrl has been
michael@0 634 * resized before this function was called.
michael@0 635 * - naturalWidth/naturalHeight : the original size of the image before
michael@0 636 * it was resized, if if was resized before this function was called.
michael@0 637 * If not provided, will be measured on the loaded image.
michael@0 638 * - maxDim : if the image should be resized before being shown, pass
michael@0 639 * a number here
michael@0 640 */
michael@0 641 setImageContent: function(imageUrl, options={}) {
michael@0 642 if (!imageUrl) {
michael@0 643 return;
michael@0 644 }
michael@0 645
michael@0 646 // Main container
michael@0 647 let vbox = this.doc.createElement("vbox");
michael@0 648 vbox.setAttribute("align", "center");
michael@0 649
michael@0 650 // Display the image
michael@0 651 let image = this.doc.createElement("image");
michael@0 652 image.setAttribute("src", imageUrl);
michael@0 653 if (options.maxDim) {
michael@0 654 image.style.maxWidth = options.maxDim + "px";
michael@0 655 image.style.maxHeight = options.maxDim + "px";
michael@0 656 }
michael@0 657 vbox.appendChild(image);
michael@0 658
michael@0 659 // Dimension label
michael@0 660 let label = this.doc.createElement("label");
michael@0 661 label.classList.add("devtools-tooltip-caption");
michael@0 662 label.classList.add("theme-comment");
michael@0 663 if (options.naturalWidth && options.naturalHeight) {
michael@0 664 label.textContent = this._getImageDimensionLabel(options.naturalWidth,
michael@0 665 options.naturalHeight);
michael@0 666 } else {
michael@0 667 // If no dimensions were provided, load the image to get them
michael@0 668 label.textContent = l10n.strings.GetStringFromName("previewTooltip.image.brokenImage");
michael@0 669 let imgObj = new this.doc.defaultView.Image();
michael@0 670 imgObj.src = imageUrl;
michael@0 671 imgObj.onload = () => {
michael@0 672 imgObj.onload = null;
michael@0 673 label.textContent = this._getImageDimensionLabel(imgObj.naturalWidth,
michael@0 674 imgObj.naturalHeight);
michael@0 675 }
michael@0 676 }
michael@0 677 vbox.appendChild(label);
michael@0 678
michael@0 679 this.content = vbox;
michael@0 680 },
michael@0 681
michael@0 682 _getImageDimensionLabel: (w, h) => w + " x " + h,
michael@0 683
michael@0 684 /**
michael@0 685 * Fill the tooltip with a new instance of the spectrum color picker widget
michael@0 686 * initialized with the given color, and return a promise that resolves to
michael@0 687 * the instance of spectrum
michael@0 688 */
michael@0 689 setColorPickerContent: function(color) {
michael@0 690 let def = promise.defer();
michael@0 691
michael@0 692 // Create an iframe to contain spectrum
michael@0 693 let iframe = this.doc.createElementNS(XHTML_NS, "iframe");
michael@0 694 iframe.setAttribute("transparent", true);
michael@0 695 iframe.setAttribute("width", "210");
michael@0 696 iframe.setAttribute("height", "216");
michael@0 697 iframe.setAttribute("flex", "1");
michael@0 698 iframe.setAttribute("class", "devtools-tooltip-iframe");
michael@0 699
michael@0 700 let panel = this.panel;
michael@0 701 let xulWin = this.doc.ownerGlobal;
michael@0 702
michael@0 703 // Wait for the load to initialize spectrum
michael@0 704 function onLoad() {
michael@0 705 iframe.removeEventListener("load", onLoad, true);
michael@0 706 let win = iframe.contentWindow.wrappedJSObject;
michael@0 707
michael@0 708 let container = win.document.getElementById("spectrum");
michael@0 709 let spectrum = new Spectrum(container, color);
michael@0 710
michael@0 711 function finalizeSpectrum() {
michael@0 712 spectrum.show();
michael@0 713 def.resolve(spectrum);
michael@0 714 }
michael@0 715
michael@0 716 // Finalize spectrum's init when the tooltip becomes visible
michael@0 717 if (panel.state == "open") {
michael@0 718 finalizeSpectrum();
michael@0 719 }
michael@0 720 else {
michael@0 721 panel.addEventListener("popupshown", function shown() {
michael@0 722 panel.removeEventListener("popupshown", shown, true);
michael@0 723 finalizeSpectrum();
michael@0 724 }, true);
michael@0 725 }
michael@0 726 }
michael@0 727 iframe.addEventListener("load", onLoad, true);
michael@0 728 iframe.setAttribute("src", SPECTRUM_FRAME);
michael@0 729
michael@0 730 // Put the iframe in the tooltip
michael@0 731 this.content = iframe;
michael@0 732
michael@0 733 return def.promise;
michael@0 734 },
michael@0 735
michael@0 736 /**
michael@0 737 * Set the content of the tooltip to be the result of CSSTransformPreviewer.
michael@0 738 * Meaning a canvas previewing a css transformation.
michael@0 739 *
michael@0 740 * @param {String} transform
michael@0 741 * The CSS transform value (e.g. "rotate(45deg) translateX(50px)")
michael@0 742 * @param {PageStyleActor} pageStyle
michael@0 743 * An instance of the PageStyleActor that will be used to retrieve
michael@0 744 * computed styles
michael@0 745 * @param {NodeActor} node
michael@0 746 * The NodeActor for the currently selected node
michael@0 747 * @return A promise that resolves when the tooltip content is ready, or
michael@0 748 * rejects if no transform is provided or the transform is invalid
michael@0 749 */
michael@0 750 setCssTransformContent: Task.async(function*(transform, pageStyle, node) {
michael@0 751 if (!transform) {
michael@0 752 throw "Missing transform";
michael@0 753 }
michael@0 754
michael@0 755 // Look into the computed styles to find the width and height and possibly
michael@0 756 // the origin if it hadn't been provided
michael@0 757 let styles = yield pageStyle.getComputed(node, {
michael@0 758 filter: "user",
michael@0 759 markMatched: false,
michael@0 760 onlyMatched: false
michael@0 761 });
michael@0 762
michael@0 763 let origin = styles["transform-origin"].value;
michael@0 764 let width = parseInt(styles["width"].value);
michael@0 765 let height = parseInt(styles["height"].value);
michael@0 766
michael@0 767 let root = this.doc.createElementNS(XHTML_NS, "div");
michael@0 768 let previewer = new CSSTransformPreviewer(root);
michael@0 769 this.content = root;
michael@0 770 if (!previewer.preview(transform, origin, width, height)) {
michael@0 771 throw "Invalid transform";
michael@0 772 }
michael@0 773 }),
michael@0 774
michael@0 775 /**
michael@0 776 * Set the content of the tooltip to display a font family preview.
michael@0 777 * This is based on Lea Verou's Dablet. See https://github.com/LeaVerou/dabblet
michael@0 778 * for more info.
michael@0 779 * @param {String} font The font family value.
michael@0 780 */
michael@0 781 setFontFamilyContent: function(font) {
michael@0 782 if (!font) {
michael@0 783 return;
michael@0 784 }
michael@0 785
michael@0 786 // Main container
michael@0 787 let vbox = this.doc.createElement("vbox");
michael@0 788 vbox.setAttribute("flex", "1");
michael@0 789
michael@0 790 // Display the font family previewer
michael@0 791 let previewer = this.doc.createElement("description");
michael@0 792 previewer.setAttribute("flex", "1");
michael@0 793 previewer.style.fontFamily = font;
michael@0 794 previewer.classList.add("devtools-tooltip-font-previewer-text");
michael@0 795 previewer.textContent = FONT_FAMILY_PREVIEW_TEXT;
michael@0 796 vbox.appendChild(previewer);
michael@0 797
michael@0 798 this.content = vbox;
michael@0 799 }
michael@0 800 };
michael@0 801
michael@0 802 /**
michael@0 803 * Base class for all (color, gradient, ...)-swatch based value editors inside
michael@0 804 * tooltips
michael@0 805 *
michael@0 806 * @param {XULDocument} doc
michael@0 807 */
michael@0 808 function SwatchBasedEditorTooltip(doc) {
michael@0 809 // Creating a tooltip instance
michael@0 810 // This one will consume outside clicks as it makes more sense to let the user
michael@0 811 // close the tooltip by clicking out
michael@0 812 // It will also close on <escape> and <enter>
michael@0 813 this.tooltip = new Tooltip(doc, {
michael@0 814 consumeOutsideClick: true,
michael@0 815 closeOnKeys: [ESCAPE_KEYCODE, RETURN_KEYCODE],
michael@0 816 noAutoFocus: false
michael@0 817 });
michael@0 818
michael@0 819 // By default, swatch-based editor tooltips revert value change on <esc> and
michael@0 820 // commit value change on <enter>
michael@0 821 this._onTooltipKeypress = (event, code) => {
michael@0 822 if (code === ESCAPE_KEYCODE) {
michael@0 823 this.revert();
michael@0 824 } else if (code === RETURN_KEYCODE) {
michael@0 825 this.commit();
michael@0 826 }
michael@0 827 };
michael@0 828 this.tooltip.on("keypress", this._onTooltipKeypress);
michael@0 829
michael@0 830 // All target swatches are kept in a map, indexed by swatch DOM elements
michael@0 831 this.swatches = new Map();
michael@0 832
michael@0 833 // When a swatch is clicked, and for as long as the tooltip is shown, the
michael@0 834 // activeSwatch property will hold the reference to the swatch DOM element
michael@0 835 // that was clicked
michael@0 836 this.activeSwatch = null;
michael@0 837
michael@0 838 this._onSwatchClick = this._onSwatchClick.bind(this);
michael@0 839 }
michael@0 840
michael@0 841 SwatchBasedEditorTooltip.prototype = {
michael@0 842 show: function() {
michael@0 843 if (this.activeSwatch) {
michael@0 844 this.tooltip.show(this.activeSwatch, "topcenter bottomleft");
michael@0 845 this.tooltip.once("hidden", () => {
michael@0 846 if (!this.eyedropperOpen) {
michael@0 847 this.activeSwatch = null;
michael@0 848 }
michael@0 849 });
michael@0 850 }
michael@0 851 },
michael@0 852
michael@0 853 hide: function() {
michael@0 854 this.tooltip.hide();
michael@0 855 },
michael@0 856
michael@0 857 /**
michael@0 858 * Add a new swatch DOM element to the list of swatch elements this editor
michael@0 859 * tooltip knows about. That means from now on, clicking on that swatch will
michael@0 860 * toggle the editor.
michael@0 861 *
michael@0 862 * @param {node} swatchEl
michael@0 863 * The element to add
michael@0 864 * @param {object} callbacks
michael@0 865 * Callbacks that will be executed when the editor wants to preview a
michael@0 866 * value change, or revert a change, or commit a change.
michael@0 867 * - onPreview: will be called when one of the sub-classes calls preview
michael@0 868 * - onRevert: will be called when the user ESCapes out of the tooltip
michael@0 869 * - onCommit: will be called when the user presses ENTER or clicks
michael@0 870 * outside the tooltip. If the user-defined onCommit returns a value,
michael@0 871 * it will be used to replace originalValue, so that the swatch-based
michael@0 872 * tooltip always knows what is the current originalValue and can use
michael@0 873 * it when reverting
michael@0 874 * @param {object} originalValue
michael@0 875 * The original value before the editor in the tooltip makes changes
michael@0 876 * This can be of any type, and will be passed, as is, in the revert
michael@0 877 * callback
michael@0 878 */
michael@0 879 addSwatch: function(swatchEl, callbacks={}, originalValue) {
michael@0 880 if (!callbacks.onPreview) callbacks.onPreview = function() {};
michael@0 881 if (!callbacks.onRevert) callbacks.onRevert = function() {};
michael@0 882 if (!callbacks.onCommit) callbacks.onCommit = function() {};
michael@0 883
michael@0 884 this.swatches.set(swatchEl, {
michael@0 885 callbacks: callbacks,
michael@0 886 originalValue: originalValue
michael@0 887 });
michael@0 888 swatchEl.addEventListener("click", this._onSwatchClick, false);
michael@0 889 },
michael@0 890
michael@0 891 removeSwatch: function(swatchEl) {
michael@0 892 if (this.swatches.has(swatchEl)) {
michael@0 893 if (this.activeSwatch === swatchEl) {
michael@0 894 this.hide();
michael@0 895 this.activeSwatch = null;
michael@0 896 }
michael@0 897 swatchEl.removeEventListener("click", this._onSwatchClick, false);
michael@0 898 this.swatches.delete(swatchEl);
michael@0 899 }
michael@0 900 },
michael@0 901
michael@0 902 _onSwatchClick: function(event) {
michael@0 903 let swatch = this.swatches.get(event.target);
michael@0 904 if (swatch) {
michael@0 905 this.activeSwatch = event.target;
michael@0 906 this.show();
michael@0 907 event.stopPropagation();
michael@0 908 }
michael@0 909 },
michael@0 910
michael@0 911 /**
michael@0 912 * Not called by this parent class, needs to be taken care of by sub-classes
michael@0 913 */
michael@0 914 preview: function(value) {
michael@0 915 if (this.activeSwatch) {
michael@0 916 let swatch = this.swatches.get(this.activeSwatch);
michael@0 917 swatch.callbacks.onPreview(value);
michael@0 918 }
michael@0 919 },
michael@0 920
michael@0 921 /**
michael@0 922 * This parent class only calls this on <esc> keypress
michael@0 923 */
michael@0 924 revert: function() {
michael@0 925 if (this.activeSwatch) {
michael@0 926 let swatch = this.swatches.get(this.activeSwatch);
michael@0 927 swatch.callbacks.onRevert(swatch.originalValue);
michael@0 928 }
michael@0 929 },
michael@0 930
michael@0 931 /**
michael@0 932 * This parent class only calls this on <enter> keypress
michael@0 933 */
michael@0 934 commit: function() {
michael@0 935 if (this.activeSwatch) {
michael@0 936 let swatch = this.swatches.get(this.activeSwatch);
michael@0 937 let newValue = swatch.callbacks.onCommit();
michael@0 938 if (typeof newValue !== "undefined") {
michael@0 939 swatch.originalValue = newValue;
michael@0 940 }
michael@0 941 }
michael@0 942 },
michael@0 943
michael@0 944 destroy: function() {
michael@0 945 this.swatches.clear();
michael@0 946 this.activeSwatch = null;
michael@0 947 this.tooltip.off("keypress", this._onTooltipKeypress);
michael@0 948 this.tooltip.destroy();
michael@0 949 }
michael@0 950 };
michael@0 951
michael@0 952 /**
michael@0 953 * The swatch color picker tooltip class is a specific class meant to be used
michael@0 954 * along with output-parser's generated color swatches.
michael@0 955 * It extends the parent SwatchBasedEditorTooltip class.
michael@0 956 * It just wraps a standard Tooltip and sets its content with an instance of a
michael@0 957 * color picker.
michael@0 958 *
michael@0 959 * @param {XULDocument} doc
michael@0 960 */
michael@0 961 function SwatchColorPickerTooltip(doc) {
michael@0 962 SwatchBasedEditorTooltip.call(this, doc);
michael@0 963
michael@0 964 // Creating a spectrum instance. this.spectrum will always be a promise that
michael@0 965 // resolves to the spectrum instance
michael@0 966 this.spectrum = this.tooltip.setColorPickerContent([0, 0, 0, 1]);
michael@0 967 this._onSpectrumColorChange = this._onSpectrumColorChange.bind(this);
michael@0 968 this._openEyeDropper = this._openEyeDropper.bind(this);
michael@0 969 }
michael@0 970
michael@0 971 module.exports.SwatchColorPickerTooltip = SwatchColorPickerTooltip;
michael@0 972
michael@0 973 SwatchColorPickerTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, {
michael@0 974 /**
michael@0 975 * Overriding the SwatchBasedEditorTooltip.show function to set spectrum's
michael@0 976 * color.
michael@0 977 */
michael@0 978 show: function() {
michael@0 979 // Call then parent class' show function
michael@0 980 SwatchBasedEditorTooltip.prototype.show.call(this);
michael@0 981 // Then set spectrum's color and listen to color changes to preview them
michael@0 982 if (this.activeSwatch) {
michael@0 983 this.currentSwatchColor = this.activeSwatch.nextSibling;
michael@0 984 let swatch = this.swatches.get(this.activeSwatch);
michael@0 985 let color = this.activeSwatch.style.backgroundColor;
michael@0 986 this.spectrum.then(spectrum => {
michael@0 987 spectrum.off("changed", this._onSpectrumColorChange);
michael@0 988 spectrum.rgb = this._colorToRgba(color);
michael@0 989 spectrum.on("changed", this._onSpectrumColorChange);
michael@0 990 spectrum.updateUI();
michael@0 991 });
michael@0 992 }
michael@0 993
michael@0 994 let tooltipDoc = this.tooltip.content.contentDocument;
michael@0 995 let eyeButton = tooltipDoc.querySelector("#eyedropper-button");
michael@0 996 eyeButton.addEventListener("click", this._openEyeDropper);
michael@0 997 },
michael@0 998
michael@0 999 _onSpectrumColorChange: function(event, rgba, cssColor) {
michael@0 1000 this._selectColor(cssColor);
michael@0 1001 },
michael@0 1002
michael@0 1003 _selectColor: function(color) {
michael@0 1004 if (this.activeSwatch) {
michael@0 1005 this.activeSwatch.style.backgroundColor = color;
michael@0 1006 this.currentSwatchColor.textContent = color;
michael@0 1007 this.preview(color);
michael@0 1008 }
michael@0 1009 },
michael@0 1010
michael@0 1011 _openEyeDropper: function() {
michael@0 1012 let chromeWindow = this.tooltip.doc.defaultView.top;
michael@0 1013 let windowType = chromeWindow.document.documentElement
michael@0 1014 .getAttribute("windowtype");
michael@0 1015 let toolboxWindow;
michael@0 1016 if (windowType != "navigator:browser") {
michael@0 1017 // this means the toolbox is in a seperate window. We need to make
michael@0 1018 // sure we'll be inspecting the browser window instead
michael@0 1019 toolboxWindow = chromeWindow;
michael@0 1020 chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
michael@0 1021 chromeWindow.focus();
michael@0 1022 }
michael@0 1023 let dropper = new Eyedropper(chromeWindow, { copyOnSelect: false });
michael@0 1024
michael@0 1025 dropper.once("select", (event, color) => {
michael@0 1026 if (toolboxWindow) {
michael@0 1027 toolboxWindow.focus();
michael@0 1028 }
michael@0 1029 this._selectColor(color);
michael@0 1030 });
michael@0 1031
michael@0 1032 dropper.once("destroy", () => {
michael@0 1033 this.eyedropperOpen = false;
michael@0 1034 this.activeSwatch = null;
michael@0 1035 })
michael@0 1036
michael@0 1037 dropper.open();
michael@0 1038 this.eyedropperOpen = true;
michael@0 1039
michael@0 1040 // close the colorpicker tooltip so that only the eyedropper is open.
michael@0 1041 this.hide();
michael@0 1042
michael@0 1043 this.tooltip.emit("eyedropper-opened", dropper);
michael@0 1044 },
michael@0 1045
michael@0 1046 _colorToRgba: function(color) {
michael@0 1047 color = new colorUtils.CssColor(color);
michael@0 1048 let rgba = color._getRGBATuple();
michael@0 1049 return [rgba.r, rgba.g, rgba.b, rgba.a];
michael@0 1050 },
michael@0 1051
michael@0 1052 destroy: function() {
michael@0 1053 SwatchBasedEditorTooltip.prototype.destroy.call(this);
michael@0 1054 this.currentSwatchColor = null;
michael@0 1055 this.spectrum.then(spectrum => {
michael@0 1056 spectrum.off("changed", this._onSpectrumColorChange);
michael@0 1057 spectrum.destroy();
michael@0 1058 });
michael@0 1059 }
michael@0 1060 });
michael@0 1061
michael@0 1062 /**
michael@0 1063 * Internal util, checks whether a css declaration is a gradient
michael@0 1064 */
michael@0 1065 function isGradientRule(property, value) {
michael@0 1066 return (property === "background" || property === "background-image") &&
michael@0 1067 value.match(GRADIENT_RE);
michael@0 1068 }
michael@0 1069
michael@0 1070 /**
michael@0 1071 * Internal util, checks whether a css declaration is a color
michael@0 1072 */
michael@0 1073 function isColorOnly(property, value) {
michael@0 1074 return property === "background-color" ||
michael@0 1075 property === "color" ||
michael@0 1076 property.match(BORDERCOLOR_RE);
michael@0 1077 }
michael@0 1078
michael@0 1079 /**
michael@0 1080 * L10N utility class
michael@0 1081 */
michael@0 1082 function L10N() {}
michael@0 1083 L10N.prototype = {};
michael@0 1084
michael@0 1085 let l10n = new L10N();
michael@0 1086
michael@0 1087 loader.lazyGetter(L10N.prototype, "strings", () => {
michael@0 1088 return Services.strings.createBundle(
michael@0 1089 "chrome://browser/locale/devtools/inspector.properties");
michael@0 1090 });

mercurial