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