Bug 1266448 - part3: use HTML tooltip for markupview image previews;r=ochameau
authorJulian Descottes <jdescottes@mozilla.com>
Sun, 15 May 2016 14:45:51 +0200
changeset 338074 a41f8a55229ca89e0608fa68fde3b4beec98c2d9
parent 338073 036722f3eb047ba5818c6144824908501af8394c
child 338075 e3977905475d792781a2c8b3231fb5eb1bcca582
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1266448
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1266448 - part3: use HTML tooltip for markupview image previews;r=ochameau MozReview-Commit-ID: E45sJPVAsxj
devtools/client/inspector/markup/markup.js
devtools/client/inspector/markup/test/browser_markup_image_tooltip.js
devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js
devtools/client/shared/widgets/HTMLTooltip.js
devtools/client/shared/widgets/tooltip-frame.xhtml
devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js
devtools/client/shared/widgets/tooltip/moz.build
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -17,29 +17,34 @@ const COLLAPSE_DATA_URL_LENGTH = 60;
 const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
 const DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE = 50;
 const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 5;
 const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 15;
 const DRAG_DROP_MIN_INITIAL_DISTANCE = 10;
 const AUTOCOMPLETE_POPUP_PANEL_ID = "markupview_autoCompletePopup";
 const ATTR_COLLAPSE_ENABLED_PREF = "devtools.markup.collapseAttributes";
 const ATTR_COLLAPSE_LENGTH_PREF = "devtools.markup.collapseAttributeLength";
+const PREVIEW_MAX_DIM_PREF = "devtools.inspector.imagePreviewTooltipSize";
 
 // Contains only void (without end tag) HTML elements
 const HTML_VOID_ELEMENTS = [ "area", "base", "br", "col", "command", "embed",
   "hr", "img", "input", "keygen", "link", "meta", "param", "source",
   "track", "wbr" ];
 
 const {UndoStack} = require("devtools/client/shared/undo");
 const {editableField, InplaceEditor} =
       require("devtools/client/shared/inplace-editor");
 const {HTMLEditor} = require("devtools/client/inspector/markup/html-editor");
 const promise = require("promise");
 const Services = require("Services");
 const {Tooltip} = require("devtools/client/shared/widgets/Tooltip");
+const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
+const {setImageTooltip, setBrokenImageTooltip} =
+      require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
+
 const EventEmitter = require("devtools/shared/event-emitter");
 const Heritage = require("sdk/core/heritage");
 const {parseAttribute} =
       require("devtools/client/shared/node-attribute-parser");
 const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis",
       Ci.nsIPrefLocalizedString).data;
 const {Task} = require("devtools/shared/task");
 const {scrollIntoViewIfNeeded} = require("devtools/shared/layout/utils");
@@ -126,17 +131,17 @@ function MarkupView(inspector, frame, co
   this._onNewSelection = this._onNewSelection.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onFocus = this._onFocus.bind(this);
   this._onMouseMove = this._onMouseMove.bind(this);
   this._onMouseLeave = this._onMouseLeave.bind(this);
   this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this);
   this._onCollapseAttributesPrefChange =
     this._onCollapseAttributesPrefChange.bind(this);
-  this._isImagePreviewTarget = this._isImagePreviewTarget.bind(this)
+  this._isImagePreviewTarget = this._isImagePreviewTarget.bind(this);
   this._onBlur = this._onBlur.bind(this);
 
   EventEmitter.decorate(this);
 
   // Listening to various events.
   this._elt.addEventListener("click", this._onMouseClick, false);
   this._elt.addEventListener("mousemove", this._onMouseMove, false);
   this._elt.addEventListener("mouseleave", this._onMouseLeave, false);
@@ -166,17 +171,18 @@ MarkupView.prototype = {
    * How long does a node flash when it mutates (in ms).
    */
   CONTAINER_FLASHING_DURATION: 500,
 
   _selectedContainer: null,
 
   _initTooltips: function () {
     this.eventDetailsTooltip = new Tooltip(this._inspector.panelDoc);
-    this.imagePreviewTooltip = new Tooltip(this._inspector.panelDoc);
+    this.imagePreviewTooltip = new HTMLTooltip(this._inspector.toolbox,
+      {type: "arrow"});
     this._enableImagePreviewTooltip();
   },
 
   _enableImagePreviewTooltip: function () {
     this.imagePreviewTooltip.startTogglingOnHover(this._elt,
       this._isImagePreviewTarget);
   },
 
@@ -2633,28 +2639,25 @@ MarkupElementContainer.prototype = Herit
       return promise.reject("_getPreview called on a non-previewable element.");
     }
 
     if (this.tooltipDataPromise) {
       // A preview request is already pending. Re-use that request.
       return this.tooltipDataPromise;
     }
 
-    let maxDim =
-      Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
-
     // Fetch the preview from the server.
     this.tooltipDataPromise = Task.spawn(function* () {
+      let maxDim = Services.prefs.getIntPref(PREVIEW_MAX_DIM_PREF);
       let preview = yield this.node.getImageData(maxDim);
       let data = yield preview.data.string();
 
       // Clear the pending preview request. We can't reuse the results later as
       // the preview contents might have changed.
       this.tooltipDataPromise = null;
-
       return { data, size: preview.size };
     }.bind(this));
 
     return this.tooltipDataPromise;
   },
 
   /**
    * Executed by MarkupView._isImagePreviewTarget which is itself called when
@@ -2676,24 +2679,29 @@ MarkupElementContainer.prototype = Herit
     // name.
     let src = this.editor.getAttributeElement("src");
     let expectedTarget = src ? src.querySelector(".link") : this.editor.tag;
     if (target !== expectedTarget) {
       return false;
     }
 
     try {
-      let {data, size} = yield this._getPreview();
+      let { data, size } = yield this._getPreview();
       // The preview is ready.
-      tooltip.setImageContent(data, size);
+      let options = {
+        naturalWidth: size.naturalWidth,
+        naturalHeight: size.naturalHeight,
+        maxDim: Services.prefs.getIntPref(PREVIEW_MAX_DIM_PREF)
+      };
+
+      yield setImageTooltip(tooltip, this.markup.doc, data, options);
     } catch (e) {
       // Indicate the failure but show the tooltip anyway.
-      tooltip.setBrokenImageContent();
+      yield setBrokenImageTooltip(tooltip, this.markup.doc);
     }
-
     return true;
   }),
 
   copyImageDataUri: function () {
     // We need to send again a request to gettooltipData even if one was sent
     // for the tooltip, because we want the full-size image
     this.node.getImageData().then(data => {
       data.data.string().then(str => {
--- a/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js
+++ b/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js
@@ -44,17 +44,17 @@ function* getImageTooltipTarget({selecto
 function* assertTooltipShownOn(element, {markup}) {
   info("Is the element a valid hover target");
   let isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, element);
   ok(isValid, "The element is a valid hover target for the image tooltip");
 }
 
 function checkImageTooltip({selector, size}, {markup}) {
   let panel = markup.imagePreviewTooltip.panel;
-  let images = panel.getElementsByTagName("image");
+  let images = panel.getElementsByTagName("img");
   is(images.length, 1, "Tooltip for [" + selector + "] contains an image");
 
   let label = panel.querySelector(".devtools-tooltip-caption");
   is(label.textContent, size,
      "Tooltip label for [" + selector + "] displays the right image size");
 
   markup.imagePreviewTooltip.hide();
 }
--- a/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js
+++ b/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js
@@ -68,16 +68,16 @@ function updateImageSrc(img, newSrc, ins
 }
 
 /**
  * Checks that the markup view tooltip contains an image element with the given
  * size.
  */
 function checkImageTooltip(size, {markup}) {
   let panel = markup.imagePreviewTooltip.panel;
-  let images = panel.getElementsByTagName("image");
+  let images = panel.getElementsByTagName("img");
   is(images.length, 1, "Tooltip contains an image");
 
   let label = panel.querySelector(".devtools-tooltip-caption");
   is(label.textContent, size, "Tooltip label displays the right image size");
 
   markup.imagePreviewTooltip.hide();
 }
