diff options
Diffstat (limited to 'devtools/client/webaudioeditor/views')
-rw-r--r-- | devtools/client/webaudioeditor/views/automation.js | 159 | ||||
-rw-r--r-- | devtools/client/webaudioeditor/views/context.js | 314 | ||||
-rw-r--r-- | devtools/client/webaudioeditor/views/inspector.js | 189 | ||||
-rw-r--r-- | devtools/client/webaudioeditor/views/properties.js | 163 | ||||
-rw-r--r-- | devtools/client/webaudioeditor/views/utils.js | 103 |
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() }); + } +}; |