summaryrefslogtreecommitdiffstats
path: root/devtools/client/webaudioeditor/views/context.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webaudioeditor/views/context.js')
-rw-r--r--devtools/client/webaudioeditor/views/context.js314
1 files changed, 314 insertions, 0 deletions
diff --git a/devtools/client/webaudioeditor/views/context.js b/devtools/client/webaudioeditor/views/context.js
new file mode 100644
index 000000000..69ecc141e
--- /dev/null
+++ b/devtools/client/webaudioeditor/views/context.js
@@ -0,0 +1,314 @@
+/* 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";
+
+/* import-globals-from ../includes.js */
+
+const { debounce } = require("sdk/lang/functional");
+const flags = require("devtools/shared/flags");
+
+// Globals for d3 stuff
+// Default properties of the graph on rerender
+const GRAPH_DEFAULTS = {
+ translate: [20, 20],
+ scale: 1
+};
+
+// Sizes of SVG arrows in graph
+const ARROW_HEIGHT = 5;
+const ARROW_WIDTH = 8;
+
+// Styles for markers as they cannot be done with CSS.
+const MARKER_STYLING = {
+ light: "#AAA",
+ dark: "#CED3D9"
+};
+Object.defineProperty(this, "MARKER_STYLING", {
+ value: MARKER_STYLING,
+ enumerable: true,
+ writable: false
+});
+
+const GRAPH_DEBOUNCE_TIMER = 100;
+
+// `gAudioNodes` events that should require the graph
+// to redraw
+const GRAPH_REDRAW_EVENTS = ["add", "connect", "disconnect", "remove"];
+
+/**
+ * Functions handling the graph UI.
+ */
+var ContextView = {
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: function () {
+ this._onGraphClick = this._onGraphClick.bind(this);
+ this._onThemeChange = this._onThemeChange.bind(this);
+ this._onStartContext = this._onStartContext.bind(this);
+ this._onEvent = this._onEvent.bind(this);
+
+ this.draw = debounce(this.draw.bind(this), GRAPH_DEBOUNCE_TIMER);
+ $("#graph-target").addEventListener("click", this._onGraphClick, false);
+
+ window.on(EVENTS.THEME_CHANGE, this._onThemeChange);
+ window.on(EVENTS.START_CONTEXT, this._onStartContext);
+ gAudioNodes.on("*", this._onEvent);
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function () {
+ // If the graph was rendered at all, then the handler
+ // for zooming in will be set. We must remove it to prevent leaks.
+ if (this._zoomBinding) {
+ this._zoomBinding.on("zoom", null);
+ }
+ $("#graph-target").removeEventListener("click", this._onGraphClick, false);
+
+ window.off(EVENTS.THEME_CHANGE, this._onThemeChange);
+ window.off(EVENTS.START_CONTEXT, this._onStartContext);
+ gAudioNodes.off("*", this._onEvent);
+ },
+
+ /**
+ * Called when a page is reloaded and waiting for a "start-context" event
+ * and clears out old content
+ */
+ resetUI: function () {
+ this.clearGraph();
+ this.resetGraphTransform();
+ },
+
+ /**
+ * Clears out the rendered graph, called when resetting the SVG elements to draw again,
+ * or when resetting the entire UI tool
+ */
+ clearGraph: function () {
+ $("#graph-target").innerHTML = "";
+ },
+
+ /**
+ * Moves the graph back to its original scale and translation.
+ */
+ resetGraphTransform: function () {
+ // Only reset if the graph was ever drawn.
+ if (this._zoomBinding) {
+ let { translate, scale } = GRAPH_DEFAULTS;
+ // Must set the `zoomBinding` so the next `zoom` event is in sync with
+ // where the graph is visually (set by the `transform` attribute).
+ this._zoomBinding.scale(scale);
+ this._zoomBinding.translate(translate);
+ d3.select("#graph-target")
+ .attr("transform", "translate(" + translate + ") scale(" + scale + ")");
+ }
+ },
+
+ getCurrentScale: function () {
+ return this._zoomBinding ? this._zoomBinding.scale() : null;
+ },
+
+ getCurrentTranslation: function () {
+ return this._zoomBinding ? this._zoomBinding.translate() : null;
+ },
+
+ /**
+ * Makes the corresponding graph node appear "focused", removing
+ * focused styles from all other nodes. If no `actorID` specified,
+ * make all nodes appear unselected.
+ */
+ focusNode: function (actorID) {
+ // Remove class "selected" from all nodes
+ Array.forEach($$(".nodes > g"), $node => $node.classList.remove("selected"));
+ // Add to "selected"
+ if (actorID) {
+ this._getNodeByID(actorID).classList.add("selected");
+ }
+ },
+
+ /**
+ * Takes an actorID and returns the corresponding DOM SVG element in the graph
+ */
+ _getNodeByID: function (actorID) {
+ return $(".nodes > g[data-id='" + actorID + "']");
+ },
+
+ /**
+ * Sets the appropriate class on an SVG node when its bypass
+ * status is toggled.
+ */
+ _bypassNode: function (node, enabled) {
+ let el = this._getNodeByID(node.id);
+ el.classList[enabled ? "add" : "remove"]("bypassed");
+ },
+
+ /**
+ * This method renders the nodes currently available in `gAudioNodes` and is
+ * throttled to be called at most every `GRAPH_DEBOUNCE_TIMER` milliseconds.
+ * It's called whenever the audio context routing changes, after being debounced.
+ */
+ draw: function () {
+ // Clear out previous SVG information
+ this.clearGraph();
+
+ let graph = new dagreD3.Digraph();
+ let renderer = new dagreD3.Renderer();
+ gAudioNodes.populateGraph(graph);
+
+ // Post-render manipulation of the nodes
+ let oldDrawNodes = renderer.drawNodes();
+ renderer.drawNodes(function (graph, root) {
+ let svgNodes = oldDrawNodes(graph, root);
+ svgNodes.each(function (n) {
+ let node = graph.node(n);
+ let classString = "audionode type-" + node.type + (node.bypassed ? " bypassed" : "");
+ this.setAttribute("class", classString);
+ this.setAttribute("data-id", node.id);
+ this.setAttribute("data-type", node.type);
+ });
+ return svgNodes;
+ });
+
+ // Post-render manipulation of edges
+ let oldDrawEdgePaths = renderer.drawEdgePaths();
+ let defaultClasses = "edgePath enter";
+
+ renderer.drawEdgePaths(function (graph, root) {
+ let svgEdges = oldDrawEdgePaths(graph, root);
+ svgEdges.each(function (e) {
+ let edge = graph.edge(e);
+
+ // We have to manually specify the default classes on the edges
+ // as to not overwrite them
+ let edgeClass = defaultClasses + (edge.param ? (" param-connection " + edge.param) : "");
+
+ this.setAttribute("data-source", edge.source);
+ this.setAttribute("data-target", edge.target);
+ this.setAttribute("data-param", edge.param ? edge.param : null);
+ this.setAttribute("class", edgeClass);
+ });
+
+ return svgEdges;
+ });
+
+ // Override Dagre-d3's post render function by passing in our own.
+ // This way we can leave styles out of it.
+ renderer.postRender((graph, root) => {
+ // We have to manually set the marker styling since we cannot
+ // do this currently with CSS, although it is in spec for SVG2
+ // https://svgwg.org/svg2-draft/painting.html#VertexMarkerProperties
+ // For now, manually set it on creation, and the `_onThemeChange`
+ // function will fire when the devtools theme changes to update the
+ // styling manually.
+ let theme = Services.prefs.getCharPref("devtools.theme");
+ let markerColor = MARKER_STYLING[theme];
+ 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: " + markerColor)
+ .append("svg:path")
+ .attr("d", "M 0 0 L 10 5 L 0 10 z");
+ }
+
+ // Reselect the previously selected audio node
+ let currentNode = InspectorView.getCurrentAudioNode();
+ if (currentNode) {
+ this.focusNode(currentNode.id);
+ }
+
+ // Fire an event upon completed rendering, with extra information
+ // if in testing mode only.
+ let info = {};
+ if (flags.testing) {
+ info = gAudioNodes.getInfo();
+ }
+ window.emit(EVENTS.UI_GRAPH_RENDERED, info.nodes, info.edges, info.paramEdges);
+ });
+
+ 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);
+
+ // Set initial translation and scale -- this puts D3's awareness of
+ // the graph in sync with what the user sees originally.
+ this.resetGraphTransform();
+ }
+ },
+
+ /**
+ * Event handlers
+ */
+
+ /**
+ * Called once "start-context" is fired, indicating that there is an audio
+ * context being created to view so render the graph.
+ */
+ _onStartContext: function () {
+ this.draw();
+ },
+
+ /**
+ * Called when `gAudioNodes` fires an event -- most events (listed
+ * in GRAPH_REDRAW_EVENTS) qualify as a redraw event.
+ */
+ _onEvent: function (eventName, ...args) {
+ // If bypassing, just toggle the class on the SVG node
+ // rather than rerendering everything
+ if (eventName === "bypass") {
+ this._bypassNode.apply(this, args);
+ }
+ if (~GRAPH_REDRAW_EVENTS.indexOf(eventName)) {
+ this.draw();
+ }
+ },
+
+ /**
+ * Fired when the devtools theme changes.
+ */
+ _onThemeChange: function (eventName, theme) {
+ let markerColor = MARKER_STYLING[theme];
+ let marker = $("#arrowhead");
+ if (marker) {
+ marker.setAttribute("style", "fill: " + markerColor);
+ }
+ },
+
+ /**
+ * Fired when a click occurs in the graph.
+ *
+ * @param Event e
+ * Click event.
+ */
+ _onGraphClick: function (e) {
+ let node = findGraphNodeParent(e.target);
+ // If node not found (clicking outside of an audio node in the graph),
+ // then ignore this event
+ if (!node)
+ return;
+
+ let id = node.getAttribute("data-id");
+
+ this.focusNode(id);
+ window.emit(EVENTS.UI_SELECT_NODE, id);
+ }
+};