gecko-dev/devtools/client/animationinspector/animation-controller.js

393 строки
13 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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/. */
/* animation-panel.js is loaded in the same scope but we don't use
import-globals-from to avoid infinite loops since animation-panel.js already
imports globals from animation-controller.js */
/* globals AnimationsPanel */
/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
"use strict";
var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
var { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
var { Task } = require("devtools/shared/task");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
loader.lazyRequireGetter(this, "AnimationsFront", "devtools/shared/fronts/animation", true);
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
// Global toolbox/inspector, set when startup is called.
var gToolbox, gInspector;
/**
* Startup the animationinspector controller and view, called by the sidebar
* widget when loading/unloading the iframe into the tab.
*/
var startup = Task.async(function* (inspector) {
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");
}
// 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.
*/
var shutdown = Task.async(function* () {
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).catch(e => console.error(e));
}
function destroy() {
return shutdown().catch(e => console.error(e));
}
/**
* Get all the server-side capabilities (traits) so the UI knows whether or not
* features should be enabled/disabled.
* @param {Target} target The current toolbox target.
* @return {Object} An object with boolean properties.
*/
var getServerTraits = Task.async(function* (target) {
let config = [
{ name: "hasToggleAll", actor: "animations",
method: "toggleAll" },
{ name: "hasToggleSeveral", actor: "animations",
method: "toggleSeveral" },
{ name: "hasSetCurrentTime", actor: "animationplayer",
method: "setCurrentTime" },
{ name: "hasMutationEvents", actor: "animations",
method: "stopAnimationPlayerUpdates" },
{ name: "hasSetPlaybackRate", actor: "animationplayer",
method: "setPlaybackRate" },
{ name: "hasSetPlaybackRates", actor: "animations",
method: "setPlaybackRates" },
{ name: "hasTargetNode", actor: "domwalker",
method: "getNodeFromActor" },
{ name: "hasSetCurrentTimes", actor: "animations",
method: "setCurrentTimes" },
{ name: "hasGetFrames", actor: "animationplayer",
method: "getFrames" },
{ name: "hasGetProperties", actor: "animationplayer",
method: "getProperties" },
{ name: "hasSetWalkerActor", actor: "animations",
method: "setWalkerActor" },
{ name: "hasGetAnimationTypes", actor: "animationplayer",
method: "getAnimationTypes" },
];
let traits = {};
for (let {name, actor, method} of config) {
traits[name] = yield target.actorHasMethod(actor, method);
}
return traits;
});
/**
* 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.
*
* AnimationPlayerFronts are available in AnimationsController.animationPlayers.
*
* Usage example:
*
* AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
* onPlayers);
* function onPlayers() {
* for (let player of AnimationsController.animationPlayers) {
* // do something with player
* }
* }
*/
var AnimationsController = {
PLAYERS_UPDATED_EVENT: "players-updated",
ALL_ANIMATIONS_TOGGLED_EVENT: "all-animations-toggled",
initialize: Task.async(function* () {
if (this.initialized) {
yield this.initialized;
return;
}
let resolver;
this.initialized = new Promise(resolve => {
resolver = resolve;
});
this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
this.onNewNodeFront = this.onNewNodeFront.bind(this);
this.onAnimationMutations = this.onAnimationMutations.bind(this);
let target = gInspector.target;
this.animationsFront = new AnimationsFront(target.client, target.form);
// Expose actor capabilities.
this.traits = yield getServerTraits(target);
if (this.destroyed) {
console.warn("Could not fully initialize the AnimationsController");
return;
}
// Let the AnimationsActor know what WalkerActor we're using. This will
// come in handy later to return references to DOM Nodes.
if (this.traits.hasSetWalkerActor) {
yield this.animationsFront.setWalkerActor(gInspector.walker);
}
this.startListeners();
yield this.onNewNodeFront();
resolver();
}),
destroy: Task.async(function* () {
if (!this.initialized) {
return;
}
if (this.destroyed) {
yield this.destroyed;
return;
}
let resolver;
this.destroyed = new Promise(resolve => {
resolver = resolve;
});
this.stopListeners();
this.destroyAnimationPlayers();
this.nodeFront = null;
if (this.animationsFront) {
this.animationsFront.destroy();
this.animationsFront = null;
}
resolver();
}),
startListeners: function () {
// Re-create the list of players when a new node is selected, except if the
// sidebar isn't visible.
gInspector.selection.on("new-node-front", this.onNewNodeFront);
gInspector.sidebar.on("select", this.onPanelVisibilityChange);
gToolbox.on("select", this.onPanelVisibilityChange);
},
stopListeners: function () {
gInspector.selection.off("new-node-front", this.onNewNodeFront);
gInspector.sidebar.off("select", this.onPanelVisibilityChange);
gToolbox.off("select", this.onPanelVisibilityChange);
if (this.isListeningToMutations) {
this.animationsFront.off("mutations", this.onAnimationMutations);
}
},
isPanelVisible: function () {
return gToolbox.currentToolId === "inspector" &&
gInspector.sidebar &&
gInspector.sidebar.getCurrentTabID() == "animationinspector";
},
onPanelVisibilityChange: Task.async(function* () {
if (this.isPanelVisible()) {
this.onNewNodeFront();
}
}),
onNewNodeFront: Task.async(function* () {
// Ignore if the panel isn't visible or the node selection hasn't changed.
if (!this.isPanelVisible() ||
this.nodeFront === gInspector.selection.nodeFront) {
return;
}
this.nodeFront = gInspector.selection.nodeFront;
let done = gInspector.updating("animationscontroller");
if (!gInspector.selection.isConnected() ||
!gInspector.selection.isElementNode()) {
this.destroyAnimationPlayers();
this.emit(this.PLAYERS_UPDATED_EVENT);
done();
return;
}
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.traits.hasToggleAll) {
return promise.resolve();
}
return this.animationsFront.toggleAll()
.then(() => this.emit(this.ALL_ANIMATIONS_TOGGLED_EVENT, this))
.catch(e => console.error(e));
},
/**
* Similar to toggleAll except that it only plays/pauses the currently known
* animations (those listed in this.animationPlayers).
* @param {Boolean} shouldPause True if the animations should be paused, false
* if they should be played.
* @return {Promise} Resolves when the playState has been changed.
*/
toggleCurrentAnimations: Task.async(function* (shouldPause) {
if (this.traits.hasToggleSeveral) {
yield this.animationsFront.toggleSeveral(this.animationPlayers,
shouldPause);
} else {
// Fall back to pausing/playing the players one by one, which is bound to
// introduce some de-synchronization.
for (let player of this.animationPlayers) {
if (shouldPause) {
yield player.pause();
} else {
yield player.play();
}
}
}
}),
/**
* Set all known animations' currentTimes to the provided time.
* @param {Number} time.
* @param {Boolean} shouldPause Should the animations be paused too.
* @return {Promise} Resolves when the current time has been set.
*/
setCurrentTimeAll: Task.async(function* (time, shouldPause) {
if (this.traits.hasSetCurrentTimes) {
yield this.animationsFront.setCurrentTimes(this.animationPlayers, time,
shouldPause);
} else {
// Fall back to pausing and setting the current time on each player, one
// by one, which is bound to introduce some de-synchronization.
for (let animation of this.animationPlayers) {
if (shouldPause) {
yield animation.pause();
}
yield animation.setCurrentTime(time);
}
}
}),
/**
* Set all known animations' playback rates to the provided rate.
* @param {Number} rate.
* @return {Promise} Resolves when the rate has been set.
*/
setPlaybackRateAll: Task.async(function* (rate) {
if (this.traits.hasSetPlaybackRates) {
// If the backend can set all playback rates at the same time, use that.
yield this.animationsFront.setPlaybackRates(this.animationPlayers, rate);
} else if (this.traits.hasSetPlaybackRate) {
// Otherwise, fall back to setting each rate individually.
for (let animation of this.animationPlayers) {
yield animation.setPlaybackRate(rate);
}
}
}),
// 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) {
this.destroyAnimationPlayers();
this.animationPlayers = yield this.animationsFront
.getAnimationPlayersForNode(nodeFront);
// Start listening for animation mutations only after the first method call
// otherwise events won't be sent.
if (!this.isListeningToMutations && this.traits.hasMutationEvents) {
this.animationsFront.on("mutations", this.onAnimationMutations);
this.isListeningToMutations = true;
}
}),
onAnimationMutations: function (changes) {
// Insert new players into this.animationPlayers when new animations are
// added.
for (let {type, player} of changes) {
if (type === "added") {
this.animationPlayers.push(player);
}
if (type === "removed") {
let index = this.animationPlayers.indexOf(player);
this.animationPlayers.splice(index, 1);
}
}
// Let the UI know the list has been updated.
this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
},
/**
* Get the latest known current time of document.timeline.
* This value is sent along with all AnimationPlayerActors' states, but it
* isn't updated after that, so this function loops over all know animations
* to find the highest value.
* @return {Number|Boolean} False is returned if this server version doesn't
* provide document's current time.
*/
get documentCurrentTime() {
let time = 0;
for (let {state} of this.animationPlayers) {
if (!state.documentCurrentTime) {
return false;
}
time = Math.max(time, state.documentCurrentTime);
}
return time;
},
destroyAnimationPlayers: function () {
this.animationPlayers = [];
}
};
EventEmitter.decorate(AnimationsController);