browser/devtools/webaudioeditor/webaudioeditor-view.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial