Bug 1431573 - Part 5: Implement playback rate chooser. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Wed, 07 Mar 2018 16:33:53 +0900
changeset 764143 fc82bd5ea1248fd907c7622827449fa73cac7bc7
parent 764142 d90958db44168241e43442bf8941a9de8fde9019
child 764144 fdba46c58ac34430c1c987216a5140b15a13cca5
push id101685
push userbmo:dakatsuka@mozilla.com
push dateWed, 07 Mar 2018 10:13:37 +0000
reviewersgl
bugs1431573
milestone60.0a1
Bug 1431573 - Part 5: Implement playback rate chooser. r?gl MozReview-Commit-ID: KK5C6TBhA5X
devtools/client/inspector/animation/animation.js
devtools/client/inspector/animation/components/AnimationToolbar.js
devtools/client/inspector/animation/components/App.js
devtools/client/inspector/animation/components/PlaybackRateSelector.js
devtools/client/inspector/animation/components/moz.build
devtools/client/themes/animation.css
--- 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.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);
     this.onElementPickerStopped = this.onElementPickerStopped.bind(this);
@@ -70,16 +71,17 @@ class AnimationInspector {
       emit: emitEventForTest,
       getAnimatedPropertyMap,
       getComputedStyle,
       getNodeFromActor,
       isAnimationsRunning,
       removeAnimationsCurrentTimeListener,
       rewindAnimationsCurrentTime,
       selectAnimation,
+      setAnimationsPlaybackRate,
       setAnimationsPlayState,
       setDetailVisibility,
       simulateAnimation,
       toggleElementPicker,
     } = this;
 
     const target = this.inspector.target;
     this.animationsFront = new AnimationsFront(target.client, target.form);
@@ -100,16 +102,17 @@ class AnimationInspector {
           getComputedStyle,
           getNodeFromActor,
           isAnimationsRunning,
           onHideBoxModelHighlighter,
           onShowBoxModelHighlighterForNode,
           removeAnimationsCurrentTimeListener,
           rewindAnimationsCurrentTime,
           selectAnimation,
+          setAnimationsPlaybackRate,
           setAnimationsPlayState,
           setDetailVisibility,
           setSelectedNode,
           simulateAnimation,
           toggleElementPicker,
         }
       )
     );
@@ -261,16 +264,22 @@ class AnimationInspector {
     await this.updateAnimations(animations);
     this.onAnimationsCurrentTimeUpdated(0);
   }
 
   selectAnimation(animation) {
     this.inspector.store.dispatch(updateSelectedAnimation(animation));
   }
 
+  async setAnimationsPlaybackRate(playbackRate) {
+    const animations = this.state.animations;
+    await this.animationsFront.setPlaybackRates(animations, playbackRate);
+    await this.updateAnimations(animations);
+  }
+
   async setAnimationsPlayState(doPlay) {
     if (doPlay) {
       await this.animationsFront.playAll();
     } else {
       await this.animationsFront.pauseAll();
     }
 
     this.updateAnimations(this.state.animations);
--- a/devtools/client/inspector/animation/components/AnimationToolbar.js
+++ b/devtools/client/inspector/animation/components/AnimationToolbar.js
@@ -5,35 +5,38 @@
 "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 CurrentTimeLabel = createFactory(require("./CurrentTimeLabel"));
 const PauseResumeButton = createFactory(require("./PauseResumeButton"));
+const PlaybackRateSelector = createFactory(require("./PlaybackRateSelector"));
 const RewindButton = createFactory(require("./RewindButton"));
 
 class AnimationToolbar extends PureComponent {
   static get propTypes() {
     return {
       addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
       removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       rewindAnimationsCurrentTime: PropTypes.func.isRequired,
+      setAnimationsPlaybackRate: PropTypes.func.isRequired,
       setAnimationsPlayState: PropTypes.func.isRequired,
     };
   }
 
   render() {
     const {
       addAnimationsCurrentTimeListener,
       animations,
       removeAnimationsCurrentTimeListener,
       rewindAnimationsCurrentTime,
+      setAnimationsPlaybackRate,
       setAnimationsPlayState,
     } = this.props;
 
     return dom.div(
       {
         className: "animation-toolbar devtools-toolbar",
       },
       RewindButton(
@@ -42,16 +45,22 @@ class AnimationToolbar extends PureCompo
         }
       ),
       PauseResumeButton(
         {
           animations,
           setAnimationsPlayState,
         }
       ),
+      PlaybackRateSelector(
+        {
+          animations,
+          setAnimationsPlaybackRate,
+        }
+      ),
       CurrentTimeLabel(
         {
           addAnimationsCurrentTimeListener,
           removeAnimationsCurrentTimeListener,
         }
       )
     );
   }
--- 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,
+      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,
     };
   }
