Bug 1155661 - 5 - Make the timeline emit data about its current state and update the play/pause button based on this; r=miker
authorPatrick Brosset <pbrosset@mozilla.com>
Wed, 16 Sep 2015 17:00:07 +0200
changeset 295850 fb954e0271ecdd382d7737064acf6101ce1c2eb1
parent 295849 0c461fc0a8236160e7e6979d46b69452a36a268a
child 295851 bb283bb54d56b03d16805823fc351ddd2ce26157
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmiker
bugs1155661
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1155661 - 5 - Make the timeline emit data about its current state and update the play/pause button based on this; r=miker
browser/devtools/animationinspector/animation-panel.js
browser/devtools/animationinspector/components.js
browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_movable.js
--- a/browser/devtools/animationinspector/animation-panel.js
+++ b/browser/devtools/animationinspector/animation-panel.js
@@ -34,31 +34,32 @@ var AnimationsPanel = {
       return;
     }
     this.initialized = promise.defer();
 
     this.playersEl = document.querySelector("#players");
     this.errorMessageEl = document.querySelector("#error-message");
     this.pickerButtonEl = document.querySelector("#element-picker");
     this.toggleAllButtonEl = document.querySelector("#toggle-all");
+    this.playTimelineButtonEl = document.querySelector("#pause-resume-timeline");
 
     // If the server doesn't support toggling all animations at once, hide the
     // whole global toolbar.
     if (!AnimationsController.traits.hasToggleAll) {
       document.querySelector("#global-toolbar").style.display = "none";
     }
 
+    // Binding functions that need to be called in scope.
+    for (let functionName of ["onPickerStarted", "onPickerStopped",
+      "refreshAnimations", "toggleAll", "onTabNavigated",
+      "onTimelineDataChanged"]) {
+      this[functionName] = this[functionName].bind(this);
+    }
     let hUtils = gToolbox.highlighterUtils;
     this.togglePicker = hUtils.togglePicker.bind(hUtils);
-    this.onPickerStarted = this.onPickerStarted.bind(this);
-    this.onPickerStopped = this.onPickerStopped.bind(this);
-    this.refreshAnimations = this.refreshAnimations.bind(this);
-    this.toggleAll = this.toggleAll.bind(this);
-    this.onTabNavigated = this.onTabNavigated.bind(this);
-    this.onTimelineTimeChanged = this.onTimelineTimeChanged.bind(this);
 
     if (AnimationsController.traits.isNewUI) {
       this.animationsTimelineComponent = new AnimationsTimeline(gInspector);
       this.animationsTimelineComponent.init(this.playersEl);
     }
 
     this.startListeners();
 
