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.

     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);
  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();
  1023     let dropper = new Eyedropper(chromeWindow, { copyOnSelect: false });
  1025     dropper.once("select", (event, color) => {
  1026       if (toolboxWindow) {
  1027         toolboxWindow.focus();
  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     });
  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);
  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);
  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 });

mercurial