2014-12-11 19:08:49 +00:00
|
|
|
/* 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";
|
|
|
|
|
|
|
|
/**
|
2015-06-11 13:45:57 +00:00
|
|
|
* Set of actors that expose the Web Animations API to devtools protocol
|
|
|
|
* clients.
|
2014-12-11 19:08:49 +00:00
|
|
|
*
|
|
|
|
* The |Animations| actor is the main entry point. It is used to discover
|
|
|
|
* animation players on given nodes.
|
|
|
|
* There should only be one instance per debugger server.
|
|
|
|
*
|
|
|
|
* The |AnimationPlayer| actor provides attributes and methods to inspect an
|
|
|
|
* animation as well as pause/resume/seek it.
|
|
|
|
*
|
|
|
|
* The Web Animation spec implementation is ongoing in Gecko, and so this set
|
|
|
|
* of actors should evolve when the implementation progresses.
|
|
|
|
*
|
|
|
|
* References:
|
|
|
|
* - WebAnimation spec:
|
|
|
|
* http://w3c.github.io/web-animations/
|
|
|
|
* - WebAnimation WebIDL files:
|
|
|
|
* /dom/webidl/Animation*.webidl
|
|
|
|
*/
|
|
|
|
|
2014-12-22 16:56:41 +00:00
|
|
|
const {Cu} = require("chrome");
|
2015-08-26 13:05:13 +00:00
|
|
|
const promise = require("promise");
|
2014-12-22 16:56:41 +00:00
|
|
|
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
|
|
|
|
const {setInterval, clearInterval} = require("sdk/timers");
|
2015-01-09 14:21:05 +00:00
|
|
|
const protocol = require("devtools/server/protocol");
|
2015-06-11 13:45:57 +00:00
|
|
|
const {ActorClass, Actor, FrontClass, Front,
|
|
|
|
Arg, method, RetVal, types} = protocol;
|
|
|
|
// Make sure the nodeActor type is know here.
|
2014-12-11 19:08:49 +00:00
|
|
|
const {NodeActor} = require("devtools/server/actors/inspector");
|
2015-02-12 15:28:42 +00:00
|
|
|
const events = require("sdk/event/core");
|
2014-12-22 16:56:41 +00:00
|
|
|
|
2015-06-11 13:45:57 +00:00
|
|
|
// How long (in ms) should we wait before polling again the state of an
|
|
|
|
// animationPlayer.
|
|
|
|
const PLAYER_DEFAULT_AUTO_REFRESH_TIMEOUT = 500;
|
2014-12-11 19:08:49 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The AnimationPlayerActor provides information about a given animation: its
|
|
|
|
* startTime, currentTime, current state, etc.
|
|
|
|
*
|
|
|
|
* Since the state of a player changes as the animation progresses it is often
|
|
|
|
* useful to call getCurrentState at regular intervals to get the current state.
|
|
|
|
*
|
2015-03-16 10:29:14 +00:00
|
|
|
* This actor also allows playing, pausing and seeking the animation.
|
2014-12-11 19:08:49 +00:00
|
|
|
*/
|
|
|
|
let AnimationPlayerActor = ActorClass({
|
|
|
|
typeName: "animationplayer",
|
|
|
|
|
2015-06-11 13:45:57 +00:00
|
|
|
events: {
|
|
|
|
"changed": {
|
|
|
|
type: "changed",
|
|
|
|
state: Arg(0, "json")
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-12-11 19:08:49 +00:00
|
|
|
/**
|
|
|
|
* @param {AnimationsActor} The main AnimationsActor instance
|
|
|
|
* @param {AnimationPlayer} The player object returned by getAnimationPlayers
|
|
|
|
* @param {Number} Temporary work-around used to retrieve duration and
|
|
|
|
* iteration count from computed-style rather than from waapi. This is needed
|
|
|
|
* to know which duration to get, in case there are multiple css animations
|
|
|
|
* applied to the same node.
|
|
|
|
*/
|
2015-04-02 10:47:34 +00:00
|
|
|
initialize: function(animationsActor, player, playerIndex) {
|
2015-01-09 14:21:05 +00:00
|
|
|
Actor.prototype.initialize.call(this, animationsActor.conn);
|
|
|
|
|
2015-06-11 13:45:57 +00:00
|
|
|
this.onAnimationMutation = this.onAnimationMutation.bind(this);
|
|
|
|
|
|
|
|
this.tabActor = animationsActor.tabActor;
|
2014-12-11 19:08:49 +00:00
|
|
|
this.player = player;
|
2015-04-14 23:48:21 +00:00
|
|
|
this.node = player.effect.target;
|
2014-12-11 19:08:49 +00:00
|
|
|
this.playerIndex = playerIndex;
|
2015-06-11 13:45:57 +00:00
|
|
|
|
|
|
|
let win = this.node.ownerDocument.defaultView;
|
|
|
|
this.styles = win.getComputedStyle(this.node);
|
|
|
|
|
|
|
|
// Listen to animation mutations on the node to alert the front when the
|
|
|
|
// current animation changes.
|
|
|
|
this.observer = new win.MutationObserver(this.onAnimationMutation);
|
|
|
|
this.observer.observe(this.node, {animations: true});
|
2014-12-11 19:08:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
destroy: function() {
|
2015-06-11 13:45:57 +00:00
|
|
|
// Only try to disconnect the observer if it's not already dead (i.e. if the
|
|
|
|
// container view hasn't navigated since).
|
|
|
|
if (this.observer && !Cu.isDeadWrapper(this.observer)) {
|
|
|
|
this.observer.disconnect();
|
|
|
|
}
|
|
|
|
this.tabActor = this.player = this.node = this.styles = this.observer = null;
|
2014-12-11 19:08:49 +00:00
|
|
|
Actor.prototype.destroy.call(this);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Release the actor, when it isn't needed anymore.
|
|
|
|
* Protocol.js uses this release method to call the destroy method.
|
|
|
|
*/
|
|
|
|
release: method(function() {}, {release: true}),
|
|
|
|
|
|
|
|
form: function(detail) {
|
|
|
|
if (detail === "actorid") {
|
|
|
|
return this.actorID;
|
|
|
|
}
|
|
|
|
|
|
|
|
let data = this.getCurrentState();
|
|
|
|
data.actor = this.actorID;
|
|
|
|
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
|
2015-07-01 05:50:49 +00:00
|
|
|
isAnimation: function(player=this.player) {
|
|
|
|
return player instanceof this.tabActor.window.CSSAnimation;
|
|
|
|
},
|
|
|
|
|
|
|
|
isTransition: function(player=this.player) {
|
|
|
|
return player instanceof this.tabActor.window.CSSTransition;
|
|
|
|
},
|
|
|
|
|
2015-02-12 06:57:00 +00:00
|
|
|
/**
|
|
|
|
* Some of the player's properties are retrieved from the node's
|
|
|
|
* computed-styles because the Web Animations API does not provide them yet.
|
|
|
|
* But the computed-styles may contain multiple animations for a node and so
|
|
|
|
* we need to know which is the index of the current animation in the style.
|
|
|
|
* @return {Number}
|
|
|
|
*/
|
|
|
|
getPlayerIndex: function() {
|
|
|
|
let names = this.styles.animationName;
|
2015-06-11 13:45:57 +00:00
|
|
|
if (names === "none") {
|
|
|
|
names = this.styles.transitionProperty;
|
|
|
|
}
|
2015-02-12 06:57:00 +00:00
|
|
|
|
2015-06-11 13:45:57 +00:00
|
|
|
// If we still don't have a name, let's fall back to the provided index
|
|
|
|
// which may, by now, be wrong, but it's the best we can do until the waapi
|
|
|
|
// gives us a way to get duration, delay, ... directly.
|
|
|
|
if (!names || names === "none") {
|
2015-02-12 06:57:00 +00:00
|
|
|
return this.playerIndex;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there's only one name.
|
2015-04-29 15:32:05 +00:00
|
|
|
if (names.includes(",") === -1) {
|
2015-02-12 06:57:00 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there are several names, retrieve the index of the animation name in
|
|
|
|
// the list.
|
2015-07-01 05:50:49 +00:00
|
|
|
let playerName = this.getName();
|
2015-02-12 06:57:00 +00:00
|
|
|
names = names.split(",").map(n => n.trim());
|
2015-06-11 13:45:57 +00:00
|
|
|
for (let i = 0; i < names.length; i++) {
|
2015-07-01 05:50:49 +00:00
|
|
|
if (names[i] === playerName) {
|
2015-02-12 06:57:00 +00:00
|
|
|
return i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-07-01 05:50:49 +00:00
|
|
|
/**
|
|
|
|
* Get the name associated with the player. This is used to match
|
|
|
|
* up the player with values in the computed animation-name or
|
|
|
|
* transition-property property.
|
|
|
|
* @return {String}
|
|
|
|
*/
|
|
|
|
getName: function() {
|
|
|
|
if (this.isAnimation()) {
|
|
|
|
return this.player.animationName;
|
|
|
|
} else if (this.isTransition()) {
|
|
|
|
return this.player.transitionProperty;
|
|
|
|
} else {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-12-11 19:08:49 +00:00
|
|
|
/**
|
|
|
|
* Get the animation duration from this player, in milliseconds.
|
|
|
|
* Note that the Web Animations API doesn't yet offer a way to retrieve this
|
|
|
|
* directly from the AnimationPlayer object, so for now, a duration is only
|
|
|
|
* returned if found in the node's computed styles.
|
|
|
|
* @return {Number}
|
|
|
|
*/
|
|
|
|
getDuration: function() {
|
|
|
|
let durationText;
|
|
|
|
if (this.styles.animationDuration !== "0s") {
|
|
|
|
durationText = this.styles.animationDuration;
|
|
|
|
} else if (this.styles.transitionDuration !== "0s") {
|
|
|
|
durationText = this.styles.transitionDuration;
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2015-02-12 06:57:00 +00:00
|
|
|
// If the computed duration has multiple entries, we need to find the right
|
|
|
|
// one.
|
2014-12-11 19:08:49 +00:00
|
|
|
if (durationText.indexOf(",") !== -1) {
|
2015-02-12 06:57:00 +00:00
|
|
|
durationText = durationText.split(",")[this.getPlayerIndex()];
|
2014-12-11 19:08:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return parseFloat(durationText) * 1000;
|
|
|
|
},
|
|
|
|
|
2015-01-14 13:59:38 +00:00
|
|
|
/**
|
|
|
|
* Get the animation delay from this player, in milliseconds.
|
|
|
|
* Note that the Web Animations API doesn't yet offer a way to retrieve this
|
|
|
|
* directly from the AnimationPlayer object, so for now, a delay is only
|
|
|
|
* returned if found in the node's computed styles.
|
|
|
|
* @return {Number}
|
|
|
|
*/
|
|
|
|
getDelay: function() {
|
|
|
|
let delayText;
|
|
|
|
if (this.styles.animationDelay !== "0s") {
|
|
|
|
delayText = this.styles.animationDelay;
|
|
|
|
} else if (this.styles.transitionDelay !== "0s") {
|
|
|
|
delayText = this.styles.transitionDelay;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (delayText.indexOf(",") !== -1) {
|
2015-02-12 06:57:00 +00:00
|
|
|
delayText = delayText.split(",")[this.getPlayerIndex()];
|
2015-01-14 13:59:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return parseFloat(delayText) * 1000;
|
|
|
|
},
|
|
|
|
|
2014-12-11 19:08:49 +00:00
|
|
|
/**
|
|
|
|
* Get the animation iteration count for this player. That is, how many times
|
|
|
|
* is the animation scheduled to run.
|
|
|
|
* Note that the Web Animations API doesn't yet offer a way to retrieve this
|
|
|
|
* directly from the AnimationPlayer object, so for now, check for
|
|
|
|
* animationIterationCount in the node's computed styles, and return that.
|
|
|
|
* This style property defaults to 1 anyway.
|
|
|
|
* @return {Number}
|
|
|
|
*/
|
|
|
|
getIterationCount: function() {
|
|
|
|
let iterationText = this.styles.animationIterationCount;
|
|
|
|
if (iterationText.indexOf(",") !== -1) {
|
2015-02-12 06:57:00 +00:00
|
|
|
iterationText = iterationText.split(",")[this.getPlayerIndex()];
|
2014-12-11 19:08:49 +00:00
|
|
|
}
|
|
|
|
|
2015-02-12 15:28:42 +00:00
|
|
|
return iterationText === "infinite"
|
|
|
|
? null
|
|
|
|
: parseInt(iterationText, 10);
|
2014-12-11 19:08:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the current state of the AnimationPlayer (currentTime, playState, ...).
|
|
|
|
* Note that the initial state is returned as the form of this actor when it
|
|
|
|
* is initialized.
|
|
|
|
* @return {Object}
|
|
|
|
*/
|
|
|
|
getCurrentState: method(function() {
|
2015-01-16 06:39:00 +00:00
|
|
|
// Note that if you add a new property to the state object, make sure you
|
|
|
|
// add the corresponding property in the AnimationPlayerFront' initialState
|
|
|
|
// getter.
|
|
|
|
let newState = {
|
|
|
|
// startTime is null whenever the animation is paused or waiting to start.
|
2014-12-11 19:08:49 +00:00
|
|
|
startTime: this.player.startTime,
|
|
|
|
currentTime: this.player.currentTime,
|
|
|
|
playState: this.player.playState,
|
2015-03-25 17:56:54 +00:00
|
|
|
playbackRate: this.player.playbackRate,
|
2015-07-01 05:50:49 +00:00
|
|
|
name: this.getName(),
|
2014-12-11 19:08:49 +00:00
|
|
|
duration: this.getDuration(),
|
2015-01-14 13:59:38 +00:00
|
|
|
delay: this.getDelay(),
|
2014-12-11 19:08:49 +00:00
|
|
|
iterationCount: this.getIterationCount(),
|
2015-01-16 06:39:00 +00:00
|
|
|
// isRunningOnCompositor is important for developers to know if their
|
|
|
|
// animation is hitting the fast path or not. Currently only true for
|
|
|
|
// Firefox OS (where we have compositor animations enabled).
|
|
|
|
// Returns false whenever the animation is paused as it is taken off the
|
|
|
|
// compositor then.
|
2015-08-27 14:59:16 +00:00
|
|
|
isRunningOnCompositor: this.player.isRunningOnCompositor,
|
|
|
|
// The document timeline's currentTime is being sent along too. This is
|
|
|
|
// not strictly related to the node's animationPlayer, but is useful to
|
|
|
|
// know the current time of the animation with respect to the document's.
|
|
|
|
documentCurrentTime: this.node.ownerDocument.timeline.currentTime
|
2014-12-11 19:08:49 +00:00
|
|
|
};
|
2015-01-16 06:39:00 +00:00
|
|
|
|
|
|
|
// If we've saved a state before, compare and only send what has changed.
|
|
|
|
// It's expected of the front to also save old states to re-construct the
|
|
|
|
// full state when an incomplete one is received.
|
|
|
|
// This is to minimize protocol traffic.
|
|
|
|
let sentState = {};
|
|
|
|
if (this.currentState) {
|
|
|
|
for (let key in newState) {
|
|
|
|
if (typeof this.currentState[key] === "undefined" ||
|
|
|
|
this.currentState[key] !== newState[key]) {
|
|
|
|
sentState[key] = newState[key];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
sentState = newState;
|
|
|
|
}
|
|
|
|
this.currentState = newState;
|
|
|
|
|
|
|
|
return sentState;
|
2014-12-11 19:08:49 +00:00
|
|
|
}, {
|
|
|
|
request: {},
|
|
|
|
response: {
|
|
|
|
data: RetVal("json")
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
|
2015-06-11 13:45:57 +00:00
|
|
|
/**
|
|
|
|
* Executed when the current animation changes, used to emit the new state
|
|
|
|
* the the front.
|
|
|
|
*/
|
|
|
|
onAnimationMutation: function(mutations) {
|
|
|
|
let hasChanged = false;
|
|
|
|
for (let {changedAnimations} of mutations) {
|
|
|
|
if (!changedAnimations.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (changedAnimations.some(animation => animation === this.player)) {
|
|
|
|
hasChanged = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hasChanged) {
|
|
|
|
events.emit(this, "changed", this.getCurrentState());
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-12-11 19:08:49 +00:00
|
|
|
/**
|
|
|
|
* Pause the player.
|
|
|
|
*/
|
|
|
|
pause: method(function() {
|
|
|
|
this.player.pause();
|
2015-03-24 00:21:08 +00:00
|
|
|
return this.player.ready;
|
2014-12-11 19:08:49 +00:00
|
|
|
}, {
|
|
|
|
request: {},
|
|
|
|
response: {}
|
|
|
|
}),
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Play the player.
|
2015-01-09 14:21:05 +00:00
|
|
|
* This method only returns when the animation has left its pending state.
|
2014-12-11 19:08:49 +00:00
|
|
|
*/
|
|
|
|
play: method(function() {
|
|
|
|
this.player.play();
|
2014-12-25 07:28:25 +00:00
|
|
|
return this.player.ready;
|
2014-12-11 19:08:49 +00:00
|
|
|
}, {
|
|
|
|
request: {},
|
|
|
|
response: {}
|
2015-01-09 14:21:05 +00:00
|
|
|
}),
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Simply exposes the player ready promise.
|
|
|
|
*
|
|
|
|
* When an animation is created/paused then played, there's a short time
|
|
|
|
* during which its playState is pending, before being set to running.
|
|
|
|
*
|
|
|
|
* If you either created a new animation using the Web Animations API or
|
|
|
|
* paused/played an existing one, and then want to access the playState, you
|
|
|
|
* might be interested to call this method.
|
|
|
|
* This is especially important for tests.
|
|
|
|
*/
|
|
|
|
ready: method(function() {
|
|
|
|
return this.player.ready;
|
|
|
|
}, {
|
|
|
|
request: {},
|
|
|
|
response: {}
|
2015-03-16 10:29:14 +00:00
|
|
|
}),
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the current time of the animation player.
|
|
|
|
*/
|
|
|
|
setCurrentTime: method(function(currentTime) {
|
|
|
|
this.player.currentTime = currentTime;
|
|
|
|
}, {
|
|
|
|
request: {
|
|
|
|
currentTime: Arg(0, "number")
|
|
|
|
},
|
|
|
|
response: {}
|
2015-03-25 17:56:54 +00:00
|
|
|
}),
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the playback rate of the animation player.
|
|
|
|
*/
|
|
|
|
setPlaybackRate: method(function(playbackRate) {
|
|
|
|
this.player.playbackRate = playbackRate;
|
|
|
|
}, {
|
|
|
|
request: {
|
|
|
|
currentTime: Arg(0, "number")
|
|
|
|
},
|
|
|
|
response: {}
|
2014-12-11 19:08:49 +00:00
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let AnimationPlayerFront = FrontClass(AnimationPlayerActor, {
|
2014-12-22 16:56:41 +00:00
|
|
|
AUTO_REFRESH_EVENT: "updated-state",
|
|
|
|
|
2014-12-11 19:08:49 +00:00
|
|
|
initialize: function(conn, form, detail, ctx) {
|
|
|
|
Front.prototype.initialize.call(this, conn, form, detail, ctx);
|
2014-12-22 16:56:41 +00:00
|
|
|
|
|
|
|
this.state = {};
|
2014-12-11 19:08:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
form: function(form, detail) {
|
|
|
|
if (detail === "actorid") {
|
|
|
|
this.actorID = form;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._form = form;
|
2014-12-22 16:56:41 +00:00
|
|
|
this.state = this.initialState;
|
2014-12-11 19:08:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
destroy: function() {
|
2014-12-22 16:56:41 +00:00
|
|
|
this.stopAutoRefresh();
|
2014-12-11 19:08:49 +00:00
|
|
|
Front.prototype.destroy.call(this);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Getter for the initial state of the player. Up to date states can be
|
|
|
|
* retrieved by calling the getCurrentState method.
|
|
|
|
*/
|
|
|
|
get initialState() {
|
|
|
|
return {
|
|
|
|
startTime: this._form.startTime,
|
|
|
|
currentTime: this._form.currentTime,
|
|
|
|
playState: this._form.playState,
|
2015-03-25 17:56:54 +00:00
|
|
|
playbackRate: this._form.playbackRate,
|
2014-12-11 19:08:49 +00:00
|
|
|
name: this._form.name,
|
|
|
|
duration: this._form.duration,
|
2015-01-14 13:59:38 +00:00
|
|
|
delay: this._form.delay,
|
2014-12-11 19:08:49 +00:00
|
|
|
iterationCount: this._form.iterationCount,
|
2015-08-27 14:59:16 +00:00
|
|
|
isRunningOnCompositor: this._form.isRunningOnCompositor,
|
|
|
|
documentCurrentTime: this._form.documentCurrentTime
|
2015-06-11 13:45:57 +00:00
|
|
|
};
|
2014-12-22 16:56:41 +00:00
|
|
|
},
|
|
|
|
|
2015-06-11 13:45:57 +00:00
|
|
|
/**
|
|
|
|
* Executed when the AnimationPlayerActor emits a "changed" event. Used to
|
|
|
|
* update the local knowledge of the state.
|
|
|
|
*/
|
|
|
|
onChanged: protocol.preEvent("changed", function(partialState) {
|
|
|
|
let {state} = this.reconstructState(partialState);
|
|
|
|
this.state = state;
|
|
|
|
}),
|
|
|
|
|
2014-12-22 16:56:41 +00:00
|
|
|
// About auto-refresh:
|
|
|
|
//
|
|
|
|
// The AnimationPlayerFront is capable of automatically refreshing its state
|
|
|
|
// by calling the getCurrentState method at regular intervals. This allows
|
|
|
|
// consumers to update their knowledge of the player's currentTime, playState,
|
|
|
|
// ... dynamically.
|
|
|
|
//
|
|
|
|
// Calling startAutoRefresh will start the automatic refreshing of the state,
|
|
|
|
// and calling stopAutoRefresh will stop it.
|
|
|
|
// Once the automatic refresh has been started, the AnimationPlayerFront emits
|
|
|
|
// "updated-state" events everytime the state changes.
|
|
|
|
//
|
|
|
|
// Note that given the time-related nature of animations, the actual state
|
|
|
|
// changes a lot more often than "updated-state" events are emitted. This is
|
|
|
|
// to avoid making many protocol requests.
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start auto-refreshing this player's state.
|
|
|
|
* @param {Number} interval Optional auto-refresh timer interval to override
|
|
|
|
* the default value.
|
|
|
|
*/
|
|
|
|
startAutoRefresh: function(interval=PLAYER_DEFAULT_AUTO_REFRESH_TIMEOUT) {
|
|
|
|
if (this.autoRefreshTimer) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.autoRefreshTimer = setInterval(this.refreshState.bind(this), interval);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop auto-refreshing this player's state.
|
|
|
|
*/
|
|
|
|
stopAutoRefresh: function() {
|
|
|
|
if (!this.autoRefreshTimer) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
clearInterval(this.autoRefreshTimer);
|
|
|
|
this.autoRefreshTimer = null;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called automatically when auto-refresh is on. Doesn't return anything, but
|
|
|
|
* emits the "updated-state" event.
|
|
|
|
*/
|
|
|
|
refreshState: Task.async(function*() {
|
|
|
|
let data = yield this.getCurrentState();
|
|
|
|
|
|
|
|
// By the time the new state is received, auto-refresh might be stopped.
|
|
|
|
if (!this.autoRefreshTimer) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-01-16 06:39:00 +00:00
|
|
|
if (this.currentStateHasChanged) {
|
2014-12-22 16:56:41 +00:00
|
|
|
this.state = data;
|
2015-03-30 09:59:46 +00:00
|
|
|
events.emit(this, this.AUTO_REFRESH_EVENT, this.state);
|
2014-12-22 16:56:41 +00:00
|
|
|
}
|
2015-01-16 06:39:00 +00:00
|
|
|
}),
|
|
|
|
|
|
|
|
/**
|
|
|
|
* getCurrentState interceptor re-constructs incomplete states since the actor
|
|
|
|
* only sends the values that have changed.
|
|
|
|
*/
|
|
|
|
getCurrentState: protocol.custom(function() {
|
|
|
|
this.currentStateHasChanged = false;
|
2015-06-11 13:45:57 +00:00
|
|
|
return this._getCurrentState().then(partialData => {
|
|
|
|
let {state, hasChanged} = this.reconstructState(partialData);
|
|
|
|
this.currentStateHasChanged = hasChanged;
|
|
|
|
return state;
|
2015-01-16 06:39:00 +00:00
|
|
|
});
|
|
|
|
}, {
|
|
|
|
impl: "_getCurrentState"
|
|
|
|
}),
|
2015-06-11 13:45:57 +00:00
|
|
|
|
|
|
|
reconstructState: function(data) {
|
|
|
|
let hasChanged = false;
|
|
|
|
|
|
|
|
for (let key in this.state) {
|
|
|
|
if (typeof data[key] === "undefined") {
|
|
|
|
data[key] = this.state[key];
|
|
|
|
} else if (data[key] !== this.state[key]) {
|
|
|
|
hasChanged = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {state: data, hasChanged};
|
|
|
|
}
|
2014-12-11 19:08:49 +00:00
|
|
|
});
|
|
|
|
|
2015-04-02 10:47:34 +00:00
|
|
|
/**
|
|
|
|
* Sent with the 'mutations' event as part of an array of changes, used to
|
|
|
|
* inform fronts of the type of change that occured.
|
|
|
|
*/
|
|
|
|
types.addDictType("animationMutationChange", {
|
|
|
|
// The type of change ("added" or "removed").
|
|
|
|
type: "string",
|
|
|
|
// The changed AnimationPlayerActor.
|
|
|
|
player: "animationplayer"
|
|
|
|
});
|
|
|
|
|
2014-12-11 19:08:49 +00:00
|
|
|
/**
|
|
|
|
* The Animations actor lists animation players for a given node.
|
|
|
|
*/
|
|
|
|
let AnimationsActor = exports.AnimationsActor = ActorClass({
|
|
|
|
typeName: "animations",
|
|
|
|
|
2015-04-02 10:47:34 +00:00
|
|
|
events: {
|
2015-06-11 13:45:57 +00:00
|
|
|
"mutations": {
|
2015-04-02 10:47:34 +00:00
|
|
|
type: "mutations",
|
|
|
|
changes: Arg(0, "array:animationMutationChange")
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-12-11 19:08:49 +00:00
|
|
|
initialize: function(conn, tabActor) {
|
|
|
|
Actor.prototype.initialize.call(this, conn);
|
2015-02-12 15:28:42 +00:00
|
|
|
this.tabActor = tabActor;
|
|
|
|
|
2015-04-02 10:47:34 +00:00
|
|
|
this.onWillNavigate = this.onWillNavigate.bind(this);
|
2015-02-12 15:28:42 +00:00
|
|
|
this.onNavigate = this.onNavigate.bind(this);
|
2015-04-02 10:47:34 +00:00
|
|
|
this.onAnimationMutation = this.onAnimationMutation.bind(this);
|
|
|
|
|
|
|
|
this.allAnimationsPaused = false;
|
|
|
|
events.on(this.tabActor, "will-navigate", this.onWillNavigate);
|
2015-02-12 15:28:42 +00:00
|
|
|
events.on(this.tabActor, "navigate", this.onNavigate);
|
2014-12-11 19:08:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
destroy: function() {
|
|
|
|
Actor.prototype.destroy.call(this);
|
2015-04-02 10:47:34 +00:00
|
|
|
events.off(this.tabActor, "will-navigate", this.onWillNavigate);
|
2015-02-12 15:28:42 +00:00
|
|
|
events.off(this.tabActor, "navigate", this.onNavigate);
|
2015-04-02 10:47:34 +00:00
|
|
|
|
|
|
|
this.stopAnimationPlayerUpdates();
|
|
|
|
this.tabActor = this.observer = this.actors = null;
|
2014-12-11 19:08:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Since AnimationsActor doesn't have a protocol.js parent actor that takes
|
|
|
|
* care of its lifetime, implementing disconnect is required to cleanup.
|
|
|
|
*/
|
|
|
|
disconnect: function() {
|
|
|
|
this.destroy();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2015-04-21 07:22:30 +00:00
|
|
|
* Retrieve the list of AnimationPlayerActor actors for currently running
|
|
|
|
* animations on a node and its descendants.
|
|
|
|
* @param {NodeActor} nodeActor The NodeActor as defined in
|
2014-12-11 19:08:49 +00:00
|
|
|
* /toolkit/devtools/server/actors/inspector
|
|
|
|
*/
|
|
|
|
getAnimationPlayersForNode: method(function(nodeActor) {
|
2015-04-21 07:22:30 +00:00
|
|
|
let animations = [
|
|
|
|
...nodeActor.rawNode.getAnimations(),
|
|
|
|
...this.getAllAnimations(nodeActor.rawNode)
|
|
|
|
];
|
2014-12-11 19:08:49 +00:00
|
|
|
|
2015-04-02 10:47:34 +00:00
|
|
|
// No care is taken here to destroy the previously stored actors because it
|
|
|
|
// is assumed that the client is responsible for lifetimes of actors.
|
|
|
|
this.actors = [];
|
2015-06-11 13:45:57 +00:00
|
|
|
for (let i = 0; i < animations.length; i++) {
|
2014-12-11 19:08:49 +00:00
|
|
|
// XXX: for now the index is passed along as the AnimationPlayerActor uses
|
|
|
|
// it to retrieve animation information from CSS.
|
2015-04-02 10:47:34 +00:00
|
|
|
let actor = AnimationPlayerActor(this, animations[i], i);
|
|
|
|
this.actors.push(actor);
|
2014-12-11 19:08:49 +00:00
|
|
|
}
|
|
|
|
|
2015-04-02 10:47:34 +00:00
|
|
|
// When a front requests the list of players for a node, start listening
|
|
|
|
// for animation mutations on this node to send updates to the front, until
|
|
|
|
// either getAnimationPlayersForNode is called again or
|
|
|
|
// stopAnimationPlayerUpdates is called.
|
|
|
|
this.stopAnimationPlayerUpdates();
|
|
|
|
let win = nodeActor.rawNode.ownerDocument.defaultView;
|
|
|
|
this.observer = new win.MutationObserver(this.onAnimationMutation);
|
2015-04-21 07:22:30 +00:00
|
|
|
this.observer.observe(nodeActor.rawNode, {
|
|
|
|
animations: true,
|
|
|
|
subtree: true
|
|
|
|
});
|
2015-04-02 10:47:34 +00:00
|
|
|
|
|
|
|
return this.actors;
|
2014-12-11 19:08:49 +00:00
|
|
|
}, {
|
|
|
|
request: {
|
|
|
|
actorID: Arg(0, "domnode")
|
|
|
|
},
|
|
|
|
response: {
|
|
|
|
players: RetVal("array:animationplayer")
|
|
|
|
}
|
2015-02-12 15:28:42 +00:00
|
|
|
}),
|
|
|
|
|
2015-04-02 10:47:34 +00:00
|
|
|
onAnimationMutation: function(mutations) {
|
|
|
|
let eventData = [];
|
2015-08-27 14:59:16 +00:00
|
|
|
let readyPromises = [];
|
2015-04-02 10:47:34 +00:00
|
|
|
|
2015-06-11 13:45:57 +00:00
|
|
|
for (let {addedAnimations, removedAnimations} of mutations) {
|
2015-04-02 10:47:34 +00:00
|
|
|
for (let player of removedAnimations) {
|
|
|
|
// Note that animations are reported as removed either when they are
|
|
|
|
// actually removed from the node (e.g. css class removed) or when they
|
|
|
|
// are finished and don't have forwards animation-fill-mode.
|
|
|
|
// In the latter case, we don't send an event, because the corresponding
|
|
|
|
// animation can still be seeked/resumed, so we want the client to keep
|
|
|
|
// its reference to the AnimationPlayerActor.
|
|
|
|
if (player.playState !== "idle") {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let index = this.actors.findIndex(a => a.player === player);
|
2015-08-24 16:22:15 +00:00
|
|
|
if (index !== -1) {
|
|
|
|
eventData.push({
|
|
|
|
type: "removed",
|
|
|
|
player: this.actors[index]
|
|
|
|
});
|
|
|
|
this.actors.splice(index, 1);
|
|
|
|
}
|
2015-04-02 10:47:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for (let player of addedAnimations) {
|
|
|
|
// If the added player already exists, it means we previously filtered
|
|
|
|
// it out when it was reported as removed. So filter it out here too.
|
|
|
|
if (this.actors.find(a => a.player === player)) {
|
|
|
|
continue;
|
|
|
|
}
|
2015-04-16 08:20:21 +00:00
|
|
|
// If the added player has the same name and target node as a player we
|
|
|
|
// already have, it means it's a transition that's re-starting. So send
|
|
|
|
// a "removed" event for the one we already have.
|
|
|
|
let index = this.actors.findIndex(a => {
|
2015-07-01 05:50:49 +00:00
|
|
|
return a.player.constructor === player.constructor &&
|
|
|
|
((a.isAnimation() &&
|
|
|
|
a.player.animationName === player.animationName) ||
|
|
|
|
(a.isTransition() &&
|
|
|
|
a.player.transitionProperty === player.transitionProperty));
|
2015-04-16 08:20:21 +00:00
|
|
|
});
|
|
|
|
if (index !== -1) {
|
|
|
|
eventData.push({
|
|
|
|
type: "removed",
|
|
|
|
player: this.actors[index]
|
|
|
|
});
|
|
|
|
this.actors.splice(index, 1);
|
|
|
|
}
|
|
|
|
|
2015-04-02 10:47:34 +00:00
|
|
|
let actor = AnimationPlayerActor(
|
2015-04-14 23:48:21 +00:00
|
|
|
this, player, player.effect.target.getAnimations().indexOf(player));
|
2015-04-02 10:47:34 +00:00
|
|
|
this.actors.push(actor);
|
|
|
|
eventData.push({
|
|
|
|
type: "added",
|
|
|
|
player: actor
|
|
|
|
});
|
2015-08-27 14:59:16 +00:00
|
|
|
readyPromises.push(player.ready);
|
2015-04-02 10:47:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (eventData.length) {
|
2015-08-27 14:59:16 +00:00
|
|
|
// Let's wait for all added animations to be ready before telling the
|
|
|
|
// front-end.
|
|
|
|
Promise.all(readyPromises).then(() => {
|
|
|
|
events.emit(this, "mutations", eventData);
|
|
|
|
});
|
2015-04-02 10:47:34 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2015-06-11 13:45:57 +00:00
|
|
|
* After the client has called getAnimationPlayersForNode for a given DOM
|
|
|
|
* node, the actor starts sending animation mutations for this node. If the
|
|
|
|
* client doesn't want this to happen anymore, it should call this method.
|
2015-04-02 10:47:34 +00:00
|
|
|
*/
|
|
|
|
stopAnimationPlayerUpdates: method(function() {
|
|
|
|
if (this.observer && !Cu.isDeadWrapper(this.observer)) {
|
|
|
|
this.observer.disconnect();
|
|
|
|
}
|
|
|
|
}, {
|
|
|
|
request: {},
|
|
|
|
response: {}
|
|
|
|
}),
|
|
|
|
|
2015-02-12 15:28:42 +00:00
|
|
|
/**
|
2015-04-21 07:22:30 +00:00
|
|
|
* Iterates through all nodes below a given rootNode (optionally also in
|
|
|
|
* nested frames) and finds all existing animation players.
|
|
|
|
* @param {DOMNode} rootNode The root node to start iterating at. Animation
|
|
|
|
* players will *not* be reported for this node.
|
|
|
|
* @param {Boolean} traverseFrames Whether we should iterate through nested
|
|
|
|
* frames too.
|
|
|
|
* @return {Array} An array of AnimationPlayer objects.
|
2015-02-12 15:28:42 +00:00
|
|
|
*/
|
2015-04-21 07:22:30 +00:00
|
|
|
getAllAnimations: function(rootNode, traverseFrames) {
|
2015-03-20 18:20:55 +00:00
|
|
|
let animations = [];
|
2015-02-12 15:28:42 +00:00
|
|
|
|
|
|
|
// These loops shouldn't be as bad as they look.
|
2015-04-21 07:22:30 +00:00
|
|
|
// Typically, there will be very few nested frames, and getElementsByTagName
|
|
|
|
// is really fast even on large DOM trees.
|
|
|
|
for (let element of rootNode.getElementsByTagNameNS("*", "*")) {
|
|
|
|
if (traverseFrames && element.contentWindow) {
|
|
|
|
animations = [
|
|
|
|
...animations,
|
|
|
|
...this.getAllAnimations(element.contentWindow.document, traverseFrames)
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
animations = [
|
|
|
|
...animations,
|
|
|
|
...element.getAnimations()
|
|
|
|
];
|
2015-02-12 15:28:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-20 18:20:55 +00:00
|
|
|
return animations;
|
2015-02-12 15:28:42 +00:00
|
|
|
},
|
|
|
|
|
2015-04-02 10:47:34 +00:00
|
|
|
onWillNavigate: function({isTopLevel}) {
|
|
|
|
if (isTopLevel) {
|
|
|
|
this.stopAnimationPlayerUpdates();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-02-12 15:28:42 +00:00
|
|
|
onNavigate: function({isTopLevel}) {
|
|
|
|
if (isTopLevel) {
|
|
|
|
this.allAnimationsPaused = false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pause all animations in the current tabActor's frames.
|
|
|
|
*/
|
|
|
|
pauseAll: method(function() {
|
2015-03-24 00:21:08 +00:00
|
|
|
let readyPromises = [];
|
2015-04-21 07:22:30 +00:00
|
|
|
// Until the WebAnimations API provides a way to play/pause via the document
|
|
|
|
// timeline, we have to iterate through the whole DOM to find all players.
|
|
|
|
for (let player of
|
|
|
|
this.getAllAnimations(this.tabActor.window.document, true)) {
|
2015-02-12 15:28:42 +00:00
|
|
|
player.pause();
|
2015-03-24 00:21:08 +00:00
|
|
|
readyPromises.push(player.ready);
|
2015-02-12 15:28:42 +00:00
|
|
|
}
|
|
|
|
this.allAnimationsPaused = true;
|
2015-03-24 00:21:08 +00:00
|
|
|
return promise.all(readyPromises);
|
2015-02-12 15:28:42 +00:00
|
|
|
}, {
|
|
|
|
request: {},
|
|
|
|
response: {}
|
|
|
|
}),
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Play all animations in the current tabActor's frames.
|
2015-06-11 13:45:57 +00:00
|
|
|
* This method only returns when animations have left their pending states.
|
2015-02-12 15:28:42 +00:00
|
|
|
*/
|
|
|
|
playAll: method(function() {
|
|
|
|
let readyPromises = [];
|
2015-04-21 07:22:30 +00:00
|
|
|
// Until the WebAnimations API provides a way to play/pause via the document
|
|
|
|
// timeline, we have to iterate through the whole DOM to find all players.
|
|
|
|
for (let player of
|
|
|
|
this.getAllAnimations(this.tabActor.window.document, true)) {
|
2015-02-12 15:28:42 +00:00
|
|
|
player.play();
|
|
|
|
readyPromises.push(player.ready);
|
|
|
|
}
|
|
|
|
this.allAnimationsPaused = false;
|
|
|
|
return promise.all(readyPromises);
|
|
|
|
}, {
|
|
|
|
request: {},
|
|
|
|
response: {}
|
|
|
|
}),
|
|
|
|
|
|
|
|
toggleAll: method(function() {
|
|
|
|
if (this.allAnimationsPaused) {
|
|
|
|
return this.playAll();
|
|
|
|
}
|
2015-06-11 13:45:57 +00:00
|
|
|
return this.pauseAll();
|
2015-02-12 15:28:42 +00:00
|
|
|
}, {
|
|
|
|
request: {},
|
|
|
|
response: {}
|
2015-08-24 16:22:15 +00:00
|
|
|
}),
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the current time of several animations at the same time.
|
|
|
|
* @param {Array} players A list of AnimationPlayerActor.
|
|
|
|
* @param {Number} time The new currentTime.
|
|
|
|
* @param {Boolean} shouldPause Should the players be paused too.
|
|
|
|
*/
|
|
|
|
setCurrentTimes: method(function(players, time, shouldPause) {
|
|
|
|
return promise.all(players.map(player => {
|
|
|
|
let pause = shouldPause ? player.pause() : promise.resolve();
|
|
|
|
return pause.then(() => player.setCurrentTime(time));
|
|
|
|
}));
|
|
|
|
}, {
|
|
|
|
request: {
|
|
|
|
players: Arg(0, "array:animationplayer"),
|
|
|
|
time: Arg(1, "number"),
|
|
|
|
shouldPause: Arg(2, "boolean")
|
|
|
|
},
|
|
|
|
response: {}
|
2014-12-11 19:08:49 +00:00
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let AnimationsFront = exports.AnimationsFront = FrontClass(AnimationsActor, {
|
|
|
|
initialize: function(client, {animationsActor}) {
|
|
|
|
Front.prototype.initialize.call(this, client, {actor: animationsActor});
|
|
|
|
this.manage(this);
|
|
|
|
},
|
|
|
|
|
|
|
|
destroy: function() {
|
|
|
|
Front.prototype.destroy.call(this);
|
|
|
|
}
|
|
|
|
});
|