123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
- /* 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);
|