summaryrefslogtreecommitdiffstats
path: root/devtools/client/animationinspector/animation-controller.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/animationinspector/animation-controller.js')
-rw-r--r--devtools/client/animationinspector/animation-controller.js390
1 files changed, 390 insertions, 0 deletions
diff --git a/devtools/client/animationinspector/animation-controller.js b/devtools/client/animationinspector/animation-controller.js
new file mode 100644
index 000000000..03c6e0e95
--- /dev/null
+++ b/devtools/client/animationinspector/animation-controller.js
@@ -0,0 +1,390 @@
+/* -*- 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" },
+ ];
+
+ 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);