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.

     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";
     6 Cu.import("resource:///modules/devtools/VariablesView.jsm");
     7 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
     8 const { debounce } = require("sdk/lang/functional");
    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;
    16 // Sizes of SVG arrows in graph
    17 const ARROW_HEIGHT = 5;
    18 const ARROW_WIDTH = 8;
    20 const GRAPH_DEBOUNCE_TIMER = 100;
    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 };
    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   },
    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   },
    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   },
    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   },
    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   },
    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   },
    94   /**
    95    * Unfocuses the corresponding graph node, called from WebAudioParamView
    96    */
    97   blurNode: function (actorID) {
    98     this._getNodeByID(actorID).classList.remove("selected");
    99   },
   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   },
   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();
   117     let graph = new dagreD3.Digraph();
   118     let edges = [];
   120     AudioNodes.forEach(node => {
   121       // Add node to graph
   122       graph.addNode(node.id, { label: node.type, id: node.id });
   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     });
   130     edges.forEach(([node, dest]) => graph.addEdge(null, node.id, dest.id, {
   131       source: node.id,
   132       target: dest.id
   133     }));
   135     let renderer = new dagreD3.Renderer();
   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     });
   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     });
   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       }
   191       // Fire an event upon completed rendering
   192       window.emit(EVENTS.UI_GRAPH_RENDERED, AudioNodes.length, edges.length);
   193     });
   195     let layout = dagreD3.layout().rankDir("LR");
   196     renderer.layout(layout).run(graph, d3.select("#graph-target"));
   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   },
   210   /**
   211    * Event handlers
   212    */
   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 };
   226 let WebAudioParamView = {
   227   _paramsView: null,
   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   },
   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   },
   247   /**
   248    * Empties out the params view.
   249    */
   250   resetUI: function () {
   251     this._paramsView.empty();
   252   },
   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;
   261     scope.focus();
   262     scope.expand();
   263   },
   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);
   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   }),
   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   },
   298   /**
   299    * Called when hovering over a variable scope.
   300    */
   301   _onMouseOver: function (e) {
   302     let id = WebAudioParamView._getScopeID(this);
   304     if (!id) return;
   306     WebAudioGraphView.focusNode(id);
   307   },
   309   /**
   310    * Called when hovering out of a variable scope.
   311    */
   312   _onMouseOut: function (e) {
   313     let id = WebAudioParamView._getScopeID(this);
   315     if (!id) return;
   317     WebAudioGraphView.blurNode(id);
   318   },
   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   },
   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;
   337     let audioParamsTitle = type + " (" + id + ")";
   338     let paramsView = this._paramsView;
   339     let paramsScopeView = paramsView.addScope(audioParamsTitle);
   341     paramsScopeView.actorID = id;
   342     paramsScopeView.expanded = false;
   344     paramsScopeView.addEventListener("mouseover", this._onMouseOver, false);
   345     paramsScopeView.addEventListener("mouseout", this._onMouseOut, false);
   347     let params = yield viewNode.getParams();
   348     params.forEach(({ param, value }) => {
   349       let descriptor = { value: value };
   350       paramsScopeView.addItem(param, descriptor);
   351     });
   353     window.emit(EVENTS.UI_ADD_NODE_LIST, id);
   354   }),
   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) {
   362   })
   363 };

mercurial