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 +