summaryrefslogtreecommitdiffstats
path: root/devtools/client/webaudioeditor/views
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webaudioeditor/views')
-rw-r--r--devtools/client/webaudioeditor/views/automation.js159
-rw-r--r--devtools/client/webaudioeditor/views/context.js314
-rw-r--r--devtools/client/webaudioeditor/views/inspector.js189
-rw-r--r--devtools/client/webaudioeditor/views/properties.js163
-rw-r--r--devtools/client/webaudioeditor/views/utils.js103
5 files changed, 928 insertions, 0 deletions
diff --git a/devtools/client/webaudioeditor/views/automation.js b/devtools/client/webaudioeditor/views/automation.js
new file mode 100644
index 000000000..2fab262bd
--- /dev/null
+++ b/devtools/client/webaudioeditor/views/automation.js
@@ -0,0 +1,159 @@
+/* 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";
+
+/**
+ * Functions handling the audio node inspector UI.
+ */
+
+var AutomationView = {
+
+ /**
+ * Initialization function called when the tool starts up.
+ */
+ initialize: function () {
+ this._buttons = $("#automation-param-toolbar-buttons");
+ this.graph = new LineGraphWidget($("#automation-graph"), { avg: false });
+ this.graph.selectionEnabled = false;
+
+ this._onButtonClick = this._onButtonClick.bind(this);
+ this._onNodeSet = this._onNodeSet.bind(this);
+ this._onResize = this._onResize.bind(this);
+
+ this._buttons.addEventListener("click", this._onButtonClick);
+ window.on(EVENTS.UI_INSPECTOR_RESIZE, this._onResize);
+ window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
+ },
+
+ /**
+ * Destruction function called when the tool cleans up.
+ */
+ destroy: function () {
+ this._buttons.removeEventListener("click", this._onButtonClick);
+ window.off(EVENTS.UI_INSPECTOR_RESIZE, this._onResize);
+ window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
+ },
+
+ /**
+ * Empties out the props view.
+ */
+ resetUI: function () {
+ this._currentNode = null;
+ },
+
+ /**
+ * On a new node selection, create the Automation panel for
+ * that specific node.
+ */
+ build: Task.async(function* () {
+ let node = this._currentNode;
+
+ let props = yield node.getParams();
+ let params = props.filter(({ flags }) => flags && flags.param);
+
+ this._createParamButtons(params);
+
+ this._selectedParamName = params[0] ? params[0].param : null;
+ this.render();
+ }),
+
+ /**
+ * Renders the graph for specified `paramName`. Called when
+ * the parameter view is changed, or when new param data events
+ * are fired for the currently specified param.
+ */
+ render: Task.async(function* () {
+ let node = this._currentNode;
+ let paramName = this._selectedParamName;
+ // Escape if either node or parameter name does not exist.
+ if (!node || !paramName) {
+ this._setState("no-params");
+ window.emit(EVENTS.UI_AUTOMATION_TAB_RENDERED, null);
+ return;
+ }
+
+ let { values, events } = yield node.getAutomationData(paramName);
+ this._setState(events.length ? "show" : "no-events");
+ yield this.graph.setDataWhenReady(values);
+ window.emit(EVENTS.UI_AUTOMATION_TAB_RENDERED, node.id);
+ }),
+
+ /**
+ * Create the buttons for each AudioParam, that when clicked,
+ * render the graph for that AudioParam.
+ */
+ _createParamButtons: function (params) {
+ this._buttons.innerHTML = "";
+ params.forEach((param, i) => {
+ let button = document.createElement("toolbarbutton");
+ button.setAttribute("class", "devtools-toolbarbutton automation-param-button");
+ button.setAttribute("data-param", param.param);
+ // Set label to the parameter name, should not be L10N'd
+ button.setAttribute("label", param.param);
+
+ // If first button, set to 'selected' for styling
+ if (i === 0) {
+ button.setAttribute("selected", true);
+ }
+
+ this._buttons.appendChild(button);
+ });
+ },
+
+ /**
+ * Internally sets the current audio node and rebuilds appropriate
+ * views.
+ */
+ _setAudioNode: function (node) {
+ this._currentNode = node;
+ if (this._currentNode) {
+ this.build();
+ }
+ },
+
+ /**
+ * Toggles the subviews to display messages whether or not
+ * the audio node has no AudioParams, no automation events, or
+ * shows the graph.
+ */
+ _setState: function (state) {
+ let contentView = $("#automation-content");
+ let emptyView = $("#automation-empty");
+
+ let graphView = $("#automation-graph-container");
+ let noEventsView = $("#automation-no-events");
+
+ contentView.hidden = state === "no-params";
+ emptyView.hidden = state !== "no-params";
+
+ graphView.hidden = state !== "show";
+ noEventsView.hidden = state !== "no-events";
+ },
+
+ /**
+ * Event handlers
+ */
+
+ _onButtonClick: function (e) {
+ Array.forEach($$(".automation-param-button"), $btn => $btn.removeAttribute("selected"));
+ let paramName = e.target.getAttribute("data-param");
+ e.target.setAttribute("selected", true);
+ this._selectedParamName = paramName;
+ this.render();
+ },
+
+ /**
+ * Called when the inspector is resized.
+ */
+ _onResize: function () {
+ this.graph.refresh();
+ },
+
+ /**
+ * Called when the inspector view determines a node is selected.
+ */
+ _onNodeSet: function (_, id) {
+ this._setAudioNode(id != null ? gAudioNodes.get(id) : null);
+ }
+};
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);
+ }
+};
diff --git a/devtools/client/webaudioeditor/views/inspector.js b/devtools/client/webaudioeditor/views/inspector.js
new file mode 100644
index 000000000..1f50bb137
--- /dev/null
+++ b/devtools/client/webaudioeditor/views/inspector.js
@@ -0,0 +1,189 @@
+/* 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 MIN_INSPECTOR_WIDTH = 300;
+
+// Strings for rendering
+const EXPAND_INSPECTOR_STRING = L10N.getStr("expandInspector");
+const COLLAPSE_INSPECTOR_STRING = L10N.getStr("collapseInspector");
+
+/**
+ * Functions handling the audio node inspector UI.
+ */
+
+var InspectorView = {
+ _currentNode: null,
+
+ // Set up config for view toggling
+ _collapseString: COLLAPSE_INSPECTOR_STRING,
+ _expandString: EXPAND_INSPECTOR_STRING,
+ _toggleEvent: EVENTS.UI_INSPECTOR_TOGGLED,
+ _animated: true,
+ _delayed: true,
+
+ /**
+ * Initialization function called when the tool starts up.
+ */
+ initialize: function () {
+ // Set up view controller
+ this.el = $("#web-audio-inspector");
+ this.splitter = $("#inspector-splitter");
+ this.el.setAttribute("width", Services.prefs.getIntPref("devtools.webaudioeditor.inspectorWidth"));
+ this.button = $("#inspector-pane-toggle");
+ mixin(this, ToggleMixin);
+ this.bindToggle();
+
+ // Hide inspector view on startup
+ this.hideImmediately();
+
+ this._onNodeSelect = this._onNodeSelect.bind(this);
+ this._onDestroyNode = this._onDestroyNode.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this._onCommandClick = this._onCommandClick.bind(this);
+
+ this.splitter.addEventListener("mouseup", this._onResize);
+ for (let $el of $$("#audio-node-toolbar toolbarbutton")) {
+ $el.addEventListener("command", this._onCommandClick);
+ }
+ window.on(EVENTS.UI_SELECT_NODE, this._onNodeSelect);
+ gAudioNodes.on("remove", this._onDestroyNode);
+ },
+
+ /**
+ * Destruction function called when the tool cleans up.
+ */
+ destroy: function () {
+ this.unbindToggle();
+ this.splitter.removeEventListener("mouseup", this._onResize);
+
+ $("#audio-node-toolbar toolbarbutton").removeEventListener("command", this._onCommandClick);
+ for (let $el of $$("#audio-node-toolbar toolbarbutton")) {
+ $el.removeEventListener("command", this._onCommandClick);
+ }
+ window.off(EVENTS.UI_SELECT_NODE, this._onNodeSelect);
+ gAudioNodes.off("remove", this._onDestroyNode);
+
+ this.el = null;
+ this.button = null;
+ this.splitter = null;
+ },
+
+ /**
+ * Takes a AudioNodeView `node` and sets it as the current
+ * node and scaffolds the inspector view based off of the new node.
+ */
+ setCurrentAudioNode: Task.async(function* (node) {
+ this._currentNode = node || null;
+
+ // If no node selected, set the inspector back to "no AudioNode selected"
+ // view.
+ if (!node) {
+ $("#web-audio-editor-details-pane-empty").removeAttribute("hidden");
+ $("#web-audio-editor-tabs").setAttribute("hidden", "true");
+ window.emit(EVENTS.UI_INSPECTOR_NODE_SET, null);
+ }
+ // Otherwise load up the tabs view and hide the empty placeholder
+ else {
+ $("#web-audio-editor-details-pane-empty").setAttribute("hidden", "true");
+ $("#web-audio-editor-tabs").removeAttribute("hidden");
+ this._buildToolbar();
+ window.emit(EVENTS.UI_INSPECTOR_NODE_SET, this._currentNode.id);
+ }
+ }),
+
+ /**
+ * Returns the current AudioNodeView.
+ */
+ getCurrentAudioNode: function () {
+ return this._currentNode;
+ },
+
+ /**
+ * Empties out the props view.
+ */
+ resetUI: function () {
+ // Set current node to empty to load empty view
+ this.setCurrentAudioNode();
+
+ // Reset AudioNode inspector and hide
+ this.hideImmediately();
+ },
+
+ _buildToolbar: function () {
+ let node = this.getCurrentAudioNode();
+
+ let bypassable = node.bypassable;
+ let bypassed = node.isBypassed();
+ let button = $("#audio-node-toolbar .bypass");
+
+ if (!bypassable) {
+ button.setAttribute("disabled", true);
+ } else {
+ button.removeAttribute("disabled");
+ }
+
+ if (!bypassable || bypassed) {
+ button.removeAttribute("checked");
+ } else {
+ button.setAttribute("checked", true);
+ }
+ },
+
+ /**
+ * Event handlers
+ */
+
+ /**
+ * Called on EVENTS.UI_SELECT_NODE, and takes an actorID `id`
+ * and calls `setCurrentAudioNode` to scaffold the inspector view.
+ */
+ _onNodeSelect: function (_, id) {
+ this.setCurrentAudioNode(gAudioNodes.get(id));
+
+ // Ensure inspector is visible when selecting a new node
+ this.show();
+ },
+
+ _onResize: function () {
+ if (this.el.getAttribute("width") < MIN_INSPECTOR_WIDTH) {
+ this.el.setAttribute("width", MIN_INSPECTOR_WIDTH);
+ }
+ Services.prefs.setIntPref("devtools.webaudioeditor.inspectorWidth", this.el.getAttribute("width"));
+ window.emit(EVENTS.UI_INSPECTOR_RESIZE);
+ },
+
+ /**
+ * Called when `DESTROY_NODE` is fired to remove the node from props view if
+ * it's currently selected.
+ */
+ _onDestroyNode: function (node) {
+ if (this._currentNode && this._currentNode.id === node.id) {
+ this.setCurrentAudioNode(null);
+ }
+ },
+
+ _onCommandClick: function (e) {
+ let node = this.getCurrentAudioNode();
+ let button = e.target;
+ let command = button.getAttribute("data-command");
+ let checked = button.getAttribute("checked");
+
+ if (button.getAttribute("disabled")) {
+ return;
+ }
+
+ if (command === "bypass") {
+ if (checked) {
+ button.removeAttribute("checked");
+ node.bypass(true);
+ } else {
+ button.setAttribute("checked", true);
+ node.bypass(false);
+ }
+ }
+ }
+};
diff --git a/devtools/client/webaudioeditor/views/properties.js b/devtools/client/webaudioeditor/views/properties.js
new file mode 100644
index 000000000..efd691e5a
--- /dev/null
+++ b/devtools/client/webaudioeditor/views/properties.js
@@ -0,0 +1,163 @@
+/* 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";
+
+const { VariablesView } = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
+
+const GENERIC_VARIABLES_VIEW_SETTINGS = {
+ searchEnabled: false,
+ editableValueTooltip: "",
+ editableNameTooltip: "",
+ preventDisableOnChange: true,
+ preventDescriptorModifiers: false,
+ eval: () => {}
+};
+
+/**
+ * Functions handling the audio node inspector UI.
+ */
+
+var PropertiesView = {
+
+ /**
+ * Initialization function called when the tool starts up.
+ */
+ initialize: function () {
+ this._onEval = this._onEval.bind(this);
+ this._onNodeSet = this._onNodeSet.bind(this);
+
+ window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
+ this._propsView = new VariablesView($("#properties-content"), GENERIC_VARIABLES_VIEW_SETTINGS);
+ this._propsView.eval = this._onEval;
+ },
+
+ /**
+ * Destruction function called when the tool cleans up.
+ */
+ destroy: function () {
+ window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
+ this._propsView = null;
+ },
+
+ /**
+ * Empties out the props view.
+ */
+ resetUI: function () {
+ this._propsView.empty();
+ this._currentNode = null;
+ },
+
+ /**
+ * Internally sets the current audio node and rebuilds appropriate
+ * views.
+ */
+ _setAudioNode: function (node) {
+ this._currentNode = node;
+ if (this._currentNode) {
+ this._buildPropertiesView();
+ }
+ },
+
+ /**
+ * Reconstructs the `Properties` tab in the inspector
+ * with the `this._currentNode` as it's source.
+ */
+ _buildPropertiesView: Task.async(function* () {
+ let propsView = this._propsView;
+ let node = this._currentNode;
+ propsView.empty();
+
+ let audioParamsScope = propsView.addScope("AudioParams");
+ let props = yield node.getParams();
+
+ // Disable AudioParams VariableView expansion
+ // when there are no props i.e. AudioDestinationNode
+ this._togglePropertiesView(!!props.length);
+
+ props.forEach(({ param, value, flags }) => {
+ let descriptor = {
+ value: value,
+ writable: !flags || !flags.readonly,
+ };
+ let item = audioParamsScope.addItem(param, descriptor);
+
+ // No items should currently display a dropdown
+ item.twisty = false;
+ });
+
+ audioParamsScope.expanded = true;
+
+ window.emit(EVENTS.UI_PROPERTIES_TAB_RENDERED, node.id);
+ }),
+
+ /**
+ * Toggles the display of the "empty" properties view when
+ * node has no properties to display.
+ */
+ _togglePropertiesView: function (show) {
+ let propsView = $("#properties-content");
+ let emptyView = $("#properties-empty");
+ (show ? propsView : emptyView).removeAttribute("hidden");
+ (show ? emptyView : propsView).setAttribute("hidden", "true");
+ },
+
+ /**
+ * Returns the scope for AudioParams in the
+ * VariablesView.
+ *
+ * @return Scope
+ */
+ _getAudioPropertiesScope: function () {
+ return this._propsView.getScopeAtIndex(0);
+ },
+
+ /**
+ * Event handlers
+ */
+
+ /**
+ * Called when the inspector view determines a node is selected.
+ */
+ _onNodeSet: function (_, id) {
+ this._setAudioNode(gAudioNodes.get(id));
+ },
+
+ /**
+ * Executed when an audio prop is changed in the UI.
+ */
+ _onEval: Task.async(function* (variable, value) {
+ let ownerScope = variable.ownerView;
+ let node = this._currentNode;
+ let propName = variable.name;
+ let error;
+
+ if (!variable._initialDescriptor.writable) {
+ error = new Error("Variable " + propName + " is not writable.");
+ } else {
+ // Cast value to proper type
+ try {
+ let number = parseFloat(value);
+ if (!isNaN(number)) {
+ value = number;
+ } else {
+ value = JSON.parse(value);
+ }
+ error = yield node.actor.setParam(propName, value);
+ }
+ catch (e) {
+ error = e;
+ }
+ }
+
+ // TODO figure out how to handle and display set prop errors
+ // and enable `test/brorwser_wa_properties-view-edit.js`
+ // Bug 994258
+ if (!error) {
+ 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);
+ }
+ })
+};
diff --git a/devtools/client/webaudioeditor/views/utils.js b/devtools/client/webaudioeditor/views/utils.js
new file mode 100644
index 000000000..6d6a96946
--- /dev/null
+++ b/devtools/client/webaudioeditor/views/utils.js
@@ -0,0 +1,103 @@
+/* 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";
+
+/**
+ * Takes an element in an SVG graph and iterates over
+ * ancestors until it finds the graph node container. If not found,
+ * returns null.
+ */
+
+function findGraphNodeParent(el) {
+ // Some targets may not contain `classList` property
+ if (!el.classList)
+ return null;
+
+ while (!el.classList.contains("nodes")) {
+ if (el.classList.contains("audionode"))
+ return el;
+ else
+ el = el.parentNode;
+ }
+ return null;
+}
+
+/**
+ * Object for use with `mix` into a view.
+ * Must have the following properties defined on the view:
+ * - `el`
+ * - `button`
+ * - `_collapseString`
+ * - `_expandString`
+ * - `_toggleEvent`
+ *
+ * Optional properties on the view can be defined to specify default
+ * visibility options.
+ * - `_animated`
+ * - `_delayed`
+ */
+var ToggleMixin = {
+
+ bindToggle: function () {
+ this._onToggle = this._onToggle.bind(this);
+ this.button.addEventListener("mousedown", this._onToggle, false);
+ },
+
+ unbindToggle: function () {
+ this.button.removeEventListener("mousedown", this._onToggle);
+ },
+
+ show: function () {
+ this._viewController({ visible: true });
+ },
+
+ hide: function () {
+ this._viewController({ visible: false });
+ },
+
+ hideImmediately: function () {
+ this._viewController({ visible: false, delayed: false, animated: false });
+ },
+
+ /**
+ * Returns a boolean indicating whether or not the view.
+ * is currently being shown.
+ */
+ isVisible: function () {
+ return !this.el.classList.contains("pane-collapsed");
+ },
+
+ /**
+ * Toggles the visibility of the view.
+ *
+ * @param object visible
+ * - visible: boolean indicating whether the panel should be shown or not
+ * - animated: boolean indiciating whether the pane should be animated
+ * - delayed: boolean indicating whether the pane's opening should wait
+ * a few cycles or not
+ */
+ _viewController: function ({ visible, animated, delayed }) {
+ let flags = {
+ visible: visible,
+ animated: animated != null ? animated : !!this._animated,
+ delayed: delayed != null ? delayed : !!this._delayed,
+ callback: () => window.emit(this._toggleEvent, visible)
+ };
+
+ ViewHelpers.togglePane(flags, this.el);
+
+ if (flags.visible) {
+ this.button.classList.remove("pane-collapsed");
+ this.button.setAttribute("tooltiptext", this._collapseString);
+ }
+ else {
+ this.button.classList.add("pane-collapsed");
+ this.button.setAttribute("tooltiptext", this._expandString);
+ }
+ },
+
+ _onToggle: function () {
+ this._viewController({ visible: !this.isVisible() });
+ }
+};