--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -33,16 +33,17 @@ class AnimationInspector {
this.addAnimationsCurrentTimeListener.bind(this);
this.getAnimatedPropertyMap = this.getAnimatedPropertyMap.bind(this);
this.getComputedStyle = this.getComputedStyle.bind(this);
this.getNodeFromActor = this.getNodeFromActor.bind(this);
this.removeAnimationsCurrentTimeListener =
this.removeAnimationsCurrentTimeListener.bind(this);
this.rewindAnimationsCurrentTime = this.rewindAnimationsCurrentTime.bind(this);
this.selectAnimation = this.selectAnimation.bind(this);
+ this.setAnimationsCurrentTime = this.setAnimationsCurrentTime.bind(this);
this.setAnimationsPlaybackRate = this.setAnimationsPlaybackRate.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.onAnimationsCurrentTimeUpdated = this.onAnimationsCurrentTimeUpdated.bind(this);
this.onElementPickerStarted = this.onElementPickerStarted.bind(this);
@@ -71,27 +72,29 @@ class AnimationInspector {
emit: emitEventForTest,
getAnimatedPropertyMap,
getComputedStyle,
getNodeFromActor,
isAnimationsRunning,
removeAnimationsCurrentTimeListener,
rewindAnimationsCurrentTime,
selectAnimation,
+ setAnimationsCurrentTime,
setAnimationsPlaybackRate,
setAnimationsPlayState,
setDetailVisibility,
simulateAnimation,
toggleElementPicker,
} = this;
const target = this.inspector.target;
this.animationsFront = new AnimationsFront(target.client, target.form);
this.animationsCurrentTimeListeners = [];
+ this.isCurrentTimeSet = false;
const provider = createElement(Provider,
{
id: "newanimationinspector",
key: "newanimationinspector",
store: this.inspector.store
},
App(
@@ -102,16 +105,17 @@ class AnimationInspector {
getComputedStyle,
getNodeFromActor,
isAnimationsRunning,
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
removeAnimationsCurrentTimeListener,
rewindAnimationsCurrentTime,
selectAnimation,
+ setAnimationsCurrentTime,
setAnimationsPlaybackRate,
setAnimationsPlayState,
setDetailVisibility,
setSelectedNode,
simulateAnimation,
toggleElementPicker,
}
)
@@ -221,16 +225,23 @@ class AnimationInspector {
}
isPanelVisible() {
return this.inspector && this.inspector.toolbox && this.inspector.sidebar &&
this.inspector.toolbox.currentToolId === "inspector" &&
this.inspector.sidebar.getCurrentTabID() === "newanimationinspector";
}
+ /**
+ * This method should call when the current time is changed.
+ * Then, dispatches the current time to listeners that are registered
+ * by addAnimationsCurrentTimeListener.
+ *
+ * @param {Number} currentTime
+ */
onAnimationsCurrentTimeUpdated(currentTime) {
for (const listener of this.animationsCurrentTimeListeners) {
listener(currentTime);
}
}
onElementPickerStarted() {
this.inspector.store.dispatch(updateElementPickerEnabled(true));
@@ -254,40 +265,58 @@ class AnimationInspector {
}
removeAnimationsCurrentTimeListener(listener) {
this.animationsCurrentTimeListeners =
this.animationsCurrentTimeListeners.filter(l => l !== listener);
}
async rewindAnimationsCurrentTime() {
- const animations = this.state.animations;
- await this.animationsFront.setCurrentTimes(animations, 0, true);
- await this.updateAnimations(animations);
- this.onAnimationsCurrentTimeUpdated(0);
+ await this.setAnimationsCurrentTime(0, true);
}
selectAnimation(animation) {
this.inspector.store.dispatch(updateSelectedAnimation(animation));
}
+ async setAnimationsCurrentTime(currentTime, shouldRefresh) {
+ this.stopAnimationsCurrentTimeTimer();
+ this.onAnimationsCurrentTimeUpdated(currentTime);
+
+ if (!shouldRefresh && this.isCurrentTimeSet) {
+ return;
+ }
+
+ const animations = this.state.animations;
+ this.isCurrentTimeSet = true;
+ await this.animationsFront.setCurrentTimes(animations, currentTime, true);
+ await this.updateAnimations(animations);
+ this.isCurrentTimeSet = false;
+
+ if (shouldRefresh) {
+ this.updateState([...animations]);
+ }
+ }
+
async setAnimationsPlaybackRate(playbackRate) {
const animations = this.state.animations;
await this.animationsFront.setPlaybackRates(animations, playbackRate);
await this.updateAnimations(animations);
+ await this.updateState([...animations]);
}
async setAnimationsPlayState(doPlay) {
if (doPlay) {
await this.animationsFront.playAll();
} else {
await this.animationsFront.pauseAll();
}
- this.updateAnimations(this.state.animations);
+ await this.updateAnimations(this.state.animations);
+ await this.updateState([...this.state.animations]);
}
setDetailVisibility(isVisible) {
this.inspector.store.dispatch(updateDetailVisibility(isVisible));
}
/**
* Returns simulatable animation by given parameters.
@@ -371,18 +400,16 @@ class AnimationInspector {
}
async updateAnimations(animations) {
const promises = animations.map(animation => {
return animation.refreshState();
});
await Promise.all(promises);
-
- this.updateState([...animations]);
}
updateState(animations) {
this.stopAnimationsCurrentTimeTimer();
// 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;
--- a/devtools/client/inspector/animation/components/AnimationListContainer.js
+++ b/devtools/client/inspector/animation/components/AnimationListContainer.js
@@ -10,49 +10,58 @@ const PropTypes = require("devtools/clie
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const AnimationList = createFactory(require("./AnimationList"));
const AnimationListHeader = createFactory(require("./AnimationListHeader"));
class AnimationListContainer extends PureComponent {
static get propTypes() {
return {
+ addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
animations: PropTypes.arrayOf(PropTypes.object).isRequired,
emitEventForTest: PropTypes.func.isRequired,
getAnimatedPropertyMap: PropTypes.func.isRequired,
getNodeFromActor: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+ removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
selectAnimation: PropTypes.func.isRequired,
+ setAnimationsCurrentTime: PropTypes.func.isRequired,
setSelectedNode: PropTypes.func.isRequired,
simulateAnimation: PropTypes.func.isRequired,
timeScale: PropTypes.object.isRequired,
};
}
render() {
const {
+ addAnimationsCurrentTimeListener,
animations,
emitEventForTest,
getAnimatedPropertyMap,
getNodeFromActor,
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
+ removeAnimationsCurrentTimeListener,
selectAnimation,
+ setAnimationsCurrentTime,
setSelectedNode,
simulateAnimation,
timeScale,
} = this.props;
return dom.div(
{
className: "animation-list-container"
},
AnimationListHeader(
{
+ addAnimationsCurrentTimeListener,
+ removeAnimationsCurrentTimeListener,
+ setAnimationsCurrentTime,
timeScale,
}
),
AnimationList(
{
animations,
emitEventForTest,
getAnimatedPropertyMap,
--- a/devtools/client/inspector/animation/components/AnimationListHeader.js
+++ b/devtools/client/inspector/animation/components/AnimationListHeader.js
@@ -5,33 +5,51 @@
"use strict";
const { createFactory, PureComponent } =
require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const AnimationTimelineTickList = createFactory(require("./AnimationTimelineTickList"));
+const CurrentTimeScrubberController =
+ createFactory(require("./CurrentTimeScrubberController"));
class AnimationListHeader extends PureComponent {
static get propTypes() {
return {
+ addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ setAnimationsCurrentTime: PropTypes.func.isRequired,
timeScale: PropTypes.object.isRequired,
};
}
render() {
- const { timeScale } = this.props;
+ const {
+ addAnimationsCurrentTimeListener,
+ removeAnimationsCurrentTimeListener,
+ setAnimationsCurrentTime,
+ timeScale,
+ } = this.props;
return dom.div(
{
className: "animation-list-header devtools-toolbar"
},
AnimationTimelineTickList(
{
timeScale
}
+ ),
+ CurrentTimeScrubberController(
+ {
+ addAnimationsCurrentTimeListener,
+ removeAnimationsCurrentTimeListener,
+ setAnimationsCurrentTime,
+ timeScale,
+ }
)
);
}
}
module.exports = AnimationListHeader;
--- a/devtools/client/inspector/animation/components/App.js
+++ b/devtools/client/inspector/animation/components/App.js
@@ -25,16 +25,17 @@ class App extends PureComponent {
getAnimatedPropertyMap: PropTypes.func.isRequired,
getComputedStyle: PropTypes.func.isRequired,
getNodeFromActor: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
rewindAnimationsCurrentTime: PropTypes.func.isRequired,
selectAnimation: PropTypes.func.isRequired,
+ setAnimationsCurrentTime: PropTypes.func.isRequired,
setAnimationsPlaybackRate: PropTypes.func.isRequired,
setAnimationsPlayState: PropTypes.func.isRequired,
setDetailVisibility: PropTypes.func.isRequired,
setSelectedNode: PropTypes.func.isRequired,
simulateAnimation: PropTypes.func.isRequired,
timeScale: PropTypes.object.isRequired,
toggleElementPicker: PropTypes.func.isRequired,
};
@@ -53,16 +54,17 @@ class App extends PureComponent {
getAnimatedPropertyMap,
getComputedStyle,
getNodeFromActor,
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
removeAnimationsCurrentTimeListener,
rewindAnimationsCurrentTime,
selectAnimation,
+ setAnimationsCurrentTime,
setAnimationsPlaybackRate,
setAnimationsPlayState,
setDetailVisibility,
setSelectedNode,
simulateAnimation,
timeScale,
toggleElementPicker,
} = this.props;
@@ -95,23 +97,26 @@ class App extends PureComponent {
simulateAnimation,
}
),
endPanelControl: true,
initialHeight: "50%",
splitterSize: 1,
startPanel: AnimationListContainer(
{
+ addAnimationsCurrentTimeListener,
animations,
emitEventForTest,
getAnimatedPropertyMap,
getNodeFromActor,
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
+ removeAnimationsCurrentTimeListener,
selectAnimation,
+ setAnimationsCurrentTime,
setSelectedNode,
simulateAnimation,
timeScale,
}
),
vert: false,
})
]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/CurrentTimeScrubber.js
@@ -0,0 +1,32 @@
+/* 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");
+
+class CurrentTimeScrubber extends PureComponent {
+ static get propTypes() {
+ return {
+ offset: PropTypes.number.isRequired,
+ };
+ }
+
+ render() {
+ const { offset } = this.props;
+
+ return dom.div(
+ {
+ className: "current-time-scrubber",
+ style: {
+ transform: `translateX(${ offset }px)`,
+ },
+ }
+ );
+ }
+}
+
+module.exports = CurrentTimeScrubber;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/CurrentTimeScrubberController.js
@@ -0,0 +1,135 @@
+/* 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 { 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 ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+const CurrentTimeScrubber = createFactory(require("./CurrentTimeScrubber"));
+
+class CurrentTimeScrubberController extends PureComponent {
+ static get propTypes() {
+ return {
+ addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ setAnimationsCurrentTime: PropTypes.func.isRequired,
+ timeScale: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ const { addAnimationsCurrentTimeListener } = props;
+ this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this);
+ this.onMouseDown = this.onMouseDown.bind(this);
+ this.onMouseMove = this.onMouseMove.bind(this);
+ this.onMouseOut = this.onMouseOut.bind(this);
+ this.onMouseUp = this.onMouseUp.bind(this);
+
+ this.state = {
+ // offset of the position for the scrubber
+ offset: 0,
+ };
+
+ addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+ }
+
+ componentDidMount() {
+ const parentEl = ReactDOM.findDOMNode(this).parentElement;
+ parentEl.addEventListener("mousedown", this.onMouseDown);
+ }
+
+ componentWillUnmount() {
+ const { removeAnimationsCurrentTimeListener } = this.props;
+ removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+ }
+
+ onCurrentTimeUpdated(currentTime) {
+ const { timeScale } = this.props;
+
+ const thisEl = ReactDOM.findDOMNode(this);
+ const offset =
+ thisEl ? currentTime / timeScale.getDuration() * thisEl.clientWidth : 0;
+ this.setState({ offset });
+ }
+
+ onMouseDown(e) {
+ const thisEl = ReactDOM.findDOMNode(this);
+ this.controllerArea = thisEl.getBoundingClientRect();
+ this.listenerTarget = thisEl.closest(".animation-list-container");
+ this.listenerTarget.addEventListener("mousemove", this.onMouseMove);
+ this.listenerTarget.addEventListener("mouseout", this.onMouseOut);
+ this.listenerTarget.addEventListener("mouseup", this.onMouseUp);
+ this.listenerTarget.classList.add("active-scrubber");
+
+ this.updateAnimationsCurrentTime(e.pageX, true);
+ }
+
+ onMouseMove(e) {
+ this.isMouseMoved = true;
+ this.updateAnimationsCurrentTime(e.pageX);
+ }
+
+ onMouseOut(e) {
+ if (!this.listenerTarget.contains(e.relatedTarget)) {
+ const endX = this.controllerArea.x + this.controllerArea.width;
+ const pageX = endX < e.pageX ? endX : e.pageX;
+ this.updateAnimationsCurrentTime(pageX, true);
+ this.uninstallListeners();
+ }
+ }
+
+ onMouseUp(e) {
+ if (this.isMouseMoved) {
+ this.updateAnimationsCurrentTime(e.pageX, true);
+ this.isMouseMoved = null;
+ }
+
+ this.uninstallListeners();
+ }
+
+ uninstallListeners() {
+ this.listenerTarget.removeEventListener("mousemove", this.onMouseMove);
+ this.listenerTarget.removeEventListener("mouseout", this.onMouseOut);
+ this.listenerTarget.removeEventListener("mouseup", this.onMouseUp);
+ this.listenerTarget.classList.remove("active-scrubber");
+ this.listenerTarget = null;
+ this.controllerArea = null;
+ }
+
+ updateAnimationsCurrentTime(pageX, needRefresh) {
+ const {
+ setAnimationsCurrentTime,
+ timeScale,
+ } = this.props;
+
+ const time = pageX - this.controllerArea.x < 0 ?
+ 0 :
+ (pageX - this.controllerArea.x) /
+ this.controllerArea.width * timeScale.getDuration();
+
+ setAnimationsCurrentTime(time, needRefresh);
+ }
+
+ render() {
+ const { offset } = this.state;
+
+ return dom.div(
+ {
+ className: "current-time-scrubber-controller devtools-toolbar",
+ },
+ CurrentTimeScrubber(
+ {
+ offset,
+ }
+ )
+ );
+ }
+}
+
+module.exports = CurrentTimeScrubberController;
--- a/devtools/client/inspector/animation/components/moz.build
+++ b/devtools/client/inspector/animation/components/moz.build
@@ -20,15 +20,17 @@ DevToolsModules(
'AnimationListContainer.js',
'AnimationListHeader.js',
'AnimationTarget.js',
'AnimationTimelineTickItem.js',
'AnimationTimelineTickList.js',
'AnimationToolbar.js',
'App.js',
'CurrentTimeLabel.js',
+ 'CurrentTimeScrubber.js',
+ 'CurrentTimeScrubberController.js',
'KeyframesProgressTickItem.js',
'KeyframesProgressTickList.js',
'NoAnimationPanel.js',
'PauseResumeButton.js',
'PlaybackRateSelector.js',
'RewindButton.js',
)
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -11,16 +11,17 @@
--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);
--rewind-image: url(chrome://devtools/skin/images/rewind.svg);
+ --scrubber-color: #dd00a9;
--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 {
@@ -90,38 +91,79 @@ select.playback-rate-selector.devtools-b
/* Animation List Container */
.animation-list-container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
width: 100%;
+ -moz-user-select: none;
+}
+
+.animation-list-container.active-scrubber {
+ cursor: col-resize;
}
/* Animation List Header */
.animation-list-header {
- display: flex;
- justify-content: flex-end;
+ display: grid;
+ grid-template-columns: var(--sidebar-width) calc(100% - var(--sidebar-width) - var(--graph-right-offset)) var(--graph-right-offset);
padding: 0;
}
/* Animation Timeline Tick List */
.animation-timeline-tick-list {
- margin-right: var(--graph-right-offset);
+ grid-column: 2/3;
position: relative;
- width: calc(100% - var(--sidebar-width) - var(--graph-right-offset));
}
.animation-timeline-tick-item {
border-left: var(--tick-line-style);
height: 100vh;
+ pointer-events: none;
position: absolute;
}
+/* Current Time Scrubber */
+.current-time-scrubber-controller {
+ cursor: col-resize;
+ grid-column: 2 / 3;
+ padding: 0;
+}
+
+.current-time-scrubber {
+ cursor: col-resize;
+ height: 100vh;
+ margin-left: -6px;
+ position: absolute;
+ width: 12px;
+ z-index: 1;
+}
+
+.current-time-scrubber::before {
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: 5px solid var(--scrubber-color);
+ content: "";
+ position: absolute;
+ top: 0;
+ width: 0;
+}
+
+.current-time-scrubber::after {
+ border-left: 1px solid var(--scrubber-color);
+ content: "";
+ height: 100%;
+ left: 5px;
+ position: absolute;
+ top: 0;
+ width: 0;
+}
+
/* Animation List */
.animation-list {
flex: 1;
list-style-type: none;
margin: 0;
overflow: auto;
padding: 0;
}
@@ -282,21 +324,23 @@ select.playback-rate-selector.devtools-b
stroke-linejoin: round;
stroke-opacity: .5;
stroke-width: 4;
text-anchor: end;
}
/* Animation Detail */
.animation-detail-container {
+ background-color: var(--theme-body-background);
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
width: 100%;
+ z-index: 1;
}
.animation-detail-header {
display: flex;
}
.animation-detail-title {
flex: 1;