@@ -52,16 +53,17 @@ class App extends PureComponent {
       getAnimatedPropertyMap,
       getComputedStyle,
       getNodeFromActor,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
       removeAnimationsCurrentTimeListener,
       rewindAnimationsCurrentTime,
       selectAnimation,
+      setAnimationsPlaybackRate,
       setAnimationsPlayState,
       setDetailVisibility,
       setSelectedNode,
       simulateAnimation,
       timeScale,
       toggleElementPicker,
     } = this.props;
 
@@ -73,16 +75,17 @@ class App extends PureComponent {
       animations.length ?
       [
         AnimationToolbar(
           {
             addAnimationsCurrentTimeListener,
             animations,
             removeAnimationsCurrentTimeListener,
             rewindAnimationsCurrentTime,
+            setAnimationsPlaybackRate,
             setAnimationsPlayState,
           }
         ),
         SplitBox({
           className: "animation-container-splitter",
           endPanel: AnimationDetailContainer(
             {
               emitEventForTest,
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/PlaybackRateSelector.js
@@ -0,0 +1,103 @@
+/* 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 { getFormatStr } = require("../utils/l10n");
+
+const PLAYBACK_RATES = [.1, .25, .5, 1, 2, 5, 10];
+
+class PlaybackRateSelector extends PureComponent {
+  static get propTypes() {
+    return {
+      animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+      setAnimationsPlaybackRate: PropTypes.func.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      options: [],
+      selected: 1,
+    };
+  }
+
+  componentWillMount() {
+    this.updateState(this.props);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.updateState(nextProps);
+  }
+
+  getPlaybackRates(animations) {
+    return sortAndUnique(animations.map(a => a.state.playbackRate));
+  }
+
+  getSelectablePlaybackRates(animationsRates) {
+    return sortAndUnique(PLAYBACK_RATES.concat(animationsRates));
+  }
+
+  onChange(e) {
+    const { setAnimationsPlaybackRate } = this.props;
+
+    if (!e.target.value) {
+      return;
+    }
+
+    setAnimationsPlaybackRate(e.target.value);
+  }
+
+  updateState(props) {
+    const { animations } = props;
+
+    let options;
+    let selected;
+    const rates = this.getPlaybackRates(animations);
+
+    if (rates.length === 1) {
+      options = this.getSelectablePlaybackRates(rates);
+      selected = rates[0];
+    } else {
+      // When the animations displayed have mixed playback rates, we can't
+      // select any of the predefined ones.
+      options = ["", ...PLAYBACK_RATES];
+      selected = "";
+    }
+
+    this.setState({ options, selected });
+  }
+
+  render() {
+    const { options, selected } = this.state;
+
+    return dom.select(
+      {
+        className: "playback-rate-selector devtools-button",
+        onChange: this.onChange.bind(this),
+      },
+      options.map(rate => {
+        return dom.option(
+          {
+            selected: rate === selected ? "true" : null,
+            value: rate,
+          },
+          rate ? getFormatStr("player.playbackRateLabel", rate) : "-"
+        );
+      })
+    );
+  }
+}
+
+function sortAndUnique(array) {
+  return [...new Set(array)].sort((a, b) => a > b);
+}
+
+module.exports = PlaybackRateSelector;
--- a/devtools/client/inspector/animation/components/moz.build
+++ b/devtools/client/inspector/animation/components/moz.build
@@ -24,10 +24,11 @@ DevToolsModules(
     'AnimationTimelineTickList.js',
     'AnimationToolbar.js',
     'App.js',
     'CurrentTimeLabel.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
@@ -62,16 +62,33 @@
 .pause-resume-button::before {
   background-image: var(--pause-image);
 }
 
 .pause-resume-button.paused::before {
   background-image: var(--resume-image);
 }
 
+select.playback-rate-selector.devtools-button {
+  background-image: url("chrome://devtools/skin/images/dropmarker.svg");
+  background-position: calc(100% - 4px) center;
+  background-repeat: no-repeat;
+  padding-right: 1em;
+  text-align: center;
+}
+
+select.playback-rate-selector.devtools-button:not(:empty):not(:disabled):not(.checked):hover {
+  background: none;
+  background-color: var(--toolbarbutton-background);
+  background-image: url("chrome://devtools/skin/images/dropmarker.svg");
+  background-position: calc(100% - 4px) center;
+  background-repeat: no-repeat;
+  border-color: var(--toolbarbutton-hover-border-color);
+}
+
 .rewind-button::before {
   background-image: var(--rewind-image);
 }
 
 /* Animation List Container */
 .animation-list-container {
   display: flex;
   flex-direction: column;