Bug 1129454 - 2 - Adds a global play/pause button to the animation panel; r=vp

This commit is contained in:
Patrick Brosset 2015-02-12 16:28:42 +01:00
Родитель 20f47d548f
Коммит b9102ef3d3
12 изменённых файлов: 329 добавлений и 41 удалений

Просмотреть файл

@ -40,10 +40,11 @@ let startup = Task.async(function*(inspector) {
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();
});
/**
@ -51,23 +52,20 @@ let startup = Task.async(function*(inspector) {
* 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);
}
/**
@ -101,6 +99,8 @@ let AnimationsController = {
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);
@ -186,6 +186,17 @@ let AnimationsController = {
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

Просмотреть файл

@ -15,6 +15,10 @@
<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>

Просмотреть файл

@ -11,6 +11,7 @@
*/
let AnimationsPanel = {
UI_UPDATED_EVENT: "ui-updated",
PANEL_INITIALIZED: "panel-initialized",
initialize: Task.async(function*() {
if (this.initialized) {
@ -21,16 +22,27 @@ let AnimationsPanel = {
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*() {
@ -47,6 +59,7 @@ let AnimationsPanel = {
yield this.destroyPlayerWidgets();
this.playersEl = this.errorMessageEl = null;
this.toggleAllButtonEl = this.pickerButtonEl = null;
this.destroyed.resolve();
}),
@ -54,25 +67,35 @@ let AnimationsPanel = {
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() {
@ -83,6 +106,29 @@ let AnimationsPanel = {
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");
@ -307,12 +353,15 @@ PlayerWidget.prototype = {
* 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();
},
/**
@ -321,6 +370,10 @@ PlayerWidget.prototype = {
* 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"});

Просмотреть файл

@ -21,5 +21,9 @@ support-files =
[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]

Просмотреть файл

@ -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");
});

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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");
});

Просмотреть файл

@ -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");
});

Просмотреть файл

@ -44,3 +44,20 @@ addMessageListener("Test:SetNodeStyle", function(msg) {
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);
});
});

Просмотреть файл

@ -18,6 +18,7 @@ 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*() {
@ -63,6 +64,9 @@ function addTab(url) {
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");
@ -73,6 +77,13 @@ function addTab(url) {
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
@ -148,10 +159,7 @@ let openAnimationInspector = Task.async(function*() {
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,
@ -283,6 +291,16 @@ let togglePlayPauseButton = Task.async(function*(widget) {
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}

Просмотреть файл

@ -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.">
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">

Просмотреть файл

@ -1,25 +1,65 @@
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;
@ -31,16 +71,33 @@ body {
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 */
@ -54,7 +111,6 @@ body {
.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;