mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-06 17:16:12 +00:00
364 lines
11 KiB
JavaScript
364 lines
11 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
"use strict";
|
|
|
|
Cu.import("resource:///modules/devtools/VariablesView.jsm");
|
|
Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
|
|
const { debounce } = require("sdk/lang/functional");
|
|
|
|
// Globals for d3 stuff
|
|
// Width/height in pixels of SVG graph
|
|
// TODO investigate to see how this works in other host types bug 994257
|
|
const WIDTH = 1000;
|
|
const HEIGHT = 400;
|
|
|
|
// Sizes of SVG arrows in graph
|
|
const ARROW_HEIGHT = 5;
|
|
const ARROW_WIDTH = 8;
|
|
|
|
const GRAPH_DEBOUNCE_TIMER = 100;
|
|
|
|
const GENERIC_VARIABLES_VIEW_SETTINGS = {
|
|
lazyEmpty: true,
|
|
lazyEmptyDelay: 10, // ms
|
|
searchEnabled: false,
|
|
editableValueTooltip: "",
|
|
editableNameTooltip: "",
|
|
preventDisableOnChange: true,
|
|
preventDescriptorModifiers: true,
|
|
eval: () => {}
|
|
};
|
|
|
|
/**
|
|
* Functions handling the graph UI.
|
|
*/
|
|
let WebAudioGraphView = {
|
|
/**
|
|
* Initialization function, called when the tool is started.
|
|
*/
|
|
initialize: function() {
|
|
this._onGraphNodeClick = this._onGraphNodeClick.bind(this);
|
|
this.draw = debounce(this.draw.bind(this), GRAPH_DEBOUNCE_TIMER);
|
|
},
|
|
|
|
/**
|
|
* Destruction function, called when the tool is closed.
|
|
*/
|
|
destroy: function() {
|
|
if (this._zoomBinding) {
|
|
this._zoomBinding.on("zoom", null);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called when a page is reloaded and waiting for a "start-context" event
|
|
* and clears out old content
|
|
*/
|
|
resetUI: function () {
|
|
$("#reload-notice").hidden = true;
|
|
$("#waiting-notice").hidden = false;
|
|
$("#content").hidden = true;
|
|
this.resetGraph();
|
|
},
|
|
|
|
/**
|
|
* Called once "start-context" is fired, indicating that there is audio context
|
|
* activity to view and inspect
|
|
*/
|
|
showContent: function () {
|
|
$("#reload-notice").hidden = true;
|
|
$("#waiting-notice").hidden = true;
|
|
$("#content").hidden = false;
|
|
this.draw();
|
|
},
|
|
|
|
/**
|
|
* Clears out the rendered graph, called when resetting the SVG elements to draw again,
|
|
* or when resetting the entire UI tool
|
|
*/
|
|
resetGraph: function () {
|
|
$("#graph-target").innerHTML = "";
|
|
},
|
|
|
|
/**
|
|
* Makes the corresponding graph node appear "focused", called from WebAudioParamView
|
|
*/
|
|
focusNode: function (actorID) {
|
|
// Remove class "selected" from all nodes
|
|
Array.prototype.forEach.call($$(".nodes > g"), $node => $node.classList.remove("selected"));
|
|
// Add to "selected"
|
|
this._getNodeByID(actorID).classList.add("selected");
|
|
},
|
|
|
|
/**
|
|
* Unfocuses the corresponding graph node, called from WebAudioParamView
|
|
*/
|
|
blurNode: function (actorID) {
|
|
this._getNodeByID(actorID).classList.remove("selected");
|
|
},
|
|
|
|
/**
|
|
* Takes an actorID and returns the corresponding DOM SVG element in the graph
|
|
*/
|
|
_getNodeByID: function (actorID) {
|
|
return $(".nodes > g[data-id='" + actorID + "']");
|
|
},
|
|
|
|
/**
|
|
* `draw` renders the ViewNodes currently available in `AudioNodes` with `AudioNodeConnections`,
|
|
* and is throttled to be called at most every `GRAPH_DEBOUNCE_TIMER` milliseconds. Is called
|
|
* whenever the audio context routing changes, after being debounced.
|
|
*/
|
|
draw: function () {
|
|
// Clear out previous SVG information
|
|
this.resetGraph();
|
|
|
|
let graph = new dagreD3.Digraph();
|
|
let edges = [];
|
|
|
|
AudioNodes.forEach(node => {
|
|
// Add node to graph
|
|
graph.addNode(node.id, { label: node.type, id: node.id });
|
|
|
|
// Add all of the connections from this node to the edge array to be added
|
|
// after all the nodes are added, otherwise edges will attempted to be created
|
|
// for nodes that have not yet been added
|
|
AudioNodeConnections.get(node, []).forEach(dest => edges.push([node, dest]));
|
|
});
|
|
|
|
edges.forEach(([node, dest]) => graph.addEdge(null, node.id, dest.id, {
|
|
source: node.id,
|
|
target: dest.id
|
|
}));
|
|
|
|
let renderer = new dagreD3.Renderer();
|
|
|
|
// Post-render manipulation of the nodes
|
|
let oldDrawNodes = renderer.drawNodes();
|
|
renderer.drawNodes(function(graph, root) {
|
|
let svgNodes = oldDrawNodes(graph, root);
|
|
svgNodes.attr("class", (n) => {
|
|
let node = graph.node(n);
|
|
return "type-" + node.label;
|
|
});
|
|
svgNodes.attr("data-id", (n) => {
|
|
let node = graph.node(n);
|
|
return node.id;
|
|
});
|
|
return svgNodes;
|
|
});
|
|
|
|
// Post-render manipulation of edges
|
|
let oldDrawEdgePaths = renderer.drawEdgePaths();
|
|
renderer.drawEdgePaths(function(graph, root) {
|
|
let svgNodes = oldDrawEdgePaths(graph, root);
|
|
svgNodes.attr("data-source", (n) => {
|
|
let edge = graph.edge(n);
|
|
return edge.source;
|
|
});
|
|
svgNodes.attr("data-target", (n) => {
|
|
let edge = graph.edge(n);
|
|
return edge.target;
|
|
});
|
|
return svgNodes;
|
|
});
|
|
|
|
// Override Dagre-d3's post render function by passing in our own.
|
|
// This way we can leave styles out of it.
|
|
renderer.postRender(function (graph, root) {
|
|
// TODO change arrowhead color depending on theme-dark/theme-light
|
|
// and possibly refactor rendering this as it's ugly
|
|
// Bug 994256
|
|
// let color = window.classList.contains("theme-dark") ? "#f5f7fa" : "#585959";
|
|
if (graph.isDirected() && root.select("#arrowhead").empty()) {
|
|
root
|
|
.append("svg:defs")
|
|
.append("svg:marker")
|
|
.attr("id", "arrowhead")
|
|
.attr("viewBox", "0 0 10 10")
|
|
.attr("refX", ARROW_WIDTH)
|
|
.attr("refY", ARROW_HEIGHT)
|
|
.attr("markerUnits", "strokewidth")
|
|
.attr("markerWidth", ARROW_WIDTH)
|
|
.attr("markerHeight", ARROW_HEIGHT)
|
|
.attr("orient", "auto")
|
|
.attr("style", "fill: #f5f7fa")
|
|
.append("svg:path")
|
|
.attr("d", "M 0 0 L 10 5 L 0 10 z");
|
|
}
|
|
|
|
// Fire an event upon completed rendering
|
|
window.emit(EVENTS.UI_GRAPH_RENDERED, AudioNodes.length, edges.length);
|
|
});
|
|
|
|
let layout = dagreD3.layout().rankDir("LR");
|
|
renderer.layout(layout).run(graph, d3.select("#graph-target"));
|
|
|
|
// Handle the sliding and zooming of the graph,
|
|
// store as `this._zoomBinding` so we can unbind during destruction
|
|
if (!this._zoomBinding) {
|
|
this._zoomBinding = d3.behavior.zoom().on("zoom", function () {
|
|
var ev = d3.event;
|
|
d3.select("#graph-target")
|
|
.attr("transform", "translate(" + ev.translate + ") scale(" + ev.scale + ")");
|
|
});
|
|
d3.select("svg").call(this._zoomBinding);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Event handlers
|
|
*/
|
|
|
|
/**
|
|
* Fired when a node in the svg graph is clicked. Used to handle triggering the AudioNodePane.
|
|
*
|
|
* @param Object AudioNodeView
|
|
* The object stored in `AudioNodes` which contains render information, but most importantly,
|
|
* the actorID under `id` property.
|
|
*/
|
|
_onGraphNodeClick: function (node) {
|
|
WebAudioParamView.focusNode(node.id);
|
|
}
|
|
};
|
|
|
|
let WebAudioParamView = {
|
|
_paramsView: null,
|
|
|
|
/**
|
|
* Initialization function called when the tool starts up.
|
|
*/
|
|
initialize: function () {
|
|
this._paramsView = new VariablesView($("#web-audio-inspector-content"), GENERIC_VARIABLES_VIEW_SETTINGS);
|
|
this._paramsView.eval = this._onEval.bind(this);
|
|
window.on(EVENTS.CREATE_NODE, this.addNode = this.addNode.bind(this));
|
|
window.on(EVENTS.DESTROY_NODE, this.removeNode = this.removeNode.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Destruction function called when the tool cleans up.
|
|
*/
|
|
destroy: function() {
|
|
window.off(EVENTS.CREATE_NODE, this.addNode);
|
|
window.off(EVENTS.DESTROY_NODE, this.removeNode);
|
|
},
|
|
|
|
/**
|
|
* Empties out the params view.
|
|
*/
|
|
resetUI: function () {
|
|
this._paramsView.empty();
|
|
},
|
|
|
|
/**
|
|
* Takes an `id` and focuses and expands the corresponding scope.
|
|
*/
|
|
focusNode: function (id) {
|
|
let scope = this._getScopeByID(id);
|
|
if (!scope) return;
|
|
|
|
scope.focus();
|
|
scope.expand();
|
|
},
|
|
|
|
/**
|
|
* Executed when an audio param is changed in the UI.
|
|
*/
|
|
_onEval: Task.async(function* (variable, value) {
|
|
let ownerScope = variable.ownerView;
|
|
let node = getViewNodeById(ownerScope.actorID);
|
|
let propName = variable.name;
|
|
let errorMessage = yield node.actor.setParam(propName, value);
|
|
|
|
// TODO figure out how to handle and display set param errors
|
|
// and enable `test/brorwser_wa_params_view_edit_error.js`
|
|
// Bug 994258
|
|
if (!errorMessage) {
|
|
ownerScope.get(propName).setGrip(value);
|
|
window.emit(EVENTS.UI_SET_PARAM, node.id, propName, value);
|
|
} else {
|
|
window.emit(EVENTS.UI_SET_PARAM_ERROR, node.id, propName, value);
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Takes an `id` and returns the corresponding variables scope.
|
|
*/
|
|
_getScopeByID: function (id) {
|
|
let view = this._paramsView;
|
|
for (let i = 0; i < view._store.length; i++) {
|
|
let scope = view.getScopeAtIndex(i);
|
|
if (scope.actorID === id)
|
|
return scope;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Called when hovering over a variable scope.
|
|
*/
|
|
_onMouseOver: function (e) {
|
|
let id = WebAudioParamView._getScopeID(this);
|
|
|
|
if (!id) return;
|
|
|
|
WebAudioGraphView.focusNode(id);
|
|
},
|
|
|
|
/**
|
|
* Called when hovering out of a variable scope.
|
|
*/
|
|
_onMouseOut: function (e) {
|
|
let id = WebAudioParamView._getScopeID(this);
|
|
|
|
if (!id) return;
|
|
|
|
WebAudioGraphView.blurNode(id);
|
|
},
|
|
|
|
/**
|
|
* Uses in event handlers, takes an element `$el` and finds the
|
|
* associated actor ID with that variable scope to be used in other contexts.
|
|
*/
|
|
_getScopeID: function ($el) {
|
|
let match = $el.parentNode.id.match(/\(([^\)]*)\)/);
|
|
return match ? match[1] : null;
|
|
},
|
|
|
|
/**
|
|
* Called when `CREATE_NODE` is fired to update the params view with the
|
|
* freshly created audio node.
|
|
*/
|
|
addNode: Task.async(function* (_, id) {
|
|
let viewNode = getViewNodeById(id);
|
|
let type = viewNode.type;
|
|
|
|
let audioParamsTitle = type + " (" + id + ")";
|
|
let paramsView = this._paramsView;
|
|
let paramsScopeView = paramsView.addScope(audioParamsTitle);
|
|
|
|
paramsScopeView.actorID = id;
|
|
paramsScopeView.expanded = false;
|
|
|
|
paramsScopeView.addEventListener("mouseover", this._onMouseOver, false);
|
|
paramsScopeView.addEventListener("mouseout", this._onMouseOut, false);
|
|
|
|
let params = yield viewNode.getParams();
|
|
params.forEach(({ param, value }) => {
|
|
let descriptor = { value: value };
|
|
paramsScopeView.addItem(param, descriptor);
|
|
});
|
|
|
|
window.emit(EVENTS.UI_ADD_NODE_LIST, id);
|
|
}),
|
|
|
|
/**
|
|
* Called when `DESTROY_NODE` is fired to remove the node from params view.
|
|
* TODO bug 994263, dependent on node GC events
|
|
*/
|
|
removeNode: Task.async(function* (viewNode) {
|
|
|
|
})
|
|
};
|