Thu, 15 Jan 2015 21:13:52 +0100
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 | }); |