browser/devtools/webaudioeditor/webaudioeditor-controller.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/webaudioeditor/webaudioeditor-controller.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,310 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +"use strict";
     1.8 +
     1.9 +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
    1.10 +
    1.11 +Cu.import("resource://gre/modules/Services.jsm");
    1.12 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.13 +Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
    1.14 +
    1.15 +// Override DOM promises with Promise.jsm helpers
    1.16 +const { defer, all } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
    1.17 +
    1.18 +const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
    1.19 +const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
    1.20 +const EventEmitter = require("devtools/toolkit/event-emitter");
    1.21 +const STRINGS_URI = "chrome://browser/locale/devtools/webaudioeditor.properties"
    1.22 +let { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
    1.23 +
    1.24 +// The panel's window global is an EventEmitter firing the following events:
    1.25 +const EVENTS = {
    1.26 +  // Fired when the first AudioNode has been created, signifying
    1.27 +  // that the AudioContext is being used and should be tracked via the editor.
    1.28 +  START_CONTEXT: "WebAudioEditor:StartContext",
    1.29 +
    1.30 +  // On node creation, connect and disconnect.
    1.31 +  CREATE_NODE: "WebAudioEditor:CreateNode",
    1.32 +  CONNECT_NODE: "WebAudioEditor:ConnectNode",
    1.33 +  DISCONNECT_NODE: "WebAudioEditor:DisconnectNode",
    1.34 +
    1.35 +  // When a node gets GC'd.
    1.36 +  DESTROY_NODE: "WebAudioEditor:DestroyNode",
    1.37 +
    1.38 +  // On a node parameter's change.
    1.39 +  CHANGE_PARAM: "WebAudioEditor:ChangeParam",
    1.40 +
    1.41 +  // When the UI is reset from tab navigation.
    1.42 +  UI_RESET: "WebAudioEditor:UIReset",
    1.43 +
    1.44 +  // When a param has been changed via the UI and successfully
    1.45 +  // pushed via the actor to the raw audio node.
    1.46 +  UI_SET_PARAM: "WebAudioEditor:UISetParam",
    1.47 +
    1.48 +  // When an audio node is added to the list pane.
    1.49 +  UI_ADD_NODE_LIST: "WebAudioEditor:UIAddNodeList",
    1.50 +
    1.51 +  // When the Audio Context graph finishes rendering.
    1.52 +  // Is called with two arguments, first representing number of nodes
    1.53 +  // rendered, second being the number of edges rendered.
    1.54 +  UI_GRAPH_RENDERED: "WebAudioEditor:UIGraphRendered"
    1.55 +};
    1.56 +
    1.57 +/**
    1.58 + * The current target and the Web Audio Editor front, set by this tool's host.
    1.59 + */
    1.60 +let gToolbox, gTarget, gFront;
    1.61 +
    1.62 +/**
    1.63 + * Track an array of audio nodes
    1.64 + */
    1.65 +let AudioNodes = [];
    1.66 +let AudioNodeConnections = new WeakMap();
    1.67 +
    1.68 +
    1.69 +// Light representation wrapping an AudioNode actor with additional properties
    1.70 +function AudioNodeView (actor) {
    1.71 +  this.actor = actor;
    1.72 +  this.id = actor.actorID;
    1.73 +}
    1.74 +
    1.75 +// A proxy for the underlying AudioNodeActor to fetch its type
    1.76 +// and subsequently assign the type to the instance.
    1.77 +AudioNodeView.prototype.getType = Task.async(function* () {
    1.78 +  this.type = yield this.actor.getType();
    1.79 +  return this.type;
    1.80 +});
    1.81 +
    1.82 +// Helper method to create connections in the AudioNodeConnections
    1.83 +// WeakMap for rendering
    1.84 +AudioNodeView.prototype.connect = function (destination) {
    1.85 +  let connections = AudioNodeConnections.get(this);
    1.86 +  if (!connections) {
    1.87 +    connections = [];
    1.88 +    AudioNodeConnections.set(this, connections);
    1.89 +  }
    1.90 +  connections.push(destination);
    1.91 +};
    1.92 +
    1.93 +// Helper method to remove audio connections from the current AudioNodeView
    1.94 +AudioNodeView.prototype.disconnect = function () {
    1.95 +  AudioNodeConnections.set(this, []);
    1.96 +};
    1.97 +
    1.98 +// Returns a promise that resolves to an array of objects containing
    1.99 +// both a `param` name property and a `value` property.
   1.100 +AudioNodeView.prototype.getParams = function () {
   1.101 +  return this.actor.getParams();
   1.102 +};
   1.103 +
   1.104 +
   1.105 +/**
   1.106 + * Initializes the web audio editor views
   1.107 + */
   1.108 +function startupWebAudioEditor() {
   1.109 +  return all([
   1.110 +    WebAudioEditorController.initialize(),
   1.111 +    WebAudioGraphView.initialize(),
   1.112 +    WebAudioParamView.initialize()
   1.113 +  ]);
   1.114 +}
   1.115 +
   1.116 +/**
   1.117 + * Destroys the web audio editor controller and views.
   1.118 + */
   1.119 +function shutdownWebAudioEditor() {
   1.120 +  return all([
   1.121 +    WebAudioEditorController.destroy(),
   1.122 +    WebAudioGraphView.destroy(),
   1.123 +    WebAudioParamView.destroy()
   1.124 +  ]);
   1.125 +}
   1.126 +
   1.127 +/**
   1.128 + * Functions handling target-related lifetime events.
   1.129 + */
   1.130 +let WebAudioEditorController = {
   1.131 +  /**
   1.132 +   * Listen for events emitted by the current tab target.
   1.133 +   */
   1.134 +  initialize: function() {
   1.135 +    this._onTabNavigated = this._onTabNavigated.bind(this);
   1.136 +    gTarget.on("will-navigate", this._onTabNavigated);
   1.137 +    gTarget.on("navigate", this._onTabNavigated);
   1.138 +    gFront.on("start-context", this._onStartContext);
   1.139 +    gFront.on("create-node", this._onCreateNode);
   1.140 +    gFront.on("connect-node", this._onConnectNode);
   1.141 +    gFront.on("disconnect-node", this._onDisconnectNode);
   1.142 +    gFront.on("change-param", this._onChangeParam);
   1.143 +
   1.144 +    // Set up events to refresh the Graph view
   1.145 +    window.on(EVENTS.CREATE_NODE, this._onUpdatedContext);
   1.146 +    window.on(EVENTS.CONNECT_NODE, this._onUpdatedContext);
   1.147 +    window.on(EVENTS.DISCONNECT_NODE, this._onUpdatedContext);
   1.148 +  },
   1.149 +
   1.150 +  /**
   1.151 +   * Remove events emitted by the current tab target.
   1.152 +   */
   1.153 +  destroy: function() {
   1.154 +    gTarget.off("will-navigate", this._onTabNavigated);
   1.155 +    gTarget.off("navigate", this._onTabNavigated);
   1.156 +    gFront.off("start-context", this._onStartContext);
   1.157 +    gFront.off("create-node", this._onCreateNode);
   1.158 +    gFront.off("connect-node", this._onConnectNode);
   1.159 +    gFront.off("disconnect-node", this._onDisconnectNode);
   1.160 +    gFront.off("change-param", this._onChangeParam);
   1.161 +    window.off(EVENTS.CREATE_NODE, this._onUpdatedContext);
   1.162 +    window.off(EVENTS.CONNECT_NODE, this._onUpdatedContext);
   1.163 +    window.off(EVENTS.DISCONNECT_NODE, this._onUpdatedContext);
   1.164 +  },
   1.165 +
   1.166 +  /**
   1.167 +   * Called when a new audio node is created, or the audio context
   1.168 +   * routing changes.
   1.169 +   */
   1.170 +  _onUpdatedContext: function () {
   1.171 +    WebAudioGraphView.draw();
   1.172 +  },
   1.173 +
   1.174 +  /**
   1.175 +   * Called for each location change in the debugged tab.
   1.176 +   */
   1.177 +  _onTabNavigated: function(event) {
   1.178 +    switch (event) {
   1.179 +      case "will-navigate": {
   1.180 +        Task.spawn(function() {
   1.181 +          // Make sure the backend is prepared to handle audio contexts.
   1.182 +          yield gFront.setup({ reload: false });
   1.183 +
   1.184 +          // Reset UI to show "Waiting for Audio Context..." and clear out
   1.185 +          // current UI.
   1.186 +          WebAudioGraphView.resetUI();
   1.187 +          WebAudioParamView.resetUI();
   1.188 +
   1.189 +          // Clear out stored audio nodes
   1.190 +          AudioNodes.length = 0;
   1.191 +          AudioNodeConnections.clear();
   1.192 +        }).then(() => window.emit(EVENTS.UI_RESET));
   1.193 +        break;
   1.194 +      }
   1.195 +      case "navigate": {
   1.196 +        // TODO Case of bfcache, needs investigating
   1.197 +        // bug 994250
   1.198 +        break;
   1.199 +      }
   1.200 +    }
   1.201 +  },
   1.202 +
   1.203 +  /**
   1.204 +   * Called after the first audio node is created in an audio context,
   1.205 +   * signaling that the audio context is being used.
   1.206 +   */
   1.207 +  _onStartContext: function() {
   1.208 +    WebAudioGraphView.showContent();
   1.209 +    window.emit(EVENTS.START_CONTEXT);
   1.210 +  },
   1.211 +
   1.212 +  /**
   1.213 +   * Called when a new node is created. Creates an `AudioNodeView` instance
   1.214 +   * for tracking throughout the editor.
   1.215 +   */
   1.216 +  _onCreateNode: Task.async(function* (nodeActor) {
   1.217 +    let node = new AudioNodeView(nodeActor);
   1.218 +    yield node.getType();
   1.219 +    AudioNodes.push(node);
   1.220 +    window.emit(EVENTS.CREATE_NODE, node.id);
   1.221 +  }),
   1.222 +
   1.223 +  /**
   1.224 +   * Called when a node is connected to another node.
   1.225 +   */
   1.226 +  _onConnectNode: Task.async(function* ({ source: sourceActor, dest: destActor }) {
   1.227 +    // Since node create and connect are probably executed back to back,
   1.228 +    // and the controller's `_onCreateNode` needs to look up type,
   1.229 +    // the edge creation could be called before the graph node is actually
   1.230 +    // created. This way, we can check and listen for the event before
   1.231 +    // adding an edge.
   1.232 +    let [source, dest] = yield waitForNodeCreation(sourceActor, destActor);
   1.233 +
   1.234 +    source.connect(dest);
   1.235 +    window.emit(EVENTS.CONNECT_NODE, source.id, dest.id);
   1.236 +
   1.237 +    function waitForNodeCreation (sourceActor, destActor) {
   1.238 +      let deferred = defer();
   1.239 +      let source = getViewNodeByActor(sourceActor);
   1.240 +      let dest = getViewNodeByActor(destActor);
   1.241 +
   1.242 +      if (!source || !dest)
   1.243 +        window.on(EVENTS.CREATE_NODE, function createNodeListener (_, id) {
   1.244 +          let createdNode = getViewNodeById(id);
   1.245 +          if (equalActors(sourceActor, createdNode.actor))
   1.246 +            source = createdNode;
   1.247 +          if (equalActors(destActor, createdNode.actor))
   1.248 +            dest = createdNode;
   1.249 +          if (source && dest) {
   1.250 +            window.off(EVENTS.CREATE_NODE, createNodeListener);
   1.251 +            deferred.resolve([source, dest]);
   1.252 +          }
   1.253 +        });
   1.254 +      else
   1.255 +        deferred.resolve([source, dest]);
   1.256 +      return deferred.promise;
   1.257 +    }
   1.258 +  }),
   1.259 +
   1.260 +  /**
   1.261 +   * Called when a node is disconnected.
   1.262 +   */
   1.263 +  _onDisconnectNode: function(nodeActor) {
   1.264 +    let node = getViewNodeByActor(nodeActor);
   1.265 +    node.disconnect();
   1.266 +    window.emit(EVENTS.DISCONNECT_NODE, node.id);
   1.267 +  },
   1.268 +
   1.269 +  /**
   1.270 +   * Called when a node param is changed.
   1.271 +   */
   1.272 +  _onChangeParam: function({ actor, param, value }) {
   1.273 +    window.emit(EVENTS.CHANGE_PARAM, getViewNodeByActor(actor), param, value);
   1.274 +  }
   1.275 +};
   1.276 +
   1.277 +/**
   1.278 + * Convenient way of emitting events from the panel window.
   1.279 + */
   1.280 +EventEmitter.decorate(this);
   1.281 +
   1.282 +/**
   1.283 + * DOM query helper.
   1.284 + */
   1.285 +function $(selector, target = document) { return target.querySelector(selector); }
   1.286 +function $$(selector, target = document) { return target.querySelectorAll(selector); }
   1.287 +
   1.288 +/**
   1.289 + * Compare `actorID` between two actors to determine if they're corresponding
   1.290 + * to the same underlying actor.
   1.291 + */
   1.292 +function equalActors (actor1, actor2) {
   1.293 +  return actor1.actorID === actor2.actorID;
   1.294 +}
   1.295 +
   1.296 +/**
   1.297 + * Returns the corresponding ViewNode by actor
   1.298 + */
   1.299 +function getViewNodeByActor (actor) {
   1.300 +  for (let i = 0; i < AudioNodes.length; i++) {
   1.301 +    if (equalActors(AudioNodes[i].actor, actor))
   1.302 +      return AudioNodes[i];
   1.303 +  }
   1.304 +  return null;
   1.305 +}
   1.306 +
   1.307 +/**
   1.308 + * Returns the corresponding ViewNode by actorID
   1.309 + */
   1.310 +function getViewNodeById (id) {
   1.311 +  return getViewNodeByActor({ actorID: id });
   1.312 +}
   1.313 +

mercurial