animation-controller.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. /* animation-panel.js is loaded in the same scope but we don't use
  6. import-globals-from to avoid infinite loops since animation-panel.js already
  7. imports globals from animation-controller.js */
  8. /* globals AnimationsPanel */
  9. /* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
  10. "use strict";
  11. var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
  12. var { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
  13. var { Task } = require("devtools/shared/task");
  14. loader.lazyRequireGetter(this, "promise");
  15. loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
  16. loader.lazyRequireGetter(this, "AnimationsFront", "devtools/shared/fronts/animation", true);
  17. const { LocalizationHelper } = require("devtools/shared/l10n");
  18. const L10N =
  19. new LocalizationHelper("devtools/client/locales/animationinspector.properties");
  20. // Global toolbox/inspector, set when startup is called.
  21. var gToolbox, gInspector;
  22. /**
  23. * Startup the animationinspector controller and view, called by the sidebar
  24. * widget when loading/unloading the iframe into the tab.
  25. */
  26. var startup = Task.async(function* (inspector) {
  27. gInspector = inspector;
  28. gToolbox = inspector.toolbox;
  29. // Don't assume that AnimationsPanel is defined here, it's in another file.
  30. if (!typeof AnimationsPanel === "undefined") {
  31. throw new Error("AnimationsPanel was not loaded in the " +
  32. "animationinspector window");
  33. }
  34. // Startup first initalizes the controller and then the panel, in sequence.
  35. // If you want to know when everything's ready, do:
  36. // AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED)
  37. yield AnimationsController.initialize();
  38. yield AnimationsPanel.initialize();
  39. });
  40. /**
  41. * Shutdown the animationinspector controller and view, called by the sidebar
  42. * widget when loading/unloading the iframe into the tab.
  43. */
  44. var shutdown = Task.async(function* () {
  45. yield AnimationsController.destroy();
  46. // Don't assume that AnimationsPanel is defined here, it's in another file.
  47. if (typeof AnimationsPanel !== "undefined") {
  48. yield AnimationsPanel.destroy();
  49. }
  50. gToolbox = gInspector = null;
  51. });
  52. // This is what makes the sidebar widget able to load/unload the panel.
  53. function setPanel(panel) {
  54. return startup(panel).catch(e => console.error(e));
  55. }
  56. function destroy() {
  57. return shutdown().catch(e => console.error(e));
  58. }
  59. /**
  60. * Get all the server-side capabilities (traits) so the UI knows whether or not
  61. * features should be enabled/disabled.
  62. * @param {Target} target The current toolbox target.
  63. * @return {Object} An object with boolean properties.
  64. */
  65. var getServerTraits = Task.async(function* (target) {
  66. let config = [
  67. { name: "hasToggleAll", actor: "animations",
  68. method: "toggleAll" },
  69. { name: "hasToggleSeveral", actor: "animations",
  70. method: "toggleSeveral" },
  71. { name: "hasSetCurrentTime", actor: "animationplayer",
  72. method: "setCurrentTime" },
  73. { name: "hasMutationEvents", actor: "animations",
  74. method: "stopAnimationPlayerUpdates" },
  75. { name: "hasSetPlaybackRate", actor: "animationplayer",
  76. method: "setPlaybackRate" },
  77. { name: "hasSetPlaybackRates", actor: "animations",
  78. method: "setPlaybackRates" },
  79. { name: "hasTargetNode", actor: "domwalker",
  80. method: "getNodeFromActor" },
  81. { name: "hasSetCurrentTimes", actor: "animations",
  82. method: "setCurrentTimes" },
  83. { name: "hasGetFrames", actor: "animationplayer",
  84. method: "getFrames" },
  85. { name: "hasGetProperties", actor: "animationplayer",
  86. method: "getProperties" },
  87. { name: "hasSetWalkerActor", actor: "animations",
  88. method: "setWalkerActor" },
  89. ];
  90. let traits = {};
  91. for (let {name, actor, method} of config) {
  92. traits[name] = yield target.actorHasMethod(actor, method);
  93. }
  94. return traits;
  95. });
  96. /**
  97. * The animationinspector controller's job is to retrieve AnimationPlayerFronts
  98. * from the server. It is also responsible for keeping the list of players up to
  99. * date when the node selection changes in the inspector, as well as making sure
  100. * no updates are done when the animationinspector sidebar panel is not visible.
  101. *
  102. * AnimationPlayerFronts are available in AnimationsController.animationPlayers.
  103. *
  104. * Usage example:
  105. *
  106. * AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
  107. * onPlayers);
  108. * function onPlayers() {
  109. * for (let player of AnimationsController.animationPlayers) {
  110. * // do something with player
  111. * }
  112. * }
  113. */
  114. var AnimationsController = {
  115. PLAYERS_UPDATED_EVENT: "players-updated",
  116. ALL_ANIMATIONS_TOGGLED_EVENT: "all-animations-toggled",
  117. initialize: Task.async(function* () {
  118. if (this.initialized) {
  119. yield this.initialized;
  120. return;
  121. }
  122. let resolver;
  123. this.initialized = new Promise(resolve => {
  124. resolver = resolve;
  125. });
  126. this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
  127. this.onNewNodeFront = this.onNewNodeFront.bind(this);
  128. this.onAnimationMutations = this.onAnimationMutations.bind(this);
  129. let target = gInspector.target;
  130. this.animationsFront = new AnimationsFront(target.client, target.form);
  131. // Expose actor capabilities.
  132. this.traits = yield getServerTraits(target);
  133. if (this.destroyed) {
  134. console.warn("Could not fully initialize the AnimationsController");
  135. return;
  136. }
  137. // Let the AnimationsActor know what WalkerActor we're using. This will
  138. // come in handy later to return references to DOM Nodes.
  139. if (this.traits.hasSetWalkerActor) {
  140. yield this.animationsFront.setWalkerActor(gInspector.walker);
  141. }
  142. this.startListeners();
  143. yield this.onNewNodeFront();
  144. resolver();
  145. }),
  146. destroy: Task.async(function* () {
  147. if (!this.initialized) {
  148. return;
  149. }
  150. if (this.destroyed) {
  151. yield this.destroyed;
  152. return;
  153. }
  154. let resolver;
  155. this.destroyed = new Promise(resolve => {
  156. resolver = resolve;
  157. });
  158. this.stopListeners();
  159. this.destroyAnimationPlayers();
  160. this.nodeFront = null;
  161. if (this.animationsFront) {
  162. this.animationsFront.destroy();
  163. this.animationsFront = null;
  164. }
  165. resolver();
  166. }),
  167. startListeners: function () {
  168. // Re-create the list of players when a new node is selected, except if the
  169. // sidebar isn't visible.
  170. gInspector.selection.on("new-node-front", this.onNewNodeFront);
  171. gInspector.sidebar.on("select", this.onPanelVisibilityChange);
  172. gToolbox.on("select", this.onPanelVisibilityChange);
  173. },
  174. stopListeners: function () {
  175. gInspector.selection.off("new-node-front", this.onNewNodeFront);
  176. gInspector.sidebar.off("select", this.onPanelVisibilityChange);
  177. gToolbox.off("select", this.onPanelVisibilityChange);
  178. if (this.isListeningToMutations) {
  179. this.animationsFront.off("mutations", this.onAnimationMutations);
  180. }
  181. },
  182. isPanelVisible: function () {
  183. return gToolbox.currentToolId === "inspector" &&
  184. gInspector.sidebar &&
  185. gInspector.sidebar.getCurrentTabID() == "animationinspector";
  186. },
  187. onPanelVisibilityChange: Task.async(function* () {
  188. if (this.isPanelVisible()) {
  189. this.onNewNodeFront();
  190. }
  191. }),
  192. onNewNodeFront: Task.async(function* () {
  193. // Ignore if the panel isn't visible or the node selection hasn't changed.
  194. if (!this.isPanelVisible() ||
  195. this.nodeFront === gInspector.selection.nodeFront) {
  196. return;
  197. }
  198. this.nodeFront = gInspector.selection.nodeFront;
  199. let done = gInspector.updating("animationscontroller");
  200. if (!gInspector.selection.isConnected() ||
  201. !gInspector.selection.isElementNode()) {
  202. this.destroyAnimationPlayers();
  203. this.emit(this.PLAYERS_UPDATED_EVENT);
  204. done();
  205. return;
  206. }
  207. yield this.refreshAnimationPlayers(this.nodeFront);
  208. this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
  209. done();
  210. }),
  211. /**
  212. * Toggle (pause/play) all animations in the current target.
  213. */
  214. toggleAll: function () {
  215. if (!this.traits.hasToggleAll) {
  216. return promise.resolve();
  217. }
  218. return this.animationsFront.toggleAll()
  219. .then(() => this.emit(this.ALL_ANIMATIONS_TOGGLED_EVENT, this))
  220. .catch(e => console.error(e));
  221. },
  222. /**
  223. * Similar to toggleAll except that it only plays/pauses the currently known
  224. * animations (those listed in this.animationPlayers).
  225. * @param {Boolean} shouldPause True if the animations should be paused, false
  226. * if they should be played.
  227. * @return {Promise} Resolves when the playState has been changed.
  228. */
  229. toggleCurrentAnimations: Task.async(function* (shouldPause) {
  230. if (this.traits.hasToggleSeveral) {
  231. yield this.animationsFront.toggleSeveral(this.animationPlayers,
  232. shouldPause);
  233. } else {
  234. // Fall back to pausing/playing the players one by one, which is bound to
  235. // introduce some de-synchronization.
  236. for (let player of this.animationPlayers) {
  237. if (shouldPause) {
  238. yield player.pause();
  239. } else {
  240. yield player.play();
  241. }
  242. }
  243. }
  244. }),
  245. /**
  246. * Set all known animations' currentTimes to the provided time.
  247. * @param {Number} time.
  248. * @param {Boolean} shouldPause Should the animations be paused too.
  249. * @return {Promise} Resolves when the current time has been set.
  250. */
  251. setCurrentTimeAll: Task.async(function* (time, shouldPause) {
  252. if (this.traits.hasSetCurrentTimes) {
  253. yield this.animationsFront.setCurrentTimes(this.animationPlayers, time,
  254. shouldPause);
  255. } else {
  256. // Fall back to pausing and setting the current time on each player, one
  257. // by one, which is bound to introduce some de-synchronization.
  258. for (let animation of this.animationPlayers) {
  259. if (shouldPause) {
  260. yield animation.pause();
  261. }
  262. yield animation.setCurrentTime(time);
  263. }
  264. }
  265. }),
  266. /**
  267. * Set all known animations' playback rates to the provided rate.
  268. * @param {Number} rate.
  269. * @return {Promise} Resolves when the rate has been set.
  270. */
  271. setPlaybackRateAll: Task.async(function* (rate) {
  272. if (this.traits.hasSetPlaybackRates) {
  273. // If the backend can set all playback rates at the same time, use that.
  274. yield this.animationsFront.setPlaybackRates(this.animationPlayers, rate);
  275. } else if (this.traits.hasSetPlaybackRate) {
  276. // Otherwise, fall back to setting each rate individually.
  277. for (let animation of this.animationPlayers) {
  278. yield animation.setPlaybackRate(rate);
  279. }
  280. }
  281. }),
  282. // AnimationPlayerFront objects are managed by this controller. They are
  283. // retrieved when refreshAnimationPlayers is called, stored in the
  284. // animationPlayers array, and destroyed when refreshAnimationPlayers is
  285. // called again.
  286. animationPlayers: [],
  287. refreshAnimationPlayers: Task.async(function* (nodeFront) {
  288. this.destroyAnimationPlayers();
  289. this.animationPlayers = yield this.animationsFront
  290. .getAnimationPlayersForNode(nodeFront);
  291. // Start listening for animation mutations only after the first method call
  292. // otherwise events won't be sent.
  293. if (!this.isListeningToMutations && this.traits.hasMutationEvents) {
  294. this.animationsFront.on("mutations", this.onAnimationMutations);
  295. this.isListeningToMutations = true;
  296. }
  297. }),
  298. onAnimationMutations: function (changes) {
  299. // Insert new players into this.animationPlayers when new animations are
  300. // added.
  301. for (let {type, player} of changes) {
  302. if (type === "added") {
  303. this.animationPlayers.push(player);
  304. }
  305. if (type === "removed") {
  306. let index = this.animationPlayers.indexOf(player);
  307. this.animationPlayers.splice(index, 1);
  308. }
  309. }
  310. // Let the UI know the list has been updated.
  311. this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
  312. },
  313. /**
  314. * Get the latest known current time of document.timeline.
  315. * This value is sent along with all AnimationPlayerActors' states, but it
  316. * isn't updated after that, so this function loops over all know animations
  317. * to find the highest value.
  318. * @return {Number|Boolean} False is returned if this server version doesn't
  319. * provide document's current time.
  320. */
  321. get documentCurrentTime() {
  322. let time = 0;
  323. for (let {state} of this.animationPlayers) {
  324. if (!state.documentCurrentTime) {
  325. return false;
  326. }
  327. time = Math.max(time, state.documentCurrentTime);
  328. }
  329. return time;
  330. },
  331. destroyAnimationPlayers: function () {
  332. this.animationPlayers = [];
  333. }
  334. };
  335. EventEmitter.decorate(AnimationsController);