Bug 1129454 - 2 - Adds a global play/pause button to the animation panel; r=vp
authorPatrick Brosset <pbrosset@mozilla.com>
Thu, 12 Feb 2015 16:28:42 +0100
changeset 242545 ecd8e5e79346d2feaa22de8259beff4d4a5e8b86
parent 242544 b04a0da319b6101555ed58555fb10c1c52a54fc9
child 242546 d74068ffe2edfdce5a810058ae02b32c64b5381f
push id660
push usermichael.l.comella@gmail.com
push dateThu, 12 Feb 2015 18:55:31 +0000
reviewersvp
bugs1129454
milestone38.0a1
Bug 1129454 - 2 - Adds a global play/pause button to the animation panel; r=vp
browser/devtools/animationinspector/animation-controller.js
browser/devtools/animationinspector/animation-inspector.xhtml
browser/devtools/animationinspector/animation-panel.js
browser/devtools/animationinspector/test/browser.ini
browser/devtools/animationinspector/test/browser_animation_toggle_button_resets_on_navigate.js
browser/devtools/animationinspector/test/browser_animation_toggle_button_toggles_animations.js
browser/devtools/animationinspector/test/browser_animation_toggle_button_updates_playerWidgets.js
browser/devtools/animationinspector/test/browser_animation_toolbar_exists.js
browser/devtools/animationinspector/test/doc_frame_script.js
browser/devtools/animationinspector/test/head.js
browser/locales/en-US/chrome/browser/devtools/animationinspector.dtd
browser/themes/shared/devtools/animationinspector.css
--- a/browser/devtools/animationinspector/animation-controller.js
+++ b/browser/devtools/animationinspector/animation-controller.js
@@ -35,44 +35,42 @@ let startup = Task.async(function*(inspe
   gInspector = inspector;
   gToolbox = inspector.toolbox;
 
   // Don't assume that AnimationsPanel is defined here, it's in another file.
   if (!typeof AnimationsPanel === "undefined") {
     throw new Error("AnimationsPanel was not loaded in the animationinspector window");
   }
 
-  yield promise.all([
-    AnimationsController.initialize(),
-    AnimationsPanel.initialize()
-  ]).then(null, Cu.reportError);
+  // Startup first initalizes the controller and then the panel, in sequence.
+  // If you want to know when everything's ready, do:
+  // AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED)
+  yield AnimationsController.initialize();
+  yield AnimationsPanel.initialize();
 });
 
 /**
  * Shutdown the animationinspector controller and view, called by the sidebar
  * widget when loading/unloading the iframe into the tab.
  */
 let shutdown = Task.async(function*() {
-  yield promise.all([
-    AnimationsController.destroy(),
-    // Don't assume that AnimationsPanel is defined here, it's in another file.
-    typeof AnimationsPanel !== "undefined"
-      ? AnimationsPanel.destroy()
-      : promise.resolve()
-  ]).then(() => {
-    gToolbox = gInspector = null;
-  }, Cu.reportError);
+  yield AnimationsController.destroy();
+  // Don't assume that AnimationsPanel is defined here, it's in another file.
+  if (typeof AnimationsPanel !== "undefined") {
+    yield AnimationsPanel.destroy()
+  }
+  gToolbox = gInspector = null;
 });
 
 // This is what makes the sidebar widget able to load/unload the panel.
 function setPanel(panel) {
-  return startup(panel);
+  return startup(panel).catch(Cu.reportError);
 }
 function destroy() {
-  return shutdown();
+  return shutdown().catch(Cu.reportError);
 }
 
 /**
  * The animationinspector controller's job is to retrieve AnimationPlayerFronts
  * from the server. It is also responsible for keeping the list of players up to
  * date when the node selection changes in the inspector, as well as making sure
  * no updates are done when the animationinspector sidebar panel is not visible.
  *
@@ -96,16 +94,18 @@ let AnimationsController = {
   initialize: Task.async(function*() {
     if (this.initialized) {
       return this.initialized.promise;
     }
     this.initialized = promise.defer();
 
     let target = gToolbox.target;
     this.animationsFront = new AnimationsFront(target.client, target.form);
+    // Not all server versions provide a way to pause all animations at once.
+    this.hasToggleAll = yield target.actorHasMethod("animations", "toggleAll");
 
     this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
     this.onNewNodeFront = this.onNewNodeFront.bind(this);
 
     this.startListeners();
 
     yield this.onNewNodeFront();
 
@@ -181,16 +181,27 @@ let AnimationsController = {
 
     this.nodeFront = gInspector.selection.nodeFront;
     yield this.refreshAnimationPlayers(this.nodeFront);
     this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
 
     done();
   }),
 
+  /**
+   * Toggle (pause/play) all animations in the current target.
+   */
+  toggleAll: function() {
+    if (!this.hasToggleAll) {
+      return promis.resolve();
+    }
+
+    return this.animationsFront.toggleAll().catch(Cu.reportError);
+  },
+
   // AnimationPlayerFront objects are managed by this controller. They are
   // retrieved when refreshAnimationPlayers is called, stored in the
   // animationPlayers array, and destroyed when refreshAnimationPlayers is
   // called again.
   animationPlayers: [],
 
   refreshAnimationPlayers: Task.async(function*(nodeFront) {
     yield this.destroyAnimationPlayers();
--- a/browser/devtools/animationinspector/animation-inspector.xhtml
+++ b/browser/devtools/animationinspector/animation-inspector.xhtml
@@ -10,16 +10,20 @@
   <head>
     <title>&title;</title>
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
     <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
     <link rel="stylesheet" href="chrome://browser/skin/devtools/animationinspector.css" type="text/css"/>
     <script type="application/javascript;version=1.8" src="chrome://browser/content/devtools/theme-switching.js"/>
   </head>
   <body class="theme-sidebar devtools-monospace" role="application">
+    <div id="toolbar" class="theme-toolbar">
+      <span class="label">&allAnimations;</span>
+      <button id="toggle-all" standalone="true" class="devtools-button"></button>
+    </div>
     <div id="players" class="theme-toolbar"></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>
     <script type="application/javascript;version=1.8" src="animation-panel.js"></script>
--- a/browser/devtools/animationinspector/animation-panel.js
+++ b/browser/devtools/animationinspector/animation-panel.js
@@ -6,88 +6,134 @@
 
 "use strict";
 
 /**
  * The main animations panel UI.
  */
 let AnimationsPanel = {
   UI_UPDATED_EVENT: "ui-updated",
+  PANEL_INITIALIZED: "panel-initialized",
 
   initialize: Task.async(function*() {
     if (this.initialized) {
       return this.initialized.promise;
     }
     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");
+
+    // If the server doesn't support toggling all animations at once, hide the
+    // whole bottom toolbar.
+    if (!AnimationsController.hasToggleAll) {
+      document.querySelector("#toolbar").style.display = "none";
+    }
 
     let hUtils = gToolbox.highlighterUtils;
     this.togglePicker = hUtils.togglePicker.bind(hUtils);
     this.onPickerStarted = this.onPickerStarted.bind(this);
     this.onPickerStopped = this.onPickerStopped.bind(this);
     this.createPlayerWidgets = this.createPlayerWidgets.bind(this);
+    this.toggleAll = this.toggleAll.bind(this);
+    this.onTabNavigated = this.onTabNavigated.bind(this);
 
     this.startListeners();
 
     this.initialized.resolve();
+
+    this.emit(this.PANEL_INITIALIZED);
   }),
 
   destroy: Task.async(function*() {
     if (!this.initialized) {
       return;
     }
 
     if (this.destroyed) {
       return this.destroyed.promise;
     }
     this.destroyed = promise.defer();
 
     this.stopListeners();
     yield this.destroyPlayerWidgets();
 
     this.playersEl = this.errorMessageEl = null;
+    this.toggleAllButtonEl = this.pickerButtonEl = null;
 
     this.destroyed.resolve();
   }),
 
   startListeners: function() {
     AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
       this.createPlayerWidgets);
+
     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);
   },
 
   stopListeners: function() {
     AnimationsController.off(AnimationsController.PLAYERS_UPDATED_EVENT,
       this.createPlayerWidgets);
+
     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);
   },
 
   displayErrorMessage: function() {
     this.errorMessageEl.style.display = "block";
+    this.playersEl.style.display = "none";
   },
 
   hideErrorMessage: function() {
     this.errorMessageEl.style.display = "none";
+    this.playersEl.style.display = "block";
   },
 
   onPickerStarted: function() {
     this.pickerButtonEl.setAttribute("checked", "true");
   },
 
   onPickerStopped: function() {
     this.pickerButtonEl.removeAttribute("checked");
   },
 
