Bug 1120833 - 2 - Fire events about changed animations in the AnimationsActor; r=past

This makes the AnimationsActor listen for animation mutations after each
call to getAnimationPlayersForNode on the code that was passed in.
Whenever animations are added or removed, an event is sent to the front
with the list of changes.
The server doesn't know when the client doesn't require updates for the
current node anymore, so it exposes a new method for this.
Note that removed events for finished aniations are skipped, because the
corresponding animations can still be seeked/resumed, so we want the
AnimationPlayerActor to be preserved.
This commit is contained in:
Patrick Brosset 2015-04-02 12:47:34 +02:00
parent de3a3b55d3
commit e01d437121
5 changed files with 250 additions and 10 deletions

View File

@ -104,6 +104,8 @@ let AnimationsController = {
this.hasToggleAll = yield target.actorHasMethod("animations", "toggleAll");
this.hasSetCurrentTime = yield target.actorHasMethod("animationplayer",
"setCurrentTime");
this.hasMutationEvents = yield target.actorHasMethod("animations",
"stopAnimationPlayerUpdates");
this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
this.onNewNodeFront = this.onNewNodeFront.bind(this);
@ -226,6 +228,12 @@ let AnimationsController = {
},
destroyAnimationPlayers: Task.async(function*() {
// Let the server know that we're not interested in receiving updates about
// players for the current node. We're either being destroyed or a new node
// has been selected.
if (this.hasMutationEvents) {
yield this.animationsFront.stopAnimationPlayerUpdates();
}
this.stopAllAutoRefresh();
for (let front of this.animationPlayers) {
yield front.release();

View File

@ -29,7 +29,7 @@ const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
const {setInterval, clearInterval} = require("sdk/timers");
const protocol = require("devtools/server/protocol");
const {ActorClass, Actor, FrontClass, Front, Arg, method, RetVal} = protocol;
const {ActorClass, Actor, FrontClass, Front, Arg, method, RetVal, types} = protocol;
const {NodeActor} = require("devtools/server/actors/inspector");
const events = require("sdk/event/core");
@ -50,19 +50,18 @@ let AnimationPlayerActor = ActorClass({
/**
* @param {AnimationsActor} The main AnimationsActor instance
* @param {AnimationPlayer} The player object returned by getAnimationPlayers
* @param {DOMNode} The node targeted by this player
* @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.
*/
initialize: function(animationsActor, player, node, playerIndex) {
initialize: function(animationsActor, player, playerIndex) {
Actor.prototype.initialize.call(this, animationsActor.conn);
this.player = player;
this.node = node;
this.node = player.source.target;
this.playerIndex = playerIndex;
this.styles = node.ownerDocument.defaultView.getComputedStyle(node);
this.styles = this.node.ownerDocument.defaultView.getComputedStyle(this.node);
},
destroy: function() {
@ -437,25 +436,50 @@ let AnimationPlayerFront = FrontClass(AnimationPlayerActor, {
}),
});
/**
* 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"
});
/**
* The Animations actor lists animation players for a given node.
*/
let AnimationsActor = exports.AnimationsActor = ActorClass({
typeName: "animations",
events: {
"mutations" : {
type: "mutations",
changes: Arg(0, "array:animationMutationChange")
}
},
initialize: function(conn, tabActor) {
Actor.prototype.initialize.call(this, conn);
this.tabActor = tabActor;
this.allAnimationsPaused = false;
this.onWillNavigate = this.onWillNavigate.bind(this);
this.onNavigate = this.onNavigate.bind(this);
this.onAnimationMutation = this.onAnimationMutation.bind(this);
this.allAnimationsPaused = false;
events.on(this.tabActor, "will-navigate", this.onWillNavigate);
events.on(this.tabActor, "navigate", this.onNavigate);
},
destroy: function() {
Actor.prototype.destroy.call(this);
events.off(this.tabActor, "will-navigate", this.onWillNavigate);
events.off(this.tabActor, "navigate", this.onNavigate);
this.tabActor = null;
this.stopAnimationPlayerUpdates();
this.tabActor = this.observer = this.actors = null;
},
/**
@ -475,14 +499,26 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
getAnimationPlayersForNode: method(function(nodeActor) {
let animations = nodeActor.rawNode.getAnimations();
let actors = [];
// 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 = [];
for (let i = 0; i < animations.length; i ++) {
// XXX: for now the index is passed along as the AnimationPlayerActor uses
// it to retrieve animation information from CSS.
actors.push(AnimationPlayerActor(this, animations[i], nodeActor.rawNode, i));
let actor = AnimationPlayerActor(this, animations[i], i);
this.actors.push(actor);
}
return actors;
// 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);
this.observer.observe(nodeActor.rawNode, {animations: true});
return this.actors;
}, {
request: {
actorID: Arg(0, "domnode")
@ -492,6 +528,63 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
}
}),
onAnimationMutation: function(mutations) {
let eventData = [];
for (let {addedAnimations, changedAnimations, removedAnimations} of mutations) {
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);
eventData.push({
type: "removed",
player: this.actors[index]
});
this.actors.splice(index, 1);
}
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;
}
let actor = AnimationPlayerActor(
this, player, player.source.target.getAnimations().indexOf(player));
this.actors.push(actor);
eventData.push({
type: "added",
player: actor
});
}
}
if (eventData.length) {
events.emit(this, "mutations", eventData);
}
},
/**
* 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.
*/
stopAnimationPlayerUpdates: method(function() {
if (this.observer && !Cu.isDeadWrapper(this.observer)) {
this.observer.disconnect();
}
}, {
request: {},
response: {}
}),
/**
* Iterates through all nodes in all of the tabActor's window documents and
* finds all existing animation players.
@ -516,6 +609,12 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
return animations;
},
onWillNavigate: function({isTopLevel}) {
if (isTopLevel) {
this.stopAnimationPlayerUpdates();
}
},
onNavigate: function({isTopLevel}) {
if (isTopLevel) {
this.allAnimationsPaused = false;

View File

@ -28,6 +28,8 @@ support-files =
[browser_animation_actors_10.js]
[browser_animation_actors_11.js]
[browser_animation_actors_12.js]
[browser_animation_actors_13.js]
[browser_animation_actors_14.js]
[browser_canvasframe_helper_01.js]
[browser_canvasframe_helper_02.js]
[browser_canvasframe_helper_03.js]

View File

@ -0,0 +1,71 @@
/* vim: set ft=javascript 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 AnimationsActor emits events about changed animations on a
// node after getAnimationPlayersForNode was called on that node.
const {AnimationsFront} = require("devtools/server/actors/animation");
const {InspectorFront} = require("devtools/server/actors/inspector");
add_task(function*() {
let doc = yield addTab(MAIN_DOMAIN + "animation.html");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let inspector = InspectorFront(client, form);
let walker = yield inspector.getWalker();
let animations = AnimationsFront(client, form);
info("Retrieve a non-animated node");
let node = yield walker.querySelector(walker.rootNode, ".not-animated");
info("Retrieve the animation player for the node");
let players = yield animations.getAnimationPlayersForNode(node);
is(players.length, 0, "The node has no animation players");
info("Listen for new animations");
let onMutations = once(animations, "mutations");
info("Add a couple of animation on the node");
yield node.modifyAttributes([
{attributeName: "class", newValue: "multiple-animations"}
]);
let changes = yield onMutations;
ok(true, "The mutations event was emitted");
is(changes.length, 2, "There are 2 changes in the mutation event");
ok(changes.every(({type}) => type === "added"), "Both changes are additions");
is(changes[0].player.initialState.name, "move",
"The first added animation is 'move'");
is(changes[1].player.initialState.name, "glow",
"The first added animation is 'glow'");
info("Store the 2 new players for comparing later");
let p1 = changes[0].player;
let p2 = changes[1].player;
info("Listen for removed animations");
onMutations = once(animations, "mutations");
info("Remove the animation css class on the node");
yield node.modifyAttributes([
{attributeName: "class", newValue: "not-animated"}
]);
changes = yield onMutations;
ok(true, "The mutations event was emitted");
is(changes.length, 2, "There are 2 changes in the mutation event");
ok(changes.every(({type}) => type === "removed"), "Both changes are removals");
ok(changes[0].player === p1 || changes[0].player === p2,
"The first removed player was one of the previously added players");
ok(changes[1].player === p1 || changes[1].player === p2,
"The second removed player was one of the previously added players");
yield closeDebuggerClient(client);
gBrowser.removeCurrentTab();
});

View File

@ -0,0 +1,60 @@
/* vim: set ft=javascript 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 AnimationsActor doesn't report finished animations as removed.
// Indeed, animations that only have the "finished" playState can be modified
// still, so we want the AnimationsActor to preserve the corresponding
// AnimationPlayerActor.
const {AnimationsFront} = require("devtools/server/actors/animation");
const {InspectorFront} = require("devtools/server/actors/inspector");
add_task(function*() {
let doc = yield addTab(MAIN_DOMAIN + "animation.html");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let inspector = InspectorFront(client, form);
let walker = yield inspector.getWalker();
let animations = AnimationsFront(client, form);
info("Retrieve a non-animated node");
let node = yield walker.querySelector(walker.rootNode, ".not-animated");
info("Retrieve the animation player for the node");
let players = yield animations.getAnimationPlayersForNode(node);
is(players.length, 0, "The node has no animation players");
info("Listen for new animations");
let reportedMutations = [];
function onMutations(mutations) {
reportedMutations = [...reportedMutations, ...mutations];
}
animations.on("mutations", onMutations);
info("Add a short animation on the node");
yield node.modifyAttributes([
{attributeName: "class", newValue: "short-animation"}
]);
info("Wait for longer than the animation's duration");
yield wait(2000);
is(reportedMutations.length, 1, "Only one mutation was reported");
is(reportedMutations[0].type, "added", "The mutation was an addition");
animations.off("mutations", onMutations);
yield closeDebuggerClient(client);
gBrowser.removeCurrentTab();
});
function wait(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}