Bug 1431573 - Part 2: Implement pause/resume button. r?gl
MozReview-Commit-ID: 9oEHKNl5rnU
--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -25,16 +25,17 @@ class AnimationInspector {
constructor(inspector, win) {
this.inspector = inspector;
this.win = win;
this.getAnimatedPropertyMap = this.getAnimatedPropertyMap.bind(this);
this.getComputedStyle = this.getComputedStyle.bind(this);
this.getNodeFromActor = this.getNodeFromActor.bind(this);
this.selectAnimation = this.selectAnimation.bind(this);
+ this.setAnimationsPlayState = this.setAnimationsPlayState.bind(this);
this.setDetailVisibility = this.setDetailVisibility.bind(this);
this.simulateAnimation = this.simulateAnimation.bind(this);
this.toggleElementPicker = this.toggleElementPicker.bind(this);
this.update = this.update.bind(this);
this.onElementPickerStarted = this.onElementPickerStarted.bind(this);
this.onElementPickerStopped = this.onElementPickerStopped.bind(this);
this.onSidebarResized = this.onSidebarResized.bind(this);
this.onSidebarSelect = this.onSidebarSelect.bind(this);
@@ -56,16 +57,17 @@ class AnimationInspector {
} = this.inspector.getPanel("boxmodel").getComponentProps();
const {
emit: emitEventForTest,
getAnimatedPropertyMap,
getComputedStyle,
getNodeFromActor,
selectAnimation,
+ setAnimationsPlayState,
setDetailVisibility,
simulateAnimation,
toggleElementPicker,
} = this;
const target = this.inspector.target;
this.animationsFront = new AnimationsFront(target.client, target.form);
@@ -79,16 +81,17 @@ class AnimationInspector {
{
emitEventForTest,
getAnimatedPropertyMap,
getComputedStyle,
getNodeFromActor,
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
selectAnimation,
+ setAnimationsPlayState,
setDetailVisibility,
setSelectedNode,
simulateAnimation,
toggleElementPicker,
}
)
);
this.provider = provider;
@@ -116,16 +119,20 @@ class AnimationInspector {
this.simulatedElement.remove();
this.simulatedElement = null;
}
this.inspector = null;
this.win = null;
}
+ get state() {
+ return this.inspector.store.getState().animations;
+ }
+
/**
* Return a map of animated property from given animation actor.
*
* @param {Object} animation
* @return {Map} A map of animated property
* key: {String} Animated property name
* value: {Array} Array of keyframe object
* Also, the keyframe object is consisted as following.
@@ -211,16 +218,26 @@ class AnimationInspector {
this.inspector.store.dispatch(updateSidebarSize(size));
}
selectAnimation(animation) {
this.inspector.store.dispatch(updateSelectedAnimation(animation));
}
+ async setAnimationsPlayState(doPlay) {
+ if (doPlay) {
+ await this.animationsFront.playAll();
+ } else {
+ await this.animationsFront.pauseAll();
+ }
+
+ this.updateAnimations(this.state.animations);
+ }
+
setDetailVisibility(isVisible) {
this.inspector.store.dispatch(updateDetailVisibility(isVisible));
}
/**
* Returns simulatable animation by given parameters.
* The returned animation is implementing Animation interface of Web Animation API.
* https://drafts.csswg.org/web-animations/#the-animation-interface
@@ -270,25 +287,45 @@ class AnimationInspector {
if (!this.inspector || !this.isPanelVisible()) {
// AnimationInspector was destroyed already or the panel is hidden.
return;
}
const done = this.inspector.updating("newanimationinspector");
const selection = this.inspector.selection;
- const animations =
+ const nextAnimations =
selection.isConnected() && selection.isElementNode()
? await this.animationsFront.getAnimationPlayersForNode(selection.nodeFront)
: [];
+ const currentAnimations = this.state.animations;
- if (!this.animations || !isAllAnimationEqual(animations, this.animations)) {
- this.inspector.store.dispatch(updateAnimations(animations));
- this.animations = animations;
- // If number of displayed animations is one, we select the animation automatically.
- this.selectAnimation(animations.length === 1 ? animations[0] : null);
+ if (!currentAnimations || !isAllAnimationEqual(currentAnimations, nextAnimations)) {
+ this.updateState(nextAnimations);
}
done();
}
+
+ async updateAnimations(animations) {
+ const promises = animations.map(animation => {
+ return animation.refreshState();
+ });
+
+ await Promise.all(promises);
+
+ this.updateState([...animations]);
+ }
+
+ updateState(animations) {
+ this.inspector.store.dispatch(updateAnimations(animations));
+ // If number of displayed animations is one, we select the animation automatically.
+ // But if selected animation is in given animations, ignores.
+ const selectedAnimation = this.state.selectedAnimation;
+
+ if (!selectedAnimation ||
+ !animations.find(animation => animation.actorID === selectedAnimation.actorID)) {
+ this.selectAnimation(animations.length === 1 ? animations[0] : null);
+ }
+ }
}
module.exports = AnimationInspector;
--- a/devtools/client/inspector/animation/components/AnimationToolbar.js
+++ b/devtools/client/inspector/animation/components/AnimationToolbar.js
@@ -1,20 +1,41 @@
/* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const PauseResumeButton = createFactory(require("./PauseResumeButton"));
class AnimationToolbar extends PureComponent {
+ static get propTypes() {
+ return {
+ animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ setAnimationsPlayState: PropTypes.func.isRequired,
+ };
+ }
+
render() {
+ const {
+ animations,
+ setAnimationsPlayState,
+ } = this.props;
+
return dom.div(
{
className: "animation-toolbar devtools-toolbar",
- }
+ },
+ PauseResumeButton(
+ {
+ animations,
+ setAnimationsPlayState,
+ }
+ )
);
}
}
module.exports = AnimationToolbar;
--- a/devtools/client/inspector/animation/components/App.js
+++ b/devtools/client/inspector/animation/components/App.js
@@ -22,16 +22,17 @@ class App extends PureComponent {
detailVisibility: PropTypes.bool.isRequired,
emitEventForTest: PropTypes.func.isRequired,
getAnimatedPropertyMap: PropTypes.func.isRequired,
getComputedStyle: PropTypes.func.isRequired,
getNodeFromActor: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
selectAnimation: PropTypes.func.isRequired,
+ setAnimationsPlayState: PropTypes.func.isRequired,
setDetailVisibility: PropTypes.func.isRequired,
setSelectedNode: PropTypes.func.isRequired,
simulateAnimation: PropTypes.func.isRequired,
toggleElementPicker: PropTypes.func.isRequired,
};
}
shouldComponentUpdate(nextProps, nextState) {
@@ -44,30 +45,36 @@ class App extends PureComponent {
detailVisibility,
emitEventForTest,
getAnimatedPropertyMap,
getComputedStyle,
getNodeFromActor,
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
selectAnimation,
+ setAnimationsPlayState,
setDetailVisibility,
setSelectedNode,
simulateAnimation,
toggleElementPicker,
} = this.props;
return dom.div(
{
id: "animation-container",
className: detailVisibility ? "animation-detail-visible" : "",
},
animations.length ?
[
- AnimationToolbar(),
+ AnimationToolbar(
+ {
+ animations,
+ setAnimationsPlayState,
+ }
+ ),
SplitBox({
className: "animation-container-splitter",
endPanel: AnimationDetailContainer(
{
emitEventForTest,
getAnimatedPropertyMap,
getComputedStyle,
setDetailVisibility,
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/PauseResumeButton.js
@@ -0,0 +1,66 @@
+/* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const { getStr } = require("../utils/l10n");
+
+class PauseResumeButton extends PureComponent {
+ static get propTypes() {
+ return {
+ animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ setAnimationsPlayState: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isPlaying: false,
+ };
+ }
+
+ componentWillMount() {
+ this.updateState(this.props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.updateState(nextProps);
+ }
+
+ onClick() {
+ const { setAnimationsPlayState } = this.props;
+ const { isPlaying } = this.state;
+
+ setAnimationsPlayState(!isPlaying);
+ }
+
+ updateState() {
+ const { animations } = this.props;
+ const isPlaying = animations.some(({state}) => state.playState === "running");
+ this.setState({ isPlaying });
+ }
+
+ render() {
+ const { isPlaying } = this.state;
+
+ return dom.button(
+ {
+ className: "pause-resume-button devtools-button" +
+ (isPlaying ? "" : " paused"),
+ onClick: this.onClick.bind(this),
+ title: isPlaying ?
+ getStr("timeline.resumedButtonTooltip") :
+ getStr("timeline.pausedButtonTooltip"),
+ }
+ );
+ }
+}
+
+module.exports = PauseResumeButton;
--- a/devtools/client/inspector/animation/components/moz.build
+++ b/devtools/client/inspector/animation/components/moz.build
@@ -22,9 +22,10 @@ DevToolsModules(
'AnimationTarget.js',
'AnimationTimelineTickItem.js',
'AnimationTimelineTickList.js',
'AnimationToolbar.js',
'App.js',
'KeyframesProgressTickItem.js',
'KeyframesProgressTickList.js',
'NoAnimationPanel.js',
+ 'PauseResumeButton.js',
)
--- a/devtools/client/inspector/animation/test/browser_animation_animation-list.js
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-list.js
@@ -9,17 +9,17 @@ add_task(async function () {
await addTab(URL_ROOT + "doc_simple_animation.html");
const { animationInspector, inspector, panel } = await openAnimationInspector();
info("Checking animation list and items existence");
ok(panel.querySelector(".animation-list"),
"The animation-list is in the DOM");
is(panel.querySelectorAll(".animation-list .animation-item").length,
- animationInspector.animations.length,
+ animationInspector.state.animations.length,
"The number of animations displayed matches the number of animations");
info("Checking the background color for the animation list items");
const animationItemEls = panel.querySelectorAll(".animation-list .animation-item");
const evenColor =
panel.ownerGlobal.getComputedStyle(animationItemEls[0]).backgroundColor;
const oddColor =
panel.ownerGlobal.getComputedStyle(animationItemEls[1]).backgroundColor;
--- a/devtools/client/inspector/animation/test/browser_animation_animation-target.js
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-target.js
@@ -9,17 +9,17 @@
// * content of element
add_task(async function () {
await addTab(URL_ROOT + "doc_simple_animation.html");
const { animationInspector, inspector, panel } = await openAnimationInspector();
info("Checking the animation target elements existance");
const animationItemEls = panel.querySelectorAll(".animation-list .animation-item");
- is(animationItemEls.length, animationInspector.animations.length,
+ is(animationItemEls.length, animationInspector.state.animations.length,
"Number of animation target element should be same to number of animations "
+ "that displays");
for (const animationItemEl of animationItemEls) {
const animationTargetEl = animationItemEl.querySelector(".animation-target");
ok(animationTargetEl,
"The animation target element should be in each animation item element");
}
--- a/devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js
@@ -14,17 +14,17 @@ const { findOptimalTimeInterval } =
// Should be kept in sync with TIME_GRADUATION_MIN_SPACING in
// AnimationTimeTickList component.
const TIME_GRADUATION_MIN_SPACING = 40;
add_task(async function () {
await addTab(URL_ROOT + "doc_simple_animation.html");
const { animationInspector, inspector, panel } = await openAnimationInspector();
- const timeScale = new TimeScale(animationInspector.animations);
+ const timeScale = new TimeScale(animationInspector.state.animations);
info("Checking animation list header element existence");
const listContainerEl = panel.querySelector(".animation-list-container");
const listHeaderEl = listContainerEl.querySelector(".devtools-toolbar");
ok(listHeaderEl, "The header element should be in animation list container element");
info("Checking time tick item elements existence");
assertTimelineTickItems(timeScale, listHeaderEl);
--- a/devtools/client/inspector/animation/test/head.js
+++ b/devtools/client/inspector/animation/test/head.js
@@ -175,40 +175,40 @@ const waitForRendering = async function
};
/**
* Wait for rendering of animation keyframes.
*
* @param {AnimationInspector} inspector
*/
const waitForAnimationDetail = async function (animationInspector) {
- if (animationInspector.animations.length === 1) {
+ if (animationInspector.state.animations.length === 1) {
await animationInspector.once("animation-keyframes-rendered");
}
};
/**
* Wait for all AnimationTarget components to be fully loaded
* (fetched their related actor and rendered).
*
* @param {AnimationInspector} animationInspector
*/
const waitForAllAnimationTargets = async function (animationInspector) {
- for (let i = 0; i < animationInspector.animations.length; i++) {
+ for (let i = 0; i < animationInspector.state.animations.length; i++) {
await animationInspector.once("animation-target-rendered");
}
};
/**
* Wait for all SummaryGraph components to be fully loaded
*
* @param {AnimationInspector} inspector
*/
const waitForAllSummaryGraph = async function (animationInspector) {
- for (let i = 0; i < animationInspector.animations.length; i++) {
+ for (let i = 0; i < animationInspector.state.animations.length; i++) {
await animationInspector.once("animation-summary-graph-rendered");
}
};
/**
* Check the <stop> element in the given linearGradientEl for the correct offset
* and color attributes.
*
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -8,30 +8,34 @@
--animation-even-background-color: rgba(0, 0, 0, 0.05);
--command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
--fast-track-image: url("images/animation-fast-track.svg");
--fill-color-cssanimation: var(--theme-contrast-background);
--fill-color-csstransition: var(--theme-highlight-blue);
--fill-color-scriptanimation: var(--theme-graphs-green);
--graph-right-offset: 10px;
--keyframe-marker-shadow-color: #c4c4c4;
+ --pause-image: url(chrome://devtools/skin/images/pause.svg);
+ --resume-image: url(chrome://devtools/skin/images/play.svg);
--sidebar-width: 200px;
--stroke-color-cssanimation: var(--theme-highlight-lightorange);
--stroke-color-csstransition: var(--theme-highlight-bluegrey);
--stroke-color-scriptanimation: var(--theme-highlight-green);
--tick-line-style: 0.5px solid rgba(128, 136, 144, 0.5);
}
:root.theme-dark {
--animation-even-background-color: rgba(255, 255, 255, 0.05);
--keyframe-marker-shadow-color: #818181;
}
:root.theme-firebug {
--command-pick-image: url(chrome://devtools/skin/images/firebug/command-pick.svg);
+ --pause-image: url(chrome://devtools/skin/images/firebug/pause.svg);
+ --resume-image: url(chrome://devtools/skin/images/firebug/play.svg);
}
/* Root element of animation inspector */
#animation-container {
display: flex;
flex-direction: column;
height: 100%;
}
@@ -43,16 +47,29 @@
#animation-container:not(.animation-detail-visible) .controlled {
display: none;
}
#animation-container .animation-container-splitter {
overflow: auto;
}
+/* Animation Toolbar */
+.animation-toolbar {
+ display: flex;
+}
+
+.pause-resume-button::before {
+ background-image: var(--pause-image);
+}
+
+.pause-resume-button.paused::before {
+ background-image: var(--resume-image);
+}
+
/* Animation List Container */
.animation-list-container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
width: 100%;
}