@@ -85,51 +86,52 @@ var AnimationsPanel = {
     if (this.animationsTimelineComponent) {
       this.animationsTimelineComponent.destroy();
       this.animationsTimelineComponent = null;
     }
     yield this.destroyPlayerWidgets();
 
     this.playersEl = this.errorMessageEl = null;
     this.toggleAllButtonEl = this.pickerButtonEl = null;
+    this.playTimelineButtonEl = null;
 
     this.destroyed.resolve();
   }),
 
   startListeners: function() {
     AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
       this.refreshAnimations);
 
     this.pickerButtonEl.addEventListener("click", this.togglePicker, false);
     gToolbox.on("picker-started", this.onPickerStarted);
     gToolbox.on("picker-stopped", this.onPickerStopped);
 
     this.toggleAllButtonEl.addEventListener("click", this.toggleAll, false);
     gToolbox.target.on("navigate", this.onTabNavigated);
 
     if (this.animationsTimelineComponent) {
-      this.animationsTimelineComponent.on("current-time-changed",
-        this.onTimelineTimeChanged);
+      this.animationsTimelineComponent.on("timeline-data-changed",
+        this.onTimelineDataChanged);
     }
   },
 
   stopListeners: function() {
     AnimationsController.off(AnimationsController.PLAYERS_UPDATED_EVENT,
       this.refreshAnimations);
 
     this.pickerButtonEl.removeEventListener("click", this.togglePicker, false);
     gToolbox.off("picker-started", this.onPickerStarted);
     gToolbox.off("picker-stopped", this.onPickerStopped);
 
     this.toggleAllButtonEl.removeEventListener("click", this.toggleAll, false);
     gToolbox.target.off("navigate", this.onTabNavigated);
 
     if (this.animationsTimelineComponent) {
-      this.animationsTimelineComponent.off("current-time-changed",
-        this.onTimelineTimeChanged);
+      this.animationsTimelineComponent.off("timeline-data-changed",
+        this.onTimelineDataChanged);
     }
   },
 
   togglePlayers: function(isVisible) {
     if (isVisible) {
       document.body.removeAttribute("empty");
       if (AnimationsController.traits.isNewUI) {
         document.body.setAttribute("timeline", "true");
@@ -169,19 +171,32 @@ var AnimationsPanel = {
     btnClass.toggle("paused");
     yield AnimationsController.toggleAll();
   }),
 
   onTabNavigated: function() {
     this.toggleAllButtonEl.classList.remove("paused");
   },
 
-  onTimelineTimeChanged: function(e, time) {
-    AnimationsController.setCurrentTimeAll(time, true)
-                        .catch(error => console.error(error));
+  onTimelineDataChanged: function(e, data) {
+    this.timelineData = data;
+    let {isPaused, isMoving, time} = data;
+
+    this.playTimelineButtonEl.classList.toggle("paused", !isMoving);
+
+    // Pause all animations and set their currentTimes (but only do this after
+    // the previous currentTime setting is done, as this gets called many times
+    // when users drag the scrubber with the mouse, and we want the server-side
+    // requests to be sequenced).
+    if (isPaused && !this.setCurrentTimeAllPromise) {
+      this.setCurrentTimeAllPromise =
+        AnimationsController.setCurrentTimeAll(time, true)
+                            .catch(error => console.error(error))
+                            .then(() => this.setCurrentTimeAllPromise = null);
+    }
   },
 
   refreshAnimations: Task.async(function*() {
     let done = gInspector.updating("animationspanel");
 
     // Empty the whole panel first.
     this.togglePlayers(true);
     yield this.destroyPlayerWidgets();
--- a/browser/devtools/animationinspector/components.js
+++ b/browser/devtools/animationinspector/components.js
@@ -656,18 +656,18 @@ exports.TimeScale = TimeScale;
  * UI component responsible for displaying a timeline for animations.
  * The timeline is essentially a graph with time along the x axis and animations
  * along the y axis.
  * The time is represented with a graduation header at the top and a current
  * time play head.
  * Animations are organized by lines, with a left margin containing the preview
  * of the target DOM element the animation applies to.
  * The current time play head can be moved by clicking/dragging in the header.
- * when this happens, the component emits "current-time-changed" events with the
- * new time.
+ * when this happens, the component emits "current-data-changed" events with the
+ * new time and state of the timeline.
  *
  * @param {InspectorPanel} inspector.
  */
 function AnimationsTimeline(inspector) {
   this.animations = [];
   this.targetNodes = [];
   this.inspector = inspector;
 
@@ -788,17 +788,22 @@ AnimationsTimeline.prototype = {
     if (offset < 0) {
       offset = 0;
     }
 
     this.scrubberEl.style.left = offset + "px";
 
     let time = TimeScale.distanceToRelativeTime(offset,
       this.timeHeaderEl.offsetWidth);
-    this.emit("current-time-changed", time);
+
+    this.emit("timeline-data-changed", {
+      isPaused: true,
+      isMoving: false,
+      time: time
+    });
   },
 
   render: function(animations, documentCurrentTime) {
     this.unrender();
 
     this.animations = animations;
     if (!this.animations.length) {
       return;
@@ -863,28 +868,45 @@ AnimationsTimeline.prototype = {
   },
 
   startAnimatingScrubber: function(time) {
     let x = TimeScale.startTimeToDistance(time, this.timeHeaderEl.offsetWidth);
     this.scrubberEl.style.left = x + "px";
 
     if (time < TimeScale.minStartTime ||
         time > TimeScale.maxEndTime) {
+      this.stopAnimatingScrubber();
+      this.emit("timeline-data-changed", {
+        isPaused: false,
+        isMoving: false,
+        time: TimeScale.distanceToRelativeTime(x, this.timeHeaderEl.offsetWidth)
+      });
       return;
     }
 
+    this.emit("timeline-data-changed", {
+      isPaused: false,
+      isMoving: true,
+      time: TimeScale.distanceToRelativeTime(x, this.timeHeaderEl.offsetWidth)
+    });
+
     let now = this.win.performance.now();
     this.rafID = this.win.requestAnimationFrame(() => {
+      if (!this.rafID) {
+        // In case the scrubber was stopped in the meantime.
+        return;
+      }
       this.startAnimatingScrubber(time + this.win.performance.now() - now);
     });
   },
 
   stopAnimatingScrubber: function() {
     if (this.rafID) {
       this.win.cancelAnimationFrame(this.rafID);
+      this.rafID = null;
     }
   },
 
   onAnimationStateChanged: function() {
     // For now, simply re-render the component. The animation front's state has
     // already been updated.
     this.render(this.animations);
   },
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_movable.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_movable.js
@@ -1,35 +1,53 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Check that the scrubber in the timeline-based UI can be moved by clicking &
 // dragging in the header area.
+// Also check that doing so changes the timeline's play/pause button to paused
+// state.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
 
   let {panel} = yield openAnimationInspectorNewUI();
 
   let timeline = panel.animationsTimelineComponent;
   let win = timeline.win;
   let timeHeaderEl = timeline.timeHeaderEl;
   let scrubberEl = timeline.scrubberEl;
+  let playTimelineButtonEl = panel.playTimelineButtonEl;
+
+  ok(!playTimelineButtonEl.classList.contains("paused"),
+     "The timeline play button is in its playing state by default");
 
   info("Mousedown in the header to move the scrubber");
-  EventUtils.synthesizeMouse(timeHeaderEl, 50, 1, {type: "mousedown"}, win);
+  yield synthesizeMouseAndWaitForTimelineChange(timeline, 50, 1, "mousedown");
   let newPos = parseInt(scrubberEl.style.left, 10);
   is(newPos, 50, "The scrubber moved on mousedown");
 
+  ok(playTimelineButtonEl.classList.contains("paused"),
+     "The timeline play button is in its paused state after mousedown");
+
   info("Continue moving the mouse and verify that the scrubber tracks it");
-  EventUtils.synthesizeMouse(timeHeaderEl, 100, 1, {type: "mousemove"}, win);
+  yield synthesizeMouseAndWaitForTimelineChange(timeline, 100, 1, "mousemove");
   newPos = parseInt(scrubberEl.style.left, 10);
   is(newPos, 100, "The scrubber followed the mouse");
 
+  ok(playTimelineButtonEl.classList.contains("paused"),
+     "The timeline play button is in its paused state after mousemove");
+
   info("Release the mouse and move again and verify that the scrubber stays");
   EventUtils.synthesizeMouse(timeHeaderEl, 100, 1, {type: "mouseup"}, win);
   EventUtils.synthesizeMouse(timeHeaderEl, 200, 1, {type: "mousemove"}, win);
   newPos = parseInt(scrubberEl.style.left, 10);
   is(newPos, 100, "The scrubber stopped following the mouse");
 });
+
+function* synthesizeMouseAndWaitForTimelineChange(timeline, x, y, type) {
+  let onDataChanged = timeline.once("timeline-data-changed");
+  EventUtils.synthesizeMouse(timeline.timeHeaderEl, x, y, {type}, timeline.win);
+  yield onDataChanged;
+}