Bug 1199589 - Display the current timeline time in the toolbar; r=bgrins
authorPatrick Brosset <pbrosset@mozilla.com>
Wed, 14 Oct 2015 10:03:29 +0200
changeset 267653 ec2bb28663937305bdbd2792ca1481e8db7f50a4
parent 267652 738e0c64591824dc87e8cc670a43f34b282f9423
child 267654 46ef1400ca8ad9338714dd06f1c2b5a2fb93eaf2
push id66539
push usercbook@mozilla.com
push dateWed, 14 Oct 2015 14:21:32 +0000
treeherdermozilla-inbound@80a002a6244a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1199589
milestone44.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 1199589 - Display the current timeline time in the toolbar; r=bgrins
devtools/client/animationinspector/animation-inspector.xhtml
devtools/client/animationinspector/animation-panel.js
devtools/client/animationinspector/components.js
devtools/client/animationinspector/test/browser.ini
devtools/client/animationinspector/test/browser_animation_timeline_currentTime.js
devtools/client/animationinspector/test/unit/test_formatStopwatchTime.js
devtools/client/animationinspector/test/unit/xpcshell.ini
devtools/client/animationinspector/utils.js
devtools/client/themes/animationinspector.css
--- a/devtools/client/animationinspector/animation-inspector.xhtml
+++ b/devtools/client/animationinspector/animation-inspector.xhtml
@@ -17,16 +17,17 @@
   <body class="theme-sidebar devtools-monospace" role="application" empty="true">
     <div id="global-toolbar" class="theme-toolbar">
       <span class="label">&allAnimations;</span>
       <button id="toggle-all" standalone="true" class="devtools-button pause-button"></button>
     </div>
     <div id="timeline-toolbar" class="theme-toolbar">
       <button id="rewind-timeline" standalone="true" class="devtools-button"></button>
       <button id="pause-resume-timeline" standalone="true" class="devtools-button pause-button paused"></button>
+      <span id="timeline-current-time" class="label"></span>
     </div>
     <div id="players"></div>
     <div id="error-message">
       <p>&invalidElement;</p>
       <p>&selectElement;</p>
       <button id="element-picker" standalone="true" class="devtools-button"></button>
     </div>
     <script type="application/javascript;version=1.8" src="animation-controller.js"></script>
--- a/devtools/client/animationinspector/animation-panel.js
+++ b/devtools/client/animationinspector/animation-panel.js
@@ -4,16 +4,17 @@
  * 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/. */
 /* globals AnimationsController, document, performance, promise,
    gToolbox, gInspector, requestAnimationFrame, cancelAnimationFrame, L10N */
 
 "use strict";
 
 const {AnimationsTimeline} = require("devtools/client/animationinspector/components");
+const {formatStopwatchTime} = require("devtools/client/animationinspector/utils");
 
 var $ = (selector, target = document) => target.querySelector(selector);
 
 /**
  * The main animations panel UI.
  */
 var AnimationsPanel = {
   UI_UPDATED_EVENT: "ui-updated",
@@ -32,16 +33,17 @@ var AnimationsPanel = {
     this.initialized = promise.defer();
 
     this.playersEl = $("#players");
     this.errorMessageEl = $("#error-message");
     this.pickerButtonEl = $("#element-picker");
     this.toggleAllButtonEl = $("#toggle-all");
     this.playTimelineButtonEl = $("#pause-resume-timeline");
     this.rewindTimelineButtonEl = $("#rewind-timeline");
+    this.timelineCurrentTimeEl = $("#timeline-current-time");
 
     // If the server doesn't support toggling all animations at once, hide the
     // whole global toolbar.
     if (!AnimationsController.traits.hasToggleAll) {
       $("#global-toolbar").style.display = "none";
     }
 
     // Binding functions that need to be called in scope.
@@ -79,16 +81,17 @@ var AnimationsPanel = {
     this.stopListeners();
 
     this.animationsTimelineComponent.destroy();
     this.animationsTimelineComponent = null;
 
     this.playersEl = this.errorMessageEl = null;
     this.toggleAllButtonEl = this.pickerButtonEl = null;
     this.playTimelineButtonEl = this.rewindTimelineButtonEl = null;
+    this.timelineCurrentTimeEl = null;
 
     this.destroyed.resolve();
   }),
 
   startListeners: function() {
     AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
       this.refreshAnimationsUI);
 
@@ -185,16 +188,26 @@ var AnimationsPanel = {
     // 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);
     }
+
+    this.displayTimelineCurrentTime();
+  },
+
+  displayTimelineCurrentTime: function() {
+    let {isMoving, isPaused, time} = this.timelineData;
+
+    if (isMoving || isPaused) {
+      this.timelineCurrentTimeEl.textContent = formatStopwatchTime(time);
+    }
   },
 
   /**
    * Make sure all known animations have their states up to date (which is
    * useful after the playState or currentTime has been changed and in case the
    * animations aren't auto-refreshing), and then refresh the UI.
    */
   refreshAnimationsStateAndUI: Task.async(function*() {
--- a/devtools/client/animationinspector/components.js
+++ b/devtools/client/animationinspector/components.js
@@ -669,17 +669,17 @@ AnimationsTimeline.prototype = {
     let x = TimeScale.startTimeToDistance(time, this.timeHeaderEl.offsetWidth);
     this.scrubberEl.style.left = x + "px";
 
     if (time < TimeScale.minStartTime ||
         time > TimeScale.maxEndTime ||
         !this.isAtLeastOneAnimationPlaying()) {
       this.stopAnimatingScrubber();
       this.emit("timeline-data-changed", {
-        isPaused: false,
+        isPaused: !this.isAtLeastOneAnimationPlaying(),
         isMoving: false,
         time: TimeScale.distanceToRelativeTime(x, this.timeHeaderEl.offsetWidth)
       });
       return;
     }
 
     this.emit("timeline-data-changed", {
       isPaused: false,
--- a/devtools/client/animationinspector/test/browser.ini
+++ b/devtools/client/animationinspector/test/browser.ini
@@ -19,16 +19,17 @@ support-files =
 [browser_animation_playerWidgets_target_nodes.js]
 [browser_animation_refresh_on_added_animation.js]
 [browser_animation_refresh_on_removed_animation.js]
 [browser_animation_refresh_when_active.js]
 [browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
 [browser_animation_shows_player_on_valid_node.js]
 [browser_animation_target_highlight_select.js]
 [browser_animation_target_highlighter_lock.js]
+[browser_animation_timeline_currentTime.js]
 [browser_animation_timeline_header.js]
 [browser_animation_timeline_pause_button.js]
 [browser_animation_timeline_rewind_button.js]
 [browser_animation_timeline_scrubber_exists.js]
 [browser_animation_timeline_scrubber_movable.js]
 [browser_animation_timeline_scrubber_moves.js]
 [browser_animation_timeline_shows_delay.js]
 [browser_animation_timeline_shows_iterations.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_currentTime.js
@@ -0,0 +1,46 @@
+/* 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 timeline toolbar displays the current time, and that it
+// changes when animations are playing, gets back to 0 when animations are
+// rewound, and stops when animations are paused.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+
+  let {panel} = yield openAnimationInspector();
+  let label = panel.timelineCurrentTimeEl;
+  ok(label, "The current time label exists");
+
+  // On page load animations are playing so the time shoud change, although we
+  // don't want to test the exact value of the time displayed, just that it
+  // actually changes.
+  info("Make sure the time displayed actually changes");
+  yield isCurrentTimeLabelChanging(panel, true);
+
+  info("Pause the animations and check that the time stops changing");
+  yield clickTimelinePlayPauseButton(panel);
+  yield isCurrentTimeLabelChanging(panel, false);
+
+  info("Rewind the animations and check that the time stops changing");
+  yield clickTimelineRewindButton(panel);
+  yield isCurrentTimeLabelChanging(panel, false);
+  is(label.textContent, "00:00.000");
+});
+
+function* isCurrentTimeLabelChanging(panel, isChanging) {
+  let label = panel.timelineCurrentTimeEl;
+
+  let time1 = label.textContent;
+  yield new Promise(r => setTimeout(r, 200));
+  let time2 = label.textContent;
+
+  if (isChanging) {
+    ok(time1 !== time2, "The text displayed in the label changes with time");
+  } else {
+    is(time1, time2, "The text displayed in the label doesn't change");
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/unit/test_formatStopwatchTime.js
@@ -0,0 +1,63 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
+const {formatStopwatchTime} = require("devtools/client/animationinspector/utils");
+
+
+const TEST_DATA = [{
+  desc: "Formatting 0",
+  time: 0,
+  expected: "00:00.000"
+}, {
+  desc: "Formatting null",
+  time: null,
+  expected: "00:00.000"
+}, {
+  desc: "Formatting undefined",
+  time: undefined,
+  expected: "00:00.000"
+}, {
+  desc: "Formatting a small number of ms",
+  time: 13,
+  expected: "00:00.013"
+}, {
+  desc: "Formatting a slightly larger number of ms",
+  time: 500,
+  expected: "00:00.500"
+}, {
+  desc: "Formatting 1 second",
+  time: 1000,
+  expected: "00:01.000"
+}, {
+  desc: "Formatting a number of seconds",
+  time: 1532,
+  expected: "00:01.532"
+}, {
+  desc: "Formatting a big number of seconds",
+  time: 58450,
+  expected: "00:58.450"
+}, {
+  desc: "Formatting 1 minute",
+  time: 60000,
+  expected: "01:00.000"
+}, {
+  desc: "Formatting a number of minutes",
+  time: 263567,
+  expected: "04:23.567"
+}, {
+  desc: "Formatting a large number of minutes",
+  time: 1000 * 60 * 60 * 3,
+  expected: "180:00.000"
+}];
+
+function run_test() {
+  for (let {desc, time, expected} of TEST_DATA) {
+    equal(formatStopwatchTime(time), expected, desc);
+  }
+}
--- a/devtools/client/animationinspector/test/unit/xpcshell.ini
+++ b/devtools/client/animationinspector/test/unit/xpcshell.ini
@@ -1,9 +1,10 @@
 [DEFAULT]
 tags = devtools
 head =
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_findOptimalTimeInterval.js]
+[test_formatStopwatchTime.js]
 [test_timeScale.js]
--- a/devtools/client/animationinspector/utils.js
+++ b/devtools/client/animationinspector/utils.js
@@ -172,8 +172,39 @@ var TargetNodeHighlighter = {
     yield this.highlighter.hide();
     this.isShown = false;
     this.emit("unhighlighted");
   })
 };
 
 EventEmitter.decorate(TargetNodeHighlighter);
 exports.TargetNodeHighlighter = TargetNodeHighlighter;
+
+/**
+ * Format a timestamp (in ms) as a mm:ss.mmm string.
+ * @param {Number} time
+ * @return {String}
+ */
+function formatStopwatchTime(time) {
+  // Format falsy values as 0
+  if (!time) {
+    return "00:00.000";
+  }
+
+  let milliseconds = parseInt(time % 1000, 10);
+  let seconds = parseInt((time / 1000) % 60, 10);
+  let minutes = parseInt((time / (1000 * 60)), 10);
+
+  let pad = (nb, max) => {
+    if (nb < max) {
+      return new Array((max+"").length - (nb+"").length + 1).join("0") + nb;
+    }
+    return nb;
+  }
+
+  minutes = pad(minutes, 10);
+  seconds = pad(seconds, 10);
+  milliseconds = pad(milliseconds, 100);
+
+  return `${minutes}:${seconds}.${milliseconds}`;
+}
+
+exports.formatStopwatchTime = formatStopwatchTime;
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -57,17 +57,18 @@ body {
 [timeline] #global-toolbar {
   display: none;
 }
 
 [timeline] #timeline-toolbar {
   display: flex;
 }
 
-#global-toolbar .label {
+#global-toolbar .label,
+#timeline-toolbar .label {
   padding: 1px 4px;
 }
 
 /* The main animations container */
 
 #players {
   height: calc(100% - var(--toolbar-height));
   overflow: auto;