From d57771a84d0616c238329e3237d619fa08291014 Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Fri, 19 Sep 2014 17:19:00 +0200 Subject: [PATCH 01/56] Bug 1057042 - refactor front end of web audio editor. r=vp --- browser/devtools/jar.mn | 10 +- browser/devtools/webaudioeditor/controller.js | 223 ++++++ browser/devtools/webaudioeditor/includes.js | 98 +++ browser/devtools/webaudioeditor/models.js | 274 ++++++++ browser/devtools/webaudioeditor/panel.js | 1 + .../devtools/webaudioeditor/test/browser.ini | 2 + .../test/browser_wa_destroy-node-01.js | 14 +- .../test/browser_wa_graph-click.js | 23 +- .../test/browser_wa_graph-markers.js | 2 +- .../test/browser_wa_graph-render-01.js | 6 +- .../test/browser_wa_graph-render-02.js | 2 +- .../test/browser_wa_graph-render-05.js | 27 + .../test/browser_wa_graph-zoom.js | 24 +- .../test/browser_wa_inspector-toggle.js | 12 +- .../test/browser_wa_inspector.js | 8 +- .../browser_wa_properties-view-edit-01.js | 4 +- .../browser_wa_properties-view-edit-02.js | 4 +- .../browser_wa_properties-view-media-nodes.js | 4 +- ...owser_wa_properties-view-params-objects.js | 4 +- .../test/browser_wa_properties-view-params.js | 4 +- .../test/browser_wa_properties-view.js | 4 +- .../test/browser_wa_reset-03.js | 14 +- .../test/doc_connect-toggle-param.html | 27 + browser/devtools/webaudioeditor/test/head.js | 9 +- .../devtools/webaudioeditor/views/context.js | 305 +++++++++ .../webaudioeditor/views/inspector.js | 240 +++++++ .../devtools/webaudioeditor/views/utils.js | 103 +++ .../webaudioeditor-controller.js | 428 ------------ .../webaudioeditor/webaudioeditor-view.js | 636 ------------------ .../webaudioeditor/webaudioeditor.xul | 8 +- 30 files changed, 1381 insertions(+), 1139 deletions(-) create mode 100644 browser/devtools/webaudioeditor/controller.js create mode 100644 browser/devtools/webaudioeditor/includes.js create mode 100644 browser/devtools/webaudioeditor/models.js create mode 100644 browser/devtools/webaudioeditor/test/browser_wa_graph-render-05.js create mode 100644 browser/devtools/webaudioeditor/test/doc_connect-toggle-param.html create mode 100644 browser/devtools/webaudioeditor/views/context.js create mode 100644 browser/devtools/webaudioeditor/views/inspector.js create mode 100644 browser/devtools/webaudioeditor/views/utils.js delete mode 100644 browser/devtools/webaudioeditor/webaudioeditor-controller.js delete mode 100644 browser/devtools/webaudioeditor/webaudioeditor-view.js diff --git a/browser/devtools/jar.mn b/browser/devtools/jar.mn index 192ac9ac6af..1b581012a14 100644 --- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -73,11 +73,15 @@ browser.jar: content/browser/devtools/shadereditor.js (shadereditor/shadereditor.js) content/browser/devtools/canvasdebugger.xul (canvasdebugger/canvasdebugger.xul) content/browser/devtools/canvasdebugger.js (canvasdebugger/canvasdebugger.js) - content/browser/devtools/webaudioeditor.xul (webaudioeditor/webaudioeditor.xul) content/browser/devtools/d3.js (shared/d3.js) + content/browser/devtools/webaudioeditor.xul (webaudioeditor/webaudioeditor.xul) content/browser/devtools/dagre-d3.js (webaudioeditor/lib/dagre-d3.js) - content/browser/devtools/webaudioeditor-controller.js (webaudioeditor/webaudioeditor-controller.js) - content/browser/devtools/webaudioeditor-view.js (webaudioeditor/webaudioeditor-view.js) + content/browser/devtools/webaudioeditor/includes.js (webaudioeditor/includes.js) + content/browser/devtools/webaudioeditor/models.js (webaudioeditor/models.js) + content/browser/devtools/webaudioeditor/controller.js (webaudioeditor/controller.js) + content/browser/devtools/webaudioeditor/views/utils.js (webaudioeditor/views/utils.js) + content/browser/devtools/webaudioeditor/views/context.js (webaudioeditor/views/context.js) + content/browser/devtools/webaudioeditor/views/inspector.js (webaudioeditor/views/inspector.js) content/browser/devtools/profiler.xul (profiler/profiler.xul) content/browser/devtools/profiler.js (profiler/profiler.js) content/browser/devtools/ui-recordings.js (profiler/ui-recordings.js) diff --git a/browser/devtools/webaudioeditor/controller.js b/browser/devtools/webaudioeditor/controller.js new file mode 100644 index 00000000000..ee8dc457744 --- /dev/null +++ b/browser/devtools/webaudioeditor/controller.js @@ -0,0 +1,223 @@ +/* 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/. */ + +/** + * A collection of `AudioNodeModel`s used throughout the editor + * to keep track of audio nodes within the audio context. + */ +let gAudioNodes = new AudioNodesCollection(); + +/** + * Initializes the web audio editor views + */ +function startupWebAudioEditor() { + return all([ + WebAudioEditorController.initialize(), + ContextView.initialize(), + InspectorView.initialize() + ]); +} + +/** + * Destroys the web audio editor controller and views. + */ +function shutdownWebAudioEditor() { + return all([ + WebAudioEditorController.destroy(), + ContextView.destroy(), + InspectorView.destroy(), + ]); +} + +/** + * Functions handling target-related lifetime events. + */ +let WebAudioEditorController = { + /** + * Listen for events emitted by the current tab target. + */ + initialize: function() { + telemetry.toolOpened("webaudioeditor"); + this._onTabNavigated = this._onTabNavigated.bind(this); + this._onThemeChange = this._onThemeChange.bind(this); + + gTarget.on("will-navigate", this._onTabNavigated); + gTarget.on("navigate", this._onTabNavigated); + gFront.on("start-context", this._onStartContext); + gFront.on("create-node", this._onCreateNode); + gFront.on("connect-node", this._onConnectNode); + gFront.on("connect-param", this._onConnectParam); + gFront.on("disconnect-node", this._onDisconnectNode); + gFront.on("change-param", this._onChangeParam); + gFront.on("destroy-node", this._onDestroyNode); + + // Hook into theme change so we can change + // the graph's marker styling, since we can't do this + // with CSS + gDevTools.on("pref-changed", this._onThemeChange); + }, + + /** + * Remove events emitted by the current tab target. + */ + destroy: function() { + telemetry.toolClosed("webaudioeditor"); + gTarget.off("will-navigate", this._onTabNavigated); + gTarget.off("navigate", this._onTabNavigated); + gFront.off("start-context", this._onStartContext); + gFront.off("create-node", this._onCreateNode); + gFront.off("connect-node", this._onConnectNode); + gFront.off("connect-param", this._onConnectParam); + gFront.off("disconnect-node", this._onDisconnectNode); + gFront.off("change-param", this._onChangeParam); + gFront.off("destroy-node", this._onDestroyNode); + gDevTools.off("pref-changed", this._onThemeChange); + }, + + /** + * Called when page is reloaded to show the reload notice and waiting + * for an audio context notice. + */ + reset: function () { + $("#content").hidden = true; + ContextView.resetUI(); + InspectorView.resetUI(); + }, + + // Since node create and connect are probably executed back to back, + // and the controller's `_onCreateNode` needs to look up type, + // the edge creation could be called before the graph node is actually + // created. This way, we can check and listen for the event before + // adding an edge. + _waitForNodeCreation: function (sourceActor, destActor) { + let deferred = defer(); + let source = gAudioNodes.get(sourceActor.actorID); + let dest = gAudioNodes.get(destActor.actorID); + + if (!source || !dest) { + gAudioNodes.on("add", function createNodeListener (createdNode) { + if (sourceActor.actorID === createdNode.id) + source = createdNode; + if (destActor.actorID === createdNode.id) + dest = createdNode; + if (source && dest) { + gAudioNodes.off("add", createNodeListener); + deferred.resolve([source, dest]); + } + }); + } + else { + deferred.resolve([source, dest]); + } + return deferred.promise; + }, + + /** + * Fired when the devtools theme changes (light, dark, etc.) + * so that the graph can update marker styling, as that + * cannot currently be done with CSS. + */ + _onThemeChange: function (event, data) { + window.emit(EVENTS.THEME_CHANGE, data.newValue); + }, + + /** + * Called for each location change in the debugged tab. + */ + _onTabNavigated: Task.async(function* (event, {isFrameSwitching}) { + switch (event) { + case "will-navigate": { + // Make sure the backend is prepared to handle audio contexts. + if (!isFrameSwitching) { + yield gFront.setup({ reload: false }); + } + + // Clear out current UI. + this.reset(); + + // When switching to an iframe, ensure displaying the reload button. + // As the document has already been loaded without being hooked. + if (isFrameSwitching) { + $("#reload-notice").hidden = false; + $("#waiting-notice").hidden = true; + } else { + // Otherwise, we are loading a new top level document, + // so we don't need to reload anymore and should receive + // new node events. + $("#reload-notice").hidden = true; + $("#waiting-notice").hidden = false; + } + + // Clear out stored audio nodes + gAudioNodes.reset(); + + window.emit(EVENTS.UI_RESET); + break; + } + case "navigate": { + // TODO Case of bfcache, needs investigating + // bug 994250 + break; + } + } + }), + + /** + * Called after the first audio node is created in an audio context, + * signaling that the audio context is being used. + */ + _onStartContext: function() { + $("#reload-notice").hidden = true; + $("#waiting-notice").hidden = true; + $("#content").hidden = false; + window.emit(EVENTS.START_CONTEXT); + }, + + /** + * Called when a new node is created. Creates an `AudioNodeView` instance + * for tracking throughout the editor. + */ + _onCreateNode: Task.async(function* (nodeActor) { + yield gAudioNodes.add(nodeActor); + }), + + /** + * Called on `destroy-node` when an AudioNode is GC'd. Removes + * from the AudioNode array and fires an event indicating the removal. + */ + _onDestroyNode: function (nodeActor) { + gAudioNodes.remove(gAudioNodes.get(nodeActor.actorID)); + }, + + /** + * Called when a node is connected to another node. + */ + _onConnectNode: Task.async(function* ({ source: sourceActor, dest: destActor }) { + let [source, dest] = yield WebAudioEditorController._waitForNodeCreation(sourceActor, destActor); + source.connect(dest); + }), + + /** + * Called when a node is conneceted to another node's AudioParam. + */ + _onConnectParam: Task.async(function* ({ source: sourceActor, dest: destActor, param }) { + let [source, dest] = yield WebAudioEditorController._waitForNodeCreation(sourceActor, destActor); + source.connect(dest, param); + }), + + /** + * Called when a node is disconnected. + */ + _onDisconnectNode: function(nodeActor) { + let node = gAudioNodes.get(nodeActor.actorID); + node.disconnect(); + }, + + /** + * Called when a node param is changed. + */ + _onChangeParam: function({ actor, param, value }) { + window.emit(EVENTS.CHANGE_PARAM, gAudioNodes.get(actor.actorID), param, value); + } +}; diff --git a/browser/devtools/webaudioeditor/includes.js b/browser/devtools/webaudioeditor/includes.js new file mode 100644 index 00000000000..ddac056ce60 --- /dev/null +++ b/browser/devtools/webaudioeditor/includes.js @@ -0,0 +1,98 @@ +/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); +Cu.import("resource:///modules/devtools/gDevTools.jsm"); + +const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; + +let { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); +let { EventTarget } = require("sdk/event/target"); +const { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); +const { Class } = require("sdk/core/heritage"); +const EventEmitter = require("devtools/toolkit/event-emitter"); +const STRINGS_URI = "chrome://browser/locale/devtools/webaudioeditor.properties" +const L10N = new ViewHelpers.L10N(STRINGS_URI); +const Telemetry = require("devtools/shared/telemetry"); +const telemetry = new Telemetry(); + +// Override DOM promises with Promise.jsm helpers +const { defer, all } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; + +/* Events fired on `window` to indicate state or actions*/ +const EVENTS = { + // Fired when the first AudioNode has been created, signifying + // that the AudioContext is being used and should be tracked via the editor. + START_CONTEXT: "WebAudioEditor:StartContext", + + // When the devtools theme changes. + THEME_CHANGE: "WebAudioEditor:ThemeChange", + + // When the UI is reset from tab navigation. + UI_RESET: "WebAudioEditor:UIReset", + + // When a param has been changed via the UI and successfully + // pushed via the actor to the raw audio node. + UI_SET_PARAM: "WebAudioEditor:UISetParam", + + // When a node is to be set in the InspectorView. + UI_SELECT_NODE: "WebAudioEditor:UISelectNode", + + // When the inspector is finished setting a new node. + UI_INSPECTOR_NODE_SET: "WebAudioEditor:UIInspectorNodeSet", + + // When the inspector is finished rendering in or out of view. + UI_INSPECTOR_TOGGLED: "WebAudioEditor:UIInspectorToggled", + + // When an audio node is finished loading in the Properties tab. + UI_PROPERTIES_TAB_RENDERED: "WebAudioEditor:UIPropertiesTabRendered", + + // When the Audio Context graph finishes rendering. + // Is called with two arguments, first representing number of nodes + // rendered, second being the number of edge connections rendering (not counting + // param edges), followed by the count of the param edges rendered. + UI_GRAPH_RENDERED: "WebAudioEditor:UIGraphRendered" +}; + +/** + * The current target and the Web Audio Editor front, set by this tool's host. + */ +let gToolbox, gTarget, gFront; + +/** + * Convenient way of emitting events from the panel window. + */ +EventEmitter.decorate(this); + +/** + * DOM query helper. + */ +function $(selector, target = document) { return target.querySelector(selector); } +function $$(selector, target = document) { return target.querySelectorAll(selector); } + +/** + * Takes an iterable collection, and a hash. Return the first + * object in the collection that matches the values in the hash. + * From Backbone.Collection#findWhere + * http://backbonejs.org/#Collection-findWhere + */ +function findWhere (collection, attrs) { + let keys = Object.keys(attrs); + for (let model of collection) { + if (keys.every(key => model[key] === attrs[key])) { + return model; + } + } + return void 0; +} + +function mixin (source, ...args) { + args.forEach(obj => Object.keys(obj).forEach(prop => source[prop] = obj[prop])); + return source; +} diff --git a/browser/devtools/webaudioeditor/models.js b/browser/devtools/webaudioeditor/models.js new file mode 100644 index 00000000000..e9ce10c2786 --- /dev/null +++ b/browser/devtools/webaudioeditor/models.js @@ -0,0 +1,274 @@ +/* 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 as different name `coreEmit`, so we don't conflict +// with the global `window` listener itself. +const { emit: coreEmit } = require("sdk/event/core"); + +/** + * Representational wrapper around AudioNodeActors. Adding and destroying + * AudioNodes should be performed through the AudioNodes collection. + * + * Events: + * - `connect`: node, destinationNode, parameter + * - `disconnect`: node + */ +const AudioNodeModel = Class({ + extends: EventTarget, + + // Will be added via AudioNodes `add` + collection: null, + + initialize: function (actor) { + this.actor = actor; + this.id = actor.actorID; + this.connections = []; + }, + + /** + * After instantiating the AudioNodeModel, calling `setup` caches values + * from the actor onto the model. In this case, only the type of audio node. + * + * @return promise + */ + setup: Task.async(function* () { + yield this.getType(); + }), + + /** + * A proxy for the underlying AudioNodeActor to fetch its type + * and subsequently assign the type to the instance. + * + * @return Promise->String + */ + getType: Task.async(function* () { + this.type = yield this.actor.getType(); + return this.type; + }), + + /** + * Stores connection data inside this instance of this audio node connecting + * to another node (destination). If connecting to another node's AudioParam, + * the second argument (param) must be populated with a string. + * + * Connecting nodes is idempotent. Upon new connection, emits "connect" event. + * + * @param AudioNodeModel destination + * @param String param + */ + connect: function (destination, param) { + let edge = findWhere(this.connections, { destination: destination.id, param: param }); + + if (!edge) { + this.connections.push({ source: this.id, destination: destination.id, param: param }); + coreEmit(this, "connect", this, destination, param); + } + }, + + /** + * Clears out all internal connection data. Emits "disconnect" event. + */ + disconnect: function () { + this.connections.length = 0; + coreEmit(this, "disconnect", this); + }, + + /** + * Returns a promise that resolves to an array of objects containing + * both a `param` name property and a `value` property. + * + * @return Promise->Object + */ + getParams: function () { + return this.actor.getParams(); + }, + + /** + * Takes a `dagreD3.Digraph` object and adds this node to + * the graph to be rendered. + * + * @param dagreD3.Digraph + */ + addToGraph: function (graph) { + graph.addNode(this.id, { + type: this.type, + label: this.type.replace(/Node$/, ""), + id: this.id + }); + }, + + /** + * Takes a `dagreD3.Digraph` object and adds edges to + * the graph to be rendered. Separate from `addToGraph`, + * as while we depend on D3/Dagre's constraints, we cannot + * add edges for nodes that have not yet been added to the graph. + * + * @param dagreD3.Digraph + */ + addEdgesToGraph: function (graph) { + for (let edge of this.connections) { + let options = { + source: this.id, + target: edge.destination + }; + + // Only add `label` if `param` specified, as this is an AudioParam + // connection then. `label` adds the magic to render with dagre-d3, + // and `param` is just more explicitly the param, ignoring + // implementation details. + if (edge.param) { + options.label = options.param = edge.param; + } + + graph.addEdge(null, this.id, edge.destination, options); + } + } +}); + + +/** + * Constructor for a Collection of `AudioNodeModel` models. + * + * Events: + * - `add`: node + * - `remove`: node + * - `connect`: node, destinationNode, parameter + * - `disconnect`: node + */ +const AudioNodesCollection = Class({ + extends: EventTarget, + + model: AudioNodeModel, + + initialize: function () { + this.models = new Set(); + this._onModelEvent = this._onModelEvent.bind(this); + }, + + /** + * Iterates over all models within the collection, calling `fn` with the + * model as the first argument. + * + * @param Function fn + */ + forEach: function (fn) { + this.models.forEach(fn); + }, + + /** + * Creates a new AudioNodeModel, passing through arguments into the AudioNodeModel + * constructor, and adds the model to the internal collection store of this + * instance. + * + * Also calls `setup` on the model itself, and sets up event piping, so that + * events emitted on each model propagate to the collection itself. + * + * Emits "add" event on instance when completed. + * + * @param Object obj + * @return Promise->AudioNodeModel + */ + add: Task.async(function* (obj) { + let node = new this.model(obj); + node.collection = this; + yield node.setup(); + + this.models.add(node); + + node.on("*", this._onModelEvent); + coreEmit(this, "add", node); + return node; + }), + + /** + * Removes an AudioNodeModel from the internal collection. Calls `delete` method + * on the model, and emits "remove" on this instance. + * + * @param AudioNodeModel node + */ + remove: function (node) { + this.models.delete(node); + coreEmit(this, "remove", node); + }, + + /** + * Empties out the internal collection of all AudioNodeModels. + */ + reset: function () { + this.models.clear(); + }, + + /** + * Takes an `id` from an AudioNodeModel and returns the corresponding + * AudioNodeModel within the collection that matches that id. Returns `null` + * if not found. + * + * @param Number id + * @return AudioNodeModel|null + */ + get: function (id) { + return findWhere(this.models, { id: id }); + }, + + /** + * Returns the count for how many models are a part of this collection. + * + * @return Number + */ + get length() { + return this.models.size; + }, + + /** + * Returns detailed information about the collection. used during tests + * to query state. Returns an object with information on node count, + * how many edges are within the data graph, as well as how many of those edges + * are for AudioParams. + * + * @return Object + */ + getInfo: function () { + let info = { + nodes: this.length, + edges: 0, + paramEdges: 0 + }; + + this.models.forEach(node => { + let paramEdgeCount = node.connections.filter(edge => edge.param).length; + info.edges += node.connections.length - paramEdgeCount; + info.paramEdges += paramEdgeCount; + }); + return info; + }, + + /** + * Adds all nodes within the collection to the passed in graph, + * as well as their corresponding edges. + * + * @param dagreD3.Digraph + */ + populateGraph: function (graph) { + this.models.forEach(node => node.addToGraph(graph)); + this.models.forEach(node => node.addEdgesToGraph(graph)); + }, + + /** + * Called when a stored model emits any event. Used to manage + * event propagation, or listening to model events to react, like + * removing a model from the collection when it's destroyed. + */ + _onModelEvent: function (eventName, node, ...args) { + if (eventName === "remove") { + // If a `remove` event from the model, remove it + // from the collection, and let the method handle the emitting on + // the collection + this.remove(node); + } else { + // Pipe the event to the collection + coreEmit(this, eventName, [node].concat(args)); + } + } +}); diff --git a/browser/devtools/webaudioeditor/panel.js b/browser/devtools/webaudioeditor/panel.js index f138b7ce9fc..fd4c7b9840d 100644 --- a/browser/devtools/webaudioeditor/panel.js +++ b/browser/devtools/webaudioeditor/panel.js @@ -35,6 +35,7 @@ WebAudioEditorPanel.prototype = { .then(() => { this.panelWin.gToolbox = this._toolbox; this.panelWin.gTarget = this.target; + this.panelWin.gFront = new WebAudioFront(this.target.client, this.target.form); return this.panelWin.startupWebAudioEditor(); }) diff --git a/browser/devtools/webaudioeditor/test/browser.ini b/browser/devtools/webaudioeditor/test/browser.ini index 943d6476055..b17a97d777c 100644 --- a/browser/devtools/webaudioeditor/test/browser.ini +++ b/browser/devtools/webaudioeditor/test/browser.ini @@ -8,6 +8,7 @@ support-files = doc_media-node-creation.html doc_destroy-nodes.html doc_connect-toggle.html + doc_connect-toggle-param.html doc_connect-param.html doc_connect-multi-param.html doc_iframe-context.html @@ -38,6 +39,7 @@ support-files = [browser_wa_graph-render-02.js] [browser_wa_graph-render-03.js] [browser_wa_graph-render-04.js] +[browser_wa_graph-render-05.js] [browser_wa_graph-selected.js] [browser_wa_graph-zoom.js] diff --git a/browser/devtools/webaudioeditor/test/browser_wa_destroy-node-01.js b/browser/devtools/webaudioeditor/test/browser_wa_destroy-node-01.js index 6343cfdce9a..d3f14a2b96b 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_destroy-node-01.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_destroy-node-01.js @@ -12,24 +12,24 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(DESTROY_NODES_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS } = panelWin; + let { gFront, $, $$, gAudioNodes } = panelWin; let started = once(gFront, "start-context"); reload(target); - let destroyed = getN(panelWin, EVENTS.DESTROY_NODE, 10); + let destroyed = getN(gAudioNodes, "remove", 10); forceCC(); let [created] = yield Promise.all([ - getNSpread(panelWin, EVENTS.CREATE_NODE, 13), + getNSpread(gAudioNodes, "add", 13), waitForGraphRendered(panelWin, 13, 2) ]); - // Since CREATE_NODE emits several arguments (eventName and actorID), let's - // flatten it to just the actorIDs - let actorIDs = created.map(ev => ev[1]); + // Flatten arrays of event arguments and take the first (AudioNodeModel) + // and get its ID. + let actorIDs = created.map(ev => ev[0].id); // Click a soon-to-be dead buffer node yield clickGraphNode(panelWin, actorIDs[5]); @@ -40,7 +40,7 @@ function spawnTest() { yield Promise.all([destroyed, waitForGraphRendered(panelWin, 3, 2)]); // Test internal storage - is(panelWin.AudioNodes.length, 3, "All nodes should be GC'd except one gain, osc and dest node."); + is(panelWin.gAudioNodes.length, 3, "All nodes should be GC'd except one gain, osc and dest node."); // Test graph rendering ok(findGraphNode(panelWin, actorIDs[0]), "dest should be in graph"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_graph-click.js b/browser/devtools/webaudioeditor/test/browser_wa_graph-click.js index 8e000fa8e92..d6b307194a0 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_graph-click.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-click.js @@ -6,13 +6,10 @@ * the correct node in the InspectorView */ -let EVENTS = null; - function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(COMPLEX_CONTEXT_URL); let panelWin = panel.panelWin; - let { gFront, $, $$, WebAudioInspectorView } = panelWin; - EVENTS = panelWin.EVENTS; + let { gFront, $, $$, InspectorView } = panelWin; let started = once(gFront, "start-context"); @@ -25,28 +22,28 @@ function spawnTest() { let nodeIds = actors.map(actor => actor.actorID); - ok(!WebAudioInspectorView.isVisible(), "InspectorView hidden on start."); + ok(!InspectorView.isVisible(), "InspectorView hidden on start."); yield clickGraphNode(panelWin, nodeIds[1], true); - ok(WebAudioInspectorView.isVisible(), "InspectorView visible after selecting a node."); - is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[1], "InspectorView has correct node set."); + ok(InspectorView.isVisible(), "InspectorView visible after selecting a node."); + is(InspectorView.getCurrentAudioNode().id, nodeIds[1], "InspectorView has correct node set."); yield clickGraphNode(panelWin, nodeIds[2]); - ok(WebAudioInspectorView.isVisible(), "InspectorView still visible after selecting another node."); - is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[2], "InspectorView has correct node set on second node."); + ok(InspectorView.isVisible(), "InspectorView still visible after selecting another node."); + is(InspectorView.getCurrentAudioNode().id, nodeIds[2], "InspectorView has correct node set on second node."); yield clickGraphNode(panelWin, nodeIds[2]); - is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[2], "Clicking the same node again works (idempotent)."); + is(InspectorView.getCurrentAudioNode().id, nodeIds[2], "Clicking the same node again works (idempotent)."); yield clickGraphNode(panelWin, $("rect", findGraphNode(panelWin, nodeIds[3]))); - is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[3], "Clicking on a works as expected."); + is(InspectorView.getCurrentAudioNode().id, nodeIds[3], "Clicking on a works as expected."); yield clickGraphNode(panelWin, $("tspan", findGraphNode(panelWin, nodeIds[4]))); - is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[4], "Clicking on a works as expected."); + is(InspectorView.getCurrentAudioNode().id, nodeIds[4], "Clicking on a works as expected."); - ok(WebAudioInspectorView.isVisible(), + ok(InspectorView.isVisible(), "InspectorView still visible after several nodes have been clicked."); yield teardown(panel); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_graph-markers.js b/browser/devtools/webaudioeditor/test/browser_wa_graph-markers.js index c04a15eda01..5961837e275 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_graph-markers.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-markers.js @@ -8,7 +8,7 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, MARKER_STYLING } = panelWin; + let { gFront, $, $$, MARKER_STYLING } = panelWin; let currentTheme = Services.prefs.getCharPref("devtools.theme"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-01.js b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-01.js index 137d5ee195f..b036cc1741f 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-01.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-01.js @@ -10,13 +10,13 @@ let connectCount = 0; function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS } = panelWin; + let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin; let started = once(gFront, "start-context"); reload(target); - panelWin.on(EVENTS.CONNECT_NODE, onConnectNode); + gAudioNodes.on("connect", onConnectNode); let [actors] = yield Promise.all([ get3(gFront, "create-node"), @@ -35,7 +35,7 @@ function spawnTest() { is(connectCount, 2, "Only two node connect events should be fired."); - panelWin.off(EVENTS.CONNECT_NODE, onConnectNode); + gAudioNodes.off("connect", onConnectNode); yield teardown(panel); finish(); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-02.js b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-02.js index dadf4a7f751..aa3b18d2e81 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-02.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-02.js @@ -8,7 +8,7 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(COMPLEX_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS } = panelWin; + let { gFront, $, $$ } = panelWin; let started = once(gFront, "start-context"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-05.js b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-05.js new file mode 100644 index 00000000000..708227ef9a6 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-05.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests to ensure that param connections trigger graph redraws + */ + +function spawnTest() { + let [target, debuggee, panel] = yield initWebAudioEditor(CONNECT_TOGGLE_PARAM_URL); + let { panelWin } = panel; + let { gFront, $, $$, EVENTS } = panelWin; + + reload(target); + + let [actors] = yield Promise.all([ + getN(gFront, "create-node", 3), + waitForGraphRendered(panelWin, 3, 1, 0) + ]); + ok(true, "Graph rendered without param connection"); + + yield waitForGraphRendered(panelWin, 3, 1, 1); + ok(true, "Graph re-rendered upon param connection"); + + yield teardown(panel); + finish(); +} + diff --git a/browser/devtools/webaudioeditor/test/browser_wa_graph-zoom.js b/browser/devtools/webaudioeditor/test/browser_wa_graph-zoom.js index f67d5eb1f8d..f90b2499195 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_graph-zoom.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-zoom.js @@ -8,7 +8,7 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, WebAudioGraphView } = panelWin; + let { gFront, $, $$, EVENTS, ContextView } = panelWin; let started = once(gFront, "start-context"); @@ -17,27 +17,27 @@ function spawnTest() { waitForGraphRendered(panelWin, 3, 2) ]); - is(WebAudioGraphView.getCurrentScale(), 1, "Default graph scale is 1."); - is(WebAudioGraphView.getCurrentTranslation()[0], 20, "Default x-translation is 20."); - is(WebAudioGraphView.getCurrentTranslation()[1], 20, "Default y-translation is 20."); + is(ContextView.getCurrentScale(), 1, "Default graph scale is 1."); + is(ContextView.getCurrentTranslation()[0], 20, "Default x-translation is 20."); + is(ContextView.getCurrentTranslation()[1], 20, "Default y-translation is 20."); // Change both attribute and D3's internal store panelWin.d3.select("#graph-target").attr("transform", "translate([100, 400]) scale(10)"); - WebAudioGraphView._zoomBinding.scale(10); - WebAudioGraphView._zoomBinding.translate([100, 400]); + ContextView._zoomBinding.scale(10); + ContextView._zoomBinding.translate([100, 400]); - is(WebAudioGraphView.getCurrentScale(), 10, "After zoom, scale is 10."); - is(WebAudioGraphView.getCurrentTranslation()[0], 100, "After zoom, x-translation is 100."); - is(WebAudioGraphView.getCurrentTranslation()[1], 400, "After zoom, y-translation is 400."); + is(ContextView.getCurrentScale(), 10, "After zoom, scale is 10."); + is(ContextView.getCurrentTranslation()[0], 100, "After zoom, x-translation is 100."); + is(ContextView.getCurrentTranslation()[1], 400, "After zoom, y-translation is 400."); yield Promise.all([ reload(target), waitForGraphRendered(panelWin, 3, 2) ]); - is(WebAudioGraphView.getCurrentScale(), 1, "After refresh, graph scale is 1."); - is(WebAudioGraphView.getCurrentTranslation()[0], 20, "After refresh, x-translation is 20."); - is(WebAudioGraphView.getCurrentTranslation()[1], 20, "After refresh, y-translation is 20."); + is(ContextView.getCurrentScale(), 1, "After refresh, graph scale is 1."); + is(ContextView.getCurrentTranslation()[0], 20, "After refresh, x-translation is 20."); + is(ContextView.getCurrentTranslation()[1], 20, "After refresh, y-translation is 20."); yield teardown(panel); finish(); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js b/browser/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js index 64f49b0b379..ed168960bfd 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js @@ -9,8 +9,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; - let gVars = WebAudioInspectorView._propsView; + let { gFront, $, $$, EVENTS, InspectorView } = panelWin; + let gVars = InspectorView._propsView; let started = once(gFront, "start-context"); @@ -22,13 +22,13 @@ function spawnTest() { ]); let nodeIds = actors.map(actor => actor.actorID); - ok(!WebAudioInspectorView.isVisible(), "InspectorView hidden on start."); + ok(!InspectorView.isVisible(), "InspectorView hidden on start."); // Open inspector pane $("#inspector-pane-toggle").click(); yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED); - ok(WebAudioInspectorView.isVisible(), "InspectorView shown after toggling."); + ok(InspectorView.isVisible(), "InspectorView shown after toggling."); ok(isVisible($("#web-audio-editor-details-pane-empty")), "InspectorView empty message should still be visible."); @@ -41,13 +41,13 @@ function spawnTest() { $("#inspector-pane-toggle").click(); yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED); - ok(!WebAudioInspectorView.isVisible(), "InspectorView back to being hidden."); + ok(!InspectorView.isVisible(), "InspectorView back to being hidden."); // Open again to test node loading while open $("#inspector-pane-toggle").click(); yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED); - ok(WebAudioInspectorView.isVisible(), "InspectorView being shown."); + ok(InspectorView.isVisible(), "InspectorView being shown."); ok(!isVisible($("#web-audio-editor-tabs")), "InspectorView tabs are still hidden."); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_inspector.js b/browser/devtools/webaudioeditor/test/browser_wa_inspector.js index 1db3f44ed4d..ff13dfbf87d 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_inspector.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_inspector.js @@ -9,8 +9,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; - let gVars = WebAudioInspectorView._propsView; + let { gFront, $, $$, EVENTS, InspectorView } = panelWin; + let gVars = InspectorView._propsView; let started = once(gFront, "start-context"); @@ -22,7 +22,7 @@ function spawnTest() { ]); let nodeIds = actors.map(actor => actor.actorID); - ok(!WebAudioInspectorView.isVisible(), "InspectorView hidden on start."); + ok(!InspectorView.isVisible(), "InspectorView hidden on start."); ok(isVisible($("#web-audio-editor-details-pane-empty")), "InspectorView empty message should show when no node's selected."); ok(!isVisible($("#web-audio-editor-tabs")), @@ -37,7 +37,7 @@ function spawnTest() { once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED) ]); - ok(WebAudioInspectorView.isVisible(), "InspectorView shown once node selected."); + ok(InspectorView.isVisible(), "InspectorView shown once node selected."); ok(!isVisible($("#web-audio-editor-details-pane-empty")), "InspectorView empty message hidden when node selected."); ok(isVisible($("#web-audio-editor-tabs")), diff --git a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js index 4b7c809aae3..4e61b1645a5 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js @@ -8,8 +8,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; - let gVars = WebAudioInspectorView._propsView; + let { gFront, $, $$, EVENTS, InspectorView } = panelWin; + let gVars = InspectorView._propsView; let started = once(gFront, "start-context"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js index bdd3a1e4e5c..dc9b5fae23b 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js @@ -8,8 +8,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(COMPLEX_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; - let gVars = WebAudioInspectorView._propsView; + let { gFront, $, $$, EVENTS, InspectorView } = panelWin; + let gVars = InspectorView._propsView; let started = once(gFront, "start-context"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-media-nodes.js b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-media-nodes.js index 6f559bb01e8..07df9848b73 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-media-nodes.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-media-nodes.js @@ -35,8 +35,8 @@ function waitForDeviceClosed() { function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(MEDIA_NODES_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; - let gVars = WebAudioInspectorView._propsView; + let { gFront, $, $$, EVENTS, InspectorView } = panelWin; + let gVars = InspectorView._propsView; // Auto enable getUserMedia let mediaPermissionPref = Services.prefs.getBoolPref(MEDIA_PERMISSION); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js index 58521b5306c..209e0b89a6f 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js @@ -9,8 +9,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(BUFFER_AND_ARRAY_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; - let gVars = WebAudioInspectorView._propsView; + let { gFront, $, $$, EVENTS, InspectorView } = panelWin; + let gVars = InspectorView._propsView; let started = once(gFront, "start-context"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params.js b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params.js index 0fce98885d6..c08838334f6 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params.js @@ -9,8 +9,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_NODES_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; - let gVars = WebAudioInspectorView._propsView; + let { gFront, $, $$, EVENTS, InspectorView } = panelWin; + let gVars = InspectorView._propsView; let started = once(gFront, "start-context"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_properties-view.js b/browser/devtools/webaudioeditor/test/browser_wa_properties-view.js index 0421de6860c..c84287eae86 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view.js @@ -8,8 +8,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; - let gVars = WebAudioInspectorView._propsView; + let { gFront, $, $$, EVENTS, InspectorView } = panelWin; + let gVars = InspectorView._propsView; let started = once(gFront, "start-context"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_reset-03.js b/browser/devtools/webaudioeditor/test/browser_wa_reset-03.js index 96dd9c95a4c..8ccbf9f7288 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_reset-03.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_reset-03.js @@ -9,7 +9,7 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, WebAudioInspectorView } = panelWin; + let { gFront, $, InspectorView } = panelWin; reload(target); @@ -20,8 +20,8 @@ function spawnTest() { let nodeIds = actors.map(actor => actor.actorID); yield clickGraphNode(panelWin, nodeIds[1], true); - ok(WebAudioInspectorView.isVisible(), "InspectorView visible after selecting a node."); - is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[1], "InspectorView has correct node set."); + ok(InspectorView.isVisible(), "InspectorView visible after selecting a node."); + is(InspectorView.getCurrentAudioNode().id, nodeIds[1], "InspectorView has correct node set."); /** * Reload @@ -35,14 +35,14 @@ function spawnTest() { ]); nodeIds = actors.map(actor => actor.actorID); - ok(!WebAudioInspectorView.isVisible(), "InspectorView hidden on start."); - ise(WebAudioInspectorView.getCurrentAudioNode(), null, + ok(!InspectorView.isVisible(), "InspectorView hidden on start."); + ise(InspectorView.getCurrentAudioNode(), null, "InspectorView has no current node set on reset."); yield clickGraphNode(panelWin, nodeIds[2], true); - ok(WebAudioInspectorView.isVisible(), + ok(InspectorView.isVisible(), "InspectorView visible after selecting a node after a reset."); - is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[2], "InspectorView has correct node set upon clicking graph node after a reset."); + is(InspectorView.getCurrentAudioNode().id, nodeIds[2], "InspectorView has correct node set upon clicking graph node after a reset."); yield teardown(panel); finish(); diff --git a/browser/devtools/webaudioeditor/test/doc_connect-toggle-param.html b/browser/devtools/webaudioeditor/test/doc_connect-toggle-param.html new file mode 100644 index 00000000000..ae3ece5db8c --- /dev/null +++ b/browser/devtools/webaudioeditor/test/doc_connect-toggle-param.html @@ -0,0 +1,27 @@ + + + + + + + Web Audio Editor test page + + + + + + + + diff --git a/browser/devtools/webaudioeditor/test/head.js b/browser/devtools/webaudioeditor/test/head.js index 0f5d0cbb226..de19e84b800 100644 --- a/browser/devtools/webaudioeditor/test/head.js +++ b/browser/devtools/webaudioeditor/test/head.js @@ -28,6 +28,7 @@ const MEDIA_NODES_URL = EXAMPLE_URL + "doc_media-node-creation.html"; const BUFFER_AND_ARRAY_URL = EXAMPLE_URL + "doc_buffer-and-array.html"; const DESTROY_NODES_URL = EXAMPLE_URL + "doc_destroy-nodes.html"; const CONNECT_TOGGLE_URL = EXAMPLE_URL + "doc_connect-toggle.html"; +const CONNECT_TOGGLE_PARAM_URL = EXAMPLE_URL + "doc_connect-toggle-param.html"; const CONNECT_PARAM_URL = EXAMPLE_URL + "doc_connect-param.html"; const CONNECT_MULTI_PARAM_URL = EXAMPLE_URL + "doc_connect-multi-param.html"; const IFRAME_CONTEXT_URL = EXAMPLE_URL + "doc_iframe-context.html"; @@ -37,7 +38,10 @@ waitForExplicitFinish(); let gToolEnabled = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled"); +gDevTools.testing = true; + registerCleanupFunction(() => { + gDevTools.testing = false; info("finish() was called, cleaning up..."); Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", gToolEnabled); @@ -210,10 +214,7 @@ function waitForGraphRendered (front, nodeCount, edgeCount, paramEdgeCount) { let deferred = Promise.defer(); let eventName = front.EVENTS.UI_GRAPH_RENDERED; front.on(eventName, function onGraphRendered (_, nodes, edges, pEdges) { - info(nodes); - info(edges) - info(pEdges); - let paramEdgesDone = paramEdgeCount ? paramEdgeCount === pEdges : true; + let paramEdgesDone = paramEdgeCount != null ? paramEdgeCount === pEdges : true; if (nodes === nodeCount && edges === edgeCount && paramEdgesDone) { front.off(eventName, onGraphRendered); deferred.resolve(); diff --git a/browser/devtools/webaudioeditor/views/context.js b/browser/devtools/webaudioeditor/views/context.js new file mode 100644 index 00000000000..139047c6c2a --- /dev/null +++ b/browser/devtools/webaudioeditor/views/context.js @@ -0,0 +1,305 @@ +/* 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 { debounce } = require("sdk/lang/functional"); + +// 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" +}; + +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. + */ +let ContextView = { + /** + * Initialization function, called when the tool is started. + */ + initialize: function() { + this._onGraphNodeClick = this._onGraphNodeClick.bind(this); + this._onThemeChange = this._onThemeChange.bind(this); + this._onNodeSelect = this._onNodeSelect.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._onGraphNodeClick, false); + + window.on(EVENTS.THEME_CHANGE, this._onThemeChange); + window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSelect); + 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._onGraphNodeClick, false); + window.off(EVENTS.THEME_CHANGE, this._onThemeChange); + window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSelect); + 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. + * Called from UI_INSPECTOR_NODE_SELECT. + */ + 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 + "']"); + }, + + /** + * 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.attr("class", (n) => { + let node = graph.node(n); + return "audionode type-" + node.type; + }); + svgNodes.attr("data-id", (n) => { + let node = graph.node(n); + return node.id; + }); + return svgNodes; + }); + + // Post-render manipulation of edges + // TODO do all of this more efficiently, rather than + // using the direct D3 helper utilities to loop over each + // edge several times + let oldDrawEdgePaths = renderer.drawEdgePaths(); + renderer.drawEdgePaths(function(graph, root) { + let svgEdges = oldDrawEdgePaths(graph, root); + svgEdges.attr("data-source", (n) => { + let edge = graph.edge(n); + return edge.source; + }); + svgEdges.attr("data-target", (n) => { + let edge = graph.edge(n); + return edge.target; + }); + svgEdges.attr("data-param", (n) => { + let edge = graph.edge(n); + return edge.param ? edge.param : null; + }); + // We have to manually specify the default classes on the edges + // as to not overwrite them + let defaultClasses = "edgePath enter"; + svgEdges.attr("class", (n) => { + let edge = graph.edge(n); + return defaultClasses + (edge.param ? (" param-connection " + edge.param) : ""); + }); + + 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 (gDevTools.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 (~GRAPH_REDRAW_EVENTS.indexOf(eventName)) { + this.draw(); + } + }, + + _onNodeSelect: function (eventName, id) { + this.focusNode(id); + }, + + /** + * 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 node in the svg graph is clicked. Used to handle triggering the AudioNodePane. + * + * @param Event e + * Click event. + */ + _onGraphNodeClick: 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; + + window.emit(EVENTS.UI_SELECT_NODE, node.getAttribute("data-id")); + } +}; diff --git a/browser/devtools/webaudioeditor/views/inspector.js b/browser/devtools/webaudioeditor/views/inspector.js new file mode 100644 index 00000000000..0a807550d86 --- /dev/null +++ b/browser/devtools/webaudioeditor/views/inspector.js @@ -0,0 +1,240 @@ +/* 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"); + +// Strings for rendering +const EXPAND_INSPECTOR_STRING = L10N.getStr("expandInspector"); +const COLLAPSE_INSPECTOR_STRING = L10N.getStr("collapseInspector"); + +// Store width as a preference rather than hardcode +// TODO bug 1009056 +const INSPECTOR_WIDTH = 300; + +const GENERIC_VARIABLES_VIEW_SETTINGS = { + searchEnabled: false, + editableValueTooltip: "", + editableNameTooltip: "", + preventDisableOnChange: true, + preventDescriptorModifiers: false, + eval: () => {} +}; + +/** + * Functions handling the audio node inspector UI. + */ + +let 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 () { + this._tabsPane = $("#web-audio-editor-tabs"); + + // Set up view controller + this.el = $("#web-audio-inspector"); + this.el.setAttribute("width", INSPECTOR_WIDTH); + this.button = $("#inspector-pane-toggle"); + mixin(this, ToggleMixin); + this.bindToggle(); + + // Hide inspector view on startup + this.hideImmediately(); + + this._onEval = this._onEval.bind(this); + this._onNodeSelect = this._onNodeSelect.bind(this); + this._onDestroyNode = this._onDestroyNode.bind(this); + + this._propsView = new VariablesView($("#properties-tabpanel-content"), GENERIC_VARIABLES_VIEW_SETTINGS); + this._propsView.eval = this._onEval; + + 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(); + window.off(EVENTS.UI_SELECT_NODE, this._onNodeSelect); + gAudioNodes.off("remove", this._onDestroyNode); + + this.el = null; + this.button = null; + this._tabsPane = null; + }, + + /** + * Takes a AudioNodeView `node` and sets it as the current + * node and scaffolds the inspector view based off of the new node. + */ + setCurrentAudioNode: 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._setTitle(); + this._buildPropertiesView() + .then(() => 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 () { + this._propsView.empty(); + // Set current node to empty to load empty view + this.setCurrentAudioNode(); + + // Reset AudioNode inspector and hide + this.hideImmediately(); + }, + + /** + * Sets the title of the Inspector view + */ + _setTitle: function () { + let node = this._currentNode; + let title = node.type.replace(/Node$/, ""); + $("#web-audio-inspector-title").setAttribute("value", title); + }, + + /** + * 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, + }; + audioParamsScope.addItem(param, descriptor); + }); + + audioParamsScope.expanded = true; + + window.emit(EVENTS.UI_PROPERTIES_TAB_RENDERED, node.id); + }), + + _togglePropertiesView: function (show) { + let propsView = $("#properties-tabpanel-content"); + let emptyView = $("#properties-tabpanel-content-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 + */ + + /** + * 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); + } + }), + + /** + * 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(); + }, + + /** + * 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); + } + } +}; diff --git a/browser/devtools/webaudioeditor/views/utils.js b/browser/devtools/webaudioeditor/views/utils.js new file mode 100644 index 00000000000..c397a16cb11 --- /dev/null +++ b/browser/devtools/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` + */ +let 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.hasAttribute("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.removeAttribute("pane-collapsed"); + this.button.setAttribute("tooltiptext", this._collapseString); + } + else { + this.button.setAttribute("pane-collapsed", ""); + this.button.setAttribute("tooltiptext", this._expandString); + } + }, + + _onToggle: function () { + this._viewController({ visible: !this.isVisible() }); + } +} diff --git a/browser/devtools/webaudioeditor/webaudioeditor-controller.js b/browser/devtools/webaudioeditor/webaudioeditor-controller.js deleted file mode 100644 index ab071add288..00000000000 --- a/browser/devtools/webaudioeditor/webaudioeditor-controller.js +++ /dev/null @@ -1,428 +0,0 @@ -/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); -Cu.import("resource:///modules/devtools/gDevTools.jsm"); - -// Override DOM promises with Promise.jsm helpers -const { defer, all } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; - -const { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); -const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; -const EventEmitter = require("devtools/toolkit/event-emitter"); -const STRINGS_URI = "chrome://browser/locale/devtools/webaudioeditor.properties" -const L10N = new ViewHelpers.L10N(STRINGS_URI); -const Telemetry = require("devtools/shared/telemetry"); -const telemetry = new Telemetry(); - -let { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); - -// The panel's window global is an EventEmitter firing the following events: -const EVENTS = { - // Fired when the first AudioNode has been created, signifying - // that the AudioContext is being used and should be tracked via the editor. - START_CONTEXT: "WebAudioEditor:StartContext", - - // On node creation, connect and disconnect. - CREATE_NODE: "WebAudioEditor:CreateNode", - CONNECT_NODE: "WebAudioEditor:ConnectNode", - DISCONNECT_NODE: "WebAudioEditor:DisconnectNode", - - // When a node gets GC'd. - DESTROY_NODE: "WebAudioEditor:DestroyNode", - - // On a node parameter's change. - CHANGE_PARAM: "WebAudioEditor:ChangeParam", - - // When the devtools theme changes. - THEME_CHANGE: "WebAudioEditor:ThemeChange", - - // When the UI is reset from tab navigation. - UI_RESET: "WebAudioEditor:UIReset", - - // When a param has been changed via the UI and successfully - // pushed via the actor to the raw audio node. - UI_SET_PARAM: "WebAudioEditor:UISetParam", - - // When a node is to be set in the InspectorView. - UI_SELECT_NODE: "WebAudioEditor:UISelectNode", - - // When the inspector is finished setting a new node. - UI_INSPECTOR_NODE_SET: "WebAudioEditor:UIInspectorNodeSet", - - // When the inspector is finished rendering in or out of view. - UI_INSPECTOR_TOGGLED: "WebAudioEditor:UIInspectorToggled", - - // When an audio node is finished loading in the Properties tab. - UI_PROPERTIES_TAB_RENDERED: "WebAudioEditor:UIPropertiesTabRendered", - - // When the Audio Context graph finishes rendering. - // Is called with two arguments, first representing number of nodes - // rendered, second being the number of edge connections rendering (not counting - // param edges), followed by the count of the param edges rendered. - UI_GRAPH_RENDERED: "WebAudioEditor:UIGraphRendered" -}; - -/** - * The current target and the Web Audio Editor front, set by this tool's host. - */ -let gToolbox, gTarget, gFront; - -/** - * Track an array of audio nodes - */ -let AudioNodes = []; -let AudioNodeConnections = new WeakMap(); // > -let AudioParamConnections = new WeakMap(); // - -// Light representation wrapping an AudioNode actor with additional properties -function AudioNodeView (actor) { - this.actor = actor; - this.id = actor.actorID; -} - -// A proxy for the underlying AudioNodeActor to fetch its type -// and subsequently assign the type to the instance. -AudioNodeView.prototype.getType = Task.async(function* () { - this.type = yield this.actor.getType(); - return this.type; -}); - -// Helper method to create connections in the AudioNodeConnections -// WeakMap for rendering. Returns a boolean indicating -// if the connection was successfully created. Will return `false` -// when the connection was previously made. -AudioNodeView.prototype.connect = function (destination) { - let connections = AudioNodeConnections.get(this) || new Set(); - AudioNodeConnections.set(this, connections); - - // Don't duplicate add. - if (!connections.has(destination)) { - connections.add(destination); - return true; - } - return false; -}; - -// Helper method to create connections in the AudioNodeConnections -// WeakMap for rendering. Returns a boolean indicating -// if the connection was successfully created. Will return `false` -// when the connection was previously made. -AudioNodeView.prototype.connectParam = function (destination, param) { - let connections = AudioParamConnections.get(this) || {}; - AudioParamConnections.set(this, connections); - - let params = connections[destination.id] = connections[destination.id] || []; - - if (!~params.indexOf(param)) { - params.push(param); - return true; - } - return false; -}; - -// Helper method to remove audio connections from the current AudioNodeView -AudioNodeView.prototype.disconnect = function () { - AudioNodeConnections.set(this, new Set()); - AudioParamConnections.set(this, {}); -}; - -// Returns a promise that resolves to an array of objects containing -// both a `param` name property and a `value` property. -AudioNodeView.prototype.getParams = function () { - return this.actor.getParams(); -}; - - -/** - * Initializes the web audio editor views - */ -function startupWebAudioEditor() { - return all([ - WebAudioEditorController.initialize(), - WebAudioGraphView.initialize(), - WebAudioInspectorView.initialize(), - ]); -} - -/** - * Destroys the web audio editor controller and views. - */ -function shutdownWebAudioEditor() { - return all([ - WebAudioEditorController.destroy(), - WebAudioGraphView.destroy(), - WebAudioInspectorView.destroy(), - ]); -} - -/** - * Functions handling target-related lifetime events. - */ -let WebAudioEditorController = { - /** - * Listen for events emitted by the current tab target. - */ - initialize: function() { - telemetry.toolOpened("webaudioeditor"); - this._onTabNavigated = this._onTabNavigated.bind(this); - this._onThemeChange = this._onThemeChange.bind(this); - gTarget.on("will-navigate", this._onTabNavigated); - gTarget.on("navigate", this._onTabNavigated); - gFront.on("start-context", this._onStartContext); - gFront.on("create-node", this._onCreateNode); - gFront.on("connect-node", this._onConnectNode); - gFront.on("connect-param", this._onConnectParam); - gFront.on("disconnect-node", this._onDisconnectNode); - gFront.on("change-param", this._onChangeParam); - gFront.on("destroy-node", this._onDestroyNode); - - // Hook into theme change so we can change - // the graph's marker styling, since we can't do this - // with CSS - gDevTools.on("pref-changed", this._onThemeChange); - - // Set up events to refresh the Graph view - window.on(EVENTS.CREATE_NODE, this._onUpdatedContext); - window.on(EVENTS.CONNECT_NODE, this._onUpdatedContext); - window.on(EVENTS.DISCONNECT_NODE, this._onUpdatedContext); - window.on(EVENTS.DESTROY_NODE, this._onUpdatedContext); - window.on(EVENTS.CONNECT_PARAM, this._onUpdatedContext); - }, - - /** - * Remove events emitted by the current tab target. - */ - destroy: function() { - telemetry.toolClosed("webaudioeditor"); - gTarget.off("will-navigate", this._onTabNavigated); - gTarget.off("navigate", this._onTabNavigated); - gFront.off("start-context", this._onStartContext); - gFront.off("create-node", this._onCreateNode); - gFront.off("connect-node", this._onConnectNode); - gFront.off("connect-param", this._onConnectParam); - gFront.off("disconnect-node", this._onDisconnectNode); - gFront.off("change-param", this._onChangeParam); - gFront.off("destroy-node", this._onDestroyNode); - window.off(EVENTS.CREATE_NODE, this._onUpdatedContext); - window.off(EVENTS.CONNECT_NODE, this._onUpdatedContext); - window.off(EVENTS.DISCONNECT_NODE, this._onUpdatedContext); - window.off(EVENTS.DESTROY_NODE, this._onUpdatedContext); - window.off(EVENTS.CONNECT_PARAM, this._onUpdatedContext); - gDevTools.off("pref-changed", this._onThemeChange); - }, - - /** - * Called when page is reloaded to show the reload notice and waiting - * for an audio context notice. - */ - reset: function () { - $("#content").hidden = true; - WebAudioGraphView.resetUI(); - WebAudioInspectorView.resetUI(); - }, - - /** - * Called when a new audio node is created, or the audio context - * routing changes. - */ - _onUpdatedContext: function () { - WebAudioGraphView.draw(); - }, - - /** - * Fired when the devtools theme changes (light, dark, etc.) - * so that the graph can update marker styling, as that - * cannot currently be done with CSS. - */ - _onThemeChange: function (event, data) { - window.emit(EVENTS.THEME_CHANGE, data.newValue); - }, - - /** - * Called for each location change in the debugged tab. - */ - _onTabNavigated: Task.async(function* (event, {isFrameSwitching}) { - switch (event) { - case "will-navigate": { - // Make sure the backend is prepared to handle audio contexts. - if (!isFrameSwitching) { - yield gFront.setup({ reload: false }); - } - - // Clear out current UI. - this.reset(); - - // When switching to an iframe, ensure displaying the reload button. - // As the document has already been loaded without being hooked. - if (isFrameSwitching) { - $("#reload-notice").hidden = false; - $("#waiting-notice").hidden = true; - } else { - // Otherwise, we are loading a new top level document, - // so we don't need to reload anymore and should receive - // new node events. - $("#reload-notice").hidden = true; - $("#waiting-notice").hidden = false; - } - - // Clear out stored audio nodes - AudioNodes.length = 0; - AudioNodeConnections.clear(); - window.emit(EVENTS.UI_RESET); - break; - } - case "navigate": { - // TODO Case of bfcache, needs investigating - // bug 994250 - break; - } - } - }), - - /** - * Called after the first audio node is created in an audio context, - * signaling that the audio context is being used. - */ - _onStartContext: function() { - $("#reload-notice").hidden = true; - $("#waiting-notice").hidden = true; - $("#content").hidden = false; - window.emit(EVENTS.START_CONTEXT); - }, - - /** - * Called when a new node is created. Creates an `AudioNodeView` instance - * for tracking throughout the editor. - */ - _onCreateNode: Task.async(function* (nodeActor) { - let node = new AudioNodeView(nodeActor); - yield node.getType(); - AudioNodes.push(node); - window.emit(EVENTS.CREATE_NODE, node.id); - }), - - /** - * Called on `destroy-node` when an AudioNode is GC'd. Removes - * from the AudioNode array and fires an event indicating the removal. - */ - _onDestroyNode: function (nodeActor) { - for (let i = 0; i < AudioNodes.length; i++) { - if (equalActors(AudioNodes[i].actor, nodeActor)) { - AudioNodes.splice(i, 1); - window.emit(EVENTS.DESTROY_NODE, nodeActor.actorID); - break; - } - } - }, - - /** - * Called when a node is connected to another node. - */ - _onConnectNode: Task.async(function* ({ source: sourceActor, dest: destActor }) { - let [source, dest] = yield waitForNodeCreation(sourceActor, destActor); - - // Connect nodes, and only emit if it's a new connection. - if (source.connect(dest)) { - window.emit(EVENTS.CONNECT_NODE, source.id, dest.id); - } - }), - - /** - * Called when a node is conneceted to another node's AudioParam. - */ - _onConnectParam: Task.async(function* ({ source: sourceActor, dest: destActor, param }) { - let [source, dest] = yield waitForNodeCreation(sourceActor, destActor); - - if (source.connectParam(dest, param)) { - window.emit(EVENTS.CONNECT_PARAM, source.id, dest.id, param); - } - }), - - /** - * Called when a node is disconnected. - */ - _onDisconnectNode: function(nodeActor) { - let node = getViewNodeByActor(nodeActor); - node.disconnect(); - window.emit(EVENTS.DISCONNECT_NODE, node.id); - }, - - /** - * Called when a node param is changed. - */ - _onChangeParam: function({ actor, param, value }) { - window.emit(EVENTS.CHANGE_PARAM, getViewNodeByActor(actor), param, value); - } -}; - -/** - * Convenient way of emitting events from the panel window. - */ -EventEmitter.decorate(this); - -/** - * DOM query helper. - */ -function $(selector, target = document) { return target.querySelector(selector); } -function $$(selector, target = document) { return target.querySelectorAll(selector); } - -/** - * Compare `actorID` between two actors to determine if they're corresponding - * to the same underlying actor. - */ -function equalActors (actor1, actor2) { - return actor1.actorID === actor2.actorID; -} - -/** - * Returns the corresponding ViewNode by actor - */ -function getViewNodeByActor (actor) { - for (let i = 0; i < AudioNodes.length; i++) { - if (equalActors(AudioNodes[i].actor, actor)) - return AudioNodes[i]; - } - return null; -} - -/** - * Returns the corresponding ViewNode by actorID - */ -function getViewNodeById (id) { - return getViewNodeByActor({ actorID: id }); -} - -// Since node create and connect are probably executed back to back, -// and the controller's `_onCreateNode` needs to look up type, -// the edge creation could be called before the graph node is actually -// created. This way, we can check and listen for the event before -// adding an edge. -function waitForNodeCreation (sourceActor, destActor) { - let deferred = defer(); - let eventName = EVENTS.CREATE_NODE; - let source = getViewNodeByActor(sourceActor); - let dest = getViewNodeByActor(destActor); - - if (!source || !dest) - window.on(eventName, function createNodeListener (_, id) { - let createdNode = getViewNodeById(id); - if (equalActors(sourceActor, createdNode.actor)) - source = createdNode; - if (equalActors(destActor, createdNode.actor)) - dest = createdNode; - if (source && dest) { - window.off(eventName, createNodeListener); - deferred.resolve([source, dest]); - } - }); - else - deferred.resolve([source, dest]); - return deferred.promise; -} diff --git a/browser/devtools/webaudioeditor/webaudioeditor-view.js b/browser/devtools/webaudioeditor/webaudioeditor-view.js deleted file mode 100644 index 8bcf4b366ea..00000000000 --- a/browser/devtools/webaudioeditor/webaudioeditor-view.js +++ /dev/null @@ -1,636 +0,0 @@ -/* 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"); - -// Strings for rendering -const EXPAND_INSPECTOR_STRING = L10N.getStr("expandInspector"); -const COLLAPSE_INSPECTOR_STRING = L10N.getStr("collapseInspector"); - -// Store width as a preference rather than hardcode -// TODO bug 1009056 -const INSPECTOR_WIDTH = 300; - -// 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" -}; - -const GRAPH_DEBOUNCE_TIMER = 100; - -const GENERIC_VARIABLES_VIEW_SETTINGS = { - searchEnabled: false, - editableValueTooltip: "", - editableNameTooltip: "", - preventDisableOnChange: true, - preventDescriptorModifiers: false, - 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._onThemeChange = this._onThemeChange.bind(this); - this._onNodeSelect = this._onNodeSelect.bind(this); - this._onStartContext = this._onStartContext.bind(this); - this._onDestroyNode = this._onDestroyNode.bind(this); - - this.draw = debounce(this.draw.bind(this), GRAPH_DEBOUNCE_TIMER); - $('#graph-target').addEventListener('click', this._onGraphNodeClick, false); - - window.on(EVENTS.THEME_CHANGE, this._onThemeChange); - window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSelect); - window.on(EVENTS.START_CONTEXT, this._onStartContext); - window.on(EVENTS.DESTROY_NODE, this._onDestroyNode); - }, - - /** - * Destruction function, called when the tool is closed. - */ - destroy: function() { - if (this._zoomBinding) { - this._zoomBinding.on("zoom", null); - } - $('#graph-target').removeEventListener('click', this._onGraphNodeClick, false); - window.off(EVENTS.THEME_CHANGE, this._onThemeChange); - window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSelect); - window.off(EVENTS.START_CONTEXT, this._onStartContext); - window.off(EVENTS.DESTROY_NODE, this._onDestroyNode); - }, - - /** - * Called when a page is reloaded and waiting for a "start-context" event - * and clears out old content - */ - resetUI: function () { - this.clearGraph(); - this.resetGraphPosition(); - }, - - /** - * 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. - */ - resetGraphPosition: function () { - 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. - * Called from UI_INSPECTOR_NODE_SELECT. - */ - 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 + "']"); - }, - - /** - * `draw` renders the ViewNodes currently available in `AudioNodes` with `AudioNodeConnections`, - * and `AudioParamConnections` 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.clearGraph(); - - let graph = new dagreD3.Digraph(); - // An array of duples/tuples of pairs [sourceNode, destNode, param]. - // `param` is optional, indicating a connection to an AudioParam, rather than - // an other AudioNode. - let edges = []; - - AudioNodes.forEach(node => { - // Add node to graph - graph.addNode(node.id, { - type: node.type, // Just for storing type data - label: node.type.replace(/Node$/, ""), // Displayed in SVG node - id: node.id // Identification - }); - - // 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, new Set()).forEach(dest => edges.push([node, dest])); - let paramConnections = AudioParamConnections.get(node, {}); - Object.keys(paramConnections).forEach(destId => { - let dest = getViewNodeById(destId); - let connections = paramConnections[destId] || []; - connections.forEach(param => edges.push([node, dest, param])); - }); - }); - - edges.forEach(([node, dest, param]) => { - let options = { - source: node.id, - target: dest.id - }; - - // Only add `label` if `param` specified, as this is an AudioParam connection then. - // `label` adds the magic to render with dagre-d3, and `param` is just more explicitly - // the param, ignoring implementation details. - if (param) { - options.label = param; - options.param = param; - } - - graph.addEdge(null, node.id, dest.id, options); - }); - - 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 "audionode type-" + node.type; - }); - svgNodes.attr("data-id", (n) => { - let node = graph.node(n); - return node.id; - }); - return svgNodes; - }); - - // Post-render manipulation of edges - // TODO do all of this more efficiently, rather than - // using the direct D3 helper utilities to loop over each - // edge several times - let oldDrawEdgePaths = renderer.drawEdgePaths(); - renderer.drawEdgePaths(function(graph, root) { - let svgEdges = oldDrawEdgePaths(graph, root); - svgEdges.attr("data-source", (n) => { - let edge = graph.edge(n); - return edge.source; - }); - svgEdges.attr("data-target", (n) => { - let edge = graph.edge(n); - return edge.target; - }); - svgEdges.attr("data-param", (n) => { - let edge = graph.edge(n); - return edge.param ? edge.param : null; - }); - // We have to manually specify the default classes on the edges - // as to not overwrite them - let defaultClasses = "edgePath enter"; - svgEdges.attr("class", (n) => { - let edge = graph.edge(n); - return defaultClasses + (edge.param ? (" param-connection " + edge.param) : ""); - }); - - 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 = WebAudioInspectorView.getCurrentAudioNode(); - if (currentNode) { - this.focusNode(currentNode.id); - } - - // Fire an event upon completed rendering - let paramEdgeCount = edges.filter(p => !!p[2]).length; - window.emit(EVENTS.UI_GRAPH_RENDERED, AudioNodes.length, edges.length - paramEdgeCount, paramEdgeCount); - }); - - 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.resetGraphPosition(); - } - }, - - /** - * 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 a node gets GC'd -- redraws the graph. - */ - _onDestroyNode: function () { - this.draw(); - }, - - _onNodeSelect: function (eventName, id) { - this.focusNode(id); - }, - - /** - * 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 node in the svg graph is clicked. Used to handle triggering the AudioNodePane. - * - * @param Event e - * Click event. - */ - _onGraphNodeClick: 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; - - window.emit(EVENTS.UI_SELECT_NODE, node.getAttribute("data-id")); - } -}; - -let WebAudioInspectorView = { - - _propsView: null, - - _currentNode: null, - - _inspectorPane: null, - _inspectorPaneToggleButton: null, - _tabsPane: null, - - /** - * Initialization function called when the tool starts up. - */ - initialize: function () { - this._inspectorPane = $("#web-audio-inspector"); - this._inspectorPaneToggleButton = $("#inspector-pane-toggle"); - this._tabsPane = $("#web-audio-editor-tabs"); - - // Hide inspector view on startup - this._inspectorPane.setAttribute("width", INSPECTOR_WIDTH); - this.toggleInspector({ visible: false, delayed: false, animated: false }); - - this._onEval = this._onEval.bind(this); - this._onNodeSelect = this._onNodeSelect.bind(this); - this._onTogglePaneClick = this._onTogglePaneClick.bind(this); - this._onDestroyNode = this._onDestroyNode.bind(this); - - this._inspectorPaneToggleButton.addEventListener("mousedown", this._onTogglePaneClick, false); - this._propsView = new VariablesView($("#properties-tabpanel-content"), GENERIC_VARIABLES_VIEW_SETTINGS); - this._propsView.eval = this._onEval; - - window.on(EVENTS.UI_SELECT_NODE, this._onNodeSelect); - window.on(EVENTS.DESTROY_NODE, this._onDestroyNode); - }, - - /** - * Destruction function called when the tool cleans up. - */ - destroy: function () { - this._inspectorPaneToggleButton.removeEventListener("mousedown", this._onTogglePaneClick); - window.off(EVENTS.UI_SELECT_NODE, this._onNodeSelect); - window.off(EVENTS.DESTROY_NODE, this._onDestroyNode); - - this._inspectorPane = null; - this._inspectorPaneToggleButton = null; - this._tabsPane = null; - }, - - /** - * Toggles the visibility of the AudioNode Inspector. - * - * @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 - * - index: the index of the tab to be selected inside the inspector - * @param number index - * Index of the tab that should be selected when shown. - */ - toggleInspector: function ({ visible, animated, delayed, index }) { - let pane = this._inspectorPane; - let button = this._inspectorPaneToggleButton; - - let flags = { - visible: visible, - animated: animated != null ? animated : true, - delayed: delayed != null ? delayed : true, - callback: () => window.emit(EVENTS.UI_INSPECTOR_TOGGLED, visible) - }; - - ViewHelpers.togglePane(flags, pane); - - if (flags.visible) { - button.removeAttribute("pane-collapsed"); - button.setAttribute("tooltiptext", COLLAPSE_INSPECTOR_STRING); - } - else { - button.setAttribute("pane-collapsed", ""); - button.setAttribute("tooltiptext", EXPAND_INSPECTOR_STRING); - } - - if (index != undefined) { - pane.selectedIndex = index; - } - }, - - /** - * Returns a boolean indicating whether or not the AudioNode inspector - * is currently being shown. - */ - isVisible: function () { - return !this._inspectorPane.hasAttribute("pane-collapsed"); - }, - - /** - * Takes a AudioNodeView `node` and sets it as the current - * node and scaffolds the inspector view based off of the new node. - */ - setCurrentAudioNode: 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._setTitle(); - this._buildPropertiesView() - .then(() => 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 () { - this._propsView.empty(); - // Set current node to empty to load empty view - this.setCurrentAudioNode(); - - // Reset AudioNode inspector and hide - this.toggleInspector({ visible: false, animated: false, delayed: false }); - }, - - /** - * Sets the title of the Inspector view - */ - _setTitle: function () { - let node = this._currentNode; - let title = node.type.replace(/Node$/, ""); - $("#web-audio-inspector-title").setAttribute("value", title); - }, - - /** - * 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, - }; - audioParamsScope.addItem(param, descriptor); - }); - - audioParamsScope.expanded = true; - - window.emit(EVENTS.UI_PROPERTIES_TAB_RENDERED, node.id); - }), - - _togglePropertiesView: function (show) { - let propsView = $("#properties-tabpanel-content"); - let emptyView = $("#properties-tabpanel-content-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 - */ - - /** - * 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); - } - }), - - /** - * Called on EVENTS.UI_SELECT_NODE, and takes an actorID `id` - * and calls `setCurrentAudioNode` to scaffold the inspector view. - */ - _onNodeSelect: function (_, id) { - this.setCurrentAudioNode(getViewNodeById(id)); - - // Ensure inspector is visible when selecting a new node - this.toggleInspector({ visible: true }); - }, - - /** - * Called when clicking on the toggling the inspector into view. - */ - _onTogglePaneClick: function () { - this.toggleInspector({ visible: !this.isVisible() }); - }, - - /** - * Called when `DESTROY_NODE` is fired to remove the node from props view if - * it's currently selected. - */ - _onDestroyNode: function (_, id) { - if (this._currentNode && this._currentNode.id === id) { - this.setCurrentAudioNode(null); - } - } -}; - -/** - * 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; -} diff --git a/browser/devtools/webaudioeditor/webaudioeditor.xul b/browser/devtools/webaudioeditor/webaudioeditor.xul index 0b01556c592..9be6b2b52b9 100644 --- a/browser/devtools/webaudioeditor/webaudioeditor.xul +++ b/browser/devtools/webaudioeditor/webaudioeditor.xul @@ -19,8 +19,12 @@ From 8c97ed5e02ca1d7dae23aec9beb806d8131ebcc7 Mon Sep 17 00:00:00 2001 From: Robert Bindar Date: Fri, 19 Sep 2014 15:35:00 -0400 Subject: [PATCH 21/56] Bug 1070209 - Fix typo in NotificationStorage.js. r=baku --- dom/notification/NotificationStorage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dom/notification/NotificationStorage.js b/dom/notification/NotificationStorage.js index 247cd2b9280..6c3470d8654 100644 --- a/dom/notification/NotificationStorage.js +++ b/dom/notification/NotificationStorage.js @@ -86,7 +86,7 @@ NotificationStorage.prototype = { timestamp: new Date().getTime(), origin: origin, data: data, - behavior: behavior + mozbehavior: behavior }; this._notifications[id] = notification; @@ -207,7 +207,7 @@ NotificationStorage.prototype = { notification.tag, notification.icon, notification.data, - notification.behavior); + notification.mozbehavior); } catch (e) { if (DEBUG) { debug("Error calling callback handle: " + e); } } From 74904f3dfa2fa7bece191c174b59741fdcbbf60f Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Mon, 22 Sep 2014 07:00:48 -0700 Subject: [PATCH 22/56] Bumping gaia.json for 4 gaia revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/1b8178de24a1 Author: Ryan VanderMeulen Desc: Merge pull request #24227 from sv99/1070006 Bug 1070006 - [calendar] unit test change chai.assert api in the new version ======== https://hg.mozilla.org/integration/gaia-central/rev/5f2cfc69dda6 Author: sv99 Desc: Bug 1070006 - [calendar] unit test change chai.assert api in the new version ======== https://hg.mozilla.org/integration/gaia-central/rev/ce3151a7058a Author: Ryan VanderMeulen Desc: Merge pull request #23856 from yfdyh000/1061096-rc Bug 1061096 - Fix the edit button is not disabled when downloads list are empty list ======== https://hg.mozilla.org/integration/gaia-central/rev/d5f0922b558d Author: YFdyh000 Desc: Bug 1061096 - Fix the edit button is not disabled --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index b5e1d5fe8e2..9e900236e34 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "dd788ef126f869dbdf50fe18f9b0291aba0eeaed", + "revision": "1b8178de24a10ae06fba8b227a07ae3c80ef26b3", "repo_path": "/integration/gaia-central" } From a47baf83fccf4fda3476d9b5cd4371f438067b17 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Mon, 22 Sep 2014 07:06:58 -0700 Subject: [PATCH 23/56] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index a1bdc97f381..95c62129883 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 15224f7defc..c6bbe63dd53 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 306d998466e..35251f683a8 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 5e650fb3820..7797e579cf1 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 15224f7defc..c6bbe63dd53 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index c7d2657237d..d4b79c7b8d1 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index d98fe6b5984..62c1374bf1a 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index e2c28eba932..6e89e8ac17d 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index 2ccea3efc65..3ab0ac4451f 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 1d56aa1081a..83d3df91788 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index b901ae9a7b5..30eb8456711 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From 68a43ba8ef2a1aaa350f221b44a1ef06729d1ebc Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Mon, 22 Sep 2014 07:30:48 -0700 Subject: [PATCH 24/56] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ======== https://hg.mozilla.org/integration/gaia-central/rev/9f881b781db0 Author: Jose M. Cantera Desc: Merge pull request #24096 from ADLR-es/bug-intermittent Bug 1053897 - Intermittent contacts/test/unit/views/list_test.js | Rende... ======== https://hg.mozilla.org/integration/gaia-central/rev/cc3c30066fc5 Author: Adrián de la Rosa Desc: Bug 1053897 - Intermittent contacts/test/unit/views/list_test.js | Render contacts list Facebook Contacts List Search phrase highlightded correctly for first letter | Error: expected false to be true --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 9e900236e34..dea57e2cb3f 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "1b8178de24a10ae06fba8b227a07ae3c80ef26b3", + "revision": "9f881b781db0956219feb8ae51cb7e6262fe4dd4", "repo_path": "/integration/gaia-central" } From 61cab9a26f87d9aa6dbaf1b36a72578b5ed20061 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Mon, 22 Sep 2014 07:37:03 -0700 Subject: [PATCH 25/56] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index 95c62129883..f396a9b7ba5 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index c6bbe63dd53..0709c57d717 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 35251f683a8..1a9d5d3941d 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 7797e579cf1..2e76cfff8b4 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index c6bbe63dd53..0709c57d717 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index d4b79c7b8d1..f02ddc38cc2 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 62c1374bf1a..47f77d316d2 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index 6e89e8ac17d..06778cf6567 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index 3ab0ac4451f..66281e02c22 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 83d3df91788..0b748c13403 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index 30eb8456711..114934f583b 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From 8a10ce9df7c23f4da8edc2d0076e933a342e254c Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Mon, 22 Sep 2014 07:45:53 -0700 Subject: [PATCH 26/56] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/a6dbe06165e8 Author: chirarobert Desc: Merge pull request #24218 from bebef1987/timezone Bug 1069911 - Update test_ftu_skip_tour setup ======== https://hg.mozilla.org/integration/gaia-central/rev/7d27608def9f Author: Bebe Desc: Bug 1069911 - Update test_ftu_skip_tour setup --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index dea57e2cb3f..b5155e5bf8f 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "9f881b781db0956219feb8ae51cb7e6262fe4dd4", + "revision": "a6dbe06165e8042a9c7d1e40ab580b690d1527ac", "repo_path": "/integration/gaia-central" } From 5b374cf735e2f4ef4a713b99143e673e55308884 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Mon, 22 Sep 2014 07:52:04 -0700 Subject: [PATCH 27/56] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index f396a9b7ba5..f2f6d07db4b 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 0709c57d717..372828b9797 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 1a9d5d3941d..be7e8a875dc 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 2e76cfff8b4..d589559af85 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 0709c57d717..372828b9797 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index f02ddc38cc2..89a1900b65f 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 47f77d316d2..e3b3a5f6016 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index 06778cf6567..05f3c7ee9f1 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index 66281e02c22..4ba2cfefd4e 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 0b748c13403..3b053bdcabd 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index 114934f583b..8f873569909 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From f6c0175afebadd9b640ec745d075079195f12515 Mon Sep 17 00:00:00 2001 From: Robin Thunell Date: Mon, 22 Sep 2014 07:58:59 -0700 Subject: [PATCH 28/56] Bug 1059208 - Add scripts for signing manifest files of Trusted Hosted Apps r=dkeeler --- security/apps/Makefile.in | 15 ++ security/apps/gen_cert_header.py | 12 +- .../tests/unit/test_signed_manifest/README.md | 17 ++ .../test_signed_manifest/create_test_files.sh | 181 ++++++++++++++++++ .../unit/test_signed_manifest/manifest.webapp | 10 + .../unit/test_signed_manifest/nss_ctypes.py | 136 +++++++++++++ .../test_signed_manifest/sign_b2g_manifest.py | 76 ++++++++ .../testInvalidSignedManifest/manifest.sig | Bin 0 -> 1501 bytes .../testValidSignedManifest/manifest.sig | Bin 0 -> 1494 bytes .../unit/test_signed_manifest/trusted_ca1.der | Bin 0 -> 928 bytes 10 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 security/manager/ssl/tests/unit/test_signed_manifest/README.md create mode 100755 security/manager/ssl/tests/unit/test_signed_manifest/create_test_files.sh create mode 100644 security/manager/ssl/tests/unit/test_signed_manifest/manifest.webapp create mode 100644 security/manager/ssl/tests/unit/test_signed_manifest/nss_ctypes.py create mode 100644 security/manager/ssl/tests/unit/test_signed_manifest/sign_b2g_manifest.py create mode 100644 security/manager/ssl/tests/unit/test_signed_manifest/testInvalidSignedManifest/manifest.sig create mode 100644 security/manager/ssl/tests/unit/test_signed_manifest/testValidSignedManifest/manifest.sig create mode 100644 security/manager/ssl/tests/unit/test_signed_manifest/trusted_ca1.der diff --git a/security/apps/Makefile.in b/security/apps/Makefile.in index a9e23da1931..9d65c6d7785 100644 --- a/security/apps/Makefile.in +++ b/security/apps/Makefile.in @@ -4,6 +4,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. GEN_CERT_HEADER = $(srcdir)/gen_cert_header.py +TEST_SSL_PATH = $(srcdir)/../manager/ssl/tests/unit/test_signed_manifest/ marketplace-prod-public.inc: marketplace-prod-public.crt $(GEN_CERT_HEADER) $(PYTHON) $(GEN_CERT_HEADER) marketplaceProdPublicRoot $< > $@ @@ -20,6 +21,18 @@ marketplace-dev-reviewers.inc: marketplace-dev-reviewers.crt $(GEN_CERT_HEADER) marketplace-stage.inc: marketplace-stage.crt $(GEN_CERT_HEADER) $(PYTHON) $(GEN_CERT_HEADER) marketplaceStageRoot $< > $@ +ifeq ($(shell test -s trusted-app-public.der; echo $$?),0) +TRUSTED_APP_PUBLIC=trusted-app-public.der +else +TRUSTED_APP_PUBLIC= +endif + +manifest-signing-root.inc: $(TRUSTED_APP_PUBLIC) $(GEN_CERT_HEADER) + $(PYTHON) $(GEN_CERT_HEADER) trustedAppPublicRoot $(TRUSTED_APP_PUBLIC) > $@ + +manifest-signing-test-root.inc: $(TEST_SSL_PATH)trusted_ca1.der $(GEN_CERT_HEADER) + $(PYTHON) $(GEN_CERT_HEADER) trustedAppTestRoot $< > $@ + xpcshell.inc: $(srcdir)/../manager/ssl/tests/unit/test_signed_apps/trusted_ca1.der $(GEN_CERT_HEADER) $(PYTHON) $(GEN_CERT_HEADER) xpcshellRoot $< > $@ @@ -29,5 +42,7 @@ export:: \ marketplace-dev-public.inc \ marketplace-dev-reviewers.inc \ marketplace-stage.inc \ + manifest-signing-root.inc \ + manifest-signing-test-root.inc \ xpcshell.inc \ $(NULL) diff --git a/security/apps/gen_cert_header.py b/security/apps/gen_cert_header.py index 58534736ed5..f77e5ccf1a3 100644 --- a/security/apps/gen_cert_header.py +++ b/security/apps/gen_cert_header.py @@ -22,8 +22,18 @@ def create_header(array_name, in_filename): print "};" return 0 +def create_empty_header(array_name): + # mfbt/ArrayUtils.h will not be able to pick up the + # correct specialization for ArrayLength(const array[0]) + # so add a value of 0 which will fail cert verification + # just the same as an empty array + print "const uint8_t " + array_name + "[] = { 0x0 };" + return 0 + if __name__ == '__main__': - if len(sys.argv) < 3: + if len(sys.argv) < 2: print 'ERROR: usage: gen_cert_header.py array_name in_filename' sys.exit(1); + if len(sys.argv) == 2: + sys.exit(create_empty_header(sys.argv[1])) sys.exit(create_header(sys.argv[1], sys.argv[2])) diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/README.md b/security/manager/ssl/tests/unit/test_signed_manifest/README.md new file mode 100644 index 00000000000..a442c705280 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_manifest/README.md @@ -0,0 +1,17 @@ +This folder contains the scripts needed to generate signed manifest files +to verify the Trusted Hosted Apps concept. + +Prerequisites: + +* NSS 3.4 or higher. +* Python 2.7 (should work with 2.6 also) +* Bash +* OpenSSL + +Usage: + +Run + I) For usage info execute ./create_test_files.sh --help + + II) Upload the signed manifest.webapp and manifest.sig to the + application hosting server. diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/create_test_files.sh b/security/manager/ssl/tests/unit/test_signed_manifest/create_test_files.sh new file mode 100755 index 00000000000..cddbe17ec1e --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_manifest/create_test_files.sh @@ -0,0 +1,181 @@ +#!/bin/bash +# +# Mode: shell-script; sh-indentation:2; +# +# 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/. + +export BASE_PATH=`dirname $0` +echo $BASE_PATH + +# location of the 'sign_b2g_manifest.py' script +export SIGN_SCR_PATH=. + +DB_PATH=${BASE_PATH}/signingDB +PASSWORD_FILE=${DB_PATH}/passwordfile +VALID_MANIFEST_PATH=${BASE_PATH}/testValidSignedManifest +INVALID_MANIFEST_PATH=${BASE_PATH}/testInvalidSignedManifest + +TRUSTED_EE=trusted_ee1 +UNTRUSTED_EE=untrusted_ee1 +TRUSTED_CA=trusted_ca1 +UNTRUSTED_CA=untrusted_ca1 + +# Print usage info +usage() { + echo + echo + tput bold + echo "NAME" + tput sgr0 + echo " create_test_files.sh - Signing a manifest for Trusted Hosted Apps." + echo + tput bold + echo "SYNOPSIS" + tput sgr0 + echo " create_test_files.sh" + echo " create_test_files.sh [--regenerate-test-certs]" + echo " create_test_files.sh [--help]" + echo + tput bold + echo "DESCRIPTION" + tput sgr0 + echo " The script signs a manifest for Trusted Hosted Apps if no parameter" + echo " is given and if the manifest file and a certificate database directory" + echo " is present in the current directory." + echo " Two directories ./testValidSignedManifest and ./testInvalidSignedManifest" + echo " are generated containing a manifest signature file each, signed with valid" + echo " and invalid certificates respectively." + echo " If the --regenerate-test-certs parameter is given, a new certificate database" + echo " directory is generated before the signing takes place." + echo " If the certificate database is not present and the --regenerate-test-certs" + echo " parameter is not given the script exits whithout any operations." + echo + tput bold + echo "OPTIONS" + echo " --regenerate-test-certs," + tput sgr0 + echo " Generates a test certificate database and then signs the manifest.webapp" + echo " file in the current directory." + echo + tput bold + echo " --help," + tput sgr0 + echo " Show this usage information." + echo +} + +# Function to create a signing database +# Parameters: +# $1: Output directory (where the DB will be created) +# $2: Password file +createDB() { + local db_path=${1} + local password_file=${2} + + mkdir -p ${db_path} + echo insecurepassword > ${password_file} + certutil -d ${db_path} -N -f ${password_file} 2>&1 >/dev/null +} + +# Add a CA cert and a signing cert to the database +# Arguments: +# $1: DB directory +# $2: CA CN (don't include the CN=, just the value) +# $3: Signing Cert CN (don't include the CN=, just the value) +# $4: CA short name (don't use spaces!) +# $5: Signing Cert short name (don't use spaces!) +# $6: Password file +addCerts() { + local db_path=${1} + local password_file=${6} + + org="O=Example Trusted Corporation,L=Mountain View,ST=CA,C=US" + ca_subj="CN=${2},${org}" + ee_subj="CN=${3},${org}" + + noisefile=/tmp/noise.$$ + head -c 32 /dev/urandom > ${noisefile} + + ca_responses=/tmp/caresponses.$$ + ee_responses=/tmp/earesponses + + echo y > ${ca_responses} # Is this a CA? + echo >> ${ca_responses} # Accept default path length constraint (no constraint) + echo y >> ${ca_responses} # Is this a critical constraint? + echo n > ${ee_responses} # Is this a CA? + echo >> ${ee_responses} # Accept default path length constraint (no constraint) + echo y >> ${ee_responses} # Is this a critical constraint? + + make_cert="certutil -d ${db_path} -f ${password_file} -S -g 2048 -Z SHA256 \ + -z ${noisefile} -y 3 -2 --extKeyUsage critical,codeSigning" + ${make_cert} -v 480 -n ${4} -m 1 -s "${ca_subj}" --keyUsage critical,certSigning \ + -t ",,CTu" -x < ${ca_responses} 2>&1 >/dev/null + ${make_cert} -v 240 -n ${5} -c ${4} -m 2 -s "${ee_subj}" --keyUsage critical,digitalSignature \ + -t ",,," < ${ee_responses} 2>&1 >/dev/null + + certutil -d ${db_path} -L -n ${4} -r -o ${SIGN_SCR_PATH}/${4}.der + + rm -f ${noisefile} ${ee_responses} ${ca_responses} +} + +# Signs a manifest +# Parameters: +# $1: Database directory +# $2: Unsigned manifest file path +# $3: Signed manifest file path +# $4: Nickname of the signing certificate +# $5: Password file +signManifest() { + local db_path=${1} + local password_file=${5} + + python ${BASE_PATH}/${SIGN_SCR_PATH}/sign_b2g_manifest.py -d ${db_path} \ + -f ${password_file} -k ${4} -i ${2} -o ${3} +} + +# Generate the necessary files to be used for the signing +generate_files() { + # First create a new couple of signing DBs + rm -rf ${DB_PATH} ${VALID_MANIFEST_PATH} ${INVALID_MANIFEST_PATH} + createDB ${DB_PATH} ${PASSWORD_FILE} + addCerts ${DB_PATH} "Trusted Valid CA" "Trusted Corp Cert" ${TRUSTED_CA} ${TRUSTED_EE} ${PASSWORD_FILE} + addCerts ${DB_PATH} "Trusted Invalid CA" "Trusted Invalid Cert" ${UNTRUSTED_CA} ${UNTRUSTED_EE} ${PASSWORD_FILE} +} + +#Start of execution +if [ ${1} ] && [ "${1}" == '--regenerate-test-certs' ]; then + generate_files +elif [ "${1}" == '--help' ]; then + usage + exit 1 +else + if [ -d ${DB_PATH} ]; then + rm -rf ${VALID_MANIFEST_PATH} ${INVALID_MANIFEST_PATH} + else + echo "Error! The directory ${DB_PATH} does not exist!" + echo "New certificate database must be created!" + usage $0 + exit 1 + fi +fi + +# Create all the test manifests +mkdir -p ${VALID_MANIFEST_PATH} +mkdir -p ${INVALID_MANIFEST_PATH} + +CURDIR=`pwd` +cd $CURDIR + +# Sign a manifest file with a known issuer +signManifest ${DB_PATH} ${BASE_PATH}/manifest.webapp \ + ${VALID_MANIFEST_PATH}/manifest.sig \ + ${TRUSTED_EE} ${PASSWORD_FILE} + +# Sign a manifest file with a unknown issuer +signManifest ${DB_PATH} ${BASE_PATH}/manifest.webapp \ + ${INVALID_MANIFEST_PATH}/manifest.sig \ + ${UNTRUSTED_EE} ${PASSWORD_FILE} + +echo "Done!" diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/manifest.webapp b/security/manager/ssl/tests/unit/test_signed_manifest/manifest.webapp new file mode 100644 index 00000000000..ad477719b48 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_manifest/manifest.webapp @@ -0,0 +1,10 @@ +{ "name": "Trusted App Example", + "description": "A Manifest for a Trusted Hosted Application", + "type": "trusted", + "launch_path": "/index.html", + "icons": { "128" : "icon-128.png" }, + "version": 1, + "csp" : "script-src https://www.example.com; style-src https://www.example.com", + "permissions": { "device-storage:videos":{ "access": "readonly" }, "device-storage:pictures":{ "access": "readwrite" } }, + "default_locale": "en-US" +} diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/nss_ctypes.py b/security/manager/ssl/tests/unit/test_signed_manifest/nss_ctypes.py new file mode 100644 index 00000000000..393ca249f77 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_manifest/nss_ctypes.py @@ -0,0 +1,136 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python +# +# 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/. + +from ctypes import * +import os +import sys + +if sys.platform == 'darwin': + libprefix = "lib" + libsuffix = ".dylib" +elif os.name == 'posix': + libprefix = "lib" + libsuffix = ".so" +else: # assume windows + libprefix = "" + libsuffix = ".dll" + +plc = cdll.LoadLibrary(libprefix + "plc4" + libsuffix) +nspr = cdll.LoadLibrary(libprefix + "nspr4" + libsuffix) +nss = cdll.LoadLibrary(libprefix + "nss3" + libsuffix) +smime = cdll.LoadLibrary(libprefix + "smime3" + libsuffix) + +nspr.PR_GetError.argtypes = [] +nspr.PR_GetError.restype = c_int32 +nspr.PR_ErrorToName.argtypes = [c_int32] +nspr.PR_ErrorToName.restype = c_char_p + +def raise_if_not_SECSuccess(rv): + SECSuccess = 0 + if (rv != SECSuccess): + raise ValueError(nspr.PR_ErrorToName(nspr.PR_GetError())) + +def raise_if_NULL(p): + if not p: + raise ValueError(nspr.PR_ErrorToName(nspr.PR_GetError())) + return p + +PRBool = c_int +SECStatus = c_int + +# from secoidt.h +SEC_OID_SHA1 = 4 + +# from certt.h +certUsageObjectSigner = 6 + +class SECItem(Structure): + _fields_ = [("type", c_int), + ("data", c_char_p), + ("len", c_uint)] + +nss.NSS_Init.argtypes = [c_char_p] +nss.NSS_Init.restype = SECStatus +def NSS_Init(db_dir): + nss.NSS_Init.argtypes = [c_char_p] + nss.NSS_Init.restype = SECStatus + raise_if_not_SECSuccess(nss.NSS_Init(db_dir)) + +nss.NSS_Shutdown.argtypes = [] +nss.NSS_Shutdown.restype = SECStatus +def NSS_Shutdown(): + raise_if_not_SECSuccess(nss.NSS_Shutdown()) + +PK11PasswordFunc = CFUNCTYPE(c_char_p, c_void_p, PRBool, c_char_p) + +# pass the result of this as the wincx parameter when a wincx is required +nss.PK11_SetPasswordFunc.argtypes = [PK11PasswordFunc] +nss.PK11_SetPasswordFunc.restype = None + +# Set the return type as *void so Python doesn't touch it +plc.PL_strdup.argtypes = [c_char_p] +plc.PL_strdup.restype = c_void_p +def SetPasswordContext(password): + def callback(slot, retry, arg): + return plc.PL_strdup(password) + wincx = PK11PasswordFunc(callback) + nss.PK11_SetPasswordFunc(wincx) + return wincx + +nss.CERT_GetDefaultCertDB.argtypes = [] +nss.CERT_GetDefaultCertDB.restype = c_void_p +def CERT_GetDefaultCertDB(): + return raise_if_NULL(nss.CERT_GetDefaultCertDB()) + +nss.PK11_FindCertFromNickname.argtypes = [c_char_p, c_void_p] +nss.PK11_FindCertFromNickname.restype = c_void_p +def PK11_FindCertFromNickname(nickname, wincx): + return raise_if_NULL(nss.PK11_FindCertFromNickname(nickname, wincx)) + +nss.CERT_DestroyCertificate.argtypes = [c_void_p] +nss.CERT_DestroyCertificate.restype = None +def CERT_DestroyCertificate(cert): + nss.CERT_DestroyCertificate(cert) + +smime.SEC_PKCS7CreateSignedData.argtypes = [c_void_p, c_int, c_void_p, + c_int, c_void_p, + c_void_p, c_void_p] +smime.SEC_PKCS7CreateSignedData.restype = c_void_p +def SEC_PKCS7CreateSignedData(cert, certusage, certdb, digestalg, digest, wincx): + item = SECItem(0, c_char_p(digest), len(digest)) + return raise_if_NULL(smime.SEC_PKCS7CreateSignedData(cert, certusage, certdb, + digestalg, + pointer(item), + None, wincx)) + +smime.SEC_PKCS7AddSigningTime.argtypes = [c_void_p] +smime.SEC_PKCS7AddSigningTime.restype = SECStatus +def SEC_PKCS7AddSigningTime(p7): + raise_if_not_SECSuccess(smime.SEC_PKCS7AddSigningTime(p7)) + +smime.SEC_PKCS7IncludeCertChain.argtypes = [c_void_p, c_void_p] +smime.SEC_PKCS7IncludeCertChain.restype = SECStatus +def SEC_PKCS7IncludeCertChain(p7, wincx): + raise_if_not_SECSuccess(smime.SEC_PKCS7IncludeCertChain(p7, wincx)) + +SEC_PKCS7EncoderOutputCallback = CFUNCTYPE(None, c_void_p, c_void_p, c_long) +smime.SEC_PKCS7Encode.argtypes = [c_void_p, SEC_PKCS7EncoderOutputCallback, + c_void_p, c_void_p, c_void_p, c_void_p] +smime.SEC_PKCS7Encode.restype = SECStatus +def SEC_PKCS7Encode(p7, bulkkey, wincx): + outputChunks = [] + def callback(chunks, data, len): + outputChunks.append(string_at(data, len)) + callbackWrapper = SEC_PKCS7EncoderOutputCallback(callback) + raise_if_not_SECSuccess(smime.SEC_PKCS7Encode(p7, callbackWrapper, + None, None, None, wincx)) + return "".join(outputChunks) + +smime.SEC_PKCS7DestroyContentInfo.argtypes = [c_void_p] +smime.SEC_PKCS7DestroyContentInfo.restype = None +def SEC_PKCS7DestroyContentInfo(p7): + smime.SEC_PKCS7DestroyContentInfo(p7) diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/sign_b2g_manifest.py b/security/manager/ssl/tests/unit/test_signed_manifest/sign_b2g_manifest.py new file mode 100644 index 00000000000..acc5ac2f441 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_manifest/sign_b2g_manifest.py @@ -0,0 +1,76 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python +# +# 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/. + +import argparse +from base64 import b64encode +from hashlib import sha1 +import sys +import ctypes +import nss_ctypes + +def nss_create_detached_signature(cert, dataToSign, wincx): + certdb = nss_ctypes.CERT_GetDefaultCertDB() + p7 = nss_ctypes.SEC_PKCS7CreateSignedData(cert, + nss_ctypes.certUsageObjectSigner, + certdb, + nss_ctypes.SEC_OID_SHA1, + sha1(dataToSign).digest(), + wincx) + try: + nss_ctypes.SEC_PKCS7AddSigningTime(p7) + nss_ctypes.SEC_PKCS7IncludeCertChain(p7, wincx) + return nss_ctypes.SEC_PKCS7Encode(p7, None, wincx) + finally: + nss_ctypes.SEC_PKCS7DestroyContentInfo(p7) + +# Sign a manifest file +def sign_file(in_file, out_file, cert, wincx): + contents = in_file.read() + in_file.close() + + # generate base64 encoded string of the sha1 digest of the input file + in_file_hash = b64encode(sha1(contents).digest()) + + # sign the base64 encoded string with the given certificate + in_file_signature = nss_create_detached_signature(cert, in_file_hash, wincx) + + # write the content of the output file + out_file.write(in_file_signature) + out_file.close() + +def main(): + parser = argparse.ArgumentParser(description='Sign a B2G app manifest.') + parser.add_argument('-d', action='store', + required=True, help='NSS database directory') + parser.add_argument('-f', action='store', + type=argparse.FileType('rb'), + required=True, help='password file') + parser.add_argument('-k', action='store', + required=True, help="nickname of signing cert.") + parser.add_argument('-i', action='store', type=argparse.FileType('rb'), + required=True, help="input manifest file (unsigned)") + parser.add_argument('-o', action='store', type=argparse.FileType('wb'), + required=True, help="output manifest file (signed)") + args = parser.parse_args() + + db_dir = args.d + password = args.f.readline().strip() + cert_nickname = args.k + cert = None + + try: + nss_ctypes.NSS_Init(db_dir) + wincx = nss_ctypes.SetPasswordContext(password) + cert = nss_ctypes.PK11_FindCertFromNickname(cert_nickname, wincx) + sign_file(args.i, args.o, cert, wincx) + return 0 + finally: + nss_ctypes.CERT_DestroyCertificate(cert) + nss_ctypes.NSS_Shutdown() + +if __name__ == "__main__": + sys.exit(main()) diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/testInvalidSignedManifest/manifest.sig b/security/manager/ssl/tests/unit/test_signed_manifest/testInvalidSignedManifest/manifest.sig new file mode 100644 index 0000000000000000000000000000000000000000..d5eee196c09c719b655c73a0eac4d42a5c300d4b GIT binary patch literal 1501 zcmXqLV!g@6snzDu_MMlJooPW6>nVdK)?-YJjE39>oNTPxe9TNztPBR+2t|wwnwS?F zG%?RLXkuzzz|6$R1VjeBa0QIqK=q|y?aZMpY{E>T!7vVoFq5;Rp_qXP8&HN_nAbPI zG_NEvGfyEbGqv1M#Xt!p&Lu4ET9KGrkdvwqQdC-8lA5C6oL^LsUzAvqnV)AUZ6FC! z#VjlYll9CiOU%gxs&X`t6X!KFF|af;Ft9K-H#Lb8=QT#=8kECaLUm`!8c0K&C4%9s z)S?oDCPoG1AYx=?U~XdMWdMqEFf}nUGCY{1o@eq+Ax>d|y6Oh*i)a4i3#80UZ(I_5 z<>?zq^RBmZa#OBnuI5eR`gKf7eBDA;cau{!?|Hj+N=7wD?=A@TeYkr5_RblK4gnLR zPdV**GNq#J*h|k2J4^nZi6<8K@~z*!F?x2!Uxka$58rn3{QNN6^8V5#-*Z=O3Ot^- zG)|gvC4)IzyWxUwN@uiwv8L%5*w)XUqNy6b)yK}H>rUq!yBFH=@;0sg3N!fbZCKq< znq6MW-0ZS`uH@VE%cNG_lX{V*xzv_%-5y2>>$8;$mfLK%Ub_3i3WnY}QWCRf*}a?L z_A1`)#MO?oi|V$8^_bLrikQq{&7VC*(;*;c$I<+fi=6dLjLeHI49pC8fPpA0%*gnk zg~@=yKn%oF1@U+cxY#(f*%(<_*_oLQ_(0I(VWz}?PvKp&5eRf zH9rSRC@>!{h)DSKvZ%!A#{A;^K+V+^^oa4IUD%bj?Ht2L&J!Q~%cQvn= z&1yeS;xqeQT!nhuHq8_{_TxwR-ygG1@A`YuYB%5HeQgs93U9cl>gHum?JIwCxL5KK zgV;rlH1~$jl{YrZ-+8d*5V!Ygug>WK=VJ|K9X6ihx@N^C2KHq|=lVpqE%o2OXo`W< zHYvjcoo zcvsIgd|hARBXaF|=z|kHyVabx%t1~B!0g4+#K=(7CfYek!doKE+<)rE=&WarJEt%+ zg?;?7>9*U;^qh#iOF3@ofxi@Q$;>q6S;!mq=))IPgS|EuZ*T4InR_PWn1HfxWLVb@ zwgU^R&R$!RoiDH@O-y^mjK&E$&DY%o%-m%5$WC%xFEBy2KJMA4yY9*rw;g6Vs589& zBKdGH8~gLw^E3{M`K0yil)I_ubtuN&;o+o2-;kHn#4l?tU|ngRy7-m$iBl@vS<1Io zHe{~klKql)ri{fxK_==?();+B&xYU27OlF?5mitm9lzCyZ_)QD&x`i1oLl>$jJumH0xp{WY+E@GTCf@yC3jnRX8Xy1w literal 0 HcmV?d00001 diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/testValidSignedManifest/manifest.sig b/security/manager/ssl/tests/unit/test_signed_manifest/testValidSignedManifest/manifest.sig new file mode 100644 index 0000000000000000000000000000000000000000..996226351151e85877de8affd9e850e8b5d76521 GIT binary patch literal 1494 zcmXqLV!g!1snzDu_MMlJooPW6>tTZ?*8NP3jE39>oNTPxe9TNztPBR+2t|wwnwaMp zG%?RGXkuzuz|6$R1VjeBa0QIqK=s97?aZMpY{E>T!7vVoFq5;Rp_qXP8&HN_nAbPI zG_NEvGfyEbGqv1M#Xt!p&Lu4ET9KGrkdvwqQdC-8lA5C6oL^LsUzAvqnV)AUX&??# z#VjlUlMPGE$por#G>{YLH8e4>G%_%-Fg7zQ z3eKrTB?e853do_u$jZRn#K_A46z5=SVq|2v=O^-UUGhQgN7sHkE$-E;c$C2Tc`+O3 zX7$M9${&ATK3cGDj{Etud-zU@_Rj8la^UtVW-+yiTb}$iYn!?Aj@rRE1}~9{8~|?JhJrDciIsZ)dWf>$-UUqMyuhBG+y>&3@PH)l)J@ zQ1QBx<>|)fn$C~y_ei+N?{(P8rnBYH0mlx>)wfPNKa>70w9abJZAXS0wH!_Fh3l9y zzG;?m%SiWqYI14iqdjoRF0@EEZWEmNJIcGQ& z*A<>w6*Ae3zcHKh+KU@{vVsC#V*K{ToXgX<`m#*T5$F&<%{Pzv%8PR`B6%(C@h?^X z1QZKuxlyO~TSTos{-sxb-@0Uj2hR_6tbEg^ z68Cs=mFPLw+6VUYCbe)L5#epvx8(Ns<<&P*cyuiZo z?M|Z4C)IXsWIcc3kwb0Je)XJRUhcjv58Bl(9+|iK|BYKewo5(IGqqRn+SK`8_ifA0 zSDdo@4VxJM0aMp6U?w)G7?7L@Di9Eb$AVa3?tqmsoXm#EWr=|dLWb3lA0-2uL>VX} zWLXTwSVX+5=Ni7QukaDM_B`~#37*|*&RganCjnsQVrgPz=;tw*nR@Mc>VA_9+f{R{ z()rfavwHowdHH|zF}pd-dNjQzzFp@%n@N9`b2!J1u4f-uBZ3#D_#HK`o~+TqedDBO zqWFSpvrEc-v@c#dK0+Xo5C4`wm;T&ZT3U&&qli}M@5`=-1z5ldPGOD3$fb1-k1 zxZ&QvpLxfoWEjm{Dz1{JUGik#{Y9et-YWsdWSZzv&*U literal 0 HcmV?d00001 diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/trusted_ca1.der b/security/manager/ssl/tests/unit/test_signed_manifest/trusted_ca1.der new file mode 100644 index 0000000000000000000000000000000000000000..bf05308ccc5ad1d583bb1f7d3ae9dddf0367e64e GIT binary patch literal 928 zcmXqLVxD8r#MH8YnTe5!iILHOmyJ`a&7Gh=Wuy3k$$x!xD2cfodEL6vTNAO$;oJ3=Axc%}tG?B>0UCOpyf! z##v2_3dq63$jZRn#K_BF(8S2W)Wpchup-s`Q}Ve>XOB+EZ%$6$HA66Tn@4I<`!mh> zzPjDJndLS-`*3z;MTFP^%aacj+1j#SMh1JZc&J~TGU;+L!<%`x?4GQNTmC4ci(8#@ zss6hak?-f95kmkl~==eG9vl3nws&-IdOZ2ih%Xz^ELF^Ad?iyEEXho*koaNM?-`6W;K&$a0< zIF|}N<4+R}-E#Et%}C`a)+U9w*RF-_yT7kt-J!V#A(c#w%!{oJEDiX9@hU6K$oQXy z)qojD8Hj-dR6znf23%|$+H8!htnAFp27DlKevmi|GZPaFa^M2f9x!kj85+4dnO|z_ zOwRoFzN^dh+j?;gX4y4=UASi-nE4^>%8kyAO3ha${B-OW2=Hv*zrSS46uT2ArmvP% z-?E$GmUqXUd?OF<`^+&^Dv4(*)@#oR*S$KFuaCp8l;a7j;pXMBK#Nz%go_(ZNL zOM|6X`g_#sf8FV_;1b*NBKwZLD{95oRD}J0^6cNzQj@d411()G9`HIgKJmT#P3LyP HDam>O4tPvp literal 0 HcmV?d00001 From 2e30280054f77c68dc9b14c8e3b0870cf976ecbe Mon Sep 17 00:00:00 2001 From: Vlatko Markovic Date: Mon, 22 Sep 2014 07:58:59 -0700 Subject: [PATCH 29/56] Bug 1059216 - Verification of Trusted Hosted Apps manifest signature, part 1. r=dkeeler,rlb --- dom/apps/AppsUtils.jsm | 81 ++++++++++- dom/apps/StoreTrustAnchor.jsm | 2 + dom/apps/TrustedHostedAppsUtils.jsm | 115 ++++++++++++++- dom/apps/Webapps.jsm | 85 ++--------- js/xpconnect/src/xpc.msg | 3 + security/apps/AppSignatureVerification.cpp | 134 ++++++++++++++++++ security/apps/AppTrustDomain.cpp | 13 ++ security/manager/ssl/public/nsIX509CertDB.idl | 28 +++- xpcom/base/ErrorList.h | 7 + xpcom/base/nsError.h | 1 + 10 files changed, 391 insertions(+), 78 deletions(-) diff --git a/dom/apps/AppsUtils.jsm b/dom/apps/AppsUtils.jsm index 09a1345925e..14b19c8191e 100644 --- a/dom/apps/AppsUtils.jsm +++ b/dom/apps/AppsUtils.jsm @@ -22,7 +22,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils", XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); -// Shared code for AppsServiceChild.jsm, Webapps.jsm and Webapps.js +// Shared code for AppsServiceChild.jsm, TrustedHostedAppsUtils.jsm, +// Webapps.jsm and Webapps.js this.EXPORTED_SYMBOLS = ["AppsUtils", "ManifestHelper", "isAbsoluteURI", "mozIApplication"]; @@ -116,6 +117,84 @@ this.AppsUtils = { return obj; }, + // Creates a nsILoadContext object with a given appId and isBrowser flag. + createLoadContext: function createLoadContext(aAppId, aIsBrowser) { + return { + associatedWindow: null, + topWindow : null, + appId: aAppId, + isInBrowserElement: aIsBrowser, + usePrivateBrowsing: false, + isContent: false, + + isAppOfType: function(appType) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext, + Ci.nsIInterfaceRequestor, + Ci.nsISupports]), + getInterface: function(iid) { + if (iid.equals(Ci.nsILoadContext)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } + } + }, + + // Sends data downloaded from aRequestChannel to a file + // identified by aId and aFileName. + getFile: function(aRequestChannel, aId, aFileName) { + let deferred = Promise.defer(); + + // Staging the file in TmpD until all the checks are done. + let file = FileUtils.getFile("TmpD", ["webapps", aId, aFileName], true); + + // We need an output stream to write the channel content to the out file. + let outputStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + // write, create, truncate + outputStream.init(file, 0x02 | 0x08 | 0x20, parseInt("0664", 8), 0); + let bufferedOutputStream = + Cc['@mozilla.org/network/buffered-output-stream;1'] + .createInstance(Ci.nsIBufferedOutputStream); + bufferedOutputStream.init(outputStream, 1024); + + // Create a listener that will give data to the file output stream. + let listener = Cc["@mozilla.org/network/simple-stream-listener;1"] + .createInstance(Ci.nsISimpleStreamListener); + + listener.init(bufferedOutputStream, { + onStartRequest: function(aRequest, aContext) { + // Nothing to do there anymore. + }, + + onStopRequest: function(aRequest, aContext, aStatusCode) { + bufferedOutputStream.close(); + outputStream.close(); + + if (!Components.isSuccessCode(aStatusCode)) { + deferred.reject({ msg: "NETWORK_ERROR", downloadAvailable: true}); + return; + } + + // If we get a 4XX or a 5XX http status, bail out like if we had a + // network error. + let responseStatus = aRequestChannel.responseStatus; + if (responseStatus >= 400 && responseStatus <= 599) { + // unrecoverable error, don't bug the user + deferred.reject({ msg: "NETWORK_ERROR", downloadAvailable: false}); + return; + } + + deferred.resolve(file); + } + }); + aRequestChannel.asyncOpen(listener, null); + + return deferred.promise; + }, + getAppByManifestURL: function getAppByManifestURL(aApps, aManifestURL) { debug("getAppByManifestURL " + aManifestURL); // This could be O(1) if |webapps| was a dictionary indexed on manifestURL diff --git a/dom/apps/StoreTrustAnchor.jsm b/dom/apps/StoreTrustAnchor.jsm index 9321bcfdf23..77c43796fbb 100644 --- a/dom/apps/StoreTrustAnchor.jsm +++ b/dom/apps/StoreTrustAnchor.jsm @@ -16,6 +16,8 @@ const APP_TRUSTED_ROOTS= ["AppMarketplaceProdPublicRoot", "AppMarketplaceDevPublicRoot", "AppMarketplaceDevReviewersRoot", "AppMarketplaceStageRoot", + "TrustedHostedAppPublicRoot", + "TrustedHostedAppTestRoot", "AppXPCShellRoot"]; this.TrustedRootCertificate = { diff --git a/dom/apps/TrustedHostedAppsUtils.jsm b/dom/apps/TrustedHostedAppsUtils.jsm index 91642b38aed..60668db3bc1 100644 --- a/dom/apps/TrustedHostedAppsUtils.jsm +++ b/dom/apps/TrustedHostedAppsUtils.jsm @@ -2,17 +2,25 @@ * 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/. */ -/* global Components, Services, dump */ +/* global Components, Services, dump, AppsUtils, NetUtil, XPCOMUtils */ "use strict"; const Cu = Components.utils; const Cc = Components.classes; const Ci = Components.interfaces; +const Cr = Components.results; +const signatureFileExtension = ".sig"; this.EXPORTED_SYMBOLS = ["TrustedHostedAppsUtils"]; +Cu.import("resource://gre/modules/AppsUtils.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); #ifdef MOZ_WIDGET_ANDROID // On Android, define the "debug" function as a binding of the "d" function @@ -65,7 +73,8 @@ this.TrustedHostedAppsUtils = { throw "CERTDB_ERROR"; } - if (siteSecurityService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP, uri.host, 0)) { + if (siteSecurityService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP, + uri.host, 0)) { debug("\tvalid certificate pinning for host: " + uri.host + "\n"); return true; } @@ -100,7 +109,7 @@ this.TrustedHostedAppsUtils = { .forEach(aList => { // aList[0] contains the directive name. // aList[1..n] contains sources. - let directiveName = aList.shift() + let directiveName = aList.shift(); let sources = aList; if ((-1 == validDirectives.indexOf(directiveName))) { @@ -144,5 +153,105 @@ this.TrustedHostedAppsUtils = { } return true; + }, + + _verifySignedFile: function(aManifestStream, aSignatureStream, aCertDb) { + let deferred = Promise.defer(); + + let root = Ci.nsIX509CertDB.TrustedHostedAppPublicRoot; + try { + // Check if we should use the test certificates. + // Please note that this should be changed if we ever allow chages to the + // prefs since that would create a way for an attacker to use the test + // root for real apps. + let useTrustedAppTestCerts = Services.prefs + .getBoolPref("dom.mozApps.use_trustedapp_test_certs"); + if (useTrustedAppTestCerts) { + root = Ci.nsIX509CertDB.TrustedHostedAppTestRoot; + } + } catch (ex) { } + + aCertDb.verifySignedManifestAsync( + root, aManifestStream, aSignatureStream, + function(aRv, aCert) { + if (Components.isSuccessCode(aRv)) { + deferred.resolve(aCert); + } else if (aRv == Cr.NS_ERROR_FILE_CORRUPTED || + aRv == Cr.NS_ERROR_SIGNED_MANIFEST_FILE_INVALID) { + deferred.reject("MANIFEST_SIGNATURE_FILE_INVALID"); + } else { + deferred.reject("MANIFEST_SIGNATURE_VERIFICATION_ERROR"); + } + } + ); + + return deferred.promise; + }, + + verifySignedManifest: function(aApp, aAppId) { + let deferred = Promise.defer(); + + let certDb; + try { + certDb = Cc["@mozilla.org/security/x509certdb;1"] + .getService(Ci.nsIX509CertDB); + } catch (e) { + debug("nsIX509CertDB error: " + e); + // unrecoverable error, don't bug the user + throw "CERTDB_ERROR"; + } + + let mRequestChannel = NetUtil.newChannel(aApp.manifestURL) + .QueryInterface(Ci.nsIHttpChannel); + mRequestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + mRequestChannel.notificationCallbacks = + AppsUtils.createLoadContext(aAppId, false); + + // The manifest signature must be located at the same path as the + // manifest and have the same file name, only the file extension + // should differ. Any fragment or query parameter will be ignored. + let signatureURL; + try { + let mURL = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(aApp.manifestURL, null, null) + .QueryInterface(Ci.nsIURL); + signatureURL = mURL.prePath + + mURL.directory + mURL.fileBaseName + signatureFileExtension; + } catch(e) { + deferred.reject("SIGNATURE_PATH_INVALID"); + return; + } + + let sRequestChannel = NetUtil.newChannel(signatureURL) + .QueryInterface(Ci.nsIHttpChannel); + sRequestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + sRequestChannel.notificationCallbacks = + AppsUtils.createLoadContext(aAppId, false); + let getAsyncFetchCallback = (resolve, reject) => + (aInputStream, aResult) => { + if (!Components.isSuccessCode(aResult)) { + debug("Failed to download file"); + reject("MANIFEST_FILE_UNAVAILABLE"); + return; + } + resolve(aInputStream); + }; + + Promise.all([ + new Promise((resolve, reject) => { + NetUtil.asyncFetch(mRequestChannel, + getAsyncFetchCallback(resolve, reject)); + }), + new Promise((resolve, reject) => { + NetUtil.asyncFetch(sRequestChannel, + getAsyncFetchCallback(resolve, reject)); + }) + ]).then(([aManifestStream, aSignatureStream]) => { + this._verifySignedFile(aManifestStream, aSignatureStream, certDb) + .then(deferred.resolve, deferred.reject); + }, deferred.reject); + + return deferred.promise; } }; diff --git a/dom/apps/Webapps.jsm b/dom/apps/Webapps.jsm index b69ad2a5885..41f2abf93b7 100755 --- a/dom/apps/Webapps.jsm +++ b/dom/apps/Webapps.jsm @@ -2021,7 +2021,7 @@ this.DOMApplicationRegistry = { xhr.setRequestHeader("If-None-Match", app.etag); } xhr.channel.notificationCallbacks = - this.createLoadContext(app.installerAppId, app.installerIsBrowser); + AppsUtils.createLoadContext(app.installerAppId, app.installerIsBrowser); xhr.addEventListener("load", onload.bind(this, xhr, oldManifest), false); xhr.addEventListener("error", (function() { @@ -2052,30 +2052,6 @@ this.DOMApplicationRegistry = { }); }, - // Creates a nsILoadContext object with a given appId and isBrowser flag. - createLoadContext: function createLoadContext(aAppId, aIsBrowser) { - return { - associatedWindow: null, - topWindow : null, - appId: aAppId, - isInBrowserElement: aIsBrowser, - usePrivateBrowsing: false, - isContent: false, - - isAppOfType: function(appType) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext, - Ci.nsIInterfaceRequestor, - Ci.nsISupports]), - getInterface: function(iid) { - if (iid.equals(Ci.nsILoadContext)) - return this; - throw Cr.NS_ERROR_NO_INTERFACE; - } - } - }, updatePackagedApp: Task.async(function*(aData, aId, aApp, aNewManifest) { debug("updatePackagedApp"); @@ -2342,8 +2318,8 @@ this.DOMApplicationRegistry = { .createInstance(Ci.nsIXMLHttpRequest); xhr.open("GET", app.manifestURL, true); xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; - xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId, - aData.isBrowser); + xhr.channel.notificationCallbacks = AppsUtils.createLoadContext(aData.appId, + aData.isBrowser); xhr.responseType = "json"; xhr.addEventListener("load", (function() { @@ -2455,8 +2431,8 @@ this.DOMApplicationRegistry = { .createInstance(Ci.nsIXMLHttpRequest); xhr.open("GET", app.manifestURL, true); xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; - xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId, - aData.isBrowser); + xhr.channel.notificationCallbacks = AppsUtils.createLoadContext(aData.appId, + aData.isBrowser); xhr.responseType = "json"; xhr.addEventListener("load", (function() { @@ -3219,52 +3195,15 @@ this.DOMApplicationRegistry = { _getPackage: function(aRequestChannel, aId, aOldApp, aNewApp) { let deferred = Promise.defer(); - // Staging the zip in TmpD until all the checks are done. - let zipFile = - FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true); - - // We need an output stream to write the channel content to the zip file. - let outputStream = Cc["@mozilla.org/network/file-output-stream;1"] - .createInstance(Ci.nsIFileOutputStream); - // write, create, truncate - outputStream.init(zipFile, 0x02 | 0x08 | 0x20, parseInt("0664", 8), 0); - let bufferedOutputStream = - Cc['@mozilla.org/network/buffered-output-stream;1'] - .createInstance(Ci.nsIBufferedOutputStream); - bufferedOutputStream.init(outputStream, 1024); - - // Create a listener that will give data to the file output stream. - let listener = Cc["@mozilla.org/network/simple-stream-listener;1"] - .createInstance(Ci.nsISimpleStreamListener); - - listener.init(bufferedOutputStream, { - onStartRequest: function(aRequest, aContext) { - // Nothing to do there anymore. - }, - - onStopRequest: function(aRequest, aContext, aStatusCode) { - bufferedOutputStream.close(); - outputStream.close(); - - if (!Components.isSuccessCode(aStatusCode)) { - deferred.reject("NETWORK_ERROR"); - return; - } - - // If we get a 4XX or a 5XX http status, bail out like if we had a - // network error. - let responseStatus = aRequestChannel.responseStatus; - if (responseStatus >= 400 && responseStatus <= 599) { - // unrecoverable error, don't bug the user - aOldApp.downloadAvailable = false; - deferred.reject("NETWORK_ERROR"); - return; - } - - deferred.resolve(zipFile); + AppsUtils.getFile(aRequestChannel, aId, "application.zip").then((aFile) => { + deferred.resolve(aFile); + }, function(rejectStatus) { + debug("Failed to download package file: " + rejectStatus.msg); + if (!rejectStatus.downloadAvailable) { + aOldApp.downloadAvailable = false; } + deferred.reject(rejectStatus.msg); }); - aRequestChannel.asyncOpen(listener, null); // send a first progress event to correctly set the DOM object's properties this._sendDownloadProgressEvent(aNewApp, 0); diff --git a/js/xpconnect/src/xpc.msg b/js/xpconnect/src/xpc.msg index 445b48b851a..22fa7b74e88 100644 --- a/js/xpconnect/src/xpc.msg +++ b/js/xpconnect/src/xpc.msg @@ -203,6 +203,9 @@ XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE , "An entry in the JAR is to XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_INVALID , "An entry in the JAR is invalid.") XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_MANIFEST_INVALID , "The JAR's manifest or signature file is invalid.") +/* Codes related to signed manifests */ +XPC_MSG_DEF(NS_ERROR_SIGNED_APP_MANIFEST_INVALID , "The signed app manifest or signature file is invalid.") + /* Codes for printing-related errors. */ XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_NO_PRINTER_AVAILABLE , "No printers available.") XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_NAME_NOT_FOUND , "The selected printer could not be found.") diff --git a/security/apps/AppSignatureVerification.cpp b/security/apps/AppSignatureVerification.cpp index c77ceb62396..8b3623c5297 100644 --- a/security/apps/AppSignatureVerification.cpp +++ b/security/apps/AppSignatureVerification.cpp @@ -12,6 +12,7 @@ #include "pkix/pkix.h" #include "pkix/pkixnss.h" +#include "pkix/ScopedPtr.h" #include "mozilla/RefPtr.h" #include "CryptoTask.h" #include "AppTrustDomain.h" @@ -20,16 +21,20 @@ #include "nsDataSignatureVerifier.h" #include "nsHashKeys.h" #include "nsIFile.h" +#include "nsIFileStreams.h" #include "nsIInputStream.h" #include "nsIStringEnumerator.h" #include "nsIZipReader.h" +#include "nsNetUtil.h" #include "nsNSSCertificate.h" #include "nsProxyRelease.h" +#include "NSSCertDBTrustDomain.h" #include "nsString.h" #include "nsTHashtable.h" #include "base64.h" #include "certdb.h" +#include "nssb64.h" #include "secmime.h" #include "plstr.h" #include "prlog.h" @@ -768,6 +773,82 @@ OpenSignedAppFile(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile, return NS_OK; } +nsresult +VerifySignedManifest(AppTrustedRoot aTrustedRoot, + nsIInputStream* aManifestStream, + nsIInputStream* aSignatureStream, + /*out, optional */ nsIX509Cert** aSignerCert) +{ + NS_ENSURE_ARG(aManifestStream); + NS_ENSURE_ARG(aSignatureStream); + + if (aSignerCert) { + *aSignerCert = nullptr; + } + + // Load signature file in buffer + ScopedAutoSECItem signatureBuffer; + nsresult rv = ReadStream(aSignatureStream, signatureBuffer); + if (NS_FAILED(rv)) { + return rv; + } + signatureBuffer.type = siBuffer; + + // Load manifest file in buffer + ScopedAutoSECItem manifestBuffer; + rv = ReadStream(aManifestStream, manifestBuffer); + if (NS_FAILED(rv)) { + return rv; + } + + // Calculate SHA1 digest of the manifest buffer + Digest manifestCalculatedDigest; + rv = manifestCalculatedDigest.DigestBuf(SEC_OID_SHA1, + manifestBuffer.data, + manifestBuffer.len - 1); // buffer is null terminated + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Get base64 encoded string from manifest buffer digest + ScopedPtr base64EncDigest(NSSBase64_EncodeItem(nullptr, + nullptr, 0, const_cast(&manifestCalculatedDigest.get()))); + if (NS_WARN_IF(!base64EncDigest)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Calculate SHA1 digest of the base64 encoded string + Digest doubleDigest; + rv = doubleDigest.DigestBuf(SEC_OID_SHA1, + reinterpret_cast(base64EncDigest.get()), + strlen(base64EncDigest.get())); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Verify the manifest signature (signed digest of the base64 encoded string) + ScopedCERTCertList builtChain; + rv = VerifySignature(aTrustedRoot, signatureBuffer, + doubleDigest.get(), builtChain); + if (NS_FAILED(rv)) { + return rv; + } + + // Return the signer's certificate to the reader if they want it. + if (aSignerCert) { + MOZ_ASSERT(CERT_LIST_HEAD(builtChain)); + nsCOMPtr signerCert = + nsNSSCertificate::Create(CERT_LIST_HEAD(builtChain)->cert); + if (NS_WARN_IF(!signerCert)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + signerCert.forget(aSignerCert); + } + + return NS_OK; +} + class OpenSignedAppFileTask MOZ_FINAL : public CryptoTask { public: @@ -803,6 +884,44 @@ private: nsCOMPtr mSignerCert; // out }; +class VerifySignedmanifestTask MOZ_FINAL : public CryptoTask +{ +public: + VerifySignedmanifestTask(AppTrustedRoot aTrustedRoot, + nsIInputStream* aManifestStream, + nsIInputStream* aSignatureStream, + nsIVerifySignedManifestCallback* aCallback) + : mTrustedRoot(aTrustedRoot) + , mManifestStream(aManifestStream) + , mSignatureStream(aSignatureStream) + , mCallback( + new nsMainThreadPtrHolder(aCallback)) + { + } + +private: + virtual nsresult CalculateResult() MOZ_OVERRIDE + { + return VerifySignedManifest(mTrustedRoot, mManifestStream, + mSignatureStream, getter_AddRefs(mSignerCert)); + } + + // nsNSSCertificate implements nsNSSShutdownObject, so there's nothing that + // needs to be released + virtual void ReleaseNSSResources() { } + + virtual void CallCallback(nsresult rv) + { + (void) mCallback->VerifySignedManifestFinished(rv, mSignerCert); + } + + const AppTrustedRoot mTrustedRoot; + const nsCOMPtr mManifestStream; + const nsCOMPtr mSignatureStream; + nsMainThreadPtrHandle mCallback; + nsCOMPtr mSignerCert; // out +}; + } // unnamed namespace NS_IMETHODIMP @@ -817,3 +936,18 @@ nsNSSCertificateDB::OpenSignedAppFileAsync( aCallback)); return task->Dispatch("SignedJAR"); } + +NS_IMETHODIMP +nsNSSCertificateDB::VerifySignedManifestAsync( + AppTrustedRoot aTrustedRoot, nsIInputStream* aManifestStream, + nsIInputStream* aSignatureStream, nsIVerifySignedManifestCallback* aCallback) +{ + NS_ENSURE_ARG_POINTER(aManifestStream); + NS_ENSURE_ARG_POINTER(aSignatureStream); + NS_ENSURE_ARG_POINTER(aCallback); + + RefPtr task( + new VerifySignedmanifestTask(aTrustedRoot, aManifestStream, + aSignatureStream, aCallback)); + return task->Dispatch("SignedManifest"); +} diff --git a/security/apps/AppTrustDomain.cpp b/security/apps/AppTrustDomain.cpp index 574495ef5ab..10e2ec9cae3 100644 --- a/security/apps/AppTrustDomain.cpp +++ b/security/apps/AppTrustDomain.cpp @@ -24,6 +24,9 @@ #include "marketplace-dev-reviewers.inc" #include "marketplace-stage.inc" #include "xpcshell.inc" +// Trusted Hosted Apps Certificates +#include "manifest-signing-root.inc" +#include "manifest-signing-test-root.inc" using namespace mozilla::pkix; @@ -79,6 +82,16 @@ AppTrustDomain::SetTrustedRoot(AppTrustedRoot trustedRoot) trustedDER.len = mozilla::ArrayLength(xpcshellRoot); break; + case nsIX509CertDB::TrustedHostedAppPublicRoot: + trustedDER.data = const_cast(trustedAppPublicRoot); + trustedDER.len = mozilla::ArrayLength(trustedAppPublicRoot); + break; + + case nsIX509CertDB::TrustedHostedAppTestRoot: + trustedDER.data = const_cast(trustedAppTestRoot); + trustedDER.len = mozilla::ArrayLength(trustedAppTestRoot); + break; + default: PR_SetError(SEC_ERROR_INVALID_ARGS, 0); return SECFailure; diff --git a/security/manager/ssl/public/nsIX509CertDB.idl b/security/manager/ssl/public/nsIX509CertDB.idl index 98dd5226c66..6c6ce77bb9a 100644 --- a/security/manager/ssl/public/nsIX509CertDB.idl +++ b/security/manager/ssl/public/nsIX509CertDB.idl @@ -12,6 +12,7 @@ interface nsIFile; interface nsIInterfaceRequestor; interface nsIZipReader; interface nsIX509CertList; +interface nsIInputStream; %{C++ #define NS_X509CERTDB_CONTRACTID "@mozilla.org/security/x509certdb;1" @@ -27,11 +28,18 @@ interface nsIOpenSignedAppFileCallback : nsISupports in nsIX509Cert aSignerCert); }; +[scriptable, function, uuid(3d6a9c87-5c5f-46fc-9410-96da6092f0f2)] +interface nsIVerifySignedManifestCallback : nsISupports +{ + void verifySignedManifestFinished(in nsresult rv, + in nsIX509Cert aSignerCert); +}; + /** * This represents a service to access and manipulate * X.509 certificates stored in a database. */ -[scriptable, uuid(dd6e4af8-23bb-41d9-a1e3-9ce925429f2f)] +[scriptable, uuid(8b01c2af-3a44-44d3-8ea5-51c2455e6c4b)] interface nsIX509CertDB : nsISupports { /** @@ -301,10 +309,28 @@ interface nsIX509CertDB : nsISupports { const AppTrustedRoot AppMarketplaceDevReviewersRoot = 4; const AppTrustedRoot AppMarketplaceStageRoot = 5; const AppTrustedRoot AppXPCShellRoot = 6; + const AppTrustedRoot TrustedHostedAppPublicRoot = 7; + const AppTrustedRoot TrustedHostedAppTestRoot = 8; void openSignedAppFileAsync(in AppTrustedRoot trustedRoot, in nsIFile aJarFile, in nsIOpenSignedAppFileCallback callback); + /** + * Given streams containing a signature and a manifest file, verifies + * that the signature is valid for the manifest. The signature must + * come from a certificate that is trusted for code signing and that + * was issued by the given trusted root. + * + * On success, NS_OK and the trusted certificate that signed the + * Manifest are returned. + * + * On failure, an error code is returned. + */ + void verifySignedManifestAsync(in AppTrustedRoot trustedRoot, + in nsIInputStream aManifestStream, + in nsIInputStream aSignatureStream, + in nsIVerifySignedManifestCallback callback); + /* * Add a cert to a cert DB from a binary string. * diff --git a/xpcom/base/ErrorList.h b/xpcom/base/ErrorList.h index 654ba69749c..0228302d8a4 100644 --- a/xpcom/base/ErrorList.h +++ b/xpcom/base/ErrorList.h @@ -873,6 +873,13 @@ ERROR(NS_ERROR_DOM_BLUETOOTH_AUTH_REJECTED, FAILURE(11)), #undef MODULE + /* ======================================================================= */ + /* 38: NS_ERROR_MODULE_SIGNED_APP */ + /* ======================================================================= */ +#define MODULE NS_ERROR_MODULE_SIGNED_APP + ERROR(NS_ERROR_SIGNED_APP_MANIFEST_INVALID, FAILURE(1)), +#undef MODULE + /* ======================================================================= */ /* 51: NS_ERROR_MODULE_GENERAL */ /* ======================================================================= */ diff --git a/xpcom/base/nsError.h b/xpcom/base/nsError.h index e4ff5103ac6..ce6a45ef9b5 100644 --- a/xpcom/base/nsError.h +++ b/xpcom/base/nsError.h @@ -72,6 +72,7 @@ #define NS_ERROR_MODULE_SIGNED_JAR 35 #define NS_ERROR_MODULE_DOM_FILESYSTEM 36 #define NS_ERROR_MODULE_DOM_BLUETOOTH 37 +#define NS_ERROR_MODULE_SIGNED_APP 38 /* NS_ERROR_MODULE_GENERAL should be used by modules that do not * care if return code values overlap. Callers of methods that From 5d6a5606370768aaa380bbe8ff0ffd7d488a4737 Mon Sep 17 00:00:00 2001 From: Vlatko Markovic Date: Mon, 22 Sep 2014 07:59:00 -0700 Subject: [PATCH 30/56] Bug 1059216 - Verification of Trusted Hosted Apps manifest signature, part 2. r=sicking --- dom/apps/TrustedHostedAppsUtils.jsm | 19 +++++++++-- dom/apps/Webapps.jsm | 49 ++++++++++++----------------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/dom/apps/TrustedHostedAppsUtils.jsm b/dom/apps/TrustedHostedAppsUtils.jsm index 60668db3bc1..44031951907 100644 --- a/dom/apps/TrustedHostedAppsUtils.jsm +++ b/dom/apps/TrustedHostedAppsUtils.jsm @@ -40,8 +40,6 @@ let debug = Services.prefs.getBoolPref("dom.mozApps.debug") ? /** * Verification functions for Trusted Hosted Apps. - * (Manifest signature verification is in Webapps.jsm as part of - * regular signature verification.) */ this.TrustedHostedAppsUtils = { @@ -174,6 +172,7 @@ this.TrustedHostedAppsUtils = { aCertDb.verifySignedManifestAsync( root, aManifestStream, aSignatureStream, function(aRv, aCert) { + debug("Signature verification returned code, cert & root: " + aRv + " " + aCert + " " + root); if (Components.isSuccessCode(aRv)) { deferred.resolve(aCert); } else if (aRv == Cr.NS_ERROR_FILE_CORRUPTED || @@ -253,5 +252,21 @@ this.TrustedHostedAppsUtils = { }, deferred.reject); return deferred.promise; + }, + + verifyManifest: function(aData) { + return new Promise((resolve, reject) => { + // sanity check on manifest host's CA (proper CA check with + // pinning is done by regular networking code) + if (!this.isHostPinned(aData.app.manifestURL)) { + reject("TRUSTED_APPLICATION_HOST_CERTIFICATE_INVALID"); + return; + } + if (!this.verifyCSPWhiteList(aData.app.manifest.csp)) { + reject("TRUSTED_APPLICATION_WHITELIST_VALIDATION_FAILED"); + return; + } + this.verifySignedManifest(aData.app, aData.appId).then(resolve, reject); + }); } }; diff --git a/dom/apps/Webapps.jsm b/dom/apps/Webapps.jsm index 41f2abf93b7..f1fb7956503 100755 --- a/dom/apps/Webapps.jsm +++ b/dom/apps/Webapps.jsm @@ -2292,24 +2292,16 @@ this.DOMApplicationRegistry = { // in which case we don't need to load it. if (app.manifest) { if (checkManifest()) { - if (this.kTrustedHosted == this.appKind(app, app.manifest)) { - // sanity check on manifest host's CA - // (proper CA check with pinning is done by regular networking code) - if (!TrustedHostedAppsUtils.isHostPinned(app.manifestURL)) { - sendError("TRUSTED_APPLICATION_HOST_CERTIFICATE_INVALID"); - return; - } - - // Signature of the manifest should be verified here. - // Bug 1059216. - - if (!TrustedHostedAppsUtils.verifyCSPWhiteList(app.manifest.csp)) { - sendError("TRUSTED_APPLICATION_WHITELIST_VALIDATION_FAILED"); - return; - } + debug("Installed manifest check OK"); + if (this.kTrustedHosted !== this.appKind(app, app.manifest)) { + installApp(); + return; } - - installApp(); + TrustedHostedAppsUtils.verifyManifest(aData) + .then(installApp, sendError); + } else { + debug("Installed manifest check failed"); + // checkManifest() sends error before return } return; } @@ -2332,21 +2324,20 @@ this.DOMApplicationRegistry = { app.manifest = xhr.response; if (checkManifest()) { + debug("Downloaded manifest check OK"); app.etag = xhr.getResponseHeader("Etag"); - if (this.kTrustedHosted == this.appKind(app, app.manifest)) { - // checking trusted host for pinning is not needed here, since - // network code will have already done that - - // Signature of the manifest should be verified here. - // Bug 1059216. - - if (!TrustedHostedAppsUtils.verifyCSPWhiteList(app.manifest.csp)) { - sendError("TRUSTED_APPLICATION_WHITELIST_VALIDATION_FAILED"); - return; - } + if (this.kTrustedHosted !== this.appKind(app, app.manifest)) { + installApp(); + return; } - installApp(); + debug("App kind: " + this.kTrustedHosted); + TrustedHostedAppsUtils.verifyManifest(aData) + .then(installApp, sendError); + return; + } else { + debug("Downloaded manifest check failed"); + // checkManifest() sends error before return } } else { sendError("MANIFEST_URL_ERROR"); From 6a02a4a80c57e3904f5bb6ff8c948d3da3004427 Mon Sep 17 00:00:00 2001 From: Vlatko Markovic Date: Mon, 22 Sep 2014 07:59:00 -0700 Subject: [PATCH 31/56] Bug 1059198 - Permission table update for Trusted Hosted Apps. r=sicking --- dom/apps/PermissionsTable.jsm | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dom/apps/PermissionsTable.jsm b/dom/apps/PermissionsTable.jsm index ae53ce250cc..c6fd8c07353 100644 --- a/dom/apps/PermissionsTable.jsm +++ b/dom/apps/PermissionsTable.jsm @@ -48,7 +48,7 @@ this.PermissionsTable = { geolocation: { }, camera: { app: DENY_ACTION, - trusted: DENY_ACTION, + trusted: PROMPT_ACTION, privileged: PROMPT_ACTION, certified: ALLOW_ACTION }, @@ -99,28 +99,28 @@ this.PermissionsTable = { geolocation: { }, "device-storage:pictures": { app: DENY_ACTION, - trusted: DENY_ACTION, + trusted: PROMPT_ACTION, privileged: PROMPT_ACTION, certified: ALLOW_ACTION, access: ["read", "write", "create"] }, "device-storage:videos": { app: DENY_ACTION, - trusted: DENY_ACTION, + trusted: PROMPT_ACTION, privileged: PROMPT_ACTION, certified: ALLOW_ACTION, access: ["read", "write", "create"] }, "device-storage:music": { app: DENY_ACTION, - trusted: DENY_ACTION, + trusted: PROMPT_ACTION, privileged: PROMPT_ACTION, certified: ALLOW_ACTION, access: ["read", "write", "create"] }, "device-storage:sdcard": { app: DENY_ACTION, - trusted: DENY_ACTION, + trusted: PROMPT_ACTION, privileged: PROMPT_ACTION, certified: ALLOW_ACTION, access: ["read", "write", "create"] @@ -368,7 +368,7 @@ this.PermissionsTable = { geolocation: { }, "audio-channel-publicnotification": { app: DENY_ACTION, - trusted: DENY_ACTION, + trusted: ALLOW_ACTION, privileged: DENY_ACTION, certified: ALLOW_ACTION }, From 0f03e516ba3f91c160fd110aa90bc2eb16f1c2fe Mon Sep 17 00:00:00 2001 From: JW Wang Date: Sun, 14 Sep 2014 23:35:00 -0400 Subject: [PATCH 32/56] Bug 962871 - Enable media mochitests on B2G debug and disable some failed tests. r=cpearce --- content/media/test/mochitest.ini | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/content/media/test/mochitest.ini b/content/media/test/mochitest.ini index 5885237e58c..57b2305f902 100644 --- a/content/media/test/mochitest.ini +++ b/content/media/test/mochitest.ini @@ -22,7 +22,7 @@ # do ok(true, "Type not supported") and stop the test. [DEFAULT] -skip-if = buildapp == 'mulet' || (buildapp == 'b2g' && (toolkit != 'gonk' || debug)) # b2g-debug,b2g-desktop(bug 918299) +skip-if = buildapp == 'mulet' || (buildapp == 'b2g' && toolkit != 'gonk') # b2g-desktop(bug 918299) support-files = 320x240.ogv 320x240.ogv^headers^ @@ -321,6 +321,7 @@ skip-if = buildapp == 'b2g' # bug 1021675 [test_can_play_type_no_ogg.html] [test_can_play_type_ogg.html] [test_chaining.html] +skip-if = toolkit == 'gonk' && debug [test_clone_media_element.html] [test_closing_connections.html] [test_constants.html] @@ -366,18 +367,24 @@ skip-if = toolkit == 'gonk' && !debug # bug 1021677 [test_mediarecorder_record_gum_video_timeslice.html] skip-if = buildapp == 'b2g' || toolkit == 'android' # mimetype check, bug 969289 [test_mediarecorder_record_immediate_stop.html] +skip-if = toolkit == 'gonk' && debug [test_mediarecorder_record_no_timeslice.html] +skip-if = toolkit == 'gonk' && debug [test_mediarecorder_record_nosrc.html] [test_mediarecorder_record_session.html] [test_mediarecorder_record_startstopstart.html] +skip-if = toolkit == 'gonk' && debug [test_mediarecorder_record_stopms.html] [test_mediarecorder_record_timeslice.html] +skip-if = toolkit == 'gonk' && debug [test_mediarecorder_reload_crash.html] [test_mediarecorder_unsupported_src.html] [test_mediarecorder_record_getdata_afterstart.html] +skip-if = toolkit == 'gonk' && debug [test_mediatrack_consuming_mediaresource.html] [test_mediatrack_consuming_mediastream.html] [test_mediatrack_events.html] +skip-if = toolkit == 'gonk' && debug # bug 1065924 [test_mediatrack_parsing_ogg.html] [test_mediatrack_replay_from_end.html] [test_metadata.html] From f6e4bfa9e3135a973722f30defe6cb87df7ce6bb Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Mon, 15 Sep 2014 10:45:00 +0200 Subject: [PATCH 33/56] Bug 1066472 - AudioParam connections in web audio editor should not appear clickable. r=vp --- browser/themes/shared/devtools/webaudioeditor.inc.css | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/browser/themes/shared/devtools/webaudioeditor.inc.css b/browser/themes/shared/devtools/webaudioeditor.inc.css index 95faced4d4d..9e33f2b8e6e 100644 --- a/browser/themes/shared/devtools/webaudioeditor.inc.css +++ b/browser/themes/shared/devtools/webaudioeditor.inc.css @@ -105,9 +105,9 @@ g.edgePath.param-connection { fill: #4c9ed9; /* Select Highlight Blue */ } -/* Text in nodes */ +/* Text in nodes and edges */ text { - cursor: pointer; + cursor: default; /* override the "text" cursor */ font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; @@ -123,6 +123,10 @@ text { fill: #f0f1f2; /* Toolbars */ } +.nodes text { + cursor: pointer; +} + /** * Inspector Styles */ From fb03f1d81f3eff591a3d534250a2b0d280524ee9 Mon Sep 17 00:00:00 2001 From: Randall Barker Date: Fri, 19 Sep 2014 13:50:00 +0200 Subject: [PATCH 34/56] Bug 1048425 - Enable support for tab sharing with Roku device. r=wesj r=mfinkle --- mobile/android/app/mobile.js | 7 + mobile/android/chrome/content/CastingApps.js | 39 ++-- mobile/android/modules/RokuApp.jsm | 209 +++++++++++++++++- .../modules/SimpleServiceDiscovery.jsm | 4 + 4 files changed, 235 insertions(+), 24 deletions(-) diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index eb4d956c944..77eea56ea0f 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -283,6 +283,13 @@ pref("browser.search.official", true); // Control media casting feature pref("browser.casting.enabled", true); +#ifdef RELEASE_BUILD +pref("browser.mirroring.enabled", false); +pref("browser.mirroring.enabled.roku", false); +#else +pref("browser.mirroring.enabled", true); +pref("browser.mirroring.enabled.roku", true); +#endif // Enable sparse localization by setting a few package locale overrides pref("chrome.override_package.global", "browser"); diff --git a/mobile/android/chrome/content/CastingApps.js b/mobile/android/chrome/content/CastingApps.js index d20d7c96ddc..3598204cb5f 100644 --- a/mobile/android/chrome/content/CastingApps.js +++ b/mobile/android/chrome/content/CastingApps.js @@ -16,6 +16,7 @@ var rokuDevice = { Cu.import("resource://gre/modules/RokuApp.jsm"); return new RokuApp(aService); }, + mirror: Services.prefs.getBoolPref("browser.mirroring.enabled.roku"), types: ["video/mp4"], extensions: ["mp4"] }; @@ -52,7 +53,7 @@ var CastingApps = { mirrorStopMenuId: -1, init: function ca_init() { - if (!this.isEnabled()) { + if (!this.isCastingEnabled()) { return; } @@ -99,22 +100,25 @@ var CastingApps = { NativeWindow.contextmenus.remove(this._castMenuId); }, + _mirrorStarted: function(stopMirrorCallback) { + this.stopMirrorCallback = stopMirrorCallback; + NativeWindow.menu.update(this.mirrorStartMenuId, { visible: false }); + NativeWindow.menu.update(this.mirrorStopMenuId, { visible: true }); + }, + serviceAdded: function(aService) { - if (aService.mirror && this.mirrorStartMenuId == -1) { + if (this.isMirroringEnabled() && aService.mirror && this.mirrorStartMenuId == -1) { this.mirrorStartMenuId = NativeWindow.menu.add({ name: Strings.browser.GetStringFromName("casting.mirrorTab"), callback: function() { - function callbackFunc(aService) { + let callbackFunc = function(aService) { let app = SimpleServiceDiscovery.findAppForService(aService); - if (app) - app.mirror(function() { - }); - } + if (app) { + app.mirror(function() {}, window, BrowserApp.selectedTab.getViewport(), this._mirrorStarted.bind(this)); + } + }.bind(this); - function filterFunc(aService) { - return aService.mirror == true; - } - this.prompt(callbackFunc, filterFunc); + this.prompt(callbackFunc, aService => aService.mirror); }.bind(this), parent: NativeWindow.menu.toolsMenuID }); @@ -125,6 +129,9 @@ var CastingApps = { if (this.tabMirror) { this.tabMirror.stop(); this.tabMirror = null; + } else if (this.stopMirrorCallback) { + this.stopMirrorCallback(); + this.stopMirrorCallback = null; } NativeWindow.menu.update(this.mirrorStartMenuId, { visible: true }); NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false }); @@ -132,7 +139,9 @@ var CastingApps = { parent: NativeWindow.menu.toolsMenuID }); } - NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false }); + if (this.mirrorStartMenuId != -1) { + NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false }); + } }, serviceLost: function(aService) { @@ -150,10 +159,14 @@ var CastingApps = { } }, - isEnabled: function isEnabled() { + isCastingEnabled: function isCastingEnabled() { return Services.prefs.getBoolPref("browser.casting.enabled"); }, + isMirroringEnabled: function isMirroringEnabled() { + return Services.prefs.getBoolPref("browser.mirroring.enabled"); + }, + observe: function (aSubject, aTopic, aData) { switch (aTopic) { case "Casting:Play": diff --git a/mobile/android/modules/RokuApp.jsm b/mobile/android/modules/RokuApp.jsm index 8fb5dfdd417..bcd637162c6 100644 --- a/mobile/android/modules/RokuApp.jsm +++ b/mobile/android/modules/RokuApp.jsm @@ -11,6 +11,10 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/Services.jsm"); +const WEBRTC_PLAYER_NAME = "WebRTC Player"; +const MIRROR_PORT = 8011; +const JSON_MESSAGE_TERMINATOR = "\r\n"; + function log(msg) { //Services.console.logStringMessage(msg); } @@ -29,13 +33,14 @@ function RokuApp(service) { #else this.app = "Firefox Nightly"; #endif - this.appID = -1; + this.mediaAppID = -1; + this.mirrorAppID = -1; } RokuApp.prototype = { status: function status(callback) { // We have no way to know if the app is running, so just return "unknown" - // but we use this call to fetch the appID for the given app name + // but we use this call to fetch the mediaAppID for the given app name let url = this.resourceURL + "query/apps"; let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); xhr.open("GET", url, true); @@ -48,7 +53,9 @@ RokuApp.prototype = { let apps = doc.querySelectorAll("app"); for (let app of apps) { if (app.textContent == this.app) { - this.appID = app.id; + this.mediaAppID = app.id; + } else if (app.textContent == WEBRTC_PLAYER_NAME) { + this.mirrorAppID = app.id } } } @@ -69,11 +76,11 @@ RokuApp.prototype = { }, start: function start(callback) { - // We need to make sure we have cached the appID - if (this.appID == -1) { + // We need to make sure we have cached the mediaAppID + if (this.mediaAppID == -1) { this.status(function() { - // If we found the appID, use it to make a new start call - if (this.appID != -1) { + // If we found the mediaAppID, use it to make a new start call + if (this.mediaAppID != -1) { this.start(callback); } else { // We failed to start the app, so let the caller know @@ -85,7 +92,7 @@ RokuApp.prototype = { // Start a given app with any extra query data. Each app uses it's own data scheme. // NOTE: Roku will also pass "source=external-control" as a param - let url = this.resourceURL + "launch/" + this.appID + "?version=" + parseInt(PROTOCOL_VERSION); + let url = this.resourceURL + "launch/" + this.mediaAppID + "?version=" + parseInt(PROTOCOL_VERSION); let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); xhr.open("POST", url, true); xhr.overrideMimeType("text/plain"); @@ -129,7 +136,7 @@ RokuApp.prototype = { }, remoteMedia: function remoteMedia(callback, listener) { - if (this.appID != -1) { + if (this.mediaAppID != -1) { if (callback) { callback(new RemoteMedia(this.resourceURL, listener)); } @@ -138,6 +145,44 @@ RokuApp.prototype = { callback(); } } + }, + + mirror: function(callback, win, viewport, mirrorStartedCallback) { + if (this.mirrorAppID == -1) { + // The status function may not have been called yet if mirrorAppID is -1 + this.status(this._createRemoteMirror.bind(this, callback, win, viewport, mirrorStartedCallback)); + } else { + this._createRemoteMirror(callback, win, viewport, mirrorStartedCallback); + } + }, + + _createRemoteMirror: function(callback, win, viewport, mirrorStartedCallback) { + if (this.mirrorAppID == -1) { + // TODO: Inform user to install Roku WebRTC Player Channel. + log("RokuApp: Failed to find Mirror App ID."); + } else { + let url = this.resourceURL + "launch/" + this.mirrorAppID; + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); + xhr.open("POST", url, true); + xhr.overrideMimeType("text/plain"); + + xhr.addEventListener("load", (function() { + // 204 seems to be returned if the channel is already running + if ((xhr.status == 200) || (xhr.status == 204)) { + this.remoteMirror = new RemoteMirror(this.resourceURL, win, viewport, mirrorStartedCallback); + } + }).bind(this), false); + + xhr.addEventListener("error", function() { + log("RokuApp: XHR Failed to launch application: " + WEBRTC_PLAYER_NAME); + }, false); + + xhr.send(null); + } + + if (callback) { + callback(); + } } } @@ -225,11 +270,153 @@ RemoteMedia.prototype = { this._sendMsg({ type: "STOP" }); }, - load: function load(aData) { - this._sendMsg({ type: "LOAD", title: aData.title, source: aData.source, poster: aData.poster }); + load: function load(data) { + this._sendMsg({ type: "LOAD", title: data.title, source: data.source, poster: data.poster }); }, get status() { return this._status; } } + +function RemoteMirror(url, win, viewport, mirrorStartedCallback) { + this._serverURI = Services.io.newURI(url , null, null); + this._window = win; + this._iceCandidates = []; + this.mirrorStarted = mirrorStartedCallback; + + // This code insures the generated tab mirror is not wider than 800 nor taller than 600 + // Better dimensions should be chosen after the Roku Channel is working. + let windowId = win.BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID; + let cWidth = Math.max(viewport.cssWidth, viewport.width); + let cHeight = Math.max(viewport.cssHeight, viewport.height); + + const MAX_WIDTH = 800; + const MAX_HEIGHT = 600; + + let tWidth = 0; + let tHeight = 0; + + if ((cWidth / MAX_WIDTH) > (cHeight / MAX_HEIGHT)) { + tHeight = Math.ceil((MAX_WIDTH / cWidth) * cHeight); + tWidth = MAX_WIDTH; + } else { + tWidth = Math.ceil((MAX_HEIGHT / cHeight) * cWidth); + tHeight = MAX_HEIGHT; + } + + let constraints = { + video: { + mediaSource: "browser", + browserWindow: windowId, + scrollWithPage: true, + advanced: [ + { + width: { min: tWidth, max: tWidth }, + height: { min: tHeight, max: tHeight } + }, + { aspectRatio: cWidth / cHeight } + ] + } + }; + + this._window.navigator.mozGetUserMedia(constraints, this._onReceiveGUMStream.bind(this), function() {}); +} + +RemoteMirror.prototype = { + _sendOffer: function(offer) { + if (!this._baseSocket) { + this._baseSocket = Cc["@mozilla.org/tcp-socket;1"].createInstance(Ci.nsIDOMTCPSocket); + } + this._jsonOffer = JSON.stringify(offer); + this._socket = this._baseSocket.open(this._serverURI.host, MIRROR_PORT, { useSecureTransport: false, binaryType: "string" }); + this._socket.onopen = this._onSocketOpen.bind(this); + this._socket.ondata = this._onSocketData.bind(this); + this._socket.onerror = this._onSocketError.bind(this); + }, + + _onReceiveGUMStream: function(stream) { + this._pc = new this._window.mozRTCPeerConnection; + this._pc.addStream(stream); + this._pc.onicecandidate = (evt => { + // Usually the last candidate is null, expected? + if (!evt.candidate) { + return; + } + let jsonCandidate = JSON.stringify(evt.candidate); + this._iceCandidates.push(jsonCandidate); + this._sendIceCandidates(); + }); + + this._pc.createOffer(offer => { + this._pc.setLocalDescription( + new this._window.mozRTCSessionDescription(offer), + () => this._sendOffer(offer), + () => log("RemoteMirror: Failed to set local description.")); + }, + () => log("RemoteMirror: Failed to create offer.")); + }, + + _stopMirror: function() { + if (this._socket) { + this._socket.close(); + this._socket = null; + } + if (this._pc) { + this._pc.close(); + this._pc = null; + } + this._jsonOffer = null; + this._iceCandidates = []; + }, + + _onSocketData: function(response) { + if (response.type == "data") { + response.data.split(JSON_MESSAGE_TERMINATOR).forEach(data => { + if (data) { + let parsedData = JSON.parse(data); + if (parsedData.type == "answer") { + this._pc.setRemoteDescription( + new this._window.mozRTCSessionDescription(parsedData), + () => this.mirrorStarted(this._stopMirror.bind(this)), + () => log("RemoteMirror: Failed to set remote description.")); + } else { + this._pc.addIceCandidate(new this._window.mozRTCIceCandidate(parsedData)) + } + } else { + log("RemoteMirror: data is null"); + } + }); + } else if (response.type == "error") { + log("RemoteMirror: Got socket error."); + this._stopMirror(); + } else { + log("RemoteMirror: Got unhandled socket event: " + response.type); + } + }, + + _onSocketError: function(err) { + log("RemoteMirror: Error socket.onerror: " + (err.data ? err.data : "NO DATA")); + this._stopMirror(); + }, + + _onSocketOpen: function() { + this._open = true; + if (this._jsonOffer) { + let jsonOffer = this._jsonOffer + JSON_MESSAGE_TERMINATOR; + this._socket.send(jsonOffer, jsonOffer.length); + this._jsonOffer = null; + this._sendIceCandidates(); + } + }, + + _sendIceCandidates: function() { + if (this._socket && this._open) { + this._iceCandidates.forEach(value => { + value = value + JSON_MESSAGE_TERMINATOR; + this._socket.send(value, value.length); + }); + this._iceCandidates = []; + } + } +}; diff --git a/mobile/android/modules/SimpleServiceDiscovery.jsm b/mobile/android/modules/SimpleServiceDiscovery.jsm index d23295b96a7..ca352e205b3 100644 --- a/mobile/android/modules/SimpleServiceDiscovery.jsm +++ b/mobile/android/modules/SimpleServiceDiscovery.jsm @@ -409,6 +409,10 @@ var SimpleServiceDiscovery = { // Only add and notify if we don't already know about this service if (!this._services.has(service.uuid)) { + let device = this._devices.get(service.target); + if (device && device.mirror) { + service.mirror = true; + } this._services.set(service.uuid, service); Services.obs.notifyObservers(null, EVENT_SERVICE_FOUND, service.uuid); } From fe761cdeabf86fecce9cb95b4bda9505d78ac083 Mon Sep 17 00:00:00 2001 From: "Carsten \"Tomcat\" Book" Date: Mon, 22 Sep 2014 10:35:48 +0200 Subject: [PATCH 35/56] Backed out changeset c2e654ecbd50 (bug 1057042) for dt test failures --- browser/devtools/jar.mn | 10 +- browser/devtools/webaudioeditor/controller.js | 223 ------ browser/devtools/webaudioeditor/includes.js | 98 --- browser/devtools/webaudioeditor/models.js | 274 -------- browser/devtools/webaudioeditor/panel.js | 1 - .../devtools/webaudioeditor/test/browser.ini | 2 - .../test/browser_wa_destroy-node-01.js | 14 +- .../test/browser_wa_graph-click.js | 23 +- .../test/browser_wa_graph-markers.js | 2 +- .../test/browser_wa_graph-render-01.js | 6 +- .../test/browser_wa_graph-render-02.js | 2 +- .../test/browser_wa_graph-render-05.js | 27 - .../test/browser_wa_graph-zoom.js | 24 +- .../test/browser_wa_inspector-toggle.js | 12 +- .../test/browser_wa_inspector.js | 8 +- .../browser_wa_properties-view-edit-01.js | 4 +- .../browser_wa_properties-view-edit-02.js | 4 +- .../browser_wa_properties-view-media-nodes.js | 4 +- ...owser_wa_properties-view-params-objects.js | 4 +- .../test/browser_wa_properties-view-params.js | 4 +- .../test/browser_wa_properties-view.js | 4 +- .../test/browser_wa_reset-03.js | 14 +- .../test/doc_connect-toggle-param.html | 27 - browser/devtools/webaudioeditor/test/head.js | 9 +- .../devtools/webaudioeditor/views/context.js | 305 --------- .../webaudioeditor/views/inspector.js | 240 ------- .../devtools/webaudioeditor/views/utils.js | 103 --- .../webaudioeditor-controller.js | 428 ++++++++++++ .../webaudioeditor/webaudioeditor-view.js | 636 ++++++++++++++++++ .../webaudioeditor/webaudioeditor.xul | 8 +- 30 files changed, 1139 insertions(+), 1381 deletions(-) delete mode 100644 browser/devtools/webaudioeditor/controller.js delete mode 100644 browser/devtools/webaudioeditor/includes.js delete mode 100644 browser/devtools/webaudioeditor/models.js delete mode 100644 browser/devtools/webaudioeditor/test/browser_wa_graph-render-05.js delete mode 100644 browser/devtools/webaudioeditor/test/doc_connect-toggle-param.html delete mode 100644 browser/devtools/webaudioeditor/views/context.js delete mode 100644 browser/devtools/webaudioeditor/views/inspector.js delete mode 100644 browser/devtools/webaudioeditor/views/utils.js create mode 100644 browser/devtools/webaudioeditor/webaudioeditor-controller.js create mode 100644 browser/devtools/webaudioeditor/webaudioeditor-view.js diff --git a/browser/devtools/jar.mn b/browser/devtools/jar.mn index 1b581012a14..192ac9ac6af 100644 --- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -73,15 +73,11 @@ browser.jar: content/browser/devtools/shadereditor.js (shadereditor/shadereditor.js) content/browser/devtools/canvasdebugger.xul (canvasdebugger/canvasdebugger.xul) content/browser/devtools/canvasdebugger.js (canvasdebugger/canvasdebugger.js) - content/browser/devtools/d3.js (shared/d3.js) content/browser/devtools/webaudioeditor.xul (webaudioeditor/webaudioeditor.xul) + content/browser/devtools/d3.js (shared/d3.js) content/browser/devtools/dagre-d3.js (webaudioeditor/lib/dagre-d3.js) - content/browser/devtools/webaudioeditor/includes.js (webaudioeditor/includes.js) - content/browser/devtools/webaudioeditor/models.js (webaudioeditor/models.js) - content/browser/devtools/webaudioeditor/controller.js (webaudioeditor/controller.js) - content/browser/devtools/webaudioeditor/views/utils.js (webaudioeditor/views/utils.js) - content/browser/devtools/webaudioeditor/views/context.js (webaudioeditor/views/context.js) - content/browser/devtools/webaudioeditor/views/inspector.js (webaudioeditor/views/inspector.js) + content/browser/devtools/webaudioeditor-controller.js (webaudioeditor/webaudioeditor-controller.js) + content/browser/devtools/webaudioeditor-view.js (webaudioeditor/webaudioeditor-view.js) content/browser/devtools/profiler.xul (profiler/profiler.xul) content/browser/devtools/profiler.js (profiler/profiler.js) content/browser/devtools/ui-recordings.js (profiler/ui-recordings.js) diff --git a/browser/devtools/webaudioeditor/controller.js b/browser/devtools/webaudioeditor/controller.js deleted file mode 100644 index ee8dc457744..00000000000 --- a/browser/devtools/webaudioeditor/controller.js +++ /dev/null @@ -1,223 +0,0 @@ -/* 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/. */ - -/** - * A collection of `AudioNodeModel`s used throughout the editor - * to keep track of audio nodes within the audio context. - */ -let gAudioNodes = new AudioNodesCollection(); - -/** - * Initializes the web audio editor views - */ -function startupWebAudioEditor() { - return all([ - WebAudioEditorController.initialize(), - ContextView.initialize(), - InspectorView.initialize() - ]); -} - -/** - * Destroys the web audio editor controller and views. - */ -function shutdownWebAudioEditor() { - return all([ - WebAudioEditorController.destroy(), - ContextView.destroy(), - InspectorView.destroy(), - ]); -} - -/** - * Functions handling target-related lifetime events. - */ -let WebAudioEditorController = { - /** - * Listen for events emitted by the current tab target. - */ - initialize: function() { - telemetry.toolOpened("webaudioeditor"); - this._onTabNavigated = this._onTabNavigated.bind(this); - this._onThemeChange = this._onThemeChange.bind(this); - - gTarget.on("will-navigate", this._onTabNavigated); - gTarget.on("navigate", this._onTabNavigated); - gFront.on("start-context", this._onStartContext); - gFront.on("create-node", this._onCreateNode); - gFront.on("connect-node", this._onConnectNode); - gFront.on("connect-param", this._onConnectParam); - gFront.on("disconnect-node", this._onDisconnectNode); - gFront.on("change-param", this._onChangeParam); - gFront.on("destroy-node", this._onDestroyNode); - - // Hook into theme change so we can change - // the graph's marker styling, since we can't do this - // with CSS - gDevTools.on("pref-changed", this._onThemeChange); - }, - - /** - * Remove events emitted by the current tab target. - */ - destroy: function() { - telemetry.toolClosed("webaudioeditor"); - gTarget.off("will-navigate", this._onTabNavigated); - gTarget.off("navigate", this._onTabNavigated); - gFront.off("start-context", this._onStartContext); - gFront.off("create-node", this._onCreateNode); - gFront.off("connect-node", this._onConnectNode); - gFront.off("connect-param", this._onConnectParam); - gFront.off("disconnect-node", this._onDisconnectNode); - gFront.off("change-param", this._onChangeParam); - gFront.off("destroy-node", this._onDestroyNode); - gDevTools.off("pref-changed", this._onThemeChange); - }, - - /** - * Called when page is reloaded to show the reload notice and waiting - * for an audio context notice. - */ - reset: function () { - $("#content").hidden = true; - ContextView.resetUI(); - InspectorView.resetUI(); - }, - - // Since node create and connect are probably executed back to back, - // and the controller's `_onCreateNode` needs to look up type, - // the edge creation could be called before the graph node is actually - // created. This way, we can check and listen for the event before - // adding an edge. - _waitForNodeCreation: function (sourceActor, destActor) { - let deferred = defer(); - let source = gAudioNodes.get(sourceActor.actorID); - let dest = gAudioNodes.get(destActor.actorID); - - if (!source || !dest) { - gAudioNodes.on("add", function createNodeListener (createdNode) { - if (sourceActor.actorID === createdNode.id) - source = createdNode; - if (destActor.actorID === createdNode.id) - dest = createdNode; - if (source && dest) { - gAudioNodes.off("add", createNodeListener); - deferred.resolve([source, dest]); - } - }); - } - else { - deferred.resolve([source, dest]); - } - return deferred.promise; - }, - - /** - * Fired when the devtools theme changes (light, dark, etc.) - * so that the graph can update marker styling, as that - * cannot currently be done with CSS. - */ - _onThemeChange: function (event, data) { - window.emit(EVENTS.THEME_CHANGE, data.newValue); - }, - - /** - * Called for each location change in the debugged tab. - */ - _onTabNavigated: Task.async(function* (event, {isFrameSwitching}) { - switch (event) { - case "will-navigate": { - // Make sure the backend is prepared to handle audio contexts. - if (!isFrameSwitching) { - yield gFront.setup({ reload: false }); - } - - // Clear out current UI. - this.reset(); - - // When switching to an iframe, ensure displaying the reload button. - // As the document has already been loaded without being hooked. - if (isFrameSwitching) { - $("#reload-notice").hidden = false; - $("#waiting-notice").hidden = true; - } else { - // Otherwise, we are loading a new top level document, - // so we don't need to reload anymore and should receive - // new node events. - $("#reload-notice").hidden = true; - $("#waiting-notice").hidden = false; - } - - // Clear out stored audio nodes - gAudioNodes.reset(); - - window.emit(EVENTS.UI_RESET); - break; - } - case "navigate": { - // TODO Case of bfcache, needs investigating - // bug 994250 - break; - } - } - }), - - /** - * Called after the first audio node is created in an audio context, - * signaling that the audio context is being used. - */ - _onStartContext: function() { - $("#reload-notice").hidden = true; - $("#waiting-notice").hidden = true; - $("#content").hidden = false; - window.emit(EVENTS.START_CONTEXT); - }, - - /** - * Called when a new node is created. Creates an `AudioNodeView` instance - * for tracking throughout the editor. - */ - _onCreateNode: Task.async(function* (nodeActor) { - yield gAudioNodes.add(nodeActor); - }), - - /** - * Called on `destroy-node` when an AudioNode is GC'd. Removes - * from the AudioNode array and fires an event indicating the removal. - */ - _onDestroyNode: function (nodeActor) { - gAudioNodes.remove(gAudioNodes.get(nodeActor.actorID)); - }, - - /** - * Called when a node is connected to another node. - */ - _onConnectNode: Task.async(function* ({ source: sourceActor, dest: destActor }) { - let [source, dest] = yield WebAudioEditorController._waitForNodeCreation(sourceActor, destActor); - source.connect(dest); - }), - - /** - * Called when a node is conneceted to another node's AudioParam. - */ - _onConnectParam: Task.async(function* ({ source: sourceActor, dest: destActor, param }) { - let [source, dest] = yield WebAudioEditorController._waitForNodeCreation(sourceActor, destActor); - source.connect(dest, param); - }), - - /** - * Called when a node is disconnected. - */ - _onDisconnectNode: function(nodeActor) { - let node = gAudioNodes.get(nodeActor.actorID); - node.disconnect(); - }, - - /** - * Called when a node param is changed. - */ - _onChangeParam: function({ actor, param, value }) { - window.emit(EVENTS.CHANGE_PARAM, gAudioNodes.get(actor.actorID), param, value); - } -}; diff --git a/browser/devtools/webaudioeditor/includes.js b/browser/devtools/webaudioeditor/includes.js deleted file mode 100644 index ddac056ce60..00000000000 --- a/browser/devtools/webaudioeditor/includes.js +++ /dev/null @@ -1,98 +0,0 @@ -/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); -Cu.import("resource:///modules/devtools/gDevTools.jsm"); - -const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; - -let { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); -let { EventTarget } = require("sdk/event/target"); -const { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); -const { Class } = require("sdk/core/heritage"); -const EventEmitter = require("devtools/toolkit/event-emitter"); -const STRINGS_URI = "chrome://browser/locale/devtools/webaudioeditor.properties" -const L10N = new ViewHelpers.L10N(STRINGS_URI); -const Telemetry = require("devtools/shared/telemetry"); -const telemetry = new Telemetry(); - -// Override DOM promises with Promise.jsm helpers -const { defer, all } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; - -/* Events fired on `window` to indicate state or actions*/ -const EVENTS = { - // Fired when the first AudioNode has been created, signifying - // that the AudioContext is being used and should be tracked via the editor. - START_CONTEXT: "WebAudioEditor:StartContext", - - // When the devtools theme changes. - THEME_CHANGE: "WebAudioEditor:ThemeChange", - - // When the UI is reset from tab navigation. - UI_RESET: "WebAudioEditor:UIReset", - - // When a param has been changed via the UI and successfully - // pushed via the actor to the raw audio node. - UI_SET_PARAM: "WebAudioEditor:UISetParam", - - // When a node is to be set in the InspectorView. - UI_SELECT_NODE: "WebAudioEditor:UISelectNode", - - // When the inspector is finished setting a new node. - UI_INSPECTOR_NODE_SET: "WebAudioEditor:UIInspectorNodeSet", - - // When the inspector is finished rendering in or out of view. - UI_INSPECTOR_TOGGLED: "WebAudioEditor:UIInspectorToggled", - - // When an audio node is finished loading in the Properties tab. - UI_PROPERTIES_TAB_RENDERED: "WebAudioEditor:UIPropertiesTabRendered", - - // When the Audio Context graph finishes rendering. - // Is called with two arguments, first representing number of nodes - // rendered, second being the number of edge connections rendering (not counting - // param edges), followed by the count of the param edges rendered. - UI_GRAPH_RENDERED: "WebAudioEditor:UIGraphRendered" -}; - -/** - * The current target and the Web Audio Editor front, set by this tool's host. - */ -let gToolbox, gTarget, gFront; - -/** - * Convenient way of emitting events from the panel window. - */ -EventEmitter.decorate(this); - -/** - * DOM query helper. - */ -function $(selector, target = document) { return target.querySelector(selector); } -function $$(selector, target = document) { return target.querySelectorAll(selector); } - -/** - * Takes an iterable collection, and a hash. Return the first - * object in the collection that matches the values in the hash. - * From Backbone.Collection#findWhere - * http://backbonejs.org/#Collection-findWhere - */ -function findWhere (collection, attrs) { - let keys = Object.keys(attrs); - for (let model of collection) { - if (keys.every(key => model[key] === attrs[key])) { - return model; - } - } - return void 0; -} - -function mixin (source, ...args) { - args.forEach(obj => Object.keys(obj).forEach(prop => source[prop] = obj[prop])); - return source; -} diff --git a/browser/devtools/webaudioeditor/models.js b/browser/devtools/webaudioeditor/models.js deleted file mode 100644 index e9ce10c2786..00000000000 --- a/browser/devtools/webaudioeditor/models.js +++ /dev/null @@ -1,274 +0,0 @@ -/* 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 as different name `coreEmit`, so we don't conflict -// with the global `window` listener itself. -const { emit: coreEmit } = require("sdk/event/core"); - -/** - * Representational wrapper around AudioNodeActors. Adding and destroying - * AudioNodes should be performed through the AudioNodes collection. - * - * Events: - * - `connect`: node, destinationNode, parameter - * - `disconnect`: node - */ -const AudioNodeModel = Class({ - extends: EventTarget, - - // Will be added via AudioNodes `add` - collection: null, - - initialize: function (actor) { - this.actor = actor; - this.id = actor.actorID; - this.connections = []; - }, - - /** - * After instantiating the AudioNodeModel, calling `setup` caches values - * from the actor onto the model. In this case, only the type of audio node. - * - * @return promise - */ - setup: Task.async(function* () { - yield this.getType(); - }), - - /** - * A proxy for the underlying AudioNodeActor to fetch its type - * and subsequently assign the type to the instance. - * - * @return Promise->String - */ - getType: Task.async(function* () { - this.type = yield this.actor.getType(); - return this.type; - }), - - /** - * Stores connection data inside this instance of this audio node connecting - * to another node (destination). If connecting to another node's AudioParam, - * the second argument (param) must be populated with a string. - * - * Connecting nodes is idempotent. Upon new connection, emits "connect" event. - * - * @param AudioNodeModel destination - * @param String param - */ - connect: function (destination, param) { - let edge = findWhere(this.connections, { destination: destination.id, param: param }); - - if (!edge) { - this.connections.push({ source: this.id, destination: destination.id, param: param }); - coreEmit(this, "connect", this, destination, param); - } - }, - - /** - * Clears out all internal connection data. Emits "disconnect" event. - */ - disconnect: function () { - this.connections.length = 0; - coreEmit(this, "disconnect", this); - }, - - /** - * Returns a promise that resolves to an array of objects containing - * both a `param` name property and a `value` property. - * - * @return Promise->Object - */ - getParams: function () { - return this.actor.getParams(); - }, - - /** - * Takes a `dagreD3.Digraph` object and adds this node to - * the graph to be rendered. - * - * @param dagreD3.Digraph - */ - addToGraph: function (graph) { - graph.addNode(this.id, { - type: this.type, - label: this.type.replace(/Node$/, ""), - id: this.id - }); - }, - - /** - * Takes a `dagreD3.Digraph` object and adds edges to - * the graph to be rendered. Separate from `addToGraph`, - * as while we depend on D3/Dagre's constraints, we cannot - * add edges for nodes that have not yet been added to the graph. - * - * @param dagreD3.Digraph - */ - addEdgesToGraph: function (graph) { - for (let edge of this.connections) { - let options = { - source: this.id, - target: edge.destination - }; - - // Only add `label` if `param` specified, as this is an AudioParam - // connection then. `label` adds the magic to render with dagre-d3, - // and `param` is just more explicitly the param, ignoring - // implementation details. - if (edge.param) { - options.label = options.param = edge.param; - } - - graph.addEdge(null, this.id, edge.destination, options); - } - } -}); - - -/** - * Constructor for a Collection of `AudioNodeModel` models. - * - * Events: - * - `add`: node - * - `remove`: node - * - `connect`: node, destinationNode, parameter - * - `disconnect`: node - */ -const AudioNodesCollection = Class({ - extends: EventTarget, - - model: AudioNodeModel, - - initialize: function () { - this.models = new Set(); - this._onModelEvent = this._onModelEvent.bind(this); - }, - - /** - * Iterates over all models within the collection, calling `fn` with the - * model as the first argument. - * - * @param Function fn - */ - forEach: function (fn) { - this.models.forEach(fn); - }, - - /** - * Creates a new AudioNodeModel, passing through arguments into the AudioNodeModel - * constructor, and adds the model to the internal collection store of this - * instance. - * - * Also calls `setup` on the model itself, and sets up event piping, so that - * events emitted on each model propagate to the collection itself. - * - * Emits "add" event on instance when completed. - * - * @param Object obj - * @return Promise->AudioNodeModel - */ - add: Task.async(function* (obj) { - let node = new this.model(obj); - node.collection = this; - yield node.setup(); - - this.models.add(node); - - node.on("*", this._onModelEvent); - coreEmit(this, "add", node); - return node; - }), - - /** - * Removes an AudioNodeModel from the internal collection. Calls `delete` method - * on the model, and emits "remove" on this instance. - * - * @param AudioNodeModel node - */ - remove: function (node) { - this.models.delete(node); - coreEmit(this, "remove", node); - }, - - /** - * Empties out the internal collection of all AudioNodeModels. - */ - reset: function () { - this.models.clear(); - }, - - /** - * Takes an `id` from an AudioNodeModel and returns the corresponding - * AudioNodeModel within the collection that matches that id. Returns `null` - * if not found. - * - * @param Number id - * @return AudioNodeModel|null - */ - get: function (id) { - return findWhere(this.models, { id: id }); - }, - - /** - * Returns the count for how many models are a part of this collection. - * - * @return Number - */ - get length() { - return this.models.size; - }, - - /** - * Returns detailed information about the collection. used during tests - * to query state. Returns an object with information on node count, - * how many edges are within the data graph, as well as how many of those edges - * are for AudioParams. - * - * @return Object - */ - getInfo: function () { - let info = { - nodes: this.length, - edges: 0, - paramEdges: 0 - }; - - this.models.forEach(node => { - let paramEdgeCount = node.connections.filter(edge => edge.param).length; - info.edges += node.connections.length - paramEdgeCount; - info.paramEdges += paramEdgeCount; - }); - return info; - }, - - /** - * Adds all nodes within the collection to the passed in graph, - * as well as their corresponding edges. - * - * @param dagreD3.Digraph - */ - populateGraph: function (graph) { - this.models.forEach(node => node.addToGraph(graph)); - this.models.forEach(node => node.addEdgesToGraph(graph)); - }, - - /** - * Called when a stored model emits any event. Used to manage - * event propagation, or listening to model events to react, like - * removing a model from the collection when it's destroyed. - */ - _onModelEvent: function (eventName, node, ...args) { - if (eventName === "remove") { - // If a `remove` event from the model, remove it - // from the collection, and let the method handle the emitting on - // the collection - this.remove(node); - } else { - // Pipe the event to the collection - coreEmit(this, eventName, [node].concat(args)); - } - } -}); diff --git a/browser/devtools/webaudioeditor/panel.js b/browser/devtools/webaudioeditor/panel.js index fd4c7b9840d..f138b7ce9fc 100644 --- a/browser/devtools/webaudioeditor/panel.js +++ b/browser/devtools/webaudioeditor/panel.js @@ -35,7 +35,6 @@ WebAudioEditorPanel.prototype = { .then(() => { this.panelWin.gToolbox = this._toolbox; this.panelWin.gTarget = this.target; - this.panelWin.gFront = new WebAudioFront(this.target.client, this.target.form); return this.panelWin.startupWebAudioEditor(); }) diff --git a/browser/devtools/webaudioeditor/test/browser.ini b/browser/devtools/webaudioeditor/test/browser.ini index ec28d7d4a47..9545b68393f 100644 --- a/browser/devtools/webaudioeditor/test/browser.ini +++ b/browser/devtools/webaudioeditor/test/browser.ini @@ -8,7 +8,6 @@ support-files = doc_media-node-creation.html doc_destroy-nodes.html doc_connect-toggle.html - doc_connect-toggle-param.html doc_connect-param.html doc_connect-multi-param.html doc_iframe-context.html @@ -40,7 +39,6 @@ support-files = [browser_wa_graph-render-02.js] [browser_wa_graph-render-03.js] [browser_wa_graph-render-04.js] -[browser_wa_graph-render-05.js] [browser_wa_graph-selected.js] [browser_wa_graph-zoom.js] diff --git a/browser/devtools/webaudioeditor/test/browser_wa_destroy-node-01.js b/browser/devtools/webaudioeditor/test/browser_wa_destroy-node-01.js index d3f14a2b96b..6343cfdce9a 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_destroy-node-01.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_destroy-node-01.js @@ -12,24 +12,24 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(DESTROY_NODES_URL); let { panelWin } = panel; - let { gFront, $, $$, gAudioNodes } = panelWin; + let { gFront, $, $$, EVENTS } = panelWin; let started = once(gFront, "start-context"); reload(target); - let destroyed = getN(gAudioNodes, "remove", 10); + let destroyed = getN(panelWin, EVENTS.DESTROY_NODE, 10); forceCC(); let [created] = yield Promise.all([ - getNSpread(gAudioNodes, "add", 13), + getNSpread(panelWin, EVENTS.CREATE_NODE, 13), waitForGraphRendered(panelWin, 13, 2) ]); - // Flatten arrays of event arguments and take the first (AudioNodeModel) - // and get its ID. - let actorIDs = created.map(ev => ev[0].id); + // Since CREATE_NODE emits several arguments (eventName and actorID), let's + // flatten it to just the actorIDs + let actorIDs = created.map(ev => ev[1]); // Click a soon-to-be dead buffer node yield clickGraphNode(panelWin, actorIDs[5]); @@ -40,7 +40,7 @@ function spawnTest() { yield Promise.all([destroyed, waitForGraphRendered(panelWin, 3, 2)]); // Test internal storage - is(panelWin.gAudioNodes.length, 3, "All nodes should be GC'd except one gain, osc and dest node."); + is(panelWin.AudioNodes.length, 3, "All nodes should be GC'd except one gain, osc and dest node."); // Test graph rendering ok(findGraphNode(panelWin, actorIDs[0]), "dest should be in graph"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_graph-click.js b/browser/devtools/webaudioeditor/test/browser_wa_graph-click.js index d6b307194a0..8e000fa8e92 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_graph-click.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-click.js @@ -6,10 +6,13 @@ * the correct node in the InspectorView */ +let EVENTS = null; + function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(COMPLEX_CONTEXT_URL); let panelWin = panel.panelWin; - let { gFront, $, $$, InspectorView } = panelWin; + let { gFront, $, $$, WebAudioInspectorView } = panelWin; + EVENTS = panelWin.EVENTS; let started = once(gFront, "start-context"); @@ -22,28 +25,28 @@ function spawnTest() { let nodeIds = actors.map(actor => actor.actorID); - ok(!InspectorView.isVisible(), "InspectorView hidden on start."); + ok(!WebAudioInspectorView.isVisible(), "InspectorView hidden on start."); yield clickGraphNode(panelWin, nodeIds[1], true); - ok(InspectorView.isVisible(), "InspectorView visible after selecting a node."); - is(InspectorView.getCurrentAudioNode().id, nodeIds[1], "InspectorView has correct node set."); + ok(WebAudioInspectorView.isVisible(), "InspectorView visible after selecting a node."); + is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[1], "InspectorView has correct node set."); yield clickGraphNode(panelWin, nodeIds[2]); - ok(InspectorView.isVisible(), "InspectorView still visible after selecting another node."); - is(InspectorView.getCurrentAudioNode().id, nodeIds[2], "InspectorView has correct node set on second node."); + ok(WebAudioInspectorView.isVisible(), "InspectorView still visible after selecting another node."); + is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[2], "InspectorView has correct node set on second node."); yield clickGraphNode(panelWin, nodeIds[2]); - is(InspectorView.getCurrentAudioNode().id, nodeIds[2], "Clicking the same node again works (idempotent)."); + is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[2], "Clicking the same node again works (idempotent)."); yield clickGraphNode(panelWin, $("rect", findGraphNode(panelWin, nodeIds[3]))); - is(InspectorView.getCurrentAudioNode().id, nodeIds[3], "Clicking on a works as expected."); + is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[3], "Clicking on a works as expected."); yield clickGraphNode(panelWin, $("tspan", findGraphNode(panelWin, nodeIds[4]))); - is(InspectorView.getCurrentAudioNode().id, nodeIds[4], "Clicking on a works as expected."); + is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[4], "Clicking on a works as expected."); - ok(InspectorView.isVisible(), + ok(WebAudioInspectorView.isVisible(), "InspectorView still visible after several nodes have been clicked."); yield teardown(panel); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_graph-markers.js b/browser/devtools/webaudioeditor/test/browser_wa_graph-markers.js index 5961837e275..c04a15eda01 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_graph-markers.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-markers.js @@ -8,7 +8,7 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, MARKER_STYLING } = panelWin; + let { gFront, $, $$, EVENTS, MARKER_STYLING } = panelWin; let currentTheme = Services.prefs.getCharPref("devtools.theme"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-01.js b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-01.js index b036cc1741f..137d5ee195f 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-01.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-01.js @@ -10,13 +10,13 @@ let connectCount = 0; function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin; + let { gFront, $, $$, EVENTS } = panelWin; let started = once(gFront, "start-context"); reload(target); - gAudioNodes.on("connect", onConnectNode); + panelWin.on(EVENTS.CONNECT_NODE, onConnectNode); let [actors] = yield Promise.all([ get3(gFront, "create-node"), @@ -35,7 +35,7 @@ function spawnTest() { is(connectCount, 2, "Only two node connect events should be fired."); - gAudioNodes.off("connect", onConnectNode); + panelWin.off(EVENTS.CONNECT_NODE, onConnectNode); yield teardown(panel); finish(); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-02.js b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-02.js index aa3b18d2e81..dadf4a7f751 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-02.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-02.js @@ -8,7 +8,7 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(COMPLEX_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$ } = panelWin; + let { gFront, $, $$, EVENTS } = panelWin; let started = once(gFront, "start-context"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-05.js b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-05.js deleted file mode 100644 index 708227ef9a6..00000000000 --- a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-05.js +++ /dev/null @@ -1,27 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -/** - * Tests to ensure that param connections trigger graph redraws - */ - -function spawnTest() { - let [target, debuggee, panel] = yield initWebAudioEditor(CONNECT_TOGGLE_PARAM_URL); - let { panelWin } = panel; - let { gFront, $, $$, EVENTS } = panelWin; - - reload(target); - - let [actors] = yield Promise.all([ - getN(gFront, "create-node", 3), - waitForGraphRendered(panelWin, 3, 1, 0) - ]); - ok(true, "Graph rendered without param connection"); - - yield waitForGraphRendered(panelWin, 3, 1, 1); - ok(true, "Graph re-rendered upon param connection"); - - yield teardown(panel); - finish(); -} - diff --git a/browser/devtools/webaudioeditor/test/browser_wa_graph-zoom.js b/browser/devtools/webaudioeditor/test/browser_wa_graph-zoom.js index f90b2499195..f67d5eb1f8d 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_graph-zoom.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-zoom.js @@ -8,7 +8,7 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, ContextView } = panelWin; + let { gFront, $, $$, EVENTS, WebAudioGraphView } = panelWin; let started = once(gFront, "start-context"); @@ -17,27 +17,27 @@ function spawnTest() { waitForGraphRendered(panelWin, 3, 2) ]); - is(ContextView.getCurrentScale(), 1, "Default graph scale is 1."); - is(ContextView.getCurrentTranslation()[0], 20, "Default x-translation is 20."); - is(ContextView.getCurrentTranslation()[1], 20, "Default y-translation is 20."); + is(WebAudioGraphView.getCurrentScale(), 1, "Default graph scale is 1."); + is(WebAudioGraphView.getCurrentTranslation()[0], 20, "Default x-translation is 20."); + is(WebAudioGraphView.getCurrentTranslation()[1], 20, "Default y-translation is 20."); // Change both attribute and D3's internal store panelWin.d3.select("#graph-target").attr("transform", "translate([100, 400]) scale(10)"); - ContextView._zoomBinding.scale(10); - ContextView._zoomBinding.translate([100, 400]); + WebAudioGraphView._zoomBinding.scale(10); + WebAudioGraphView._zoomBinding.translate([100, 400]); - is(ContextView.getCurrentScale(), 10, "After zoom, scale is 10."); - is(ContextView.getCurrentTranslation()[0], 100, "After zoom, x-translation is 100."); - is(ContextView.getCurrentTranslation()[1], 400, "After zoom, y-translation is 400."); + is(WebAudioGraphView.getCurrentScale(), 10, "After zoom, scale is 10."); + is(WebAudioGraphView.getCurrentTranslation()[0], 100, "After zoom, x-translation is 100."); + is(WebAudioGraphView.getCurrentTranslation()[1], 400, "After zoom, y-translation is 400."); yield Promise.all([ reload(target), waitForGraphRendered(panelWin, 3, 2) ]); - is(ContextView.getCurrentScale(), 1, "After refresh, graph scale is 1."); - is(ContextView.getCurrentTranslation()[0], 20, "After refresh, x-translation is 20."); - is(ContextView.getCurrentTranslation()[1], 20, "After refresh, y-translation is 20."); + is(WebAudioGraphView.getCurrentScale(), 1, "After refresh, graph scale is 1."); + is(WebAudioGraphView.getCurrentTranslation()[0], 20, "After refresh, x-translation is 20."); + is(WebAudioGraphView.getCurrentTranslation()[1], 20, "After refresh, y-translation is 20."); yield teardown(panel); finish(); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js b/browser/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js index ed168960bfd..64f49b0b379 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js @@ -9,8 +9,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, InspectorView } = panelWin; - let gVars = InspectorView._propsView; + let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; + let gVars = WebAudioInspectorView._propsView; let started = once(gFront, "start-context"); @@ -22,13 +22,13 @@ function spawnTest() { ]); let nodeIds = actors.map(actor => actor.actorID); - ok(!InspectorView.isVisible(), "InspectorView hidden on start."); + ok(!WebAudioInspectorView.isVisible(), "InspectorView hidden on start."); // Open inspector pane $("#inspector-pane-toggle").click(); yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED); - ok(InspectorView.isVisible(), "InspectorView shown after toggling."); + ok(WebAudioInspectorView.isVisible(), "InspectorView shown after toggling."); ok(isVisible($("#web-audio-editor-details-pane-empty")), "InspectorView empty message should still be visible."); @@ -41,13 +41,13 @@ function spawnTest() { $("#inspector-pane-toggle").click(); yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED); - ok(!InspectorView.isVisible(), "InspectorView back to being hidden."); + ok(!WebAudioInspectorView.isVisible(), "InspectorView back to being hidden."); // Open again to test node loading while open $("#inspector-pane-toggle").click(); yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED); - ok(InspectorView.isVisible(), "InspectorView being shown."); + ok(WebAudioInspectorView.isVisible(), "InspectorView being shown."); ok(!isVisible($("#web-audio-editor-tabs")), "InspectorView tabs are still hidden."); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_inspector.js b/browser/devtools/webaudioeditor/test/browser_wa_inspector.js index ff13dfbf87d..1db3f44ed4d 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_inspector.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_inspector.js @@ -9,8 +9,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, InspectorView } = panelWin; - let gVars = InspectorView._propsView; + let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; + let gVars = WebAudioInspectorView._propsView; let started = once(gFront, "start-context"); @@ -22,7 +22,7 @@ function spawnTest() { ]); let nodeIds = actors.map(actor => actor.actorID); - ok(!InspectorView.isVisible(), "InspectorView hidden on start."); + ok(!WebAudioInspectorView.isVisible(), "InspectorView hidden on start."); ok(isVisible($("#web-audio-editor-details-pane-empty")), "InspectorView empty message should show when no node's selected."); ok(!isVisible($("#web-audio-editor-tabs")), @@ -37,7 +37,7 @@ function spawnTest() { once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED) ]); - ok(InspectorView.isVisible(), "InspectorView shown once node selected."); + ok(WebAudioInspectorView.isVisible(), "InspectorView shown once node selected."); ok(!isVisible($("#web-audio-editor-details-pane-empty")), "InspectorView empty message hidden when node selected."); ok(isVisible($("#web-audio-editor-tabs")), diff --git a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js index 4e61b1645a5..4b7c809aae3 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js @@ -8,8 +8,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, InspectorView } = panelWin; - let gVars = InspectorView._propsView; + let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; + let gVars = WebAudioInspectorView._propsView; let started = once(gFront, "start-context"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js index dc9b5fae23b..bdd3a1e4e5c 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js @@ -8,8 +8,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(COMPLEX_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, InspectorView } = panelWin; - let gVars = InspectorView._propsView; + let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; + let gVars = WebAudioInspectorView._propsView; let started = once(gFront, "start-context"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-media-nodes.js b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-media-nodes.js index 07df9848b73..6f559bb01e8 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-media-nodes.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-media-nodes.js @@ -35,8 +35,8 @@ function waitForDeviceClosed() { function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(MEDIA_NODES_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, InspectorView } = panelWin; - let gVars = InspectorView._propsView; + let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; + let gVars = WebAudioInspectorView._propsView; // Auto enable getUserMedia let mediaPermissionPref = Services.prefs.getBoolPref(MEDIA_PERMISSION); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js index 209e0b89a6f..58521b5306c 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js @@ -9,8 +9,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(BUFFER_AND_ARRAY_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, InspectorView } = panelWin; - let gVars = InspectorView._propsView; + let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; + let gVars = WebAudioInspectorView._propsView; let started = once(gFront, "start-context"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params.js b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params.js index c08838334f6..0fce98885d6 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params.js @@ -9,8 +9,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_NODES_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, InspectorView } = panelWin; - let gVars = InspectorView._propsView; + let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; + let gVars = WebAudioInspectorView._propsView; let started = once(gFront, "start-context"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_properties-view.js b/browser/devtools/webaudioeditor/test/browser_wa_properties-view.js index c84287eae86..0421de6860c 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view.js @@ -8,8 +8,8 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, $$, EVENTS, InspectorView } = panelWin; - let gVars = InspectorView._propsView; + let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin; + let gVars = WebAudioInspectorView._propsView; let started = once(gFront, "start-context"); diff --git a/browser/devtools/webaudioeditor/test/browser_wa_reset-03.js b/browser/devtools/webaudioeditor/test/browser_wa_reset-03.js index 8ccbf9f7288..96dd9c95a4c 100644 --- a/browser/devtools/webaudioeditor/test/browser_wa_reset-03.js +++ b/browser/devtools/webaudioeditor/test/browser_wa_reset-03.js @@ -9,7 +9,7 @@ function spawnTest() { let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL); let { panelWin } = panel; - let { gFront, $, InspectorView } = panelWin; + let { gFront, $, WebAudioInspectorView } = panelWin; reload(target); @@ -20,8 +20,8 @@ function spawnTest() { let nodeIds = actors.map(actor => actor.actorID); yield clickGraphNode(panelWin, nodeIds[1], true); - ok(InspectorView.isVisible(), "InspectorView visible after selecting a node."); - is(InspectorView.getCurrentAudioNode().id, nodeIds[1], "InspectorView has correct node set."); + ok(WebAudioInspectorView.isVisible(), "InspectorView visible after selecting a node."); + is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[1], "InspectorView has correct node set."); /** * Reload @@ -35,14 +35,14 @@ function spawnTest() { ]); nodeIds = actors.map(actor => actor.actorID); - ok(!InspectorView.isVisible(), "InspectorView hidden on start."); - ise(InspectorView.getCurrentAudioNode(), null, + ok(!WebAudioInspectorView.isVisible(), "InspectorView hidden on start."); + ise(WebAudioInspectorView.getCurrentAudioNode(), null, "InspectorView has no current node set on reset."); yield clickGraphNode(panelWin, nodeIds[2], true); - ok(InspectorView.isVisible(), + ok(WebAudioInspectorView.isVisible(), "InspectorView visible after selecting a node after a reset."); - is(InspectorView.getCurrentAudioNode().id, nodeIds[2], "InspectorView has correct node set upon clicking graph node after a reset."); + is(WebAudioInspectorView.getCurrentAudioNode().id, nodeIds[2], "InspectorView has correct node set upon clicking graph node after a reset."); yield teardown(panel); finish(); diff --git a/browser/devtools/webaudioeditor/test/doc_connect-toggle-param.html b/browser/devtools/webaudioeditor/test/doc_connect-toggle-param.html deleted file mode 100644 index ae3ece5db8c..00000000000 --- a/browser/devtools/webaudioeditor/test/doc_connect-toggle-param.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - Web Audio Editor test page - - - - - - - - diff --git a/browser/devtools/webaudioeditor/test/head.js b/browser/devtools/webaudioeditor/test/head.js index d792cbb9b8a..d088c36a3cf 100644 --- a/browser/devtools/webaudioeditor/test/head.js +++ b/browser/devtools/webaudioeditor/test/head.js @@ -28,7 +28,6 @@ const MEDIA_NODES_URL = EXAMPLE_URL + "doc_media-node-creation.html"; const BUFFER_AND_ARRAY_URL = EXAMPLE_URL + "doc_buffer-and-array.html"; const DESTROY_NODES_URL = EXAMPLE_URL + "doc_destroy-nodes.html"; const CONNECT_TOGGLE_URL = EXAMPLE_URL + "doc_connect-toggle.html"; -const CONNECT_TOGGLE_PARAM_URL = EXAMPLE_URL + "doc_connect-toggle-param.html"; const CONNECT_PARAM_URL = EXAMPLE_URL + "doc_connect-param.html"; const CONNECT_MULTI_PARAM_URL = EXAMPLE_URL + "doc_connect-multi-param.html"; const IFRAME_CONTEXT_URL = EXAMPLE_URL + "doc_iframe-context.html"; @@ -38,10 +37,7 @@ waitForExplicitFinish(); let gToolEnabled = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled"); -gDevTools.testing = true; - registerCleanupFunction(() => { - gDevTools.testing = false; info("finish() was called, cleaning up..."); Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", gToolEnabled); @@ -219,7 +215,10 @@ function waitForGraphRendered (front, nodeCount, edgeCount, paramEdgeCount) { let deferred = Promise.defer(); let eventName = front.EVENTS.UI_GRAPH_RENDERED; front.on(eventName, function onGraphRendered (_, nodes, edges, pEdges) { - let paramEdgesDone = paramEdgeCount != null ? paramEdgeCount === pEdges : true; + info(nodes); + info(edges) + info(pEdges); + let paramEdgesDone = paramEdgeCount ? paramEdgeCount === pEdges : true; if (nodes === nodeCount && edges === edgeCount && paramEdgesDone) { front.off(eventName, onGraphRendered); deferred.resolve(); diff --git a/browser/devtools/webaudioeditor/views/context.js b/browser/devtools/webaudioeditor/views/context.js deleted file mode 100644 index 139047c6c2a..00000000000 --- a/browser/devtools/webaudioeditor/views/context.js +++ /dev/null @@ -1,305 +0,0 @@ -/* 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 { debounce } = require("sdk/lang/functional"); - -// 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" -}; - -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. - */ -let ContextView = { - /** - * Initialization function, called when the tool is started. - */ - initialize: function() { - this._onGraphNodeClick = this._onGraphNodeClick.bind(this); - this._onThemeChange = this._onThemeChange.bind(this); - this._onNodeSelect = this._onNodeSelect.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._onGraphNodeClick, false); - - window.on(EVENTS.THEME_CHANGE, this._onThemeChange); - window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSelect); - 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._onGraphNodeClick, false); - window.off(EVENTS.THEME_CHANGE, this._onThemeChange); - window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSelect); - 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. - * Called from UI_INSPECTOR_NODE_SELECT. - */ - 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 + "']"); - }, - - /** - * 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.attr("class", (n) => { - let node = graph.node(n); - return "audionode type-" + node.type; - }); - svgNodes.attr("data-id", (n) => { - let node = graph.node(n); - return node.id; - }); - return svgNodes; - }); - - // Post-render manipulation of edges - // TODO do all of this more efficiently, rather than - // using the direct D3 helper utilities to loop over each - // edge several times - let oldDrawEdgePaths = renderer.drawEdgePaths(); - renderer.drawEdgePaths(function(graph, root) { - let svgEdges = oldDrawEdgePaths(graph, root); - svgEdges.attr("data-source", (n) => { - let edge = graph.edge(n); - return edge.source; - }); - svgEdges.attr("data-target", (n) => { - let edge = graph.edge(n); - return edge.target; - }); - svgEdges.attr("data-param", (n) => { - let edge = graph.edge(n); - return edge.param ? edge.param : null; - }); - // We have to manually specify the default classes on the edges - // as to not overwrite them - let defaultClasses = "edgePath enter"; - svgEdges.attr("class", (n) => { - let edge = graph.edge(n); - return defaultClasses + (edge.param ? (" param-connection " + edge.param) : ""); - }); - - 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 (gDevTools.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 (~GRAPH_REDRAW_EVENTS.indexOf(eventName)) { - this.draw(); - } - }, - - _onNodeSelect: function (eventName, id) { - this.focusNode(id); - }, - - /** - * 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 node in the svg graph is clicked. Used to handle triggering the AudioNodePane. - * - * @param Event e - * Click event. - */ - _onGraphNodeClick: 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; - - window.emit(EVENTS.UI_SELECT_NODE, node.getAttribute("data-id")); - } -}; diff --git a/browser/devtools/webaudioeditor/views/inspector.js b/browser/devtools/webaudioeditor/views/inspector.js deleted file mode 100644 index 0a807550d86..00000000000 --- a/browser/devtools/webaudioeditor/views/inspector.js +++ /dev/null @@ -1,240 +0,0 @@ -/* 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"); - -// Strings for rendering -const EXPAND_INSPECTOR_STRING = L10N.getStr("expandInspector"); -const COLLAPSE_INSPECTOR_STRING = L10N.getStr("collapseInspector"); - -// Store width as a preference rather than hardcode -// TODO bug 1009056 -const INSPECTOR_WIDTH = 300; - -const GENERIC_VARIABLES_VIEW_SETTINGS = { - searchEnabled: false, - editableValueTooltip: "", - editableNameTooltip: "", - preventDisableOnChange: true, - preventDescriptorModifiers: false, - eval: () => {} -}; - -/** - * Functions handling the audio node inspector UI. - */ - -let 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 () { - this._tabsPane = $("#web-audio-editor-tabs"); - - // Set up view controller - this.el = $("#web-audio-inspector"); - this.el.setAttribute("width", INSPECTOR_WIDTH); - this.button = $("#inspector-pane-toggle"); - mixin(this, ToggleMixin); - this.bindToggle(); - - // Hide inspector view on startup - this.hideImmediately(); - - this._onEval = this._onEval.bind(this); - this._onNodeSelect = this._onNodeSelect.bind(this); - this._onDestroyNode = this._onDestroyNode.bind(this); - - this._propsView = new VariablesView($("#properties-tabpanel-content"), GENERIC_VARIABLES_VIEW_SETTINGS); - this._propsView.eval = this._onEval; - - 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(); - window.off(EVENTS.UI_SELECT_NODE, this._onNodeSelect); - gAudioNodes.off("remove", this._onDestroyNode); - - this.el = null; - this.button = null; - this._tabsPane = null; - }, - - /** - * Takes a AudioNodeView `node` and sets it as the current - * node and scaffolds the inspector view based off of the new node. - */ - setCurrentAudioNode: 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._setTitle(); - this._buildPropertiesView() - .then(() => 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 () { - this._propsView.empty(); - // Set current node to empty to load empty view - this.setCurrentAudioNode(); - - // Reset AudioNode inspector and hide - this.hideImmediately(); - }, - - /** - * Sets the title of the Inspector view - */ - _setTitle: function () { - let node = this._currentNode; - let title = node.type.replace(/Node$/, ""); - $("#web-audio-inspector-title").setAttribute("value", title); - }, - - /** - * 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, - }; - audioParamsScope.addItem(param, descriptor); - }); - - audioParamsScope.expanded = true; - - window.emit(EVENTS.UI_PROPERTIES_TAB_RENDERED, node.id); - }), - - _togglePropertiesView: function (show) { - let propsView = $("#properties-tabpanel-content"); - let emptyView = $("#properties-tabpanel-content-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 - */ - - /** - * 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); - } - }), - - /** - * 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(); - }, - - /** - * 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); - } - } -}; diff --git a/browser/devtools/webaudioeditor/views/utils.js b/browser/devtools/webaudioeditor/views/utils.js deleted file mode 100644 index c397a16cb11..00000000000 --- a/browser/devtools/webaudioeditor/views/utils.js +++ /dev/null @@ -1,103 +0,0 @@ -/* 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` - */ -let 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.hasAttribute("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.removeAttribute("pane-collapsed"); - this.button.setAttribute("tooltiptext", this._collapseString); - } - else { - this.button.setAttribute("pane-collapsed", ""); - this.button.setAttribute("tooltiptext", this._expandString); - } - }, - - _onToggle: function () { - this._viewController({ visible: !this.isVisible() }); - } -} diff --git a/browser/devtools/webaudioeditor/webaudioeditor-controller.js b/browser/devtools/webaudioeditor/webaudioeditor-controller.js new file mode 100644 index 00000000000..ab071add288 --- /dev/null +++ b/browser/devtools/webaudioeditor/webaudioeditor-controller.js @@ -0,0 +1,428 @@ +/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); +Cu.import("resource:///modules/devtools/gDevTools.jsm"); + +// Override DOM promises with Promise.jsm helpers +const { defer, all } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; + +const { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); +const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; +const EventEmitter = require("devtools/toolkit/event-emitter"); +const STRINGS_URI = "chrome://browser/locale/devtools/webaudioeditor.properties" +const L10N = new ViewHelpers.L10N(STRINGS_URI); +const Telemetry = require("devtools/shared/telemetry"); +const telemetry = new Telemetry(); + +let { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); + +// The panel's window global is an EventEmitter firing the following events: +const EVENTS = { + // Fired when the first AudioNode has been created, signifying + // that the AudioContext is being used and should be tracked via the editor. + START_CONTEXT: "WebAudioEditor:StartContext", + + // On node creation, connect and disconnect. + CREATE_NODE: "WebAudioEditor:CreateNode", + CONNECT_NODE: "WebAudioEditor:ConnectNode", + DISCONNECT_NODE: "WebAudioEditor:DisconnectNode", + + // When a node gets GC'd. + DESTROY_NODE: "WebAudioEditor:DestroyNode", + + // On a node parameter's change. + CHANGE_PARAM: "WebAudioEditor:ChangeParam", + + // When the devtools theme changes. + THEME_CHANGE: "WebAudioEditor:ThemeChange", + + // When the UI is reset from tab navigation. + UI_RESET: "WebAudioEditor:UIReset", + + // When a param has been changed via the UI and successfully + // pushed via the actor to the raw audio node. + UI_SET_PARAM: "WebAudioEditor:UISetParam", + + // When a node is to be set in the InspectorView. + UI_SELECT_NODE: "WebAudioEditor:UISelectNode", + + // When the inspector is finished setting a new node. + UI_INSPECTOR_NODE_SET: "WebAudioEditor:UIInspectorNodeSet", + + // When the inspector is finished rendering in or out of view. + UI_INSPECTOR_TOGGLED: "WebAudioEditor:UIInspectorToggled", + + // When an audio node is finished loading in the Properties tab. + UI_PROPERTIES_TAB_RENDERED: "WebAudioEditor:UIPropertiesTabRendered", + + // When the Audio Context graph finishes rendering. + // Is called with two arguments, first representing number of nodes + // rendered, second being the number of edge connections rendering (not counting + // param edges), followed by the count of the param edges rendered. + UI_GRAPH_RENDERED: "WebAudioEditor:UIGraphRendered" +}; + +/** + * The current target and the Web Audio Editor front, set by this tool's host. + */ +let gToolbox, gTarget, gFront; + +/** + * Track an array of audio nodes + */ +let AudioNodes = []; +let AudioNodeConnections = new WeakMap(); // > +let AudioParamConnections = new WeakMap(); // + +// Light representation wrapping an AudioNode actor with additional properties +function AudioNodeView (actor) { + this.actor = actor; + this.id = actor.actorID; +} + +// A proxy for the underlying AudioNodeActor to fetch its type +// and subsequently assign the type to the instance. +AudioNodeView.prototype.getType = Task.async(function* () { + this.type = yield this.actor.getType(); + return this.type; +}); + +// Helper method to create connections in the AudioNodeConnections +// WeakMap for rendering. Returns a boolean indicating +// if the connection was successfully created. Will return `false` +// when the connection was previously made. +AudioNodeView.prototype.connect = function (destination) { + let connections = AudioNodeConnections.get(this) || new Set(); + AudioNodeConnections.set(this, connections); + + // Don't duplicate add. + if (!connections.has(destination)) { + connections.add(destination); + return true; + } + return false; +}; + +// Helper method to create connections in the AudioNodeConnections +// WeakMap for rendering. Returns a boolean indicating +// if the connection was successfully created. Will return `false` +// when the connection was previously made. +AudioNodeView.prototype.connectParam = function (destination, param) { + let connections = AudioParamConnections.get(this) || {}; + AudioParamConnections.set(this, connections); + + let params = connections[destination.id] = connections[destination.id] || []; + + if (!~params.indexOf(param)) { + params.push(param); + return true; + } + return false; +}; + +// Helper method to remove audio connections from the current AudioNodeView +AudioNodeView.prototype.disconnect = function () { + AudioNodeConnections.set(this, new Set()); + AudioParamConnections.set(this, {}); +}; + +// Returns a promise that resolves to an array of objects containing +// both a `param` name property and a `value` property. +AudioNodeView.prototype.getParams = function () { + return this.actor.getParams(); +}; + + +/** + * Initializes the web audio editor views + */ +function startupWebAudioEditor() { + return all([ + WebAudioEditorController.initialize(), + WebAudioGraphView.initialize(), + WebAudioInspectorView.initialize(), + ]); +} + +/** + * Destroys the web audio editor controller and views. + */ +function shutdownWebAudioEditor() { + return all([ + WebAudioEditorController.destroy(), + WebAudioGraphView.destroy(), + WebAudioInspectorView.destroy(), + ]); +} + +/** + * Functions handling target-related lifetime events. + */ +let WebAudioEditorController = { + /** + * Listen for events emitted by the current tab target. + */ + initialize: function() { + telemetry.toolOpened("webaudioeditor"); + this._onTabNavigated = this._onTabNavigated.bind(this); + this._onThemeChange = this._onThemeChange.bind(this); + gTarget.on("will-navigate", this._onTabNavigated); + gTarget.on("navigate", this._onTabNavigated); + gFront.on("start-context", this._onStartContext); + gFront.on("create-node", this._onCreateNode); + gFront.on("connect-node", this._onConnectNode); + gFront.on("connect-param", this._onConnectParam); + gFront.on("disconnect-node", this._onDisconnectNode); + gFront.on("change-param", this._onChangeParam); + gFront.on("destroy-node", this._onDestroyNode); + + // Hook into theme change so we can change + // the graph's marker styling, since we can't do this + // with CSS + gDevTools.on("pref-changed", this._onThemeChange); + + // Set up events to refresh the Graph view + window.on(EVENTS.CREATE_NODE, this._onUpdatedContext); + window.on(EVENTS.CONNECT_NODE, this._onUpdatedContext); + window.on(EVENTS.DISCONNECT_NODE, this._onUpdatedContext); + window.on(EVENTS.DESTROY_NODE, this._onUpdatedContext); + window.on(EVENTS.CONNECT_PARAM, this._onUpdatedContext); + }, + + /** + * Remove events emitted by the current tab target. + */ + destroy: function() { + telemetry.toolClosed("webaudioeditor"); + gTarget.off("will-navigate", this._onTabNavigated); + gTarget.off("navigate", this._onTabNavigated); + gFront.off("start-context", this._onStartContext); + gFront.off("create-node", this._onCreateNode); + gFront.off("connect-node", this._onConnectNode); + gFront.off("connect-param", this._onConnectParam); + gFront.off("disconnect-node", this._onDisconnectNode); + gFront.off("change-param", this._onChangeParam); + gFront.off("destroy-node", this._onDestroyNode); + window.off(EVENTS.CREATE_NODE, this._onUpdatedContext); + window.off(EVENTS.CONNECT_NODE, this._onUpdatedContext); + window.off(EVENTS.DISCONNECT_NODE, this._onUpdatedContext); + window.off(EVENTS.DESTROY_NODE, this._onUpdatedContext); + window.off(EVENTS.CONNECT_PARAM, this._onUpdatedContext); + gDevTools.off("pref-changed", this._onThemeChange); + }, + + /** + * Called when page is reloaded to show the reload notice and waiting + * for an audio context notice. + */ + reset: function () { + $("#content").hidden = true; + WebAudioGraphView.resetUI(); + WebAudioInspectorView.resetUI(); + }, + + /** + * Called when a new audio node is created, or the audio context + * routing changes. + */ + _onUpdatedContext: function () { + WebAudioGraphView.draw(); + }, + + /** + * Fired when the devtools theme changes (light, dark, etc.) + * so that the graph can update marker styling, as that + * cannot currently be done with CSS. + */ + _onThemeChange: function (event, data) { + window.emit(EVENTS.THEME_CHANGE, data.newValue); + }, + + /** + * Called for each location change in the debugged tab. + */ + _onTabNavigated: Task.async(function* (event, {isFrameSwitching}) { + switch (event) { + case "will-navigate": { + // Make sure the backend is prepared to handle audio contexts. + if (!isFrameSwitching) { + yield gFront.setup({ reload: false }); + } + + // Clear out current UI. + this.reset(); + + // When switching to an iframe, ensure displaying the reload button. + // As the document has already been loaded without being hooked. + if (isFrameSwitching) { + $("#reload-notice").hidden = false; + $("#waiting-notice").hidden = true; + } else { + // Otherwise, we are loading a new top level document, + // so we don't need to reload anymore and should receive + // new node events. + $("#reload-notice").hidden = true; + $("#waiting-notice").hidden = false; + } + + // Clear out stored audio nodes + AudioNodes.length = 0; + AudioNodeConnections.clear(); + window.emit(EVENTS.UI_RESET); + break; + } + case "navigate": { + // TODO Case of bfcache, needs investigating + // bug 994250 + break; + } + } + }), + + /** + * Called after the first audio node is created in an audio context, + * signaling that the audio context is being used. + */ + _onStartContext: function() { + $("#reload-notice").hidden = true; + $("#waiting-notice").hidden = true; + $("#content").hidden = false; + window.emit(EVENTS.START_CONTEXT); + }, + + /** + * Called when a new node is created. Creates an `AudioNodeView` instance + * for tracking throughout the editor. + */ + _onCreateNode: Task.async(function* (nodeActor) { + let node = new AudioNodeView(nodeActor); + yield node.getType(); + AudioNodes.push(node); + window.emit(EVENTS.CREATE_NODE, node.id); + }), + + /** + * Called on `destroy-node` when an AudioNode is GC'd. Removes + * from the AudioNode array and fires an event indicating the removal. + */ + _onDestroyNode: function (nodeActor) { + for (let i = 0; i < AudioNodes.length; i++) { + if (equalActors(AudioNodes[i].actor, nodeActor)) { + AudioNodes.splice(i, 1); + window.emit(EVENTS.DESTROY_NODE, nodeActor.actorID); + break; + } + } + }, + + /** + * Called when a node is connected to another node. + */ + _onConnectNode: Task.async(function* ({ source: sourceActor, dest: destActor }) { + let [source, dest] = yield waitForNodeCreation(sourceActor, destActor); + + // Connect nodes, and only emit if it's a new connection. + if (source.connect(dest)) { + window.emit(EVENTS.CONNECT_NODE, source.id, dest.id); + } + }), + + /** + * Called when a node is conneceted to another node's AudioParam. + */ + _onConnectParam: Task.async(function* ({ source: sourceActor, dest: destActor, param }) { + let [source, dest] = yield waitForNodeCreation(sourceActor, destActor); + + if (source.connectParam(dest, param)) { + window.emit(EVENTS.CONNECT_PARAM, source.id, dest.id, param); + } + }), + + /** + * Called when a node is disconnected. + */ + _onDisconnectNode: function(nodeActor) { + let node = getViewNodeByActor(nodeActor); + node.disconnect(); + window.emit(EVENTS.DISCONNECT_NODE, node.id); + }, + + /** + * Called when a node param is changed. + */ + _onChangeParam: function({ actor, param, value }) { + window.emit(EVENTS.CHANGE_PARAM, getViewNodeByActor(actor), param, value); + } +}; + +/** + * Convenient way of emitting events from the panel window. + */ +EventEmitter.decorate(this); + +/** + * DOM query helper. + */ +function $(selector, target = document) { return target.querySelector(selector); } +function $$(selector, target = document) { return target.querySelectorAll(selector); } + +/** + * Compare `actorID` between two actors to determine if they're corresponding + * to the same underlying actor. + */ +function equalActors (actor1, actor2) { + return actor1.actorID === actor2.actorID; +} + +/** + * Returns the corresponding ViewNode by actor + */ +function getViewNodeByActor (actor) { + for (let i = 0; i < AudioNodes.length; i++) { + if (equalActors(AudioNodes[i].actor, actor)) + return AudioNodes[i]; + } + return null; +} + +/** + * Returns the corresponding ViewNode by actorID + */ +function getViewNodeById (id) { + return getViewNodeByActor({ actorID: id }); +} + +// Since node create and connect are probably executed back to back, +// and the controller's `_onCreateNode` needs to look up type, +// the edge creation could be called before the graph node is actually +// created. This way, we can check and listen for the event before +// adding an edge. +function waitForNodeCreation (sourceActor, destActor) { + let deferred = defer(); + let eventName = EVENTS.CREATE_NODE; + let source = getViewNodeByActor(sourceActor); + let dest = getViewNodeByActor(destActor); + + if (!source || !dest) + window.on(eventName, function createNodeListener (_, id) { + let createdNode = getViewNodeById(id); + if (equalActors(sourceActor, createdNode.actor)) + source = createdNode; + if (equalActors(destActor, createdNode.actor)) + dest = createdNode; + if (source && dest) { + window.off(eventName, createNodeListener); + deferred.resolve([source, dest]); + } + }); + else + deferred.resolve([source, dest]); + return deferred.promise; +} diff --git a/browser/devtools/webaudioeditor/webaudioeditor-view.js b/browser/devtools/webaudioeditor/webaudioeditor-view.js new file mode 100644 index 00000000000..8bcf4b366ea --- /dev/null +++ b/browser/devtools/webaudioeditor/webaudioeditor-view.js @@ -0,0 +1,636 @@ +/* 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"); + +// Strings for rendering +const EXPAND_INSPECTOR_STRING = L10N.getStr("expandInspector"); +const COLLAPSE_INSPECTOR_STRING = L10N.getStr("collapseInspector"); + +// Store width as a preference rather than hardcode +// TODO bug 1009056 +const INSPECTOR_WIDTH = 300; + +// 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" +}; + +const GRAPH_DEBOUNCE_TIMER = 100; + +const GENERIC_VARIABLES_VIEW_SETTINGS = { + searchEnabled: false, + editableValueTooltip: "", + editableNameTooltip: "", + preventDisableOnChange: true, + preventDescriptorModifiers: false, + 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._onThemeChange = this._onThemeChange.bind(this); + this._onNodeSelect = this._onNodeSelect.bind(this); + this._onStartContext = this._onStartContext.bind(this); + this._onDestroyNode = this._onDestroyNode.bind(this); + + this.draw = debounce(this.draw.bind(this), GRAPH_DEBOUNCE_TIMER); + $('#graph-target').addEventListener('click', this._onGraphNodeClick, false); + + window.on(EVENTS.THEME_CHANGE, this._onThemeChange); + window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSelect); + window.on(EVENTS.START_CONTEXT, this._onStartContext); + window.on(EVENTS.DESTROY_NODE, this._onDestroyNode); + }, + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function() { + if (this._zoomBinding) { + this._zoomBinding.on("zoom", null); + } + $('#graph-target').removeEventListener('click', this._onGraphNodeClick, false); + window.off(EVENTS.THEME_CHANGE, this._onThemeChange); + window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSelect); + window.off(EVENTS.START_CONTEXT, this._onStartContext); + window.off(EVENTS.DESTROY_NODE, this._onDestroyNode); + }, + + /** + * Called when a page is reloaded and waiting for a "start-context" event + * and clears out old content + */ + resetUI: function () { + this.clearGraph(); + this.resetGraphPosition(); + }, + + /** + * 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. + */ + resetGraphPosition: function () { + 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. + * Called from UI_INSPECTOR_NODE_SELECT. + */ + 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 + "']"); + }, + + /** + * `draw` renders the ViewNodes currently available in `AudioNodes` with `AudioNodeConnections`, + * and `AudioParamConnections` 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.clearGraph(); + + let graph = new dagreD3.Digraph(); + // An array of duples/tuples of pairs [sourceNode, destNode, param]. + // `param` is optional, indicating a connection to an AudioParam, rather than + // an other AudioNode. + let edges = []; + + AudioNodes.forEach(node => { + // Add node to graph + graph.addNode(node.id, { + type: node.type, // Just for storing type data + label: node.type.replace(/Node$/, ""), // Displayed in SVG node + id: node.id // Identification + }); + + // 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, new Set()).forEach(dest => edges.push([node, dest])); + let paramConnections = AudioParamConnections.get(node, {}); + Object.keys(paramConnections).forEach(destId => { + let dest = getViewNodeById(destId); + let connections = paramConnections[destId] || []; + connections.forEach(param => edges.push([node, dest, param])); + }); + }); + + edges.forEach(([node, dest, param]) => { + let options = { + source: node.id, + target: dest.id + }; + + // Only add `label` if `param` specified, as this is an AudioParam connection then. + // `label` adds the magic to render with dagre-d3, and `param` is just more explicitly + // the param, ignoring implementation details. + if (param) { + options.label = param; + options.param = param; + } + + graph.addEdge(null, node.id, dest.id, options); + }); + + 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 "audionode type-" + node.type; + }); + svgNodes.attr("data-id", (n) => { + let node = graph.node(n); + return node.id; + }); + return svgNodes; + }); + + // Post-render manipulation of edges + // TODO do all of this more efficiently, rather than + // using the direct D3 helper utilities to loop over each + // edge several times + let oldDrawEdgePaths = renderer.drawEdgePaths(); + renderer.drawEdgePaths(function(graph, root) { + let svgEdges = oldDrawEdgePaths(graph, root); + svgEdges.attr("data-source", (n) => { + let edge = graph.edge(n); + return edge.source; + }); + svgEdges.attr("data-target", (n) => { + let edge = graph.edge(n); + return edge.target; + }); + svgEdges.attr("data-param", (n) => { + let edge = graph.edge(n); + return edge.param ? edge.param : null; + }); + // We have to manually specify the default classes on the edges + // as to not overwrite them + let defaultClasses = "edgePath enter"; + svgEdges.attr("class", (n) => { + let edge = graph.edge(n); + return defaultClasses + (edge.param ? (" param-connection " + edge.param) : ""); + }); + + 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 = WebAudioInspectorView.getCurrentAudioNode(); + if (currentNode) { + this.focusNode(currentNode.id); + } + + // Fire an event upon completed rendering + let paramEdgeCount = edges.filter(p => !!p[2]).length; + window.emit(EVENTS.UI_GRAPH_RENDERED, AudioNodes.length, edges.length - paramEdgeCount, paramEdgeCount); + }); + + 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.resetGraphPosition(); + } + }, + + /** + * 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 a node gets GC'd -- redraws the graph. + */ + _onDestroyNode: function () { + this.draw(); + }, + + _onNodeSelect: function (eventName, id) { + this.focusNode(id); + }, + + /** + * 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 node in the svg graph is clicked. Used to handle triggering the AudioNodePane. + * + * @param Event e + * Click event. + */ + _onGraphNodeClick: 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; + + window.emit(EVENTS.UI_SELECT_NODE, node.getAttribute("data-id")); + } +}; + +let WebAudioInspectorView = { + + _propsView: null, + + _currentNode: null, + + _inspectorPane: null, + _inspectorPaneToggleButton: null, + _tabsPane: null, + + /** + * Initialization function called when the tool starts up. + */ + initialize: function () { + this._inspectorPane = $("#web-audio-inspector"); + this._inspectorPaneToggleButton = $("#inspector-pane-toggle"); + this._tabsPane = $("#web-audio-editor-tabs"); + + // Hide inspector view on startup + this._inspectorPane.setAttribute("width", INSPECTOR_WIDTH); + this.toggleInspector({ visible: false, delayed: false, animated: false }); + + this._onEval = this._onEval.bind(this); + this._onNodeSelect = this._onNodeSelect.bind(this); + this._onTogglePaneClick = this._onTogglePaneClick.bind(this); + this._onDestroyNode = this._onDestroyNode.bind(this); + + this._inspectorPaneToggleButton.addEventListener("mousedown", this._onTogglePaneClick, false); + this._propsView = new VariablesView($("#properties-tabpanel-content"), GENERIC_VARIABLES_VIEW_SETTINGS); + this._propsView.eval = this._onEval; + + window.on(EVENTS.UI_SELECT_NODE, this._onNodeSelect); + window.on(EVENTS.DESTROY_NODE, this._onDestroyNode); + }, + + /** + * Destruction function called when the tool cleans up. + */ + destroy: function () { + this._inspectorPaneToggleButton.removeEventListener("mousedown", this._onTogglePaneClick); + window.off(EVENTS.UI_SELECT_NODE, this._onNodeSelect); + window.off(EVENTS.DESTROY_NODE, this._onDestroyNode); + + this._inspectorPane = null; + this._inspectorPaneToggleButton = null; + this._tabsPane = null; + }, + + /** + * Toggles the visibility of the AudioNode Inspector. + * + * @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 + * - index: the index of the tab to be selected inside the inspector + * @param number index + * Index of the tab that should be selected when shown. + */ + toggleInspector: function ({ visible, animated, delayed, index }) { + let pane = this._inspectorPane; + let button = this._inspectorPaneToggleButton; + + let flags = { + visible: visible, + animated: animated != null ? animated : true, + delayed: delayed != null ? delayed : true, + callback: () => window.emit(EVENTS.UI_INSPECTOR_TOGGLED, visible) + }; + + ViewHelpers.togglePane(flags, pane); + + if (flags.visible) { + button.removeAttribute("pane-collapsed"); + button.setAttribute("tooltiptext", COLLAPSE_INSPECTOR_STRING); + } + else { + button.setAttribute("pane-collapsed", ""); + button.setAttribute("tooltiptext", EXPAND_INSPECTOR_STRING); + } + + if (index != undefined) { + pane.selectedIndex = index; + } + }, + + /** + * Returns a boolean indicating whether or not the AudioNode inspector + * is currently being shown. + */ + isVisible: function () { + return !this._inspectorPane.hasAttribute("pane-collapsed"); + }, + + /** + * Takes a AudioNodeView `node` and sets it as the current + * node and scaffolds the inspector view based off of the new node. + */ + setCurrentAudioNode: 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._setTitle(); + this._buildPropertiesView() + .then(() => 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 () { + this._propsView.empty(); + // Set current node to empty to load empty view + this.setCurrentAudioNode(); + + // Reset AudioNode inspector and hide + this.toggleInspector({ visible: false, animated: false, delayed: false }); + }, + + /** + * Sets the title of the Inspector view + */ + _setTitle: function () { + let node = this._currentNode; + let title = node.type.replace(/Node$/, ""); + $("#web-audio-inspector-title").setAttribute("value", title); + }, + + /** + * 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, + }; + audioParamsScope.addItem(param, descriptor); + }); + + audioParamsScope.expanded = true; + + window.emit(EVENTS.UI_PROPERTIES_TAB_RENDERED, node.id); + }), + + _togglePropertiesView: function (show) { + let propsView = $("#properties-tabpanel-content"); + let emptyView = $("#properties-tabpanel-content-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 + */ + + /** + * 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); + } + }), + + /** + * Called on EVENTS.UI_SELECT_NODE, and takes an actorID `id` + * and calls `setCurrentAudioNode` to scaffold the inspector view. + */ + _onNodeSelect: function (_, id) { + this.setCurrentAudioNode(getViewNodeById(id)); + + // Ensure inspector is visible when selecting a new node + this.toggleInspector({ visible: true }); + }, + + /** + * Called when clicking on the toggling the inspector into view. + */ + _onTogglePaneClick: function () { + this.toggleInspector({ visible: !this.isVisible() }); + }, + + /** + * Called when `DESTROY_NODE` is fired to remove the node from props view if + * it's currently selected. + */ + _onDestroyNode: function (_, id) { + if (this._currentNode && this._currentNode.id === id) { + this.setCurrentAudioNode(null); + } + } +}; + +/** + * 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; +} diff --git a/browser/devtools/webaudioeditor/webaudioeditor.xul b/browser/devtools/webaudioeditor/webaudioeditor.xul index 9be6b2b52b9..0b01556c592 100644 --- a/browser/devtools/webaudioeditor/webaudioeditor.xul +++ b/browser/devtools/webaudioeditor/webaudioeditor.xul @@ -19,12 +19,8 @@ + diff --git a/browser/components/loop/standalone/content/js/standaloneClient.js b/browser/components/loop/standalone/content/js/standaloneClient.js index e5f3746c738..cd4df3ee51d 100644 --- a/browser/components/loop/standalone/content/js/standaloneClient.js +++ b/browser/components/loop/standalone/content/js/standaloneClient.js @@ -122,7 +122,7 @@ loop.StandaloneClient = (function($) { try { cb(null, this._validate(sessionData, expectedCallsProperties)); } catch (err) { - console.log("Error requesting call info", err); + console.error("Error requesting call info", err.message); cb(err); } }.bind(this)); diff --git a/browser/components/loop/standalone/content/js/webapp.js b/browser/components/loop/standalone/content/js/webapp.js index a0e717cc2fd..fffb8295e5d 100644 --- a/browser/components/loop/standalone/content/js/webapp.js +++ b/browser/components/loop/standalone/content/js/webapp.js @@ -5,7 +5,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global loop:true, React */ -/* jshint newcap:false */ +/* jshint newcap:false, maxlen:false */ var loop = loop || {}; loop.webapp = (function($, _, OT, mozL10n) { @@ -17,12 +17,6 @@ loop.webapp = (function($, _, OT, mozL10n) { var sharedModels = loop.shared.models, sharedViews = loop.shared.views; - /** - * App router. - * @type {loop.webapp.WebappRouter} - */ - var router; - /** * Homepage view. */ @@ -30,7 +24,7 @@ loop.webapp = (function($, _, OT, mozL10n) { render: function() { return ( React.DOM.p(null, mozL10n.get("welcome")) - ) + ); } }); @@ -104,7 +98,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }, render: function() { - /* jshint ignore:start */ return ( React.DOM.div({className: "expired-url-info"}, React.DOM.div({className: "info-panel"}, @@ -115,7 +108,6 @@ loop.webapp = (function($, _, OT, mozL10n) { PromoteFirefoxView({helper: this.props.helper}) ) ); - /* jshint ignore:end */ } }); @@ -146,7 +138,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }); return ( - /* jshint ignore:start */ React.DOM.header({className: "standalone-header header-box container-box"}, ConversationBranding(null), React.DOM.div({className: "loop-logo", title: "Firefox WebRTC! logo"}), @@ -157,7 +148,6 @@ loop.webapp = (function($, _, OT, mozL10n) { callUrlCreationDateString ) ) - /* jshint ignore:end */ ); } }); @@ -176,7 +166,7 @@ loop.webapp = (function($, _, OT, mozL10n) { getInitialState: function() { return { callState: this.props.callState || "connecting" - } + }; }, propTypes: { @@ -200,7 +190,6 @@ loop.webapp = (function($, _, OT, mozL10n) { render: function() { var callState = mozL10n.get("call_progress_" + this.state.callState + "_description"); return ( - /* jshint ignore:start */ React.DOM.div({className: "container"}, React.DOM.div({className: "container-box"}, React.DOM.header({className: "pending-header header-box"}, @@ -229,7 +218,6 @@ loop.webapp = (function($, _, OT, mozL10n) { ConversationFooter(null) ) - /* jshint ignore:end */ ); } }); @@ -237,18 +225,21 @@ loop.webapp = (function($, _, OT, mozL10n) { /** * Conversation launcher view. A ConversationModel is associated and attached * as a `model` property. + * + * Required properties: + * - {loop.shared.models.ConversationModel} model Conversation model. + * - {loop.shared.models.NotificationCollection} notifications */ var StartConversationView = React.createClass({displayName: 'StartConversationView', - /** - * Constructor. - * - * Required options: - * - {loop.shared.models.ConversationModel} model Conversation model. - * - {loop.shared.models.NotificationCollection} notifications - * - */ + propTypes: { + model: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + // XXX Check more tightly here when we start injecting window.loop.* + notifications: React.PropTypes.object.isRequired, + client: React.PropTypes.object.isRequired + }, - getInitialProps: function() { + getDefaultProps: function() { return {showCallOptionsMenu: false}; }, @@ -260,14 +251,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }; }, - propTypes: { - model: React.PropTypes.instanceOf(sharedModels.ConversationModel) - .isRequired, - // XXX Check more tightly here when we start injecting window.loop.* - notifications: React.PropTypes.object.isRequired, - client: React.PropTypes.object.isRequired - }, - componentDidMount: function() { // Listen for events & hide dropdown menu if user clicks away window.addEventListener("click", this.clickHandler); @@ -348,7 +331,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }); return ( - /* jshint ignore:start */ React.DOM.div({className: "container"}, React.DOM.div({className: "container-box"}, @@ -407,7 +389,37 @@ loop.webapp = (function($, _, OT, mozL10n) { ConversationFooter(null) ) - /* jshint ignore:end */ + ); + } + }); + + /** + * Ended conversation view. + */ + var EndedConversationView = React.createClass({displayName: 'EndedConversationView', + propTypes: { + conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + sdk: React.PropTypes.object.isRequired, + feedbackApiClient: React.PropTypes.object.isRequired, + onAfterFeedbackReceived: React.PropTypes.func.isRequired + }, + + render: function() { + return ( + React.DOM.div({className: "ended-conversation"}, + sharedViews.FeedbackView({ + feedbackApiClient: this.props.feedbackApiClient, + onAfterFeedbackReceived: this.props.onAfterFeedbackReceived} + ), + sharedViews.ConversationView({ + initiate: false, + sdk: this.props.sdk, + model: this.props.conversation, + audio: {enabled: false, visible: false}, + video: {enabled: false, visible: false}} + ) + ) ); } }); @@ -426,7 +438,8 @@ loop.webapp = (function($, _, OT, mozL10n) { helper: React.PropTypes.instanceOf(WebappHelper).isRequired, notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) .isRequired, - sdk: React.PropTypes.object.isRequired + sdk: React.PropTypes.object.isRequired, + feedbackApiClient: React.PropTypes.object.isRequired }, getInitialState: function() { @@ -450,13 +463,23 @@ loop.webapp = (function($, _, OT, mozL10n) { this.props.conversation.off(null, null, this); }, + shouldComponentUpdate: function(nextProps, nextState) { + // Only rerender if current state has actually changed + return nextState.callStatus !== this.state.callStatus; + }, + + callStatusSwitcher: function(status) { + return function() { + this.setState({callStatus: status}); + }.bind(this); + }, + /** * Renders the conversation views. */ render: function() { switch (this.state.callStatus) { case "failure": - case "end": case "start": { return ( StartConversationView({ @@ -472,19 +495,30 @@ loop.webapp = (function($, _, OT, mozL10n) { case "connected": { return ( sharedViews.ConversationView({ + initiate: true, sdk: this.props.sdk, model: this.props.conversation, video: {enabled: this.props.conversation.hasVideoStream("outgoing")}} ) ); } + case "end": { + return ( + EndedConversationView({ + sdk: this.props.sdk, + conversation: this.props.conversation, + feedbackApiClient: this.props.feedbackApiClient, + onAfterFeedbackReceived: this.callStatusSwitcher("start")} + ) + ); + } case "expired": { return ( CallUrlExpiredView({helper: this.props.helper}) ); } default: { - return HomeView(null) + return HomeView(null); } } }, @@ -494,7 +528,7 @@ loop.webapp = (function($, _, OT, mozL10n) { * @param {{code: number, message: string}} error */ _notifyError: function(error) { - console.log(error); + console.error(error); this.props.notifications.errorL10n("connection_error_see_console_notification"); this.setState({callStatus: "end"}); }, @@ -628,13 +662,15 @@ loop.webapp = (function($, _, OT, mozL10n) { * @param {String} reason The reason the call was terminated. */ _handleCallTerminated: function(reason) { - this.setState({callStatus: "end"}); - // For reasons other than cancel, display some notification text. if (reason !== "cancel") { // XXX This should really display the call failed view - bug 1046959 // will implement this. this.props.notifications.errorL10n("call_timeout_notification_text"); } + // redirects the user to the call start view + // XXX should switch callStatus to failed for specific reasons when we + // get the call failed view; for now, switch back to start. + this.setState({callStatus: "start"}); }, /** @@ -657,7 +693,8 @@ loop.webapp = (function($, _, OT, mozL10n) { helper: React.PropTypes.instanceOf(WebappHelper).isRequired, notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) .isRequired, - sdk: React.PropTypes.object.isRequired + sdk: React.PropTypes.object.isRequired, + feedbackApiClient: React.PropTypes.object.isRequired }, getInitialState: function() { @@ -679,7 +716,8 @@ loop.webapp = (function($, _, OT, mozL10n) { conversation: this.props.conversation, helper: this.props.helper, notifications: this.props.notifications, - sdk: this.props.sdk} + sdk: this.props.sdk, + feedbackApiClient: this.props.feedbackApiClient} ) ); } else { @@ -721,6 +759,12 @@ loop.webapp = (function($, _, OT, mozL10n) { var conversation = new sharedModels.ConversationModel({}, { sdk: OT }); + var feedbackApiClient = new loop.FeedbackAPIClient( + loop.config.feedbackApiUrl, { + product: loop.config.feedbackProductName, + user_agent: navigator.userAgent, + url: document.location.origin + }); // Obtain the loopToken and pass it to the conversation var locationHash = helper.locationHash(); @@ -733,7 +777,8 @@ loop.webapp = (function($, _, OT, mozL10n) { conversation: conversation, helper: helper, notifications: notifications, - sdk: OT} + sdk: OT, + feedbackApiClient: feedbackApiClient} ), document.querySelector("#main")); // Set the 'lang' and 'dir' attributes to when the page is translated @@ -746,6 +791,7 @@ loop.webapp = (function($, _, OT, mozL10n) { PendingConversationView: PendingConversationView, StartConversationView: StartConversationView, OutgoingConversationView: OutgoingConversationView, + EndedConversationView: EndedConversationView, HomeView: HomeView, UnsupportedBrowserView: UnsupportedBrowserView, UnsupportedDeviceView: UnsupportedDeviceView, diff --git a/browser/components/loop/standalone/content/js/webapp.jsx b/browser/components/loop/standalone/content/js/webapp.jsx index dda3ba1173c..0630e824745 100644 --- a/browser/components/loop/standalone/content/js/webapp.jsx +++ b/browser/components/loop/standalone/content/js/webapp.jsx @@ -5,7 +5,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global loop:true, React */ -/* jshint newcap:false */ +/* jshint newcap:false, maxlen:false */ var loop = loop || {}; loop.webapp = (function($, _, OT, mozL10n) { @@ -17,12 +17,6 @@ loop.webapp = (function($, _, OT, mozL10n) { var sharedModels = loop.shared.models, sharedViews = loop.shared.views; - /** - * App router. - * @type {loop.webapp.WebappRouter} - */ - var router; - /** * Homepage view. */ @@ -30,7 +24,7 @@ loop.webapp = (function($, _, OT, mozL10n) { render: function() { return (

{mozL10n.get("welcome")}

- ) + ); } }); @@ -104,7 +98,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }, render: function() { - /* jshint ignore:start */ return (
@@ -115,7 +108,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
); - /* jshint ignore:end */ } }); @@ -146,7 +138,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }); return ( - /* jshint ignore:start */
@@ -157,7 +148,6 @@ loop.webapp = (function($, _, OT, mozL10n) { {callUrlCreationDateString}
- /* jshint ignore:end */ ); } }); @@ -176,7 +166,7 @@ loop.webapp = (function($, _, OT, mozL10n) { getInitialState: function() { return { callState: this.props.callState || "connecting" - } + }; }, propTypes: { @@ -200,7 +190,6 @@ loop.webapp = (function($, _, OT, mozL10n) { render: function() { var callState = mozL10n.get("call_progress_" + this.state.callState + "_description"); return ( - /* jshint ignore:start */
@@ -229,7 +218,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
- /* jshint ignore:end */ ); } }); @@ -237,18 +225,21 @@ loop.webapp = (function($, _, OT, mozL10n) { /** * Conversation launcher view. A ConversationModel is associated and attached * as a `model` property. + * + * Required properties: + * - {loop.shared.models.ConversationModel} model Conversation model. + * - {loop.shared.models.NotificationCollection} notifications */ var StartConversationView = React.createClass({ - /** - * Constructor. - * - * Required options: - * - {loop.shared.models.ConversationModel} model Conversation model. - * - {loop.shared.models.NotificationCollection} notifications - * - */ + propTypes: { + model: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + // XXX Check more tightly here when we start injecting window.loop.* + notifications: React.PropTypes.object.isRequired, + client: React.PropTypes.object.isRequired + }, - getInitialProps: function() { + getDefaultProps: function() { return {showCallOptionsMenu: false}; }, @@ -260,14 +251,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }; }, - propTypes: { - model: React.PropTypes.instanceOf(sharedModels.ConversationModel) - .isRequired, - // XXX Check more tightly here when we start injecting window.loop.* - notifications: React.PropTypes.object.isRequired, - client: React.PropTypes.object.isRequired - }, - componentDidMount: function() { // Listen for events & hide dropdown menu if user clicks away window.addEventListener("click", this.clickHandler); @@ -348,7 +331,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }); return ( - /* jshint ignore:start */
@@ -407,7 +389,37 @@ loop.webapp = (function($, _, OT, mozL10n) {
- /* jshint ignore:end */ + ); + } + }); + + /** + * Ended conversation view. + */ + var EndedConversationView = React.createClass({ + propTypes: { + conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + sdk: React.PropTypes.object.isRequired, + feedbackApiClient: React.PropTypes.object.isRequired, + onAfterFeedbackReceived: React.PropTypes.func.isRequired + }, + + render: function() { + return ( +
+ + +
); } }); @@ -426,7 +438,8 @@ loop.webapp = (function($, _, OT, mozL10n) { helper: React.PropTypes.instanceOf(WebappHelper).isRequired, notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) .isRequired, - sdk: React.PropTypes.object.isRequired + sdk: React.PropTypes.object.isRequired, + feedbackApiClient: React.PropTypes.object.isRequired }, getInitialState: function() { @@ -450,13 +463,23 @@ loop.webapp = (function($, _, OT, mozL10n) { this.props.conversation.off(null, null, this); }, + shouldComponentUpdate: function(nextProps, nextState) { + // Only rerender if current state has actually changed + return nextState.callStatus !== this.state.callStatus; + }, + + callStatusSwitcher: function(status) { + return function() { + this.setState({callStatus: status}); + }.bind(this); + }, + /** * Renders the conversation views. */ render: function() { switch (this.state.callStatus) { case "failure": - case "end": case "start": { return ( ); } + case "end": { + return ( + + ); + } case "expired": { return ( ); } default: { - return + return ; } } }, @@ -494,7 +528,7 @@ loop.webapp = (function($, _, OT, mozL10n) { * @param {{code: number, message: string}} error */ _notifyError: function(error) { - console.log(error); + console.error(error); this.props.notifications.errorL10n("connection_error_see_console_notification"); this.setState({callStatus: "end"}); }, @@ -628,13 +662,15 @@ loop.webapp = (function($, _, OT, mozL10n) { * @param {String} reason The reason the call was terminated. */ _handleCallTerminated: function(reason) { - this.setState({callStatus: "end"}); - // For reasons other than cancel, display some notification text. if (reason !== "cancel") { // XXX This should really display the call failed view - bug 1046959 // will implement this. this.props.notifications.errorL10n("call_timeout_notification_text"); } + // redirects the user to the call start view + // XXX should switch callStatus to failed for specific reasons when we + // get the call failed view; for now, switch back to start. + this.setState({callStatus: "start"}); }, /** @@ -657,7 +693,8 @@ loop.webapp = (function($, _, OT, mozL10n) { helper: React.PropTypes.instanceOf(WebappHelper).isRequired, notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) .isRequired, - sdk: React.PropTypes.object.isRequired + sdk: React.PropTypes.object.isRequired, + feedbackApiClient: React.PropTypes.object.isRequired }, getInitialState: function() { @@ -680,6 +717,7 @@ loop.webapp = (function($, _, OT, mozL10n) { helper={this.props.helper} notifications={this.props.notifications} sdk={this.props.sdk} + feedbackApiClient={this.props.feedbackApiClient} /> ); } else { @@ -721,6 +759,12 @@ loop.webapp = (function($, _, OT, mozL10n) { var conversation = new sharedModels.ConversationModel({}, { sdk: OT }); + var feedbackApiClient = new loop.FeedbackAPIClient( + loop.config.feedbackApiUrl, { + product: loop.config.feedbackProductName, + user_agent: navigator.userAgent, + url: document.location.origin + }); // Obtain the loopToken and pass it to the conversation var locationHash = helper.locationHash(); @@ -734,6 +778,7 @@ loop.webapp = (function($, _, OT, mozL10n) { helper={helper} notifications={notifications} sdk={OT} + feedbackApiClient={feedbackApiClient} />, document.querySelector("#main")); // Set the 'lang' and 'dir' attributes to when the page is translated @@ -746,6 +791,7 @@ loop.webapp = (function($, _, OT, mozL10n) { PendingConversationView: PendingConversationView, StartConversationView: StartConversationView, OutgoingConversationView: OutgoingConversationView, + EndedConversationView: EndedConversationView, HomeView: HomeView, UnsupportedBrowserView: UnsupportedBrowserView, UnsupportedDeviceView: UnsupportedDeviceView, diff --git a/browser/components/loop/standalone/content/l10n/loop.en-US.properties b/browser/components/loop/standalone/content/l10n/loop.en-US.properties index 12ccf926f67..64c326c3268 100644 --- a/browser/components/loop/standalone/content/l10n/loop.en-US.properties +++ b/browser/components/loop/standalone/content/l10n/loop.en-US.properties @@ -46,3 +46,32 @@ clientShortname=WebRTC! call_url_creation_date_label=(from {{call_url_creation_date}}) call_progress_connecting_description=Connecting… call_progress_ringing_description=Ringing… + +feedback_call_experience_heading2=How was your conversation? +feedback_what_makes_you_sad=What makes you sad? +feedback_thank_you_heading=Thank you for your feedback! +feedback_category_audio_quality=Audio quality +feedback_category_video_quality=Video quality +feedback_category_was_disconnected=Was disconnected +feedback_category_confusing=Confusing +feedback_category_other=Other: +feedback_custom_category_text_placeholder=What went wrong? +feedback_submit_button=Submit +feedback_back_button=Back +## LOCALIZATION NOTE (feedback_window_will_close_in2): +## Gaia l10n format; see https://github.com/mozilla-b2g/gaia/blob/f108c706fae43cd61628babdd9463e7695b2496e/apps/email/locales/email.en-US.properties#L387 +## In this item, don't translate the part between {{..}} +feedback_window_will_close_in2={[ plural(countdown) ]} +feedback_window_will_close_in2[one] = This window will close in {{countdown}} second +feedback_window_will_close_in2[two] = This window will close in {{countdown}} seconds +feedback_window_will_close_in2[few] = This window will close in {{countdown}} seconds +feedback_window_will_close_in2[many] = This window will close in {{countdown}} seconds +feedback_window_will_close_in2[other] = This window will close in {{countdown}} seconds + +## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after +## a signed-in to signed-in user call. +## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback +feedback_rejoin_button=Rejoin +## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of +## an abusive user. +feedback_report_user_button=Report User diff --git a/browser/components/loop/standalone/server.js b/browser/components/loop/standalone/server.js index ba8fdfb93dc..d3bf4c07291 100644 --- a/browser/components/loop/standalone/server.js +++ b/browser/components/loop/standalone/server.js @@ -7,17 +7,21 @@ var app = express(); var port = process.env.PORT || 3000; var loopServerPort = process.env.LOOP_SERVER_PORT || 5000; +var feedbackApiUrl = process.env.LOOP_FEEDBACK_API_URL || + "https://input.allizom.org/api/v1/feedback"; +var feedbackProductName = process.env.LOOP_FEEDBACK_PRODUCT_NAME || "Loop"; function getConfigFile(req, res) { "use strict"; res.set('Content-Type', 'text/javascript'); - res.send( - "var loop = loop || {};" + - "loop.config = loop.config || {};" + - "loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';" + - "loop.config.pendingCallTimeout = 20000;" - ); + res.send([ + "var loop = loop || {};", + "loop.config = loop.config || {};", + "loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';", + "loop.config.feedbackApiUrl = '" + feedbackApiUrl + "';", + "loop.config.feedbackProductName = '" + feedbackProductName + "';", + ].join("\n")); } app.get('/content/config.js', getConfigFile); diff --git a/browser/components/loop/test/desktop-local/conversation_test.js b/browser/components/loop/test/desktop-local/conversation_test.js index ba89cd09479..c4ba9e7f899 100644 --- a/browser/components/loop/test/desktop-local/conversation_test.js +++ b/browser/components/loop/test/desktop-local/conversation_test.js @@ -108,8 +108,7 @@ describe("loop.conversation", function() { beforeEach(function() { client = new loop.Client(); conversation = new loop.shared.models.ConversationModel({}, { - sdk: {}, - pendingCallTimeout: 1000, + sdk: {} }); sandbox.spy(conversation, "setIncomingSessionData"); sandbox.stub(conversation, "setOutgoingSessionData"); diff --git a/browser/components/loop/test/shared/feedbackApiClient_test.js b/browser/components/loop/test/shared/feedbackApiClient_test.js index 1b6f1a0da3b..fbcb705a13e 100644 --- a/browser/components/loop/test/shared/feedbackApiClient_test.js +++ b/browser/components/loop/test/shared/feedbackApiClient_test.js @@ -138,6 +138,13 @@ describe("loop.FeedbackAPIClient", function() { expect(parsed.user_agent).eql("MOZAGENT"); }); + it("should send url information when provided", function() { + client.send({url: "http://fake.invalid"}, function(){}); + + var parsed = JSON.parse(requests[0].requestBody); + expect(parsed.url).eql("http://fake.invalid"); + }); + it("should throw on invalid feedback data", function() { expect(function() { client.send("invalid data", function(){}); diff --git a/browser/components/loop/test/shared/router_test.js b/browser/components/loop/test/shared/router_test.js index f9223fc6813..c825c19ab84 100644 --- a/browser/components/loop/test/shared/router_test.js +++ b/browser/components/loop/test/shared/router_test.js @@ -60,8 +60,7 @@ describe("loop.shared.router", function() { conversation = new loop.shared.models.ConversationModel({ loopToken: "fakeToken" }, { - sdk: {}, - pendingCallTimeout: 1000 + sdk: {} }); }); diff --git a/browser/components/loop/test/shared/views_test.js b/browser/components/loop/test/shared/views_test.js index 910dee34815..cdcf6a8dea5 100644 --- a/browser/components/loop/test/shared/views_test.js +++ b/browser/components/loop/test/shared/views_test.js @@ -187,13 +187,12 @@ describe("loop.shared.views", function() { initSession: sandbox.stub().returns(fakeSession) }; model = new sharedModels.ConversationModel(fakeSessionData, { - sdk: fakeSDK, - pendingCallTimeout: 1000 + sdk: fakeSDK }); }); describe("#componentDidMount", function() { - it("should start a session", function() { + it("should start a session by default", function() { sandbox.stub(model, "startSession"); mountTestComponent({ @@ -205,6 +204,19 @@ describe("loop.shared.views", function() { sinon.assert.calledOnce(model.startSession); }); + it("shouldn't start a session if initiate is false", function() { + sandbox.stub(model, "startSession"); + + mountTestComponent({ + initiate: false, + sdk: fakeSDK, + model: model, + video: {enabled: true} + }); + + sinon.assert.notCalled(model.startSession); + }); + it("should set the correct stream publish options", function() { var component = mountTestComponent({ diff --git a/browser/components/loop/test/standalone/index.html b/browser/components/loop/test/standalone/index.html index ef496f0c88b..cece91948a6 100644 --- a/browser/components/loop/test/standalone/index.html +++ b/browser/components/loop/test/standalone/index.html @@ -36,6 +36,7 @@ + diff --git a/browser/components/loop/test/standalone/webapp_test.js b/browser/components/loop/test/standalone/webapp_test.js index f090639cefd..52bc57e7a26 100644 --- a/browser/components/loop/test/standalone/webapp_test.js +++ b/browser/components/loop/test/standalone/webapp_test.js @@ -13,11 +13,15 @@ describe("loop.webapp", function() { var sharedModels = loop.shared.models, sharedViews = loop.shared.views, sandbox, - notifications; + notifications, + feedbackApiClient; beforeEach(function() { sandbox = sinon.sandbox.create(); notifications = new sharedModels.NotificationCollection(); + feedbackApiClient = new loop.FeedbackAPIClient("http://invalid", { + product: "Loop" + }); }); afterEach(function() { @@ -31,6 +35,7 @@ describe("loop.webapp", function() { sandbox.stub(React, "renderComponent"); sandbox.stub(loop.webapp.WebappHelper.prototype, "locationHash").returns("#call/fake-Token"); + loop.config.feedbackApiUrl = "http://fake.invalid"; conversationSetStub = sandbox.stub(sharedModels.ConversationModel.prototype, "set"); }); @@ -77,7 +82,8 @@ describe("loop.webapp", function() { client: client, conversation: conversation, notifications: notifications, - sdk: {} + sdk: {}, + feedbackApiClient: feedbackApiClient }); }); @@ -305,7 +311,7 @@ describe("loop.webapp", function() { conversation.trigger("session:ended"); TestUtils.findRenderedComponentWithType(ocView, - loop.webapp.StartConversationView); + loop.webapp.EndedConversationView); }); }); @@ -314,7 +320,7 @@ describe("loop.webapp", function() { conversation.trigger("session:peer-hungup"); TestUtils.findRenderedComponentWithType(ocView, - loop.webapp.StartConversationView); + loop.webapp.EndedConversationView); }); it("should notify the user", function() { @@ -333,7 +339,7 @@ describe("loop.webapp", function() { conversation.trigger("session:network-disconnected"); TestUtils.findRenderedComponentWithType(ocView, - loop.webapp.StartConversationView); + loop.webapp.EndedConversationView); }); it("should notify the user", function() { @@ -474,8 +480,10 @@ describe("loop.webapp", function() { loop.webapp.WebappRootView({ client: client, helper: webappHelper, + notifications: notifications, sdk: sdk, - conversation: conversationModel + conversation: conversationModel, + feedbackApiClient: feedbackApiClient })); } @@ -772,6 +780,32 @@ describe("loop.webapp", function() { }); }); + describe("EndedConversationView", function() { + var view, conversation; + + beforeEach(function() { + conversation = new sharedModels.ConversationModel({}, { + sdk: {} + }); + view = React.addons.TestUtils.renderIntoDocument( + loop.webapp.EndedConversationView({ + conversation: conversation, + sdk: {}, + feedbackApiClient: feedbackApiClient, + onAfterFeedbackReceived: function(){} + }) + ); + }); + + it("should render a ConversationView", function() { + TestUtils.findRenderedComponentWithType(view, sharedViews.ConversationView); + }); + + it("should render a FeedbackView", function() { + TestUtils.findRenderedComponentWithType(view, sharedViews.FeedbackView); + }); + }); + describe("PromoteFirefoxView", function() { describe("#render", function() { it("should not render when using Firefox", function() { diff --git a/browser/components/loop/ui/fake-l10n.js b/browser/components/loop/ui/fake-l10n.js index 77b8297382e..daf2a9dd494 100644 --- a/browser/components/loop/ui/fake-l10n.js +++ b/browser/components/loop/ui/fake-l10n.js @@ -9,6 +9,10 @@ * @type {Object} */ navigator.mozL10n = document.mozL10n = { + initialize: function(){}, + + getDirection: function(){}, + get: function(stringId, vars) { // upcase the first letter diff --git a/browser/components/loop/ui/fake-mozLoop.js b/browser/components/loop/ui/fake-mozLoop.js index d8b5b77cf92..411590015f3 100644 --- a/browser/components/loop/ui/fake-mozLoop.js +++ b/browser/components/loop/ui/fake-mozLoop.js @@ -7,6 +7,7 @@ * @type {Object} */ navigator.mozLoop = { + ensureRegistered: function() {}, getLoopCharPref: function() {}, getLoopBoolPref: function() {} }; diff --git a/browser/components/loop/ui/ui-showcase.css b/browser/components/loop/ui/ui-showcase.css index eea87cc2cc0..7a6ed557cd8 100644 --- a/browser/components/loop/ui/ui-showcase.css +++ b/browser/components/loop/ui/ui-showcase.css @@ -37,7 +37,7 @@ .showcase > section { position: relative; - padding-top: 12em; + padding-top: 14em; clear: both; } @@ -149,3 +149,9 @@ * When tokbox inserts the markup into the page the problem goes away */ bottom: auto; } + +.standalone .ended-conversation .remote_wrapper, +.standalone .video-layout-wrapper { + /* Removes the fake video image for ended conversations */ + background: none; +} diff --git a/browser/components/loop/ui/ui-showcase.js b/browser/components/loop/ui/ui-showcase.js index ece54dffc0e..047f7cc19b9 100644 --- a/browser/components/loop/ui/ui-showcase.js +++ b/browser/components/loop/ui/ui-showcase.js @@ -23,6 +23,7 @@ var CallUrlExpiredView = loop.webapp.CallUrlExpiredView; var PendingConversationView = loop.webapp.PendingConversationView; var StartConversationView = loop.webapp.StartConversationView; + var EndedConversationView = loop.webapp.EndedConversationView; // 3. Shared components var ConversationToolbar = loop.shared.views.ConversationToolbar; @@ -338,6 +339,19 @@ ) ), + Section({name: "EndedConversationView"}, + Example({summary: "Displays the feedback form"}, + React.DOM.div({className: "standalone"}, + EndedConversationView({sdk: mockSDK, + video: {enabled: true}, + audio: {enabled: true}, + conversation: mockConversationModel, + feedbackApiClient: stageFeedbackApiClient, + onAfterFeedbackReceived: noop}) + ) + ) + ), + Section({name: "AlertMessages"}, Example({summary: "Various alerts"}, React.DOM.div({className: "alert alert-warning"}, diff --git a/browser/components/loop/ui/ui-showcase.jsx b/browser/components/loop/ui/ui-showcase.jsx index 715f45feb75..eb7cfb8e34e 100644 --- a/browser/components/loop/ui/ui-showcase.jsx +++ b/browser/components/loop/ui/ui-showcase.jsx @@ -23,6 +23,7 @@ var CallUrlExpiredView = loop.webapp.CallUrlExpiredView; var PendingConversationView = loop.webapp.PendingConversationView; var StartConversationView = loop.webapp.StartConversationView; + var EndedConversationView = loop.webapp.EndedConversationView; // 3. Shared components var ConversationToolbar = loop.shared.views.ConversationToolbar; @@ -338,6 +339,19 @@ +
+ +
+ +
+
+
+
From 0d75c798f17993f67a81f1ad185119f1d4e8878e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 17 Sep 2014 09:49:00 -0400 Subject: [PATCH 40/56] Bug 1045660 - Add type attribute to webide runtimes. r=paul --- browser/devtools/webide/modules/runtimes.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/browser/devtools/webide/modules/runtimes.js b/browser/devtools/webide/modules/runtimes.js index b17dbe49fd0..3957e5c0075 100644 --- a/browser/devtools/webide/modules/runtimes.js +++ b/browser/devtools/webide/modules/runtimes.js @@ -13,11 +13,21 @@ const promise = require("promise"); const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties"); +// These type strings are used for logging events to Telemetry +let RuntimeTypes = { + usb: "USB", + wifi: "WIFI", + simulator: "SIMULATOR", + remote: "REMOTE", + local: "LOCAL" +}; + function USBRuntime(id) { this.id = id; } USBRuntime.prototype = { + type: RuntimeTypes.usb, connect: function(connection) { let device = Devices.getByName(this.id); if (!device) { @@ -59,6 +69,7 @@ function WiFiRuntime(deviceName) { } WiFiRuntime.prototype = { + type: RuntimeTypes.wifi, connect: function(connection) { let service = discovery.getRemoteService("devtools", this.deviceName); if (!service) { @@ -82,6 +93,7 @@ function SimulatorRuntime(version) { } SimulatorRuntime.prototype = { + type: RuntimeTypes.simulator, connect: function(connection) { let port = ConnectionManager.getFreeTCPPort(); let simulator = Simulator.getByVersion(this.version); @@ -105,6 +117,7 @@ SimulatorRuntime.prototype = { } let gLocalRuntime = { + type: RuntimeTypes.local, connect: function(connection) { if (!DebuggerServer.initialized) { DebuggerServer.init(); @@ -121,6 +134,7 @@ let gLocalRuntime = { } let gRemoteRuntime = { + type: RuntimeTypes.remote, connect: function(connection) { let win = Services.wm.getMostRecentWindow("devtools:webide"); if (!win) { From f05c48731dc50c6667c430394cf20fe994de5dd2 Mon Sep 17 00:00:00 2001 From: Alexandre Poirot Date: Thu, 18 Sep 2014 07:27:00 -0400 Subject: [PATCH 41/56] Bug 1045660 - Remember last selected runtime in WebIDE. r=jryans --- browser/devtools/webide/content/webide.js | 43 +++++++++ .../devtools/webide/modules/app-manager.js | 10 +- browser/devtools/webide/modules/runtimes.js | 3 + browser/devtools/webide/test/chrome.ini | 1 + .../webide/test/test_autoconnect_runtime.html | 91 +++++++++++++++++++ browser/devtools/webide/webide-prefs.js | 1 + 6 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 browser/devtools/webide/test/test_autoconnect_runtime.html diff --git a/browser/devtools/webide/content/webide.js b/browser/devtools/webide/content/webide.js index 0b59b64894f..174677dd6aa 100644 --- a/browser/devtools/webide/content/webide.js +++ b/browser/devtools/webide/content/webide.js @@ -75,6 +75,8 @@ let UI = { } Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false); + this.lastConnectedRuntime = Services.prefs.getCharPref("devtools.webide.lastConnectedRuntime"); + this.setupDeck(); }, @@ -125,6 +127,7 @@ let UI = { switch (what) { case "runtimelist": this.updateRuntimeList(); + this.autoConnectRuntime(); break; case "connection": this.updateRuntimeButton(); @@ -145,6 +148,7 @@ let UI = { break; case "runtime": this.updateRuntimeButton(); + this.saveLastConnectedRuntime(); break; case "project-validated": this.updateTitle(); @@ -343,6 +347,34 @@ let UI = { } }, + autoConnectRuntime: function () { + // Automatically reconnect to the previously selected runtime, + // if available and has an ID + if (AppManager.selectedRuntime || !this.lastConnectedRuntime) { + return; + } + let [_, type, id] = this.lastConnectedRuntime.match(/^(\w+):(.+)$/); + + type = type.toLowerCase(); + + // Local connection is mapped to AppManager.runtimeList.custom array + if (type == "local") { + type = "custom"; + } + + // We support most runtimes except simulator, that needs to be manually + // launched + if (type == "usb" || type == "wifi" || type == "custom") { + for (let runtime of AppManager.runtimeList[type]) { + // Some runtimes do not expose getID function and don't support + // autoconnect (like remote connection) + if (typeof(runtime.getID) == "function" && runtime.getID() == id) { + this.connectToRuntime(runtime); + } + } + } + }, + connectToRuntime: function(runtime) { let name = runtime.getName(); let promise = AppManager.connectToRuntime(runtime); @@ -359,6 +391,17 @@ let UI = { } }, + saveLastConnectedRuntime: function () { + if (AppManager.selectedRuntime && + typeof(AppManager.selectedRuntime.getID) === "function") { + this.lastConnectedRuntime = AppManager.selectedRuntime.type + ":" + AppManager.selectedRuntime.getID(); + } else { + this.lastConnectedRuntime = ""; + } + Services.prefs.setCharPref("devtools.webide.lastConnectedRuntime", + this.lastConnectedRuntime); + }, + /********** PROJECTS **********/ // Panel & button diff --git a/browser/devtools/webide/modules/app-manager.js b/browser/devtools/webide/modules/app-manager.js index dd4792e9cf3..73a504eb0df 100644 --- a/browser/devtools/webide/modules/app-manager.js +++ b/browser/devtools/webide/modules/app-manager.js @@ -651,7 +651,15 @@ exports.AppManager = AppManager = { let r = new USBRuntime(id); this.runtimeList.usb.push(r); r.updateNameFromADB().then( - () => this.update("runtimelist"), () => {}); + () => { + this.update("runtimelist"); + // Also update the runtime button label, if the currently selected + // runtime name changes + if (r == this.selectedRuntime) { + this.update("runtime"); + } + }, + () => {}); } this.update("runtimelist"); }, diff --git a/browser/devtools/webide/modules/runtimes.js b/browser/devtools/webide/modules/runtimes.js index 3957e5c0075..e9c667457ea 100644 --- a/browser/devtools/webide/modules/runtimes.js +++ b/browser/devtools/webide/modules/runtimes.js @@ -131,6 +131,9 @@ let gLocalRuntime = { getName: function() { return Strings.GetStringFromName("local_runtime"); }, + getID: function () { + return "local"; + } } let gRemoteRuntime = { diff --git a/browser/devtools/webide/test/chrome.ini b/browser/devtools/webide/test/chrome.ini index 9af2900f9c1..65cc7281f98 100644 --- a/browser/devtools/webide/test/chrome.ini +++ b/browser/devtools/webide/test/chrome.ini @@ -31,3 +31,4 @@ support-files = [test_manifestUpdate.html] [test_addons.html] [test_deviceinfo.html] +[test_autoconnect_runtime.html] diff --git a/browser/devtools/webide/test/test_autoconnect_runtime.html b/browser/devtools/webide/test/test_autoconnect_runtime.html new file mode 100644 index 00000000000..6087a98af97 --- /dev/null +++ b/browser/devtools/webide/test/test_autoconnect_runtime.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + diff --git a/browser/devtools/webide/webide-prefs.js b/browser/devtools/webide/webide-prefs.js index 3c828ee27ae..6f6807e4f57 100644 --- a/browser/devtools/webide/webide-prefs.js +++ b/browser/devtools/webide/webide-prefs.js @@ -15,3 +15,4 @@ pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozil pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi"); pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org"); pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000"); +pref("devtools.webide.lastConnectedRuntime", ""); From 5e40fd4a34f27b45080b7d005dcdf01cc1cd533c Mon Sep 17 00:00:00 2001 From: Alexandre Poirot Date: Thu, 18 Sep 2014 05:44:00 -0400 Subject: [PATCH 42/56] Bug 1067424 - Convert webapps actor to SDK module. r=jryans --- toolkit/devtools/server/actors/webapps.js | 26 +++++++++++++++-------- toolkit/devtools/server/main.js | 6 +++++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/toolkit/devtools/server/actors/webapps.js b/toolkit/devtools/server/actors/webapps.js index 044edf40727..cde6be96a4c 100644 --- a/toolkit/devtools/server/actors/webapps.js +++ b/toolkit/devtools/server/actors/webapps.js @@ -4,10 +4,7 @@ "use strict"; -let Cu = Components.utils; -let Cc = Components.classes; -let Ci = Components.interfaces; -let CC = Components.Constructor; +let {Cu, Cc, Ci} = require("chrome"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -16,6 +13,17 @@ Cu.import("resource://gre/modules/FileUtils.jsm"); let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +let DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); +let { ActorPool } = require("devtools/server/actors/common"); +let { DebuggerServer } = require("devtools/server/main"); +let Services = require("Services"); + +let AppFramesMock = null; + +exports.setAppFramesMock = function (mock) { + AppFramesMock = mock; +} + DevToolsUtils.defineLazyGetter(this, "AppFrames", () => { try { return Cu.import("resource://gre/modules/AppFrames.jsm", {}).AppFrames; @@ -518,11 +526,11 @@ WebappsActor.prototype = { // frame script. That will flush the jar cache for this app and allow // loading fresh updated resources if we reload its document. let FlushFrameScript = function (path) { - let jar = Components.classes["@mozilla.org/file/local;1"] - .createInstance(Components.interfaces.nsILocalFile); + let jar = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); jar.initWithPath(path); - let obs = Components.classes["@mozilla.org/observer-service;1"] - .getService(Components.interfaces.nsIObserverService); + let obs = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); obs.notifyObservers(jar, "flush-cache-entry", null); }; for each (let frame in self._appFrames()) { @@ -1038,4 +1046,4 @@ WebappsActor.prototype.requestTypes = { "getIconAsDataURL": WebappsActor.prototype.getIconAsDataURL }; -DebuggerServer.addGlobalActor(WebappsActor, "webappsActor"); +exports.WebappsActor = WebappsActor; diff --git a/toolkit/devtools/server/main.js b/toolkit/devtools/server/main.js index 9ab852f455f..a9417175c93 100644 --- a/toolkit/devtools/server/main.js +++ b/toolkit/devtools/server/main.js @@ -442,7 +442,11 @@ var DebuggerServer = { }); } - this.addActors("resource://gre/modules/devtools/server/actors/webapps.js"); + this.registerModule("devtools/server/actors/webapps", { + prefix: "webapps", + constructor: "WebappsActor", + type: { global: true } + }); this.registerModule("devtools/server/actors/device", { prefix: "device", constructor: "DeviceActor", From a1fc7aa0118b0e7003ffb0b416358cbd6ea2a8c5 Mon Sep 17 00:00:00 2001 From: Alexandre Poirot Date: Thu, 18 Sep 2014 05:44:00 -0400 Subject: [PATCH 43/56] Bug 1067424 - Test for webapps actor getAppActor method. r=jryans --- .../apps/tests/debugger-protocol-helper.js | 40 +++++++++++++++++++ .../apps/tests/test_webapps_actor.html | 31 ++++++++++++++ toolkit/devtools/server/actors/webapps.js | 4 ++ toolkit/devtools/server/child.js | 6 +++ 4 files changed, 81 insertions(+) diff --git a/toolkit/devtools/apps/tests/debugger-protocol-helper.js b/toolkit/devtools/apps/tests/debugger-protocol-helper.js index 0434ee86b0e..b215932cab0 100644 --- a/toolkit/devtools/apps/tests/debugger-protocol-helper.js +++ b/toolkit/devtools/apps/tests/debugger-protocol-helper.js @@ -7,6 +7,9 @@ const Cu = Components.utils; const { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm"); const { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm"); +const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +const {require} = devtools; + const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm"); const { Services } = Cu.import("resource://gre/modules/Services.jsm"); @@ -114,9 +117,46 @@ addMessageListener("install", function (aMessage) { } }); +addMessageListener("getAppActor", function (aMessage) { + let { manifestURL } = aMessage; + let request = {type: "getAppActor", manifestURL: manifestURL}; + webappActorRequest(request, function (aResponse) { + sendAsyncMessage("appActor", aResponse); + }); +}); + +let Frames = []; +addMessageListener("addFrame", function (aMessage) { + let win = Services.wm.getMostRecentWindow("navigator:browser"); + let doc = win.document; + let frame = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe"); + frame.setAttribute("mozbrowser", "true"); + if (aMessage.mozapp) { + frame.setAttribute("mozapp", aMessage.mozapp); + } + if (aMessage.remote) { + frame.setAttribute("remote", aMessage.remote); + } + if (aMessage.src) { + frame.setAttribute("src", aMessage.src); + } + doc.documentElement.appendChild(frame); + Frames.push(frame); + sendAsyncMessage("frameAdded"); +}); + addMessageListener("cleanup", function () { webappActorRequest({type: "unwatchApps"}, function () { gClient.close(); }); }); +let AppFramesMock = { + list: function () { + return Frames; + }, + addObserver: function () {}, + removeObserver: function () {} +}; + +require("devtools/server/actors/webapps").setAppFramesMock(AppFramesMock); diff --git a/toolkit/devtools/apps/tests/test_webapps_actor.html b/toolkit/devtools/apps/tests/test_webapps_actor.html index 063217da72c..84424bd5eb5 100644 --- a/toolkit/devtools/apps/tests/test_webapps_actor.html +++ b/toolkit/devtools/apps/tests/test_webapps_actor.html @@ -224,6 +224,21 @@ var steps = [ info("== SETUP == Disable certified app access"); SpecialPowers.popPrefEnv(next); }, + function() { + info("== TEST == Get packaged app actor"); + addFrame( + { mozapp: PACKAGED_APP_MANIFEST, remote: true }, + function () { + getAppActor(PACKAGED_APP_MANIFEST, function (response) { + let tabActor = response.actor; + ok(!!tabActor, "TabActor is correctly instanciated in child.js"); + ok("actor" in tabActor, "Tab actor is available in child"); + ok("consoleActor" in tabActor, "Console actor is available in child"); + next(); + }); + }); + + }, function() { info("== TEST == Uninstall packaged app"); uninstall(PACKAGED_APP_MANIFEST); @@ -309,6 +324,22 @@ function uninstall(manifestURL) { }); } +function getAppActor(manifestURL, callback) { + mm.addMessageListener("appActor", function onAppActor(aResponse) { + mm.removeMessageListener("appActor", onAppActor); + callback(aResponse); + }); + mm.sendAsyncMessage("getAppActor", { manifestURL: manifestURL }); +} + +function addFrame(options, callback) { + mm.addMessageListener("frameAdded", function onFrameAdded() { + mm.removeMessageListener("frameAdded", onFrameAdded); + callback(); + }); + mm.sendAsyncMessage("addFrame", options); +} + diff --git a/toolkit/devtools/server/actors/webapps.js b/toolkit/devtools/server/actors/webapps.js index cde6be96a4c..def41829a97 100644 --- a/toolkit/devtools/server/actors/webapps.js +++ b/toolkit/devtools/server/actors/webapps.js @@ -25,6 +25,10 @@ exports.setAppFramesMock = function (mock) { } DevToolsUtils.defineLazyGetter(this, "AppFrames", () => { + // Offer a way for unit test to provide a mock + if (AppFramesMock) { + return AppFramesMock; + } try { return Cu.import("resource://gre/modules/AppFrames.jsm", {}).AppFrames; } catch(e) {} diff --git a/toolkit/devtools/server/child.js b/toolkit/devtools/server/child.js index ff37fd1b14d..e2b3e70b99b 100644 --- a/toolkit/devtools/server/child.js +++ b/toolkit/devtools/server/child.js @@ -4,6 +4,8 @@ "use strict"; +try { + let chromeGlobal = this; // Encapsulate in its own scope to allows loading this frame script @@ -64,3 +66,7 @@ let chromeGlobal = this; }); addMessageListener("debug:disconnect", onDisconnect); })(); + +} catch(e) { + dump("Exception in app child process: " + e + "\n"); +} From 7b67e00244ceba267e9adc8158e930156dd6a4c1 Mon Sep 17 00:00:00 2001 From: Tim Taubert Date: Fri, 19 Sep 2014 13:40:40 +0200 Subject: [PATCH 44/56] Bug 1068384 - Remove legacy code that handled the app menu's update item r=mstange,stefanh --- widget/cocoa/nsMenuBarX.h | 1 - widget/cocoa/nsMenuBarX.mm | 28 ---------------------------- 2 files changed, 29 deletions(-) diff --git a/widget/cocoa/nsMenuBarX.h b/widget/cocoa/nsMenuBarX.h index 6fc2d818886..4171a71716e 100644 --- a/widget/cocoa/nsMenuBarX.h +++ b/widget/cocoa/nsMenuBarX.h @@ -103,7 +103,6 @@ public: // The following content nodes have been removed from the menu system. // We save them here for use in command handling. nsCOMPtr mAboutItemContent; - nsCOMPtr mUpdateItemContent; nsCOMPtr mPrefItemContent; nsCOMPtr mQuitItemContent; diff --git a/widget/cocoa/nsMenuBarX.mm b/widget/cocoa/nsMenuBarX.mm index 1934d5697b0..66e4bb99276 100644 --- a/widget/cocoa/nsMenuBarX.mm +++ b/widget/cocoa/nsMenuBarX.mm @@ -37,7 +37,6 @@ BOOL gSomeMenuBarPainted = NO; // window does not have a quit or pref item. We don't need strong refs here because // these items are always strong ref'd by their owning menu bar (instance variable). static nsIContent* sAboutItemContent = nullptr; -static nsIContent* sUpdateItemContent = nullptr; static nsIContent* sPrefItemContent = nullptr; static nsIContent* sQuitItemContent = nullptr; @@ -75,8 +74,6 @@ nsMenuBarX::~nsMenuBarX() // hidden window, thus we need to invalidate the weak references. if (sAboutItemContent == mAboutItemContent) sAboutItemContent = nullptr; - if (sUpdateItemContent == mUpdateItemContent) - sUpdateItemContent = nullptr; if (sQuitItemContent == mQuitItemContent) sQuitItemContent = nullptr; if (sPrefItemContent == mPrefItemContent) @@ -478,13 +475,6 @@ void nsMenuBarX::AquifyMenuBar() if (!sAboutItemContent) sAboutItemContent = mAboutItemContent; - // Hide the software update menu item, since it belongs in the application - // menu on Mac OS X. - HideItem(domDoc, NS_LITERAL_STRING("updateSeparator"), nullptr); - HideItem(domDoc, NS_LITERAL_STRING("checkForUpdates"), getter_AddRefs(mUpdateItemContent)); - if (!sUpdateItemContent) - sUpdateItemContent = mUpdateItemContent; - // remove quit item and its separator HideItem(domDoc, NS_LITERAL_STRING("menu_FileQuitSeparator"), nullptr); HideItem(domDoc, NS_LITERAL_STRING("menu_FileQuitItem"), getter_AddRefs(mQuitItemContent)); @@ -600,7 +590,6 @@ nsresult nsMenuBarX::CreateApplicationMenu(nsMenuX* inMenu) ======================== = About This App = <- aboutName - = Check for Updates... = <- checkForUpdates ======================== = Preferences... = <- menu_preferences ======================== @@ -644,17 +633,6 @@ nsresult nsMenuBarX::CreateApplicationMenu(nsMenuX* inMenu) addAboutSeparator = TRUE; } - // Add the software update menu item - itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("checkForUpdates"), @selector(menuItemHit:), - eCommand_ID_Update, nsMenuBarX::sNativeEventTarget); - if (itemBeingAdded) { - [sApplicationMenu addItem:itemBeingAdded]; - [itemBeingAdded release]; - itemBeingAdded = nil; - - addAboutSeparator = TRUE; - } - // Add separator if either the About item or software update item exists if (addAboutSeparator) [sApplicationMenu addItem:[NSMenuItem separatorItem]]; @@ -950,12 +928,6 @@ static BOOL gMenuItemsExecuteCommands = YES; nsMenuUtilsX::DispatchCommandTo(mostSpecificContent); return; } - else if (tag == eCommand_ID_Update) { - nsIContent* mostSpecificContent = sUpdateItemContent; - if (menuBar && menuBar->mUpdateItemContent) - mostSpecificContent = menuBar->mUpdateItemContent; - nsMenuUtilsX::DispatchCommandTo(mostSpecificContent); - } else if (tag == eCommand_ID_Prefs) { nsIContent* mostSpecificContent = sPrefItemContent; if (menuBar && menuBar->mPrefItemContent) From ff558ffd0f1cdba7552ba00ec464b2d3e40ff575 Mon Sep 17 00:00:00 2001 From: Martyn Haigh Date: Mon, 22 Sep 2014 15:28:28 +0100 Subject: [PATCH 45/56] Bug 1061163 - Rename tab_row to tabs_layout_item_view (r=lucasr) --- mobile/android/base/resources/values-land/layout.xml | 2 +- mobile/android/base/resources/values-large-v11/layout.xml | 2 +- mobile/android/base/resources/values/layout.xml | 2 +- mobile/android/base/tabs/TabsLayoutAdapter.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mobile/android/base/resources/values-land/layout.xml b/mobile/android/base/resources/values-land/layout.xml index 863376cabcb..00cc1d62454 100644 --- a/mobile/android/base/resources/values-land/layout.xml +++ b/mobile/android/base/resources/values-land/layout.xml @@ -4,5 +4,5 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - @layout/tabs_item_cell + @layout/tabs_item_cell diff --git a/mobile/android/base/resources/values-large-v11/layout.xml b/mobile/android/base/resources/values-large-v11/layout.xml index 863376cabcb..00cc1d62454 100644 --- a/mobile/android/base/resources/values-large-v11/layout.xml +++ b/mobile/android/base/resources/values-large-v11/layout.xml @@ -4,5 +4,5 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - @layout/tabs_item_cell + @layout/tabs_item_cell diff --git a/mobile/android/base/resources/values/layout.xml b/mobile/android/base/resources/values/layout.xml index 9823217a76b..854537a2ed9 100644 --- a/mobile/android/base/resources/values/layout.xml +++ b/mobile/android/base/resources/values/layout.xml @@ -4,5 +4,5 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - @layout/tabs_item_row + @layout/tabs_item_row diff --git a/mobile/android/base/tabs/TabsLayoutAdapter.java b/mobile/android/base/tabs/TabsLayoutAdapter.java index 6baaefdfa6d..26cbcc27165 100644 --- a/mobile/android/base/tabs/TabsLayoutAdapter.java +++ b/mobile/android/base/tabs/TabsLayoutAdapter.java @@ -81,7 +81,7 @@ public class TabsLayoutAdapter extends BaseAdapter { } View newView(int position, ViewGroup parent) { - final View view = mInflater.inflate(R.layout.tabs_row, parent, false); + final View view = mInflater.inflate(R.layout.tabs_layout_item_view, parent, false); final TabsLayoutItemView item = new TabsLayoutItemView(view); view.setTag(item); return view; From 2edb9eebdc016b3410c226c182e3244ec64796ef Mon Sep 17 00:00:00 2001 From: Martyn Haigh Date: Mon, 22 Sep 2014 15:37:34 +0100 Subject: [PATCH 46/56] Bug 1055539 - Rename TabsPanel internal class from TabsListContainer to PanelViewContainer (r=lucasr) --- .../layout-large-land-v11/tabs_panel.xml | 2 +- .../base/resources/layout/tabs_panel.xml | 2 +- mobile/android/base/tabs/TabsPanel.java | 38 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml b/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml index 4b05034e333..3a84e4b8362 100644 --- a/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml +++ b/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml @@ -26,7 +26,7 @@ - - diff --git a/mobile/android/base/tabs/TabsPanel.java b/mobile/android/base/tabs/TabsPanel.java index 7e09fbeb706..58a85c77abd 100644 --- a/mobile/android/base/tabs/TabsPanel.java +++ b/mobile/android/base/tabs/TabsPanel.java @@ -77,7 +77,7 @@ public class TabsPanel extends LinearLayout private final GeckoApp mActivity; private final LightweightTheme mTheme; private RelativeLayout mHeader; - private TabsListContainer mTabsContainer; + private PanelViewContainer mPanelsContainer; private PanelView mPanel; private PanelView mPanelNormal; private PanelView mPanelPrivate; @@ -137,7 +137,7 @@ public class TabsPanel extends LinearLayout private void initialize() { mHeader = (RelativeLayout) findViewById(R.id.tabs_panel_header); - mTabsContainer = (TabsListContainer) findViewById(R.id.tabs_container); + mPanelsContainer = (PanelViewContainer) findViewById(R.id.tabs_container); mPanelNormal = (PanelView) findViewById(R.id.normal_tabs); mPanelNormal.setTabsPanel(this); @@ -264,10 +264,10 @@ public class TabsPanel extends LinearLayout return mActivity.onOptionsItemSelected(item); } - private static int getTabContainerHeight(TabsListContainer listContainer) { - Resources resources = listContainer.getContext().getResources(); + private static int getPanelsContainerHeight(PanelViewContainer panelsContainer) { + Resources resources = panelsContainer.getContext().getResources(); - PanelView panelView = listContainer.getCurrentPanelView(); + PanelView panelView = panelsContainer.getCurrentPanelView(); if (panelView != null && !panelView.shouldExpand()) { return resources.getDimensionPixelSize(R.dimen.tabs_tray_horizontal_height); } @@ -276,7 +276,7 @@ public class TabsPanel extends LinearLayout int screenHeight = resources.getDisplayMetrics().heightPixels; Rect windowRect = new Rect(); - listContainer.getWindowVisibleDisplayFrame(windowRect); + panelsContainer.getWindowVisibleDisplayFrame(windowRect); int windowHeight = windowRect.bottom - windowRect.top; // The web content area should have at least 1.5x the height of the action bar. @@ -323,9 +323,9 @@ public class TabsPanel extends LinearLayout onLightweightThemeChanged(); } - // Tabs List Container holds the ListView - static class TabsListContainer extends FrameLayout { - public TabsListContainer(Context context, AttributeSet attrs) { + // Panel View Container holds the ListView + static class PanelViewContainer extends FrameLayout { + public PanelViewContainer(Context context, AttributeSet attrs) { super(context, attrs); } @@ -346,7 +346,7 @@ public class TabsPanel extends LinearLayout @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (!GeckoAppShell.getGeckoInterface().hasTabsSideBar()) { - int heightSpec = MeasureSpec.makeMeasureSpec(getTabContainerHeight(TabsListContainer.this), MeasureSpec.EXACTLY); + int heightSpec = MeasureSpec.makeMeasureSpec(getPanelsContainerHeight(PanelViewContainer.this), MeasureSpec.EXACTLY); super.onMeasure(widthMeasureSpec, heightSpec); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); @@ -468,7 +468,7 @@ public class TabsPanel extends LinearLayout dispatchLayoutChange(getWidth(), getHeight()); } else { int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height); - int height = actionBarHeight + getTabContainerHeight(mTabsContainer); + int height = actionBarHeight + getPanelsContainerHeight(mPanelsContainer); dispatchLayoutChange(getWidth(), height); } mHeaderVisible = true; @@ -526,13 +526,13 @@ public class TabsPanel extends LinearLayout final int tabsPanelWidth = getWidth(); if (mVisible) { ViewHelper.setTranslationX(mHeader, -tabsPanelWidth); - ViewHelper.setTranslationX(mTabsContainer, -tabsPanelWidth); + ViewHelper.setTranslationX(mPanelsContainer, -tabsPanelWidth); // The footer view is only present on the sidebar, v11+. ViewHelper.setTranslationX(mFooter, -tabsPanelWidth); } final int translationX = (mVisible ? 0 : -tabsPanelWidth); - animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_X, translationX); + animator.attach(mPanelsContainer, PropertyAnimator.Property.TRANSLATION_X, translationX); animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_X, translationX); animator.attach(mFooter, PropertyAnimator.Property.TRANSLATION_X, translationX); @@ -542,16 +542,16 @@ public class TabsPanel extends LinearLayout final int translationY = (mVisible ? 0 : -toolbarHeight); if (mVisible) { ViewHelper.setTranslationY(mHeader, -toolbarHeight); - ViewHelper.setTranslationY(mTabsContainer, -toolbarHeight); - ViewHelper.setAlpha(mTabsContainer, 0.0f); + ViewHelper.setTranslationY(mPanelsContainer, -toolbarHeight); + ViewHelper.setAlpha(mPanelsContainer, 0.0f); } - animator.attach(mTabsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f); - animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY); + animator.attach(mPanelsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f); + animator.attach(mPanelsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY); animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_Y, translationY); } mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null); - mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mPanelsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null); } public void finishTabsAnimation() { @@ -560,7 +560,7 @@ public class TabsPanel extends LinearLayout } mHeader.setLayerType(View.LAYER_TYPE_NONE, null); - mTabsContainer.setLayerType(View.LAYER_TYPE_NONE, null); + mPanelsContainer.setLayerType(View.LAYER_TYPE_NONE, null); // If the tray is now hidden, call hide() on current panel and unset it as the current panel // to avoid hide() being called again when the tray is opened next. From c7dbdd6f9819a47799748f12866d3f3c9f0000ce Mon Sep 17 00:00:00 2001 From: Martyn Haigh Date: Mon, 22 Sep 2014 16:29:35 +0100 Subject: [PATCH 47/56] Bug 1056920 - Create grid based layout for tabs (r=lucasr) --- mobile/android/base/BrowserApp.java | 2 +- mobile/android/base/moz.build | 1 + .../base/resources/values-v11/themes.xml | 1 + .../android/base/resources/values/attrs.xml | 3 + .../android/base/resources/values/dimens.xml | 3 + .../android/base/resources/values/styles.xml | 12 + .../android/base/resources/values/themes.xml | 1 + mobile/android/base/tabs/TabsGridLayout.java | 239 ++++++++++++++++++ 8 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 mobile/android/base/tabs/TabsGridLayout.java diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index 02490f2c804..6c941be1e84 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -1322,7 +1322,7 @@ public class BrowserApp extends GeckoApp if (mMainLayoutAnimator != null) mMainLayoutAnimator.stop(); - boolean isSideBar = (HardwareUtils.isTablet() && getOrientation() == Configuration.ORIENTATION_LANDSCAPE); + boolean isSideBar = !NewTabletUI.isEnabled(this) && (HardwareUtils.isTablet() && getOrientation() == Configuration.ORIENTATION_LANDSCAPE); final int sidebarWidth = getResources().getDimensionPixelSize(R.dimen.tabs_sidebar_width); ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) mTabsPanel.getLayoutParams(); diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 3869e435b2e..516ecd67e89 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -395,6 +395,7 @@ gbjar.sources += [ 'tabs/RemoteTabsSetupPanel.java', 'tabs/RemoteTabsVerificationPanel.java', 'tabs/TabCurve.java', + 'tabs/TabsGridLayout.java', 'tabs/TabsLayoutAdapter.java', 'tabs/TabsLayoutItemView.java', 'tabs/TabsListLayout.java', diff --git a/mobile/android/base/resources/values-v11/themes.xml b/mobile/android/base/resources/values-v11/themes.xml index 4d069328f43..aedc56781a9 100644 --- a/mobile/android/base/resources/values-v11/themes.xml +++ b/mobile/android/base/resources/values-v11/themes.xml @@ -50,6 +50,7 @@ @style/Widget.TopSitesGridItemView @style/Widget.TopSitesGridView @style/Widget.PanelGridView + @style/Widget.TabsGridLayout @style/Widget.TopSitesThumbnailView @style/Widget.HomeListView @style/Widget.GeckoMenuListView diff --git a/mobile/android/base/resources/values/attrs.xml b/mobile/android/base/resources/values/attrs.xml index a9f1212eade..9783bcd7af7 100644 --- a/mobile/android/base/resources/values/attrs.xml +++ b/mobile/android/base/resources/values/attrs.xml @@ -38,6 +38,9 @@ + + + diff --git a/mobile/android/base/resources/values/dimens.xml b/mobile/android/base/resources/values/dimens.xml index b340a6de368..50ed658e8cd 100644 --- a/mobile/android/base/resources/values/dimens.xml +++ b/mobile/android/base/resources/values/dimens.xml @@ -105,6 +105,9 @@ 150dp + + 200dp + 95dp diff --git a/mobile/android/base/resources/values/styles.xml b/mobile/android/base/resources/values/styles.xml index ca3929f8845..f2ce81cd812 100644 --- a/mobile/android/base/resources/values/styles.xml +++ b/mobile/android/base/resources/values/styles.xml @@ -179,6 +179,18 @@ true + + + + + + - - - - - -