Bug 1270462 - part1: extract devtools tooltip toggle logic to separate file;r=bgrins,jsnajdr

The code used to make the tooltip appear/disappear when hovering targets
has been extracted to a separate class that can be shared between the
current Tooltip.js implementation and the upcoming HTMLTooltip.

MozReview-Commit-ID: UYSjPFeMYK

--HG--
extra : rebase_source : 5dcca2d5887ffc98fec621092640073a0909c13f
This commit is contained in:
Julian Descottes 2016-05-06 14:54:30 +02:00
Родитель e87387e53d
Коммит 87fe331949
14 изменённых файлов: 224 добавлений и 207 удалений

Просмотреть файл

@ -23,8 +23,6 @@ const SEARCH_TOKEN_FLAG = "#";
const SEARCH_LINE_FLAG = ":";
const SEARCH_VARIABLE_FLAG = "*";
const SEARCH_AUTOFILL = [SEARCH_GLOBAL_FLAG, SEARCH_FUNCTION_FLAG, SEARCH_TOKEN_FLAG];
const EDITOR_VARIABLE_HOVER_DELAY = 750; // ms
const EDITOR_VARIABLE_POPUP_POSITION = "topcenter bottomleft";
const TOOLBAR_ORDER_POPUP_POSITION = "topcenter bottomleft";
const RESIZE_REFRESH_RATE = 50; // ms
const PROMISE_DEBUGGER_URL =

Просмотреть файл

@ -846,7 +846,7 @@ function intendOpenVarPopup(aPanel, aPosition, aButtonPushed) {
deferred.resolve(true);
}
},
tooltip.defaultShowDelay + 1000
bubble.TOOLTIP_SHOW_DELAY + 1000
);
return deferred.promise;

Просмотреть файл