--- a/devtools/client/shared/widgets/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/HTMLTooltip.js
@@ -2,16 +2,18 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const EventEmitter = require("devtools/shared/event-emitter");
+const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
+
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 
 const IFRAME_URL = "chrome://devtools/content/shared/widgets/tooltip-frame.xhtml";
 const IFRAME_CONTAINER_ID = "tooltip-iframe-container";
 
 const POSITION = {
   TOP: "top",
@@ -66,16 +68,20 @@ function HTMLTooltip(toolbox,
   this.autofocus = autofocus;
   this.consumeOutsideClicks = consumeOutsideClicks;
 
   // Use the topmost window to listen for click events to close the tooltip
   this.topWindow = this.doc.defaultView.top;
 
   this._onClick = this._onClick.bind(this);
 
+  this._toggle = new TooltipToggle(this);
+  this.startTogglingOnHover = this._toggle.start.bind(this._toggle);
+  this.stopTogglingOnHover = this._toggle.stop.bind(this._toggle);
+
   this.container = this._createContainer();
 
   // Promise that will resolve when the container can be filled with content.
   this.containerReady = new Promise(resolve => {
     if (this._isXUL()) {
       // In XUL context, load a placeholder document in the iframe container.
       let onLoad = () => {
         this.frame.removeEventListener("load", onLoad, true);
@@ -229,17 +235,17 @@ HTMLTooltip.prototype = {
     let container = this.doc.createElementNS(XHTML_NS, "div");
     container.setAttribute("type", this.type);
     container.classList.add("tooltip-container");
 
     let html;
     if (this._isXUL()) {
       html = '<iframe class="devtools-tooltip-iframe tooltip-panel"></iframe>';
     } else {
-      html = '<div class="tooltip-panel theme-body"></div>';
+      html = '<div class="tooltip-panel"></div>';
     }
 
     if (this.type === TYPE.ARROW) {
       html += '<div class="tooltip-arrow"></div>';
     }
     container.innerHTML = html;
     return container;
   },
--- a/devtools/client/shared/widgets/tooltip-frame.xhtml
+++ b/devtools/client/shared/widgets/tooltip-frame.xhtml
@@ -12,14 +12,18 @@
     html, body, #tooltip-iframe-container {
       margin: 0;
       padding: 0;
       width: 100%;
       height: 100%;
       overflow: hidden;
       color: var(--theme-body-color);
     }
+
+    :root[platform="linux"] body {
+      font-size: 80%;
+    }
   </style>
 </head>
-<body role="application" class="theme-body">
+<body role="application">
   <div id="tooltip-iframe-container"></div>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js
@@ -0,0 +1,145 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Services = require("Services");
+loader.lazyGetter(this, "GetStringFromName", () => {
+  let bundle = Services.strings.createBundle(
+    "chrome://devtools/locale/inspector.properties");
+  return key => {
+    return bundle.GetStringFromName(key);
+  };
+});
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+// Default image tooltip max dimension
+const MAX_DIMENSION = 200;
+const CONTAINER_MIN_WIDTH = 100;
+const LABEL_HEIGHT = 20;
+const IMAGE_PADDING = 4;
+
+/**
+ * Image preview tooltips should be provided with the naturalHeight and
+ * naturalWidth value for the image to display. This helper loads the provided
+ * image URL in an image object in order to retrieve the image dimensions after
+ * the load.
+ *
+ * @param {Document} doc the document element to use to create the image object
+ * @param {String} imageUrl the url of the image to measure
+ * @return {Promise} returns a promise that will resolve after the iamge load:
+ *         - {Number} naturalWidth natural width of the loaded image
+ *         - {Number} naturalHeight natural height of the loaded image
+ */
+function getImageDimensions(doc, imageUrl) {
+  return new Promise(resolve => {
+    let imgObj = new doc.defaultView.Image();
+    imgObj.onload = () => {
+      imgObj.onload = null;
+      let { naturalWidth, naturalHeight } = imgObj;
+      resolve({ naturalWidth, naturalHeight });
+    };
+    imgObj.src = imageUrl;
+  });
+}
+
+/**
+ * Set the tooltip content of a provided HTMLTooltip instance to display an
+ * image preview matching the provided imageUrl.
+ *
+ * @param {HTMLTooltip} tooltip
+ *        The tooltip instance on which the image preview content should be set
+ * @param {Document} doc
+ *        A document element to create the HTML elements needed for the tooltip
+ * @param {String} imageUrl
+ *        Absolute URL of the image to display in the tooltip
+ * @param {Object} options
+ *        - {Number} naturalWidth mandatory, width of the image to display
+ *        - {Number} naturalHeight mandatory, height of the image to display
+ *        - {Number} maxDim optional, max width/height of the preview
+ *        - {Boolean} hideDimensionLabel optional, pass true to hide the label
+ * @return {Promise} promise that will resolve when the tooltip content has been
+ *         set
+ */
+function setImageTooltip(tooltip, doc, imageUrl, options) {
+  let {naturalWidth, naturalHeight, hideDimensionLabel, maxDim} = options;
+  maxDim = maxDim || MAX_DIMENSION;
+
+  let imgHeight = naturalHeight;
+  let imgWidth = naturalWidth;
+  if (imgHeight > maxDim || imgWidth > maxDim) {
+    let scale = maxDim / Math.max(imgHeight, imgWidth);
+    // Only allow integer values to avoid rounding errors.
+    imgHeight = Math.floor(scale * naturalHeight);
+    imgWidth = Math.ceil(scale * naturalWidth);
+  }
+
+  // Create tooltip content
+  let div = doc.createElementNS(XHTML_NS, "div");
+  div.style.cssText = `
+    height: 100%;
+    min-width: 100px;
+    display: flex;
+    flex-direction: column;
+    text-align: center;`;
+  let html = `
+    <div style="flex: 1;
+                display: flex;
+                padding: ${IMAGE_PADDING}px;
+                align-items: center;
+                justify-content: center;
+                min-height: 1px;">
+      <img style="height: ${imgHeight}px; max-height: 100%;" src="${imageUrl}"/>
+    </div>`;
+
+  if (!hideDimensionLabel) {
+    let label = naturalWidth + " \u00D7 " + naturalHeight;
+    html += `
+      <div style="height: ${LABEL_HEIGHT}px;
+                  text-align: center;">
+        <span class="theme-comment devtools-tooltip-caption">${label}</span>
+      </div>`;
+  }
+  div.innerHTML = html;
+
+  // Calculate tooltip dimensions
+  let height = imgHeight + 2 * IMAGE_PADDING;
+  if (!hideDimensionLabel) {
+    height += LABEL_HEIGHT;
+  }
+  let width = Math.max(CONTAINER_MIN_WIDTH, imgWidth + 2 * IMAGE_PADDING);
+
+  return tooltip.setContent(div, width, height);
+}
+
+/*
+ * Set the tooltip content of a provided HTMLTooltip instance to display a
+ * fallback error message when an image preview tooltip can not be displayed.
+ *
+ * @param {HTMLTooltip} tooltip
+ *        The tooltip instance on which the image preview content should be set
+ * @param {Document} doc
+ *        A document element to create the HTML elements needed for the tooltip
+ * @return {Promise} promise that will resolve when the tooltip content has been
+ *         set
+ */
+function setBrokenImageTooltip(tooltip, doc) {
+  let div = doc.createElementNS(XHTML_NS, "div");
+  div.style.cssText = `
+    box-sizing: border-box;
+    height: 100%;
+    text-align: center;
+    line-height: 30px;`;
+
+  let message = GetStringFromName("previewTooltip.image.brokenImage");
+  div.textContent = message;
+  return tooltip.setContent(div, 150, 30);
+}
+
+module.exports.getImageDimensions = getImageDimensions;
+module.exports.setImageTooltip = setImageTooltip;
+module.exports.setBrokenImageTooltip = setBrokenImageTooltip;
--- a/devtools/client/shared/widgets/tooltip/moz.build
+++ b/devtools/client/shared/widgets/tooltip/moz.build
@@ -1,9 +1,10 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'ImageTooltipHelper.js',
     'TooltipToggle.js',
 )