Wed, 31 Dec 2014 06:09:35 +0100
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 | }; |