@ -25,6 +25,17 @@ function VariableBubbleView(DebuggerController, DebuggerView) {
}
VariableBubbleView.prototype = {
/**
* Delay before showing the variables bubble tooltip when hovering a valid
* target.
*/
TOOLTIP_SHOW_DELAY: 750,
/**
* Tooltip position for the variables bubble tooltip.
*/
TOOLTIP_POSITION: "topcenter bottomleft",
/**
* Initialization function, called when the debugger is started.
*/
@ -49,8 +60,7 @@ VariableBubbleView.prototype = {
event: "keydown"
}]
});
this._tooltip.defaultPosition = EDITOR_VARIABLE_POPUP_POSITION;
this._tooltip.defaultShowDelay = EDITOR_VARIABLE_HOVER_DELAY;
this._tooltip.defaultPosition = this.TOOLTIP_POSITION;
this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding);
},
@ -275,7 +285,7 @@ VariableBubbleView.prototype = {
// Allow events to settle down first. If the mouse hovers over
// a certain point in the editor long enough, try showing a variable bubble.
setNamedTimeout("editor-mouse-move",
EDITOR_VARIABLE_HOVER_DELAY, () => this._findIdentifier(e.clientX, e.clientY));
this.TOOLTIP_SHOW_DELAY, () => this._findIdentifier(e.clientX, e.clientY));
},
/**

Просмотреть файл

@ -16,7 +16,7 @@ add_task(function* () {
let target = img.editor.getAttributeElement("src").querySelector(".link");
info("Check that the src attribute of the image is a valid tooltip target");
let isValid = yield markup.tooltip.isValidHoverTarget(target);
let isValid = yield isHoverTooltipTarget(markup.tooltip, target);
ok(isValid, "The element is a valid tooltip target");
info("Start dragging the test div");
@ -24,7 +24,7 @@ add_task(function* () {
info("Now check that the src attribute of the image isn't a valid target");
try {
yield markup.tooltip.isValidHoverTarget(target);
yield isHoverTooltipTarget(markup.tooltip, target);
isValid = true;
} catch (e) {
isValid = false;
@ -35,6 +35,6 @@ add_task(function* () {
yield simulateNodeDrop(inspector, "div");
info("Check again the src attribute of the image");
isValid = yield markup.tooltip.isValidHoverTarget(target);
isValid = yield isHoverTooltipTarget(markup.tooltip, target);
ok(isValid, "The element is a valid tooltip target");
});

Просмотреть файл

@ -43,7 +43,7 @@ function* getImageTooltipTarget({selector}, inspector) {
function* assertTooltipShownOn(element, {markup}) {
info("Is the element a valid hover target");
let isValid = yield markup.tooltip.isValidHoverTarget(element);
let isValid = yield isHoverTooltipTarget(markup.tooltip, element);
ok(isValid, "The element is a valid hover target for the image tooltip");
}

Просмотреть файл

@ -37,7 +37,7 @@ add_task(function* () {
ok(target, "Found the src attribute in the markup view.");
info("Showing tooltip on the src link.");
yield inspector.markup.tooltip.isValidHoverTarget(target);
yield isHoverTooltipTarget(inspector.markup.tooltip, target);
checkImageTooltip(INITIAL_SRC_SIZE, inspector);
@ -48,7 +48,7 @@ add_task(function* () {
ok(target, "Found the src attribute in the markup view after mutation.");
info("Showing tooltip on the src link.");
yield inspector.markup.tooltip.isValidHoverTarget(target);
yield isHoverTooltipTarget(inspector.markup.tooltip, target);
info("Checking that the new image was shown.");
checkImageTooltip(UPDATED_SRC_SIZE, inspector);

Просмотреть файл

@ -169,36 +169,6 @@ var focusEditableField = Task.async(function* (ruleView, editable, xOffset = 1,
return onEdit;
});
/**
* Given a tooltip object instance (see Tooltip.js), checks if it is set to
* toggle and hover and if so, checks if the given target is a valid hover
* target. This won't actually show the tooltip (the less we interact with XUL
* panels during test runs, the better).
*
* @return a promise that resolves when the answer is known
*/
function isHoverTooltipTarget(tooltip, target) {
if (!tooltip._basedNode || !tooltip.panel) {
return promise.reject(new Error(
"The tooltip passed isn't set to toggle on hover or is not a tooltip"));
}
return tooltip.isValidHoverTarget(target);
}
/**
* Same as isHoverTooltipTarget except that it will fail the test if there is no
* tooltip defined on hover of the given element
*
* @return a promise
*/
function assertHoverTooltipOn(tooltip, element) {
return isHoverTooltipTarget(tooltip, element).then(() => {
ok(true, "A tooltip is defined on hover of the given element");
}, () => {
ok(false, "No tooltip is defined on hover of the given element");
});
}
/**
* When a tooltip is closed, this ends up "commiting" the value changed within
* the tooltip (e.g. the color in case of a colorpicker) which, in turn, ends up

Просмотреть файл

@ -198,36 +198,6 @@ var focusEditableField = Task.async(function*(ruleView, editable, xOffset=1,
return onEdit;
});
/**
* Given a tooltip object instance (see Tooltip.js), checks if it is set to
* toggle and hover and if so, checks if the given target is a valid hover
* target. This won't actually show the tooltip (the less we interact with XUL
* panels during test runs, the better).
*
* @return a promise that resolves when the answer is known
*/
function isHoverTooltipTarget(tooltip, target) {
if (!tooltip._basedNode || !tooltip.panel) {
return promise.reject(new Error(
"The tooltip passed isn't set to toggle on hover or is not a tooltip"));
}
return tooltip.isValidHoverTarget(target);
}
/**
* Same as isHoverTooltipTarget except that it will fail the test if there is no
* tooltip defined on hover of the given element
*
* @return a promise
*/
function assertHoverTooltipOn(tooltip, element) {
return isHoverTooltipTarget(tooltip, element).then(() => {
ok(true, "A tooltip is defined on hover of the given element");
}, () => {
ok(false, "No tooltip is defined on hover of the given element");
});
}
/**
* Polls a given function waiting for it to return true.
*

Просмотреть файл

@ -777,3 +777,33 @@ function synthesizeKeys(input, win) {
EventUtils.synthesizeKey(key, {}, win);
}
}
/**
* Given a tooltip object instance (see Tooltip.js), checks if it is set to
* toggle and hover and if so, checks if the given target is a valid hover
* target. This won't actually show the tooltip (the less we interact with XUL
* panels during test runs, the better).
*
* @return a promise that resolves when the answer is known
*/
function isHoverTooltipTarget(tooltip, target) {
if (!tooltip._toggle._baseNode || !tooltip.panel) {
return promise.reject(new Error(
"The tooltip passed isn't set to toggle on hover or is not a tooltip"));
}
return tooltip._toggle.isValidHoverTarget(target);
}
/**
* Same as isHoverTooltipTarget except that it will fail the test if there is no
* tooltip defined on hover of the given element
*
* @return a promise
*/
function assertHoverTooltipOn(tooltip, element) {
return isHoverTooltipTarget(tooltip, element).then(() => {
ok(true, "A tooltip is defined on hover of the given element");
}, () => {
ok(false, "No tooltip is defined on hover of the given element");
});
}

Просмотреть файл

@ -64,7 +64,8 @@ function test() {
*/
function showTooltipOn(tooltip, element) {
return Task.spawn(function*() {
let isTarget = yield tooltip.isValidHoverTarget(element);
let isValidTarget = yield tooltip._toggle.isValidHoverTarget(element);
ok(isValidTarget, "Element is a valid tooltip target");
let onShown = tooltip.once("shown");
tooltip.show();
yield onShown;

Просмотреть файл

@ -11,6 +11,7 @@ const {CubicBezierWidget} =
require("devtools/client/shared/widgets/CubicBezierWidget");
const {MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
const EventEmitter = require("devtools/shared/event-emitter");
const {colorUtils} = require("devtools/client/shared/css-color");
const Heritage = require("sdk/core/heritage");
@ -188,8 +189,11 @@ function Tooltip(doc, options) {
}, options);
this.panel = PanelFactory.get(doc, this.options);
// Used for namedTimeouts in the mouseover handling
this.uid = "tooltip-" + Date.now();
// Create tooltip toggle helper and decorate the Tooltip instance with
// shortcut methods.
this._toggle = new TooltipToggle(this);
this.startTogglingOnHover = this._toggle.start.bind(this._toggle);
this.stopTogglingOnHover = this._toggle.stop.bind(this._toggle);
// Emit show/hide events when the panel does.
for (let eventName of POPUP_EVENTS) {
@ -242,7 +246,6 @@ Tooltip.prototype = {
// px
defaultOffsetY: 0,
// px
defaultShowDelay: 50,
/**
* Show the tooltip. It might be wise to append some content first if you
@ -333,9 +336,7 @@ Tooltip.prototype = {
this.content = null;
if (this._basedNode) {
this.stopTogglingOnHover();
}
this._toggle.destroy();
this.doc = null;
@ -343,134 +344,6 @@ Tooltip.prototype = {
this.panel = null;
},
/**
* Show/hide the tooltip when the mouse hovers over particular nodes.
*
* 2 Ways to make this work:
* - Provide a single node to attach the tooltip to, as the baseNode, and
* omit the second targetNodeCb argument
* - Provide a baseNode that is the container of possibly numerous children
* elements that may receive a tooltip. In this case, provide the second
* targetNodeCb argument to decide wether or not a child should receive
* a tooltip.
*
* This works by tracking mouse movements on a base container node (baseNode)
* and showing the tooltip when the mouse stops moving. The targetNodeCb
* callback is used to know whether or not the particular element being
* hovered over should indeed receive the tooltip. If you don't provide it
* it's equivalent to a function that always returns true.
*
* Note that if you call this function a second time, it will itself call
* stopTogglingOnHover before adding mouse tracking listeners again.
*
* @param {node} baseNode
* The container for all target nodes
* @param {Function} targetNodeCb
* A function that accepts a node argument and returns true or false
* (or a promise that resolves or rejects) to signify if the tooltip
* should be shown on that node or not.
* If the promise rejects, it must reject `false` as value.
* Any other value is going to be logged as unexpected error.
* Additionally, the function receives a second argument which is the
* tooltip instance itself, to be used to add/modify the content of the
* tooltip if needed. If omitted, the tooltip will be shown everytime.
* @param {Number} showDelay
* An optional delay that will be observed before showing the tooltip.
* Defaults to this.defaultShowDelay.
*/
startTogglingOnHover: function(baseNode, targetNodeCb,
showDelay=this.defaultShowDelay) {
if (this._basedNode) {
this.stopTogglingOnHover();
}
if (!baseNode) {
// Calling tool is in the process of being destroyed.
return;
}
this._basedNode = baseNode;
this._showDelay = showDelay;
this._targetNodeCb = targetNodeCb || (() => true);
this._onBaseNodeMouseMove = this._onBaseNodeMouseMove.bind(this);
this._onBaseNodeMouseLeave = this._onBaseNodeMouseLeave.bind(this);
baseNode.addEventListener("mousemove", this._onBaseNodeMouseMove, false);
baseNode.addEventListener("mouseleave", this._onBaseNodeMouseLeave, false);
},
/**
* If the startTogglingOnHover function has been used previously, and you want
* to get rid of this behavior, then call this function to remove the mouse
* movement tracking
*/
stopTogglingOnHover: function() {
clearNamedTimeout(this.uid);
if (!this._basedNode) {
return;
}
this._basedNode.removeEventListener("mousemove",
this._onBaseNodeMouseMove, false);
this._basedNode.removeEventListener("mouseleave",
this._onBaseNodeMouseLeave, false);
this._basedNode = null;
this._targetNodeCb = null;
this._lastHovered = null;
},
_onBaseNodeMouseMove: function(event) {
if (event.target !== this._lastHovered) {
this.hide();
this._lastHovered = event.target;
setNamedTimeout(this.uid, this._showDelay, () => {
this.isValidHoverTarget(event.target).then(target => {
this.show(target);
}, reason => {
if (reason === false) {
// isValidHoverTarget rejects with false if the tooltip should
// not be shown. This can be safely ignored.
return;
}
// Report everything else. Reason might be error that should not be
// hidden.
console.error("isValidHoverTarget rejected with an unexpected reason:");
console.error(reason);
});
});
}
},
/**
* Is the given target DOMNode a valid node for toggling the tooltip on hover.
* This delegates to the user-defined _targetNodeCb callback.
* @return a promise that resolves or rejects depending if the tooltip should
* be shown or not. If it resolves, it does to the actual anchor to be used
*/
isValidHoverTarget: function(target) {
// Execute the user-defined callback which should return either true/false
// or a promise that resolves or rejects
let res = this._targetNodeCb(target, this);
// The callback can additionally return a DOMNode to replace the anchor of
// the tooltip when shown
if (res && res.then) {
return res.then(arg => {
return arg instanceof Ci.nsIDOMNode ? arg : target;
});
}
let newTarget = res instanceof Ci.nsIDOMNode ? res : target;
return res ? promise.resolve(newTarget) : promise.reject(false);
},
_onBaseNodeMouseLeave: function() {
clearNamedTimeout(this.uid);
this._lastHovered = null;
this.hide();
},
/**
* Set the content of this tooltip. Will first empty the tooltip and then
* append the new content element.

Просмотреть файл

@ -4,6 +4,10 @@
# 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/.
DIRS += [
'tooltip',
]
DevToolsModules(
'AbstractTreeItem.jsm',
'BarGraphWidget.js',

Просмотреть файл

@ -0,0 +1,152 @@
/* -*- 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 DEFAULT_SHOW_DELAY = 50;
/**
* Tooltip helper designed to show/hide the tooltip when the mouse hovers over
* particular nodes.
*
* This works by tracking mouse movements on a base container node (baseNode)
* and showing the tooltip when the mouse stops moving. A callback can be
* provided to the start() method to know whether or not the node being
* hovered over should indeed receive the tooltip.
*/
function TooltipToggle(tooltip) {
this.tooltip = tooltip;
this.win = tooltip.doc.defaultView;
this._onMouseMove = this._onMouseMove.bind(this);
this._onMouseLeave = this._onMouseLeave.bind(this);
}
module.exports.TooltipToggle = TooltipToggle;
TooltipToggle.prototype = {
/**
* Start tracking mouse movements on the provided baseNode to show the
* tooltip.
*
* 2 Ways to make this work:
* - Provide a single node to attach the tooltip to, as the baseNode, and
* omit the second targetNodeCb argument
* - Provide a baseNode that is the container of possibly numerous children
* elements that may receive a tooltip. In this case, provide the second
* targetNodeCb argument to decide wether or not a child should receive
* a tooltip.
*
* Note that if you call this function a second time, it will itself call
* stop() before adding mouse tracking listeners again.
*
* @param {node} baseNode
* The container for all target nodes
* @param {Function} targetNodeCb
* A function that accepts a node argument and returns true or false
* (or a promise that resolves or rejects) to signify if the tooltip
* should be shown on that node or not.
* If the promise rejects, it must reject `false` as value.
* Any other value is going to be logged as unexpected error.
* Additionally, the function receives a second argument which is the
* tooltip instance itself, to be used to add/modify the content of the
* tooltip if needed. If omitted, the tooltip will be shown everytime.
* @param {Number} showDelay
* An optional delay that will be observed before showing the tooltip.
* Defaults to DEFAULT_SHOW_DELAY.
*/
start: function (baseNode, targetNodeCb, showDelay = DEFAULT_SHOW_DELAY) {
this.stop();
if (!baseNode) {
// Calling tool is in the process of being destroyed.
return;
}
this._baseNode = baseNode;
this._showDelay = showDelay;
this._targetNodeCb = targetNodeCb || (() => true);
baseNode.addEventListener("mousemove", this._onMouseMove, false);
baseNode.addEventListener("mouseleave", this._onMouseLeave, false);
},
/**
* If the start() function has been used previously, and you want to get rid
* of this behavior, then call this function to remove the mouse movement
* tracking
*/
stop: function () {
this.win.clearTimeout(this.toggleTimer);
if (!this._baseNode) {
return;
}
this._baseNode.removeEventListener("mousemove", this._onMouseMove, false);
this._baseNode.removeEventListener("mouseleave", this._onMouseLeave, false);
this._baseNode = null;
this._targetNodeCb = null;
this._lastHovered = null;
},
_onMouseMove: function (event) {
if (event.target !== this._lastHovered) {
this.tooltip.hide();
this._lastHovered = event.target;
this.win.clearTimeout(this.toggleTimer);
this.toggleTimer = this.win.setTimeout(() => {
this.isValidHoverTarget(event.target).then(target => {
this.tooltip.show(target);
}, reason => {
if (reason === false) {
// isValidHoverTarget rejects with false if the tooltip should
// not be shown. This can be safely ignored.
return;
}
console.error("isValidHoverTarget rejected with unexpected reason:");
console.error(reason);
});
}, this._showDelay);
}
},
/**
* Is the given target DOMNode a valid node for toggling the tooltip on hover.
* This delegates to the user-defined _targetNodeCb callback.
* @return a promise that resolves or rejects depending if the tooltip should
* be shown or not. If it resolves, it does to the actual anchor to be used
*/
isValidHoverTarget: function (target) {
// Execute the user-defined callback which should return either true/false
// or a promise that resolves or rejects
let res = this._targetNodeCb(target, this.tooltip);
// The callback can additionally return a DOMNode to replace the anchor of
// the tooltip when shown
if (res && res.then) {
return res.then(arg => {
return arg && arg.nodeName ? arg : target;
});
}
let newTarget = res && res.nodeName ? res : target;
return new Promise((resolve, reject) => {
res ? resolve(newTarget) : reject(false);
});
},
_onMouseLeave: function () {
this.win.clearTimeout(this.toggleTimer);
this._lastHovered = null;
this.tooltip.hide();
},
destroy: function () {
this.stop();
}
};

Просмотреть файл

@ -0,0 +1,9 @@
# -*- 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(
'TooltipToggle.js',
)