browser/devtools/webaudioeditor/webaudioeditor-view.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:133bfb42f2b9
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
4 "use strict";
5
6 Cu.import("resource:///modules/devtools/VariablesView.jsm");
7 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
8 const { debounce } = require("sdk/lang/functional");
9
10 // Globals for d3 stuff
11 // Width/height in pixels of SVG graph
12 // TODO investigate to see how this works in other host types bug 994257
13 const WIDTH = 1000;
14 const HEIGHT = 400;
15
16 // Sizes of SVG arrows in graph
17 const ARROW_HEIGHT = 5;
18 const ARROW_WIDTH = 8;
19
20 const GRAPH_DEBOUNCE_TIMER = 100;
21
22 const GENERIC_VARIABLES_VIEW_SETTINGS = {
23 lazyEmpty: true,
24 lazyEmptyDelay: 10, // ms
25 searchEnabled: false,
26 editableValueTooltip: "",
27 editableNameTooltip: "",
28 preventDisableOnChange: true,
29 preventDescriptorModifiers: true,
30 eval: () => {}
31 };
32
33 /**
34 * Functions handling the graph UI.
35 */
36 let WebAudioGraphView = {
37 /**
38 * Initialization function, called when the tool is started.
39 */
40 initialize: function() {
41 this._onGraphNodeClick = this._onGraphNodeClick.bind(this);
42 this.draw = debounce(this.draw.bind(this), GRAPH_DEBOUNCE_TIMER);
43 },
44
45 /**
46 * Destruction function, called when the tool is closed.
47 */
48 destroy: function() {
49 if (this._zoomBinding) {
50 this._zoomBinding.on("zoom", null);
51 }
52 },
53
54 /**
55 * Called when a page is reloaded and waiting for a "start-context" event
56 * and clears out old content
57 */
58 resetUI: function () {
59 $("#reload-notice").hidden = true;
60 $("#waiting-notice").hidden = false;
61 $("#content").hidden = true;
62 this.resetGraph();
63 },
64
65 /**
66 * Called once "start-context" is fired, indicating that there is audio context
67 * activity to view and inspect
68 */
69 showContent: function () {
70 $("#reload-notice").hidden = true;
71 $("#waiting-notice").hidden = true;
72 $("#content").hidden = false;
73 this.draw();
74 },
75
76 /**
77 * Clears out the rendered graph, called when resetting the SVG elements to draw again,
78 * or when resetting the entire UI tool
79 */
80 resetGraph: function () {
81 $("#graph-target").innerHTML = "";
82 },
83
84 /**
85 * Makes the corresponding graph node appear "focused", called from WebAudioParamView
86 */
87 focusNode: function (actorID) {
88 // Remove class "selected" from all nodes
89 Array.prototype.forEach.call($$(".nodes > g"), $node => $node.classList.remove("selected"));
90 // Add to "selected"
91 this._getNodeByID(actorID).classList.add("selected");
92 },
93
94 /**
95 * Unfocuses the corresponding graph node, called from WebAudioParamView
96 */
97 blurNode: function (actorID) {
98 this._getNodeByID(actorID).classList.remove("selected");
99 },
100
101 /**
102 * Takes an actorID and returns the corresponding DOM SVG element in the graph
103 */
104 _getNodeByID: function (actorID) {
105 return $(".nodes > g[data-id='" + actorID + "']");
106 },
107
108 /**
109 * `draw` renders the ViewNodes currently available in `AudioNodes` with `AudioNodeConnections`,
110 * and is throttled to be called at most every `GRAPH_DEBOUNCE_TIMER` milliseconds. Is called
111 * whenever the audio context routing changes, after being debounced.
112 */
113 draw: function () {
114 // Clear out previous SVG information
115 this.resetGraph();
116
117 let graph = new dagreD3.Digraph();
118 let edges = [];
119
120 AudioNodes.forEach(node => {
121 // Add node to graph
122 graph.addNode(node.id, { label: node.type, id: node.id });
123
124 // Add all of the connections from this node to the edge array to be added
125 // after all the nodes are added, otherwise edges will attempted to be created
126 // for nodes that have not yet been added
127 AudioNodeConnections.get(node, []).forEach(dest => edges.push([node, dest]));
128 });
129
130 edges.forEach(([node, dest]) => graph.addEdge(null, node.id, dest.id, {
131 source: node.id,
132 target: dest.id
133 }));
134
135 let renderer = new dagreD3.Renderer();
136
137 // Post-render manipulation of the nodes
138 let oldDrawNodes = renderer.drawNodes();
139 renderer.drawNodes(function(graph, root) {
140 let svgNodes = oldDrawNodes(graph, root);
141 svgNodes.attr("class", (n) => {
142 let node = graph.node(n);
143 return "type-" + node.label;
144 });
145 svgNodes.attr("data-id", (n) => {
146 let node = graph.node(n);
147 return node.id;
148 });
149 return svgNodes;
150 });
151
152 // Post-render manipulation of edges
153 let oldDrawEdgePaths = renderer.drawEdgePaths();
154 renderer.drawEdgePaths(function(graph, root) {
155 let svgNodes = oldDrawEdgePaths(graph, root);
156 svgNodes.attr("data-source", (n) => {
157 let edge = graph.edge(n);
158 return edge.source;
159 });
160 svgNodes.attr("data-target", (n) => {
161 let edge = graph.edge(n);
162 return edge.target;
163 });
164 return svgNodes;
165 });
166
167 // Override Dagre-d3's post render function by passing in our own.
168 // This way we can leave styles out of it.
169 renderer.postRender(function (graph, root) {
170 // TODO change arrowhead color depending on theme-dark/theme-light
171 // and possibly refactor rendering this as it's ugly
172 // Bug 994256
173 // let color = window.classList.contains("theme-dark") ? "#f5f7fa" : "#585959";
174 if (graph.isDirected() && root.select("#arrowhead").empty()) {
175 root
176 .append("svg:defs")
177 .append("svg:marker")
178 .attr("id", "arrowhead")
179 .attr("viewBox", "0 0 10 10")
180 .attr("refX", ARROW_WIDTH)
181 .attr("refY", ARROW_HEIGHT)
182 .attr("markerUnits", "strokewidth")
183 .attr("markerWidth", ARROW_WIDTH)
184 .attr("markerHeight", ARROW_HEIGHT)
185 .attr("orient", "auto")
186 .attr("style", "fill: #f5f7fa")
187 .append("svg:path")
188 .attr("d", "M 0 0 L 10 5 L 0 10 z");
189 }
190
191 // Fire an event upon completed rendering
192 window.emit(EVENTS.UI_GRAPH_RENDERED, AudioNodes.length, edges.length);
193 });
194
195 let layout = dagreD3.layout().rankDir("LR");
196 renderer.layout(layout).run(graph, d3.select("#graph-target"));
197
198 // Handle the sliding and zooming of the graph,
199 // store as `this._zoomBinding` so we can unbind during destruction
200 if (!this._zoomBinding) {
201 this._zoomBinding = d3.behavior.zoom().on("zoom", function () {
202 var ev = d3.event;
203 d3.select("#graph-target")
204 .attr("transform", "translate(" + ev.translate + ") scale(" + ev.scale + ")");
205 });
206 d3.select("svg").call(this._zoomBinding);
207 }
208 },
209
210 /**
211 * Event handlers
212 */
213
214 /**
215 * Fired when a node in the svg graph is clicked. Used to handle triggering the AudioNodePane.
216 *
217 * @param Object AudioNodeView
218 * The object stored in `AudioNodes` which contains render information, but most importantly,
219 * the actorID under `id` property.
220 */
221 _onGraphNodeClick: function (node) {
222 WebAudioParamView.focusNode(node.id);
223 }
224 };
225
226 let WebAudioParamView = {
227 _paramsView: null,
228
229 /**
230 * Initialization function called when the tool starts up.
231 */
232 initialize: function () {
233 this._paramsView = new VariablesView($("#web-audio-inspector-content"), GENERIC_VARIABLES_VIEW_SETTINGS);
234 this._paramsView.eval = this._onEval.bind(this);
235 window.on(EVENTS.CREATE_NODE, this.addNode = this.addNode.bind(this));
236 window.on(EVENTS.DESTROY_NODE, this.removeNode = this.removeNode.bind(this));
237 },
238
239 /**
240 * Destruction function called when the tool cleans up.
241 */
242 destroy: function() {
243 window.off(EVENTS.CREATE_NODE, this.addNode);
244 window.off(EVENTS.DESTROY_NODE, this.removeNode);
245 },
246
247 /**
248 * Empties out the params view.
249 */
250 resetUI: function () {
251 this._paramsView.empty();
252 },
253
254 /**
255 * Takes an `id` and focuses and expands the corresponding scope.
256 */
257 focusNode: function (id) {
258 let scope = this._getScopeByID(id);
259 if (!scope) return;
260
261 scope.focus();
262 scope.expand();
263 },
264
265 /**
266 * Executed when an audio param is changed in the UI.
267 */
268 _onEval: Task.async(function* (variable, value) {
269 let ownerScope = variable.ownerView;
270 let node = getViewNodeById(ownerScope.actorID);
271 let propName = variable.name;
272 let errorMessage = yield node.actor.setParam(propName, value);
273
274 // TODO figure out how to handle and display set param errors
275 // and enable `test/brorwser_wa_params_view_edit_error.js`
276 // Bug 994258
277 if (!errorMessage) {
278 ownerScope.get(propName).setGrip(value);
279 window.emit(EVENTS.UI_SET_PARAM, node.id, propName, value);
280 } else {
281 window.emit(EVENTS.UI_SET_PARAM_ERROR, node.id, propName, value);
282 }
283 }),
284
285 /**
286 * Takes an `id` and returns the corresponding variables scope.
287 */
288 _getScopeByID: function (id) {
289 let view = this._paramsView;
290 for (let i = 0; i < view._store.length; i++) {
291 let scope = view.getScopeAtIndex(i);
292 if (scope.actorID === id)
293 return scope;
294 }
295 return null;
296 },
297
298 /**
299 * Called when hovering over a variable scope.
300 */
301 _onMouseOver: function (e) {
302 let id = WebAudioParamView._getScopeID(this);
303
304 if (!id) return;
305
306 WebAudioGraphView.focusNode(id);
307 },
308
309 /**
310 * Called when hovering out of a variable scope.
311 */
312 _onMouseOut: function (e) {
313 let id = WebAudioParamView._getScopeID(this);
314
315 if (!id) return;
316
317 WebAudioGraphView.blurNode(id);
318 },
319
320 /**
321 * Uses in event handlers, takes an element `$el` and finds the
322 * associated actor ID with that variable scope to be used in other contexts.
323 */
324 _getScopeID: function ($el) {
325 let match = $el.parentNode.id.match(/\(([^\)]*)\)/);
326 return match ? match[1] : null;
327 },
328
329 /**
330 * Called when `CREATE_NODE` is fired to update the params view with the
331 * freshly created audio node.
332 */
333 addNode: Task.async(function* (_, id) {
334 let viewNode = getViewNodeById(id);
335 let type = viewNode.type;
336
337 let audioParamsTitle = type + " (" + id + ")";
338 let paramsView = this._paramsView;
339 let paramsScopeView = paramsView.addScope(audioParamsTitle);
340
341 paramsScopeView.actorID = id;
342 paramsScopeView.expanded = false;
343
344 paramsScopeView.addEventListener("mouseover", this._onMouseOver, false);
345 paramsScopeView.addEventListener("mouseout", this._onMouseOut, false);
346
347 let params = yield viewNode.getParams();
348 params.forEach(({ param, value }) => {
349 let descriptor = { value: value };
350 paramsScopeView.addItem(param, descriptor);
351 });
352
353 window.emit(EVENTS.UI_ADD_NODE_LIST, id);
354 }),
355
356 /**
357 * Called when `DESTROY_NODE` is fired to remove the node from params view.
358 * TODO bug 994263, dependent on node GC events
359 */
360 removeNode: Task.async(function* (viewNode) {
361
362 })
363 };

mercurial