browser/devtools/webaudioeditor/webaudioeditor-view.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/webaudioeditor/webaudioeditor-view.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,363 @@
     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 +Cu.import("resource:///modules/devtools/VariablesView.jsm");
    1.10 +Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
    1.11 +const { debounce } = require("sdk/lang/functional");
    1.12 +
    1.13 +// Globals for d3 stuff
    1.14 +// Width/height in pixels of SVG graph
    1.15 +// TODO investigate to see how this works in other host types bug 994257
    1.16 +const WIDTH = 1000;
    1.17 +const HEIGHT = 400;
    1.18 +
    1.19 +// Sizes of SVG arrows in graph
    1.20 +const ARROW_HEIGHT = 5;
    1.21 +const ARROW_WIDTH = 8;
    1.22 +
    1.23 +const GRAPH_DEBOUNCE_TIMER = 100;
    1.24 +
    1.25 +const GENERIC_VARIABLES_VIEW_SETTINGS = {
    1.26 +  lazyEmpty: true,
    1.27 +  lazyEmptyDelay: 10, // ms
    1.28 +  searchEnabled: false,
    1.29 +  editableValueTooltip: "",
    1.30 +  editableNameTooltip: "",
    1.31 +  preventDisableOnChange: true,
    1.32 +  preventDescriptorModifiers: true,
    1.33 +  eval: () => {}
    1.34 +};
    1.35 +
    1.36 +/**
    1.37 + * Functions handling the graph UI.
    1.38 + */
    1.39 +let WebAudioGraphView = {
    1.40 +  /**
    1.41 +   * Initialization function, called when the tool is started.
    1.42 +   */
    1.43 +  initialize: function() {
    1.44 +    this._onGraphNodeClick = this._onGraphNodeClick.bind(this);
    1.45 +    this.draw = debounce(this.draw.bind(this), GRAPH_DEBOUNCE_TIMER);
    1.46 +  },
    1.47 +
    1.48 +  /**
    1.49 +   * Destruction function, called when the tool is closed.
    1.50 +   */
    1.51 +  destroy: function() {
    1.52 +    if (this._zoomBinding) {
    1.53 +      this._zoomBinding.on("zoom", null);
    1.54 +    }
    1.55 +  },
    1.56 +
    1.57 +  /**
    1.58 +   * Called when a page is reloaded and waiting for a "start-context" event
    1.59 +   * and clears out old content
    1.60 +   */
    1.61 +  resetUI: function () {
    1.62 +    $("#reload-notice").hidden = true;
    1.63 +    $("#waiting-notice").hidden = false;
    1.64 +    $("#content").hidden = true;
    1.65 +    this.resetGraph();
    1.66 +  },
    1.67 +
    1.68 +  /**
    1.69 +   * Called once "start-context" is fired, indicating that there is audio context
    1.70 +   * activity to view and inspect
    1.71 +   */
    1.72 +  showContent: function () {
    1.73 +    $("#reload-notice").hidden = true;
    1.74 +    $("#waiting-notice").hidden = true;
    1.75 +    $("#content").hidden = false;
    1.76 +    this.draw();
    1.77 +  },
    1.78 +
    1.79 +  /**
    1.80 +   * Clears out the rendered graph, called when resetting the SVG elements to draw again,
    1.81 +   * or when resetting the entire UI tool
    1.82 +   */
    1.83 +  resetGraph: function () {
    1.84 +    $("#graph-target").innerHTML = "";
    1.85 +  },
    1.86 +
    1.87 +  /**
    1.88 +   * Makes the corresponding graph node appear "focused", called from WebAudioParamView
    1.89 +   */
    1.90 +  focusNode: function (actorID) {
    1.91 +    // Remove class "selected" from all nodes
    1.92 +    Array.prototype.forEach.call($$(".nodes > g"), $node => $node.classList.remove("selected"));
    1.93 +    // Add to "selected"
    1.94 +    this._getNodeByID(actorID).classList.add("selected");
    1.95 +  },
    1.96 +
    1.97 +  /**
    1.98 +   * Unfocuses the corresponding graph node, called from WebAudioParamView
    1.99 +   */
   1.100 +  blurNode: function (actorID) {
   1.101 +    this._getNodeByID(actorID).classList.remove("selected");
   1.102 +  },
   1.103 +
   1.104 +  /**
   1.105 +   * Takes an actorID and returns the corresponding DOM SVG element in the graph
   1.106 +   */
   1.107 +  _getNodeByID: function (actorID) {
   1.108 +    return $(".nodes > g[data-id='" + actorID + "']");
   1.109 +  },
   1.110 +
   1.111 +  /**
   1.112 +   * `draw` renders the ViewNodes currently available in `AudioNodes` with `AudioNodeConnections`,
   1.113 +   * and is throttled to be called at most every `GRAPH_DEBOUNCE_TIMER` milliseconds. Is called
   1.114 +   * whenever the audio context routing changes, after being debounced.
   1.115 +   */
   1.116 +  draw: function () {
   1.117 +    // Clear out previous SVG information
   1.118 +    this.resetGraph();
   1.119 +
   1.120 +    let graph = new dagreD3.Digraph();
   1.121 +    let edges = [];
   1.122 +
   1.123 +    AudioNodes.forEach(node => {
   1.124 +      // Add node to graph
   1.125 +      graph.addNode(node.id, { label: node.type, id: node.id });
   1.126 +
   1.127 +      // Add all of the connections from this node to the edge array to be added
   1.128 +      // after all the nodes are added, otherwise edges will attempted to be created
   1.129 +      // for nodes that have not yet been added
   1.130 +      AudioNodeConnections.get(node, []).forEach(dest => edges.push([node, dest]));
   1.131 +    });
   1.132 +
   1.133 +    edges.forEach(([node, dest]) => graph.addEdge(null, node.id, dest.id, {
   1.134 +      source: node.id,
   1.135 +      target: dest.id
   1.136 +    }));
   1.137 +
   1.138 +    let renderer = new dagreD3.Renderer();
   1.139 +
   1.140 +    // Post-render manipulation of the nodes
   1.141 +    let oldDrawNodes = renderer.drawNodes();
   1.142 +    renderer.drawNodes(function(graph, root) {
   1.143 +      let svgNodes = oldDrawNodes(graph, root);
   1.144 +      svgNodes.attr("class", (n) => {
   1.145 +        let node = graph.node(n);
   1.146 +        return "type-" + node.label;
   1.147 +      });
   1.148 +      svgNodes.attr("data-id", (n) => {
   1.149 +        let node = graph.node(n);
   1.150 +        return node.id;
   1.151 +      });
   1.152 +      return svgNodes;
   1.153 +    });
   1.154 +
   1.155 +    // Post-render manipulation of edges
   1.156 +    let oldDrawEdgePaths = renderer.drawEdgePaths();
   1.157 +    renderer.drawEdgePaths(function(graph, root) {
   1.158 +      let svgNodes = oldDrawEdgePaths(graph, root);
   1.159 +      svgNodes.attr("data-source", (n) => {
   1.160 +        let edge = graph.edge(n);
   1.161 +        return edge.source;
   1.162 +      });
   1.163 +      svgNodes.attr("data-target", (n) => {
   1.164 +        let edge = graph.edge(n);
   1.165 +        return edge.target;
   1.166 +      });
   1.167 +      return svgNodes;
   1.168 +    });
   1.169 +
   1.170 +    // Override Dagre-d3's post render function by passing in our own.
   1.171 +    // This way we can leave styles out of it.
   1.172 +    renderer.postRender(function (graph, root) {
   1.173 +      // TODO change arrowhead color depending on theme-dark/theme-light
   1.174 +      // and possibly refactor rendering this as it's ugly
   1.175 +      // Bug 994256
   1.176 +      // let color = window.classList.contains("theme-dark") ? "#f5f7fa" : "#585959";
   1.177 +      if (graph.isDirected() && root.select("#arrowhead").empty()) {
   1.178 +        root
   1.179 +          .append("svg:defs")
   1.180 +          .append("svg:marker")
   1.181 +          .attr("id", "arrowhead")
   1.182 +          .attr("viewBox", "0 0 10 10")
   1.183 +          .attr("refX", ARROW_WIDTH)
   1.184 +          .attr("refY", ARROW_HEIGHT)
   1.185 +          .attr("markerUnits", "strokewidth")
   1.186 +          .attr("markerWidth", ARROW_WIDTH)
   1.187 +          .attr("markerHeight", ARROW_HEIGHT)
   1.188 +          .attr("orient", "auto")
   1.189 +          .attr("style", "fill: #f5f7fa")
   1.190 +          .append("svg:path")
   1.191 +          .attr("d", "M 0 0 L 10 5 L 0 10 z");
   1.192 +      }
   1.193 +
   1.194 +      // Fire an event upon completed rendering
   1.195 +      window.emit(EVENTS.UI_GRAPH_RENDERED, AudioNodes.length, edges.length);
   1.196 +    });
   1.197 +
   1.198 +    let layout = dagreD3.layout().rankDir("LR");
   1.199 +    renderer.layout(layout).run(graph, d3.select("#graph-target"));
   1.200 +
   1.201 +    // Handle the sliding and zooming of the graph,
   1.202 +    // store as `this._zoomBinding` so we can unbind during destruction
   1.203 +    if (!this._zoomBinding) {
   1.204 +      this._zoomBinding = d3.behavior.zoom().on("zoom", function () {
   1.205 +        var ev = d3.event;
   1.206 +        d3.select("#graph-target")
   1.207 +          .attr("transform", "translate(" + ev.translate + ") scale(" + ev.scale + ")");
   1.208 +      });
   1.209 +      d3.select("svg").call(this._zoomBinding);
   1.210 +    }
   1.211 +  },
   1.212 +
   1.213 +  /**
   1.214 +   * Event handlers
   1.215 +   */
   1.216 +
   1.217 +  /**
   1.218 +   * Fired when a node in the svg graph is clicked. Used to handle triggering the AudioNodePane.
   1.219 +   *
   1.220 +   * @param Object AudioNodeView
   1.221 +   *        The object stored in `AudioNodes` which contains render information, but most importantly,
   1.222 +   *        the actorID under `id` property.
   1.223 +   */
   1.224 +  _onGraphNodeClick: function (node) {
   1.225 +    WebAudioParamView.focusNode(node.id);
   1.226 +  }
   1.227 +};
   1.228 +
   1.229 +let WebAudioParamView = {
   1.230 +  _paramsView: null,
   1.231 +
   1.232 +  /**
   1.233 +   * Initialization function called when the tool starts up.
   1.234 +   */
   1.235 +  initialize: function () {
   1.236 +    this._paramsView = new VariablesView($("#web-audio-inspector-content"), GENERIC_VARIABLES_VIEW_SETTINGS);
   1.237 +    this._paramsView.eval = this._onEval.bind(this);
   1.238 +    window.on(EVENTS.CREATE_NODE, this.addNode = this.addNode.bind(this));
   1.239 +    window.on(EVENTS.DESTROY_NODE, this.removeNode = this.removeNode.bind(this));
   1.240 +  },
   1.241 +
   1.242 +  /**
   1.243 +   * Destruction function called when the tool cleans up.
   1.244 +   */
   1.245 +  destroy: function() {
   1.246 +    window.off(EVENTS.CREATE_NODE, this.addNode);
   1.247 +    window.off(EVENTS.DESTROY_NODE, this.removeNode);
   1.248 +  },
   1.249 +
   1.250 +  /**
   1.251 +   * Empties out the params view.
   1.252 +   */
   1.253 +  resetUI: function () {
   1.254 +    this._paramsView.empty();
   1.255 +  },
   1.256 +
   1.257 +  /**
   1.258 +   * Takes an `id` and focuses and expands the corresponding scope.
   1.259 +   */
   1.260 +  focusNode: function (id) {
   1.261 +    let scope = this._getScopeByID(id);
   1.262 +    if (!scope) return;
   1.263 +
   1.264 +    scope.focus();
   1.265 +    scope.expand();
   1.266 +  },
   1.267 +
   1.268 +  /**
   1.269 +   * Executed when an audio param is changed in the UI.
   1.270 +   */
   1.271 +  _onEval: Task.async(function* (variable, value) {
   1.272 +    let ownerScope = variable.ownerView;
   1.273 +    let node = getViewNodeById(ownerScope.actorID);
   1.274 +    let propName = variable.name;
   1.275 +    let errorMessage = yield node.actor.setParam(propName, value);
   1.276 +
   1.277 +    // TODO figure out how to handle and display set param errors
   1.278 +    // and enable `test/brorwser_wa_params_view_edit_error.js`
   1.279 +    // Bug 994258
   1.280 +    if (!errorMessage) {
   1.281 +      ownerScope.get(propName).setGrip(value);
   1.282 +      window.emit(EVENTS.UI_SET_PARAM, node.id, propName, value);
   1.283 +    } else {
   1.284 +      window.emit(EVENTS.UI_SET_PARAM_ERROR, node.id, propName, value);
   1.285 +    }
   1.286 +  }),
   1.287 +
   1.288 +  /**
   1.289 +   * Takes an `id` and returns the corresponding variables scope.
   1.290 +   */
   1.291 +  _getScopeByID: function (id) {
   1.292 +    let view = this._paramsView;
   1.293 +    for (let i = 0; i < view._store.length; i++) {
   1.294 +      let scope = view.getScopeAtIndex(i);
   1.295 +      if (scope.actorID === id)
   1.296 +        return scope;
   1.297 +    }
   1.298 +    return null;
   1.299 +  },
   1.300 +
   1.301 +  /**
   1.302 +   * Called when hovering over a variable scope.
   1.303 +   */
   1.304 +  _onMouseOver: function (e) {
   1.305 +    let id = WebAudioParamView._getScopeID(this);
   1.306 +
   1.307 +    if (!id) return;
   1.308 +
   1.309 +    WebAudioGraphView.focusNode(id);
   1.310 +  },
   1.311 +
   1.312 +  /**
   1.313 +   * Called when hovering out of a variable scope.
   1.314 +   */
   1.315 +  _onMouseOut: function (e) {
   1.316 +    let id = WebAudioParamView._getScopeID(this);
   1.317 +
   1.318 +    if (!id) return;
   1.319 +
   1.320 +    WebAudioGraphView.blurNode(id);
   1.321 +  },
   1.322 +
   1.323 +  /**
   1.324 +   * Uses in event handlers, takes an element `$el` and finds the
   1.325 +   * associated actor ID with that variable scope to be used in other contexts.
   1.326 +   */
   1.327 +  _getScopeID: function ($el) {
   1.328 +    let match = $el.parentNode.id.match(/\(([^\)]*)\)/);
   1.329 +    return match ? match[1] : null;
   1.330 +  },
   1.331 +
   1.332 +  /**
   1.333 +   * Called when `CREATE_NODE` is fired to update the params view with the
   1.334 +   * freshly created audio node.
   1.335 +   */
   1.336 +  addNode: Task.async(function* (_, id) {
   1.337 +    let viewNode = getViewNodeById(id);
   1.338 +    let type = viewNode.type;
   1.339 +
   1.340 +    let audioParamsTitle = type + " (" + id + ")";
   1.341 +    let paramsView = this._paramsView;
   1.342 +    let paramsScopeView = paramsView.addScope(audioParamsTitle);
   1.343 +
   1.344 +    paramsScopeView.actorID = id;
   1.345 +    paramsScopeView.expanded = false;
   1.346 +
   1.347 +    paramsScopeView.addEventListener("mouseover", this._onMouseOver, false);
   1.348 +    paramsScopeView.addEventListener("mouseout", this._onMouseOut, false);
   1.349 +
   1.350 +    let params = yield viewNode.getParams();
   1.351 +    params.forEach(({ param, value }) => {
   1.352 +      let descriptor = { value: value };
   1.353 +      paramsScopeView.addItem(param, descriptor);
   1.354 +    });
   1.355 +
   1.356 +    window.emit(EVENTS.UI_ADD_NODE_LIST, id);
   1.357 +  }),
   1.358 +
   1.359 +  /**
   1.360 +   * Called when `DESTROY_NODE` is fired to remove the node from params view.
   1.361 +   * TODO bug 994263, dependent on node GC events
   1.362 +   */
   1.363 +  removeNode: Task.async(function* (viewNode) {
   1.364 +
   1.365 +  })
   1.366 +};

mercurial