+  toggleAll: Task.async(function*() {
+    let btnClass = this.toggleAllButtonEl.classList;
+
+    // Toggling all animations is async and it may be some time before each of
+    // the current players get their states updated, so toggle locally too, to
+    // avoid the timelines from jumping back and forth.
+    if (this.playerWidgets) {
+      let currentWidgetStateChange = [];
+      for (let widget of this.playerWidgets) {
+        currentWidgetStateChange.push(btnClass.contains("paused")
+          ? widget.play() : widget.pause());
+      }
+      yield promise.all(currentWidgetStateChange).catch(Cu.reportError);
+    }
+
+    btnClass.toggle("paused");
+    yield AnimationsController.toggleAll();
+  }),
+
+  onTabNavigated: function() {
+    this.toggleAllButtonEl.classList.remove("paused");
+  },
+
   createPlayerWidgets: Task.async(function*() {
     let done = gInspector.updating("animationspanel");
 
     // Empty the whole panel first.
     this.hideErrorMessage();
     yield this.destroyPlayerWidgets();
 
     // If there are no players to show, show the error message instead and return.
@@ -302,30 +348,37 @@ PlayerWidget.prototype = {
   },
 
   /**
    * Pause the animation player via this widget.
    * @return {Promise} Resolves when the player is paused, the button is
    * switched to the right state, and the timeline animation is stopped.
    */
   pause: function() {
+    if (this.player.state.playState === "finished") {
+      return;
+    }
+
     // Switch to the right className on the element right away to avoid waiting
     // for the next state update to change the playPause icon.
     this.updateWidgetState({playState: "paused"});
-    return this.player.pause().then(() => {
-      this.stopTimelineAnimation();
-    });
+    this.stopTimelineAnimation();
+    return this.player.pause();
   },
 
   /**
    * Play the animation player via this widget.
    * @return {Promise} Resolves when the player is playing, the button is
    * switched to the right state, and the timeline animation is started.
    */
   play: function() {
+    if (this.player.state.playState === "finished") {
+      return;
+    }
+
     // Switch to the right className on the element right away to avoid waiting
     // for the next state update to change the playPause icon.
     this.updateWidgetState({playState: "running"});
     this.startTimelineAnimation();
     return this.player.play();
   },
 
   updateWidgetState: function({playState}) {
--- a/browser/devtools/animationinspector/test/browser.ini
+++ b/browser/devtools/animationinspector/test/browser.ini
@@ -16,10 +16,14 @@ support-files =
 [browser_animation_playerWidgets_dont_show_time_after_duration.js]
 [browser_animation_playerWidgets_meta_data.js]
 [browser_animation_playerWidgets_state_after_pause.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_timeline_animates.js]
 [browser_animation_timeline_waits_for_delay.js]
+[browser_animation_toggle_button_resets_on_navigate.js]
+[browser_animation_toggle_button_toggles_animations.js]
+[browser_animation_toggle_button_updates_playerWidgets.js]
+[browser_animation_toolbar_exists.js]
 [browser_animation_ui_updates_when_animation_changes.js]
 [browser_animation_ui_updates_when_animation_data_changes.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_toggle_button_resets_on_navigate.js
@@ -0,0 +1,29 @@
+/* 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";
+
+// Test that a page navigation resets the state of the global toggle button.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel} = yield openAnimationInspector();
+
+  ok(!panel.toggleAllButtonEl.classList.contains("paused"),
+    "The toggle button is in its running state by default");
+
+  info("Toggle all animations, so that they pause");
+  yield panel.toggleAll();
+  ok(panel.toggleAllButtonEl.classList.contains("paused"),
+    "The toggle button now is in its paused state");
+
+  info("Reloading the page");
+  let onNewRoot = inspector.once("new-root");
+  yield reloadTab();
+  yield onNewRoot;
+  yield inspector.once("inspector-updated");
+
+  ok(!panel.toggleAllButtonEl.classList.contains("paused"),
+    "The toggle button is back in its running state");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_toggle_button_toggles_animations.js
@@ -0,0 +1,30 @@
+/* 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";
+
+// Test that the main toggle button actually toggles animations.
+// This test doesn't need to be extra careful about checking that *all*
+// animations have been paused (including inside iframes) because there's an
+// actor test in /toolkit/devtools/server/tests/browser/ that does this.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel} = yield openAnimationInspector();
+
+  info("Click the toggle button");
+  yield panel.toggleAll();
+  yield checkState("paused");
+
+  info("Click again the toggle button");
+  yield panel.toggleAll();
+  yield checkState("running");
+});
+
+function* checkState(state) {
+  for (let selector of [".animated", ".multi", ".long"]) {
+    let playState = yield getAnimationPlayerState(selector);
+    is(playState, state, "The animation on node " + selector + " is " + state);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_toggle_button_updates_playerWidgets.js
@@ -0,0 +1,35 @@
+/* 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";
+
+// Test that pressing the main toggle button also updates the displayed
+// player widgets.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel} = yield openAnimationInspector();
+
+  info("Select an animated node");
+  yield selectNode(".animated", inspector);
+  let widget = panel.playerWidgets[0];
+
+  info("Click the toggle button to pause all animations");
+  let onRefresh = widget.player.once(widget.player.AUTO_REFRESH_EVENT);
+  yield panel.toggleAll();
+  yield onRefresh;
+
+  info("Checking the selected node's animation player widget's state");
+  is(widget.player.state.playState, "paused", "The player front's state is paused");
+  ok(widget.el.classList.contains("paused"), "The widget's UI is in paused state");
+
+  info("Click the toggle button to play all animations");
+  onRefresh = widget.player.once(widget.player.AUTO_REFRESH_EVENT);
+  yield panel.toggleAll();
+  yield onRefresh;
+
+  info("Checking the selected node's animation player widget's state again");
+  is(widget.player.state.playState, "running", "The player front's state is running");
+  ok(widget.el.classList.contains("running"), "The widget's UI is in running state");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_toolbar_exists.js
@@ -0,0 +1,26 @@
+/* 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";
+
+// Test that the animation panel has a top toolbar that contains the play/pause
+// button and that is displayed at all times.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel, window} = yield openAnimationInspector();
+  let doc = window.document;
+
+  let toolbar = doc.querySelector("#toolbar");
+  ok(toolbar, "The panel contains the toolbar element");
+  ok(toolbar.querySelector("#toggle-all"), "The toolbar contains the toggle button");
+  ok(isNodeVisible(toolbar), "The toolbar is visible");
+
+  info("Select an animated node");
+  yield selectNode(".animated", inspector);
+
+  toolbar = doc.querySelector("#toolbar");
+  ok(toolbar, "The panel still contains the toolbar element");
+  ok(isNodeVisible(toolbar), "The toolbar is still visible");
+});
--- a/browser/devtools/animationinspector/test/doc_frame_script.js
+++ b/browser/devtools/animationinspector/test/doc_frame_script.js
@@ -39,8 +39,25 @@ addMessageListener("Test:ToggleAnimation
 addMessageListener("Test:SetNodeStyle", function(msg) {
   let {propertyName, propertyValue} = msg.data;
   let {node} = msg.objects;
 
   node.style[propertyName] = propertyValue;
 
   sendAsyncMessage("Test:SetNodeStyle");
 });
+
+/**
+ * Get the current playState of an animation player on a given node.
+ * @param {Object} data
+ * - {Number} animationIndex The index of the node's animationPlayers to check
+ * @param {Object} objects
+ * - {DOMNode} node The node to check
+ */
+addMessageListener("Test:GetAnimationPlayerState", function(msg) {
+  let {animationIndex} = msg.data;
+  let {node} = msg.objects;
+
+  let player = node.getAnimationPlayers()[animationIndex];
+  player.ready.then(() => {
+    sendAsyncMessage("Test:GetAnimationPlayerState", player.playState);
+  });
+});
--- a/browser/devtools/animationinspector/test/head.js
+++ b/browser/devtools/animationinspector/test/head.js
@@ -13,16 +13,17 @@ const {console} = Components.utils.impor
 const {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
 
 // All tests are asynchronous
 waitForExplicitFinish();
 
 const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/animationinspector/test/";
 const ROOT_TEST_DIR = getRootDirectory(gTestPath);
 const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
+const COMMON_FRAME_SCRIPT_URL = "chrome://browser/content/devtools/frame-script-utils.js";
 
 // Auto clean-up when a test ends
 registerCleanupFunction(function*() {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   yield gDevTools.closeToolbox(target);
 
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
@@ -58,27 +59,37 @@ function addTab(url) {
   window.focus();
 
   let tab = window.gBrowser.selectedTab = window.gBrowser.addTab(url);
   let browser = tab.linkedBrowser;
 
   info("Loading the helper frame script " + FRAME_SCRIPT_URL);
   browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
 
+  info("Loading the helper frame script " + COMMON_FRAME_SCRIPT_URL);
+  browser.messageManager.loadFrameScript(COMMON_FRAME_SCRIPT_URL, false);
+
   browser.addEventListener("load", function onload() {
     browser.removeEventListener("load", onload, true);
     info("URL '" + url + "' loading complete");
 
     def.resolve(tab);
   }, true);
 
   return def.promise;
 }
 
 /**
+ * Reload the current tab location.
+ */
+function reloadTab() {
+  return executeInContent("devtools:test:reload", {}, {}, false);
+}
+
+/**
  * Simple DOM node accesor function that takes either a node or a string css
  * selector as argument and returns the corresponding node
  * @param {String|DOMNode} nodeOrSelector
  * @return {DOMNode|CPOW} Note that in e10s mode a CPOW object is returned which
  * doesn't implement *all* of the DOMNode's properties
  */
 function getNode(nodeOrSelector) {
   info("Getting the node for '" + nodeOrSelector + "'");
@@ -143,20 +154,17 @@ let openAnimationInspector = Task.async(
   inspector.sidebar.select("animationinspector");
 
   info("Waiting for the inspector and sidebar to be ready");
   yield promise.all(initPromises);
 
   let win = inspector.sidebar.getWindowForTab("animationinspector");
   let {AnimationsController, AnimationsPanel} = win;
 
-  yield promise.all([
-    AnimationsController.initialized,
-    AnimationsPanel.initialized
-  ]);
+  yield AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED);
 
   return {
     toolbox: toolbox,
     inspector: inspector,
     controller: AnimationsController,
     panel: AnimationsPanel,
     window: win
   };
@@ -279,15 +287,25 @@ let togglePlayPauseButton = Task.async(f
 
   yield onClicked;
 
   // Wait for the next sate change event to make sure the state is updated
   yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
 });
 
 /**
+ * Get the current playState of an animation player on a given node.
+ */
+let getAnimationPlayerState = Task.async(function*(selector, animationIndex=0) {
+  let playState = yield executeInContent("Test:GetAnimationPlayerState",
+                                         {animationIndex},
+                                         {node: getNode(selector)});
+  return playState;
+});
+
+/**
  * Is the given node visible in the page (rendered in the frame tree).
  * @param {DOMNode}
  * @return {Boolean}
  */
 function isNodeVisible(node) {
   return !!node.getClientRects().length;
 }
--- a/browser/locales/en-US/chrome/browser/devtools/animationinspector.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/animationinspector.dtd
@@ -1,24 +1,29 @@
 <!-- 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/. -->
+     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/. -->
 
 <!-- LOCALIZATION NOTE : FILE This file contains the Animations panel strings.
-  - The Animations panel is part of the Inspector sidebar -->
+     The Animations panel is part of the Inspector sidebar -->
 
 <!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
-  - keep it in English, or another language commonly spoken among web developers.
-  - You want to make that choice consistent across the developer tools.
-  - A good criteria is the language in which you'd find the best
-  - documentation on web development on the web. -->
+     keep it in English, or another language commonly spoken among web
+     developers. You want to make that choice consistent across the developer
+     tools. A good criteria is the language in which you'd find the best
+     documentation on web development on the web. -->
 
 <!-- LOCALIZATION NOTE (title): This is the label shown in the sidebar tab -->
-<!ENTITY title                  "Animations">
+<!ENTITY title "Animations">
 
 <!-- LOCALIZATION NOTE (invalidElement): This is the label shown in the panel
-  - when an invalid node is currently selected in the inspector. -->
-<!ENTITY invalidElement         "No animations were found for the current element.">
+     when an invalid node is currently selected in the inspector. -->
+<!ENTITY invalidElement "No animations were found for the current element.">
 
 <!-- LOCALIZATION NOTE (selectElement): This is the label shown in the panel
-  - when an invalid node is currently selected in the inspector, to invite the
-  - user to select a new node by clicking on the element-picker icon. -->
-<!ENTITY selectElement         "Pick another element from the page.">
\ No newline at end of file
+     when an invalid node is currently selected in the inspector, to invite the
+     user to select a new node by clicking on the element-picker icon. -->
+<!ENTITY selectElement "Pick another element from the page.">
+
+<!-- LOCALIZATION NOTE (allAnimations): This is the label shown at the bottom of
+     the panel, in a toolbar, to let the user know the toolbar applies to all
+     animations, not just the ones applying to the current element. -->
+<!ENTITY allAnimations "All animations">
--- a/browser/themes/shared/devtools/animationinspector.css
+++ b/browser/themes/shared/devtools/animationinspector.css
@@ -1,65 +1,121 @@
+html {
+  height: 100%;
+}
+
 body {
   margin: 0;
   padding: 0;
+  display : flex;
+  flex-direction: column;
+  height: 100%;
+  overflow: hidden;
+  color: var(--theme-content-color3);
+}
+
+/* The top toolbar, containing the toggle-all button */
+
+#toolbar {
+  border-bottom: 1px solid var(--theme-splitter-color);
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: flex-end;
+  height: 20px;
+}
+
+#toolbar .label {
+  padding: 1px 4px;
+}
+
+#toggle-all {
+  border-width: 0px 1px;
+  min-height: 20px;
 }
 
 /* The error message, shown when an invalid/unanimated element is selected */
 
 #error-message {
-  margin-top: 10%;
+  padding-top: 10%;
   text-align: center;
+  flex: 1;
+  overflow: auto;
 
   /* The error message is hidden by default */
   display: none;
 }
 
-/* Element picker button */
+/* The animation players container */
 
-#element-picker {
+#players {
+  flex: 1;
+  overflow: auto;
+}
+
+/* Element picker and toggle-all buttons */
+
+#element-picker,
+#toggle-all {
   position: relative;
 }
 
-#element-picker::before {
+#element-picker::before,
+#toggle-all::before {
   content: "";
   display: block;
   width: 16px;
   height: 16px;
   position: absolute;
   left: 50%;
   top: 50%;
   margin: -8px 0 0 -8px;
   background-image: url("chrome://browser/skin/devtools/command-pick.png");
 }
 
+#toggle-all::before {
+  background-image: url("debugger-pause.png");
+}
+
 #element-picker[checked]::before {
   background-position: -48px 0;
   filter: none; /* Icon is blue when checked, don't invert for light theme */
 }
 
+#toggle-all.paused::before {
+  background-image: url("debugger-play.png");
+}
+
 @media (min-resolution: 2dppx) {
-  #element-picker::before {
+  #element-picker::before,
+  #toggle-all::before {
     background-image: url("chrome://browser/skin/devtools/command-pick@2x.png");
     background-size: 64px;
   }
+
+  #toggle-all::before {
+    background-image: url("debugger-pause@2x.png");
+  }
+
+  #toggle-all.paused::before {
+    background-image: url("debugger-play@2x.png");
+  }
 }
 
 /* Disabled playerWidget when the animation has ended */
 
 .finished {
   pointer-events: none;
   opacity: .5;
 }
 
 /* Animation title gutter, contains the name, duration, iteration */
 
 .animation-title {
   background-color: var(--theme-toolbar-background);
-  color: var(--theme-content-color3);
   border-bottom: 1px solid var(--theme-splitter-color);
   padding: 1px 4px;
   word-wrap: break-word;
   overflow: auto;
 }
 
 .animation-title .meta-data {
   float: right;