From 42980ba3b825e567ea326e2370b0d549c81eb068 Mon Sep 17 00:00:00 2001 From: Nicolas Chevobbe Date: Tue, 5 Jan 2016 12:58:48 +0100 Subject: [PATCH] Bug 1228080 - Split the animation-inspector's components into multiple files. r=pbro --HG-- extra : commitid : BvhS3xAo90X --- .../animationinspector/animation-panel.js | 6 +- .../client/animationinspector/components.js | 1350 ----------------- .../components/animation-details.js | 160 ++ .../components/animation-target-node.js | 320 ++++ .../components/animation-time-block.js | 157 ++ .../components/animation-timeline.js | 433 ++++++ .../components/keyframes.js | 70 + .../animationinspector/components/moz.build | 12 + .../components/rate-selector.js | 95 ++ devtools/client/animationinspector/moz.build | 5 +- .../test/browser_animation_timeline_header.js | 8 +- .../test/unit/test_getCssPropertyName.js | 2 +- .../test/unit/test_timeScale.js | 3 +- devtools/client/animationinspector/utils.js | 141 ++ 14 files changed, 1401 insertions(+), 1361 deletions(-) delete mode 100644 devtools/client/animationinspector/components.js create mode 100644 devtools/client/animationinspector/components/animation-details.js create mode 100644 devtools/client/animationinspector/components/animation-target-node.js create mode 100644 devtools/client/animationinspector/components/animation-time-block.js create mode 100644 devtools/client/animationinspector/components/animation-timeline.js create mode 100644 devtools/client/animationinspector/components/keyframes.js create mode 100644 devtools/client/animationinspector/components/moz.build create mode 100644 devtools/client/animationinspector/components/rate-selector.js diff --git a/devtools/client/animationinspector/animation-panel.js b/devtools/client/animationinspector/animation-panel.js index 190343417c10..e941cb246973 100644 --- a/devtools/client/animationinspector/animation-panel.js +++ b/devtools/client/animationinspector/animation-panel.js @@ -8,10 +8,8 @@ "use strict"; -const { - AnimationsTimeline, - RateSelector -} = require("devtools/client/animationinspector/components"); +const {AnimationsTimeline} = require("devtools/client/animationinspector/components/animation-timeline"); +const {RateSelector} = require("devtools/client/animationinspector/components/rate-selector"); const {formatStopwatchTime} = require("devtools/client/animationinspector/utils"); var $ = (selector, target = document) => target.querySelector(selector); diff --git a/devtools/client/animationinspector/components.js b/devtools/client/animationinspector/components.js deleted file mode 100644 index 40bc3fa076f7..000000000000 --- a/devtools/client/animationinspector/components.js +++ /dev/null @@ -1,1350 +0,0 @@ -/* -*- 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"; - -// Set of reusable UI components for the animation-inspector UI. -// All components in this module share a common API: -// 1. construct the component: -// let c = new ComponentName(); -// 2. initialize the markup of the component in a given parent node: -// c.init(containerElement); -// 3. render the component, passing in some sort of state: -// This may be called over and over again when the state changes, to update -// the component output. -// c.render(state); -// 4. destroy the component: -// c.destroy(); - -const {Cu} = require("chrome"); -Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm"); -const {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); -const { - createNode, - drawGraphElementBackground, - findOptimalTimeInterval, - TargetNodeHighlighter -} = require("devtools/client/animationinspector/utils"); - -const STRINGS_URI = "chrome://devtools/locale/animationinspector.properties"; -const L10N = new ViewHelpers.L10N(STRINGS_URI); -const MILLIS_TIME_FORMAT_MAX_DURATION = 4000; -// The minimum spacing between 2 time graduation headers in the timeline (px). -const TIME_GRADUATION_MIN_SPACING = 40; -// List of playback rate presets displayed in the timeline toolbar. -const PLAYBACK_RATES = [.1, .25, .5, 1, 2, 5, 10]; -// When the container window is resized, the timeline background gets refreshed, -// but only after a timer, and the timer is reset if the window is continuously -// resized. -const TIMELINE_BACKGROUND_RESIZE_DEBOUNCE_TIMER = 50; - -/** - * UI component responsible for displaying a preview of the target dom node of - * a given animation. - * @param {InspectorPanel} inspector Requires a reference to the inspector-panel - * to highlight and select the node, as well as refresh it when there are - * mutations. - * @param {Object} options Supported properties are: - * - compact {Boolean} Defaults to false. If true, nodes will be previewed like - * tag#id.class instead of - */ -function AnimationTargetNode(inspector, options={}) { - this.inspector = inspector; - this.options = options; - - this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this); - this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this); - this.onSelectNodeClick = this.onSelectNodeClick.bind(this); - this.onMarkupMutations = this.onMarkupMutations.bind(this); - this.onHighlightNodeClick = this.onHighlightNodeClick.bind(this); - this.onTargetHighlighterLocked = this.onTargetHighlighterLocked.bind(this); - - EventEmitter.decorate(this); -} - -exports.AnimationTargetNode = AnimationTargetNode; - -AnimationTargetNode.prototype = { - init: function(containerEl) { - let document = containerEl.ownerDocument; - - // Init the markup for displaying the target node. - this.el = createNode({ - parent: containerEl, - attributes: { - "class": "animation-target" - } - }); - - // Icon to select the node in the inspector. - this.highlightNodeEl = createNode({ - parent: this.el, - nodeType: "span", - attributes: { - "class": "node-highlighter", - "title": L10N.getStr("node.highlightNodeLabel") - } - }); - - // Wrapper used for mouseover/out event handling. - this.previewEl = createNode({ - parent: this.el, - nodeType: "span", - attributes: { - "title": L10N.getStr("node.selectNodeLabel") - } - }); - - if (!this.options.compact) { - this.previewEl.appendChild(document.createTextNode("<")); - } - - // Tag name. - this.tagNameEl = createNode({ - parent: this.previewEl, - nodeType: "span", - attributes: { - "class": "tag-name theme-fg-color3" - } - }); - - // Id attribute container. - this.idEl = createNode({ - parent: this.previewEl, - nodeType: "span" - }); - - if (!this.options.compact) { - createNode({ - parent: this.idEl, - nodeType: "span", - attributes: { - "class": "attribute-name theme-fg-color2" - }, - textContent: "id" - }); - this.idEl.appendChild(document.createTextNode("=\"")); - } else { - createNode({ - parent: this.idEl, - nodeType: "span", - attributes: { - "class": "theme-fg-color2" - }, - textContent: "#" - }); - } - - createNode({ - parent: this.idEl, - nodeType: "span", - attributes: { - "class": "attribute-value theme-fg-color6" - } - }); - - if (!this.options.compact) { - this.idEl.appendChild(document.createTextNode("\"")); - } - - // Class attribute container. - this.classEl = createNode({ - parent: this.previewEl, - nodeType: "span" - }); - - if (!this.options.compact) { - createNode({ - parent: this.classEl, - nodeType: "span", - attributes: { - "class": "attribute-name theme-fg-color2" - }, - textContent: "class" - }); - this.classEl.appendChild(document.createTextNode("=\"")); - } else { - createNode({ - parent: this.classEl, - nodeType: "span", - attributes: { - "class": "theme-fg-color6" - }, - textContent: "." - }); - } - - createNode({ - parent: this.classEl, - nodeType: "span", - attributes: { - "class": "attribute-value theme-fg-color6" - } - }); - - if (!this.options.compact) { - this.classEl.appendChild(document.createTextNode("\"")); - this.previewEl.appendChild(document.createTextNode(">")); - } - - this.startListeners(); - }, - - startListeners: function() { - // Init events for highlighting and selecting the node. - this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver); - this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut); - this.previewEl.addEventListener("click", this.onSelectNodeClick); - this.highlightNodeEl.addEventListener("click", this.onHighlightNodeClick); - - // Start to listen for markupmutation events. - this.inspector.on("markupmutation", this.onMarkupMutations); - - // Listen to the target node highlighter. - TargetNodeHighlighter.on("highlighted", this.onTargetHighlighterLocked); - }, - - stopListeners: function() { - TargetNodeHighlighter.off("highlighted", this.onTargetHighlighterLocked); - this.inspector.off("markupmutation", this.onMarkupMutations); - this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver); - this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut); - this.previewEl.removeEventListener("click", this.onSelectNodeClick); - this.highlightNodeEl.removeEventListener("click", this.onHighlightNodeClick); - }, - - destroy: function() { - TargetNodeHighlighter.unhighlight().catch(e => console.error(e)); - - this.stopListeners(); - - this.el.remove(); - this.el = this.tagNameEl = this.idEl = this.classEl = null; - this.highlightNodeEl = this.previewEl = null; - this.nodeFront = this.inspector = this.playerFront = null; - }, - - get highlighterUtils() { - if (this.inspector && this.inspector.toolbox) { - return this.inspector.toolbox.highlighterUtils; - } - return null; - }, - - onPreviewMouseOver: function() { - if (!this.nodeFront || !this.highlighterUtils) { - return; - } - this.highlighterUtils.highlightNodeFront(this.nodeFront) - .catch(e => console.error(e)); - }, - - onPreviewMouseOut: function() { - if (!this.nodeFront || !this.highlighterUtils) { - return; - } - this.highlighterUtils.unhighlight() - .catch(e => console.error(e)); - }, - - onSelectNodeClick: function() { - if (!this.nodeFront) { - return; - } - this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector"); - }, - - onHighlightNodeClick: function(e) { - e.stopPropagation(); - - let classList = this.highlightNodeEl.classList; - - let isHighlighted = classList.contains("selected"); - if (isHighlighted) { - classList.remove("selected"); - TargetNodeHighlighter.unhighlight().then(() => { - this.emit("target-highlighter-unlocked"); - }, e => console.error(e)); - } else { - classList.add("selected"); - TargetNodeHighlighter.highlight(this).then(() => { - this.emit("target-highlighter-locked"); - }, e => console.error(e)); - } - }, - - onTargetHighlighterLocked: function(e, animationTargetNode) { - if (animationTargetNode !== this) { - this.highlightNodeEl.classList.remove("selected"); - } - }, - - onMarkupMutations: function(e, mutations) { - if (!this.nodeFront || !this.playerFront) { - return; - } - - for (let {target} of mutations) { - if (target === this.nodeFront) { - // Re-render with the same nodeFront to update the output. - this.render(this.playerFront); - break; - } - } - }, - - render: Task.async(function*(playerFront) { - this.playerFront = playerFront; - this.nodeFront = undefined; - - try { - this.nodeFront = yield this.inspector.walker.getNodeFromActor( - playerFront.actorID, ["node"]); - } catch (e) { - if (!this.el) { - // The panel was destroyed in the meantime. Just log a warning. - console.warn("Cound't retrieve the animation target node, widget " + - "destroyed"); - } else { - // This was an unexpected error, log it. - console.error(e); - } - return; - } - - if (!this.nodeFront || !this.el) { - return; - } - - let {tagName, attributes} = this.nodeFront; - - this.tagNameEl.textContent = tagName.toLowerCase(); - - let idIndex = attributes.findIndex(({name}) => name === "id"); - if (idIndex > -1 && attributes[idIndex].value) { - this.idEl.querySelector(".attribute-value").textContent = - attributes[idIndex].value; - this.idEl.style.display = "inline"; - } else { - this.idEl.style.display = "none"; - } - - let classIndex = attributes.findIndex(({name}) => name === "class"); - if (classIndex > -1 && attributes[classIndex].value) { - let value = attributes[classIndex].value; - if (this.options.compact) { - value = value.split(" ").join("."); - } - - this.classEl.querySelector(".attribute-value").textContent = value; - this.classEl.style.display = "inline"; - } else { - this.classEl.style.display = "none"; - } - - this.emit("target-retrieved"); - }) -}; - -/** - * UI component responsible for displaying a playback rate selector UI. - * The rendering logic is such that a predefined list of rates is generated. - * If *all* animations passed to render share the same rate, then that rate is - * selected in the element, otherwise, the empty value is selected. + * If the rate that all animations share isn't part of the list of predefined + * rates, than that rate is added to the list. + */ +function RateSelector() { + this.onRateChanged = this.onRateChanged.bind(this); + EventEmitter.decorate(this); +} + +exports.RateSelector = RateSelector; + +RateSelector.prototype = { + init: function(containerEl) { + this.selectEl = createNode({ + parent: containerEl, + nodeType: "select", + attributes: {"class": "devtools-button"} + }); + + this.selectEl.addEventListener("change", this.onRateChanged); + }, + + destroy: function() { + this.selectEl.removeEventListener("change", this.onRateChanged); + this.selectEl.remove(); + this.selectEl = null; + }, + + getAnimationsRates: function(animations) { + return sortedUnique(animations.map(a => a.state.playbackRate)); + }, + + getAllRates: function(animations) { + let animationsRates = this.getAnimationsRates(animations); + if (animationsRates.length > 1) { + return PLAYBACK_RATES; + } + + return sortedUnique(PLAYBACK_RATES.concat(animationsRates)); + }, + + render: function(animations) { + let allRates = this.getAnimationsRates(animations); + let hasOneRate = allRates.length === 1; + + this.selectEl.innerHTML = ""; + + if (!hasOneRate) { + // When the animations displayed have mixed playback rates, we can't + // select any of the predefined ones, instead, insert an empty rate. + createNode({ + parent: this.selectEl, + nodeType: "option", + attributes: {value: "", selector: "true"}, + textContent: "-" + }); + } + for (let rate of this.getAllRates(animations)) { + let option = createNode({ + parent: this.selectEl, + nodeType: "option", + attributes: {value: rate}, + textContent: L10N.getFormatStr("player.playbackRateLabel", rate) + }); + + // If there's only one rate and this is the option for it, select it. + if (hasOneRate && rate === allRates[0]) { + option.setAttribute("selected", "true"); + } + } + }, + + onRateChanged: function() { + let rate = parseFloat(this.selectEl.value); + if (!isNaN(rate)) { + this.emit("rate-changed", rate); + } + } +}; + +let sortedUnique = arr => [...new Set(arr)].sort((a, b) => a > b); diff --git a/devtools/client/animationinspector/moz.build b/devtools/client/animationinspector/moz.build index ddad05fb324d..e98919ee8e3a 100644 --- a/devtools/client/animationinspector/moz.build +++ b/devtools/client/animationinspector/moz.build @@ -7,7 +7,10 @@ BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini'] +DIRS += [ + 'components' +] + DevToolsModules( - 'components.js', 'utils.js', ) diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_header.js b/devtools/client/animationinspector/test/browser_animation_timeline_header.js index 8945bec8a8b2..e3d861ce4ad9 100644 --- a/devtools/client/animationinspector/test/browser_animation_timeline_header.js +++ b/devtools/client/animationinspector/test/browser_animation_timeline_header.js @@ -8,9 +8,11 @@ requestLongerTimeout(2); // Check that the timeline shows correct time graduations in the header. -const {findOptimalTimeInterval} = require("devtools/client/animationinspector/utils"); -const {TimeScale} = require("devtools/client/animationinspector/components"); -// Should be kept in sync with TIME_GRADUATION_MIN_SPACING in components.js +const { + findOptimalTimeInterval, + TimeScale +} = require("devtools/client/animationinspector/utils"); +// Should be kept in sync with TIME_GRADUATION_MIN_SPACING in animation-timeline.js const TIME_GRADUATION_MIN_SPACING = 40; add_task(function*() { diff --git a/devtools/client/animationinspector/test/unit/test_getCssPropertyName.js b/devtools/client/animationinspector/test/unit/test_getCssPropertyName.js index 016cf48c30c7..4432c1e08d8f 100644 --- a/devtools/client/animationinspector/test/unit/test_getCssPropertyName.js +++ b/devtools/client/animationinspector/test/unit/test_getCssPropertyName.js @@ -7,7 +7,7 @@ var Cu = Components.utils; const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); -const {getCssPropertyName} = require("devtools/client/animationinspector/components"); +const {getCssPropertyName} = require("devtools/client/animationinspector/components/animation-details"); const TEST_DATA = [{ jsName: "alllowercase", diff --git a/devtools/client/animationinspector/test/unit/test_timeScale.js b/devtools/client/animationinspector/test/unit/test_timeScale.js index 29fa35aaae67..9ee4b8a594f0 100644 --- a/devtools/client/animationinspector/test/unit/test_timeScale.js +++ b/devtools/client/animationinspector/test/unit/test_timeScale.js @@ -7,8 +7,7 @@ var Cu = Components.utils; const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); -const {TimeScale} = require("devtools/client/animationinspector/components"); - +const {TimeScale} = require("devtools/client/animationinspector/utils"); const TEST_ANIMATIONS = [{ desc: "Testing a few standard animations", animations: [{ diff --git a/devtools/client/animationinspector/utils.js b/devtools/client/animationinspector/utils.js index bbf6eff2267b..49bc4481f973 100644 --- a/devtools/client/animationinspector/utils.js +++ b/devtools/client/animationinspector/utils.js @@ -7,11 +7,14 @@ "use strict"; const {Cu} = require("chrome"); +Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm"); const {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); var {loader} = Cu.import("resource://devtools/shared/Loader.jsm"); loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter"); +const STRINGS_URI = "chrome://devtools/locale/animationinspector.properties"; +const L10N = new ViewHelpers.L10N(STRINGS_URI); // How many times, maximum, can we loop before we find the optimal time // interval in the timeline graph. const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100; @@ -27,6 +30,8 @@ const TIME_INTERVAL_OPACITY_MIN = 32; // byte const TIME_INTERVAL_OPACITY_ADD = 32; +const MILLIS_TIME_FORMAT_MAX_DURATION = 4000; + /** * DOM node creation helper function. * @param {Object} Options to customize the node to be created. @@ -208,3 +213,139 @@ function formatStopwatchTime(time) { } exports.formatStopwatchTime = formatStopwatchTime; + +/** + * The TimeScale helper object is used to know which size should something be + * displayed with in the animation panel, depending on the animations that are + * currently displayed. + * If there are 5 animations displayed, and the first one starts at 10000ms and + * the last one ends at 20000ms, then this helper can be used to convert any + * time in this range to a distance in pixels. + * + * For the helper to know how to convert, it needs to know all the animations. + * Whenever a new animation is added to the panel, addAnimation(state) should be + * called. reset() can be called to start over. + */ +var TimeScale = { + minStartTime: Infinity, + maxEndTime: 0, + + /** + * Add a new animation to time scale. + * @param {Object} state A PlayerFront.state object. + */ + addAnimation: function(state) { + let {previousStartTime, delay, duration, + iterationCount, playbackRate} = state; + + // Negative-delayed animations have their startTimes set such that we would + // be displaying the delay outside the time window if we didn't take it into + // account here. + let relevantDelay = delay < 0 ? delay / playbackRate : 0; + previousStartTime = previousStartTime || 0; + + this.minStartTime = Math.min(this.minStartTime, + previousStartTime + relevantDelay); + let length = (delay / playbackRate) + + ((duration / playbackRate) * + (!iterationCount ? 1 : iterationCount)); + let endTime = previousStartTime + length; + this.maxEndTime = Math.max(this.maxEndTime, endTime); + }, + + /** + * Reset the current time scale. + */ + reset: function() { + this.minStartTime = Infinity; + this.maxEndTime = 0; + }, + + /** + * Convert a startTime to a distance in %, in the current time scale. + * @param {Number} time + * @return {Number} + */ + startTimeToDistance: function(time) { + time -= this.minStartTime; + return this.durationToDistance(time); + }, + + /** + * Convert a duration to a distance in %, in the current time scale. + * @param {Number} time + * @return {Number} + */ + durationToDistance: function(duration) { + return duration * 100 / this.getDuration(); + }, + + /** + * Convert a distance in % to a time, in the current time scale. + * @param {Number} distance + * @return {Number} + */ + distanceToTime: function(distance) { + return this.minStartTime + (this.getDuration() * distance / 100); + }, + + /** + * Convert a distance in % to a time, in the current time scale. + * The time will be relative to the current minimum start time. + * @param {Number} distance + * @return {Number} + */ + distanceToRelativeTime: function(distance) { + let time = this.distanceToTime(distance); + return time - this.minStartTime; + }, + + /** + * Depending on the time scale, format the given time as milliseconds or + * seconds. + * @param {Number} time + * @return {String} The formatted time string. + */ + formatTime: function(time) { + // Format in milliseconds if the total duration is short enough. + if (this.getDuration() <= MILLIS_TIME_FORMAT_MAX_DURATION) { + return L10N.getFormatStr("timeline.timeGraduationLabel", time.toFixed(0)); + } + + // Otherwise format in seconds. + return L10N.getFormatStr("player.timeLabel", (time / 1000).toFixed(1)); + }, + + getDuration: function() { + return this.maxEndTime - this.minStartTime; + }, + + /** + * Given an animation, get the various dimensions (in %) useful to draw the + * animation in the timeline. + */ + getAnimationDimensions: function({state}) { + let start = state.previousStartTime || 0; + let duration = state.duration; + let rate = state.playbackRate; + let count = state.iterationCount; + let delay = state.delay || 0; + + // The start position. + let x = this.startTimeToDistance(start + (delay / rate)); + // The width for a single iteration. + let w = this.durationToDistance(duration / rate); + // The width for all iterations. + let iterationW = w * (count || 1); + // The start position of the delay. + let delayX = this.durationToDistance((delay < 0 ? 0 : delay) / rate); + // The width of the delay. + let delayW = this.durationToDistance(Math.abs(delay) / rate); + // The width of the delay if it is positive, 0 otherwise. + let negativeDelayW = delay < 0 ? delayW : 0; + + return {x, w, iterationW, delayX, delayW, negativeDelayW}; + } +}; + +exports.TimeScale = TimeScale;