diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 66d0f48d663..f59b43f48da 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1166,6 +1166,9 @@ pref("devtools.scratchpad.recentFilesMax", 10); pref("devtools.styleeditor.enabled", true); pref("devtools.styleeditor.transitions", true); +// Enable the Shader Editor. +pref("devtools.shadereditor.enabled", false); + // Enable tools for Chrome development. pref("devtools.chrome.enabled", false); diff --git a/browser/devtools/debugger/moz.build b/browser/devtools/debugger/moz.build index 267cbbe712f..ae60acb34f6 100644 --- a/browser/devtools/debugger/moz.build +++ b/browser/devtools/debugger/moz.build @@ -1,4 +1,3 @@ -# -*- 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 diff --git a/browser/devtools/jar.mn b/browser/devtools/jar.mn index 394c68bf03b..265d9e81d50 100644 --- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -51,6 +51,8 @@ browser.jar: content/browser/devtools/debugger-view.js (debugger/debugger-view.js) content/browser/devtools/debugger-toolbar.js (debugger/debugger-toolbar.js) content/browser/devtools/debugger-panes.js (debugger/debugger-panes.js) + content/browser/devtools/shadereditor.xul (shadereditor/shadereditor.xul) + content/browser/devtools/shadereditor.js (shadereditor/shadereditor.js) content/browser/devtools/profiler.xul (profiler/profiler.xul) content/browser/devtools/cleopatra.html (profiler/cleopatra/cleopatra.html) content/browser/devtools/profiler/cleopatra/css/ui.css (profiler/cleopatra/css/ui.css) diff --git a/browser/devtools/main.js b/browser/devtools/main.js index d7ebf7d7e78..ed33aac989e 100644 --- a/browser/devtools/main.js +++ b/browser/devtools/main.js @@ -27,6 +27,7 @@ loader.lazyGetter(this, "InspectorPanel", () => require("devtools/inspector/insp loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/webconsole/panel").WebConsolePanel); loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/debugger/debugger-panel").DebuggerPanel); loader.lazyImporter(this, "StyleEditorPanel", "resource:///modules/devtools/StyleEditorPanel.jsm"); +loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shadereditor/panel").ShaderEditorPanel); loader.lazyGetter(this, "ProfilerPanel", () => require("devtools/profiler/panel")); loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/netmonitor-panel").NetMonitorPanel); loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel); @@ -36,6 +37,7 @@ const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties"; const inspectorProps = "chrome://browser/locale/devtools/inspector.properties"; const debuggerProps = "chrome://browser/locale/devtools/debugger.properties"; const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties"; +const shaderEditorProps = "chrome://browser/locale/devtools/shadereditor.properties"; const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties"; const profilerProps = "chrome://browser/locale/devtools/profiler.properties"; const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties"; @@ -44,6 +46,7 @@ loader.lazyGetter(this, "toolboxStrings", () => Services.strings.createBundle(to loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle(webConsoleProps)); loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps)); loader.lazyGetter(this, "styleEditorStrings", () => Services.strings.createBundle(styleEditorProps)); +loader.lazyGetter(this, "shaderEditorStrings", () => Services.strings.createBundle(shaderEditorProps)); loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(inspectorProps)); loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps)); loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps)); @@ -166,11 +169,30 @@ Tools.styleEditor = { } }; +Tools.shaderEditor = { + id: "shadereditor", + ordinal: 5, + visibilityswitch: "devtools.shadereditor.enabled", + icon: "chrome://browser/skin/devtools/tool-styleeditor.png", + url: "chrome://browser/content/devtools/shadereditor.xul", + label: l10n("ToolboxShaderEditor.label", shaderEditorStrings), + tooltip: l10n("ToolboxShaderEditor.tooltip", shaderEditorStrings), + + isTargetSupported: function(target) { + return true; + }, + + build: function(iframeWindow, toolbox) { + let panel = new ShaderEditorPanel(iframeWindow, toolbox); + return panel.open(); + } +}; + Tools.jsprofiler = { id: "jsprofiler", accesskey: l10n("profiler.accesskey", profilerStrings), key: l10n("profiler2.commandkey", profilerStrings), - ordinal: 5, + ordinal: 6, modifiers: "shift", visibilityswitch: "devtools.profiler.enabled", icon: "chrome://browser/skin/devtools/tool-profiler.png", @@ -193,7 +215,7 @@ Tools.netMonitor = { id: "netmonitor", accesskey: l10n("netmonitor.accesskey", netMonitorStrings), key: l10n("netmonitor.commandkey", netMonitorStrings), - ordinal: 6, + ordinal: 7, modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift", visibilityswitch: "devtools.netmonitor.enabled", icon: "chrome://browser/skin/devtools/tool-network.png", @@ -214,7 +236,7 @@ Tools.netMonitor = { Tools.scratchpad = { id: "scratchpad", - ordinal: 7, + ordinal: 8, visibilityswitch: "devtools.scratchpad.enabled", icon: "chrome://browser/skin/devtools/tool-scratchpad.png", url: "chrome://browser/content/devtools/scratchpad.xul", @@ -234,10 +256,11 @@ Tools.scratchpad = { let defaultTools = [ Tools.options, - Tools.styleEditor, Tools.webConsole, - Tools.jsdebugger, Tools.inspector, + Tools.jsdebugger, + Tools.styleEditor, + Tools.shaderEditor, Tools.jsprofiler, Tools.netMonitor, Tools.scratchpad diff --git a/browser/devtools/shadereditor/moz.build b/browser/devtools/shadereditor/moz.build index 493f80dc6ef..1978c0d9f9b 100644 --- a/browser/devtools/shadereditor/moz.build +++ b/browser/devtools/shadereditor/moz.build @@ -4,3 +4,10 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. TEST_DIRS += ['test'] + +JS_MODULES_PATH = 'modules/devtools/shadereditor' + +EXTRA_JS_MODULES += [ + 'panel.js' +] + diff --git a/browser/devtools/shadereditor/panel.js b/browser/devtools/shadereditor/panel.js new file mode 100644 index 00000000000..102df56e9d2 --- /dev/null +++ b/browser/devtools/shadereditor/panel.js @@ -0,0 +1,65 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 { Cc, Ci, Cu, Cr } = require("chrome"); +const promise = require("sdk/core/promise"); +const EventEmitter = require("devtools/shared/event-emitter"); +const { WebGLFront } = require("devtools/server/actors/webgl"); + +function ShaderEditorPanel(iframeWindow, toolbox) { + this.panelWin = iframeWindow; + this._toolbox = toolbox; + this._destroyer = null; + + EventEmitter.decorate(this); +}; + +exports.ShaderEditorPanel = ShaderEditorPanel; + +ShaderEditorPanel.prototype = { + open: function() { + let targetPromise; + + // Local debugging needs to make the target remote. + if (!this.target.isRemote) { + targetPromise = this.target.makeRemote(); + } else { + targetPromise = promise.resolve(this.target); + } + + return targetPromise + .then(() => { + this.panelWin.gTarget = this.target; + this.panelWin.gFront = new WebGLFront(this.target.client, this.target.form); + return this.panelWin.startupShaderEditor(); + }) + .then(() => { + this.isReady = true; + this.emit("ready"); + return this; + }) + .then(null, function onError(aReason) { + Cu.reportError("ShaderEditorPanel open failed. " + + aReason.error + ": " + aReason.message); + }); + }, + + // DevToolPanel API + + get target() this._toolbox.target, + + destroy: function() { + // Make sure this panel is not already destroyed. + if (this._destroyer) { + return this._destroyer; + } + + return this._destroyer = this.panelWin.shutdownShaderEditor().then(() => { + this.emit("destroyed"); + }); + } +}; diff --git a/browser/devtools/shadereditor/shadereditor.js b/browser/devtools/shadereditor/shadereditor.js new file mode 100644 index 00000000000..11f6ea61187 --- /dev/null +++ b/browser/devtools/shadereditor/shadereditor.js @@ -0,0 +1,396 @@ +/* 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://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/devtools/Loader.jsm"); +Cu.import("resource:///modules/devtools/SideMenuWidget.jsm"); +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); + +const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; +const promise = require("sdk/core/promise"); +const EventEmitter = require("devtools/shared/event-emitter"); +const Editor = require("devtools/sourceeditor/editor"); + +// The panel's window global is an EventEmitter firing the following events: +const EVENTS = { + // When the vertex and fragment sources were shown in the editor. + SOURCES_SHOWN: "ShaderEditor:SourcesShown", + // When a shader's source was edited and compiled via the editor. + SHADER_COMPILED: "ShaderEditor:ShaderCompiled" +}; + +const STRINGS_URI = "chrome://browser/locale/devtools/shadereditor.properties" +const HIGHLIGHT_COLOR = [1, 0, 0, 1]; +const BLACKBOX_COLOR = [0, 0, 0, 0]; +const TYPING_MAX_DELAY = 500; +const SHADERS_AUTOGROW_ITEMS = 4; +const DEFAULT_EDITOR_CONFIG = { + mode: Editor.modes.text, + lineNumbers: true, + showAnnotationRuler: true +}; + +/** + * The current target and the WebGL Editor front, set by this tool's host. + */ +let gTarget, gFront; + +/** + * Initializes the shader editor controller and views. + */ +function startupShaderEditor() { + return promise.all([ + EventsHandler.initialize(), + ShadersListView.initialize(), + ShadersEditorsView.initialize() + ]); +} + +/** + * Destroys the shader editor controller and views. + */ +function shutdownShaderEditor() { + return promise.all([ + EventsHandler.destroy(), + ShadersListView.destroy(), + ShadersEditorsView.destroy() + ]); +} + +/** + * Functions handling target-related lifetime events. + */ +let EventsHandler = { + /** + * Listen for events emitted by the current tab target. + */ + initialize: function() { + this._onWillNavigate = this._onWillNavigate.bind(this); + this._onProgramLinked = this._onProgramLinked.bind(this); + gTarget.on("will-navigate", this._onWillNavigate); + gFront.on("program-linked", this._onProgramLinked); + + }, + + /** + * Remove events emitted by the current tab target. + */ + destroy: function() { + gTarget.off("will-navigate", this._onWillNavigate); + gFront.off("program-linked", this._onProgramLinked); + }, + + /** + * Called for each location change in the debugged tab. + */ + _onWillNavigate: function() { + gFront.setup(); + + ShadersListView.empty(); + ShadersEditorsView.setText({ vs: "", fs: "" }); + $("#reload-notice").hidden = true; + $("#waiting-notice").hidden = false; + $("#content").hidden = true; + }, + + /** + * Called every time a program was linked in the debugged tab. + */ + _onProgramLinked: function(programActor) { + $("#waiting-notice").hidden = true; + $("#reload-notice").hidden = true; + $("#content").hidden = false; + ShadersListView.addProgram(programActor); + } +}; + +/** + * Functions handling the sources UI. + */ +let ShadersListView = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the tool is started. + */ + initialize: function() { + this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), { + showArrows: true, + showItemCheckboxes: true + }); + + this._onShaderSelect = this._onShaderSelect.bind(this); + this._onShaderCheck = this._onShaderCheck.bind(this); + this._onShaderMouseEnter = this._onShaderMouseEnter.bind(this); + this._onShaderMouseLeave = this._onShaderMouseLeave.bind(this); + + this.widget.addEventListener("select", this._onShaderSelect, false); + this.widget.addEventListener("check", this._onShaderCheck, false); + this.widget.addEventListener("mouseenter", this._onShaderMouseEnter, true); + this.widget.addEventListener("mouseleave", this._onShaderMouseLeave, true); + }, + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function() { + this.widget.removeEventListener("select", this._onShaderSelect, false); + this.widget.removeEventListener("check", this._onShaderCheck, false); + this.widget.removeEventListener("mouseenter", this._onShaderMouseEnter, true); + this.widget.removeEventListener("mouseleave", this._onShaderMouseLeave, true); + }, + + /** + * Adds a program to this programs container. + * + * @param object programActor + * The program actor coming from the active thread. + */ + addProgram: function(programActor) { + // Currently, there's no good way of differentiating between programs + // in a way that helps humans. It will be a good idea to implement a + // standard of allowing debuggees to add some identifiable metadata to their + // program sources or instances. + let label = L10N.getFormatStr("shadersList.programLabel", this.itemCount); + + // Append a program item to this container. + this.push([label, ""], { + index: -1, /* specifies on which position should the item be appended */ + relaxed: true, /* this container should allow dupes & degenerates */ + attachment: { + programActor: programActor, + checkboxState: true, + checkboxTooltip: L10N.getStr("shadersList.blackboxLabel") + } + }); + + // Make sure there's always a selected item available. + if (!this.selectedItem) { + this.selectedIndex = 0; + } + }, + + /** + * The select listener for the sources container. + */ + _onShaderSelect: function({ detail: sourceItem }) { + if (!sourceItem) { + return; + } + // The container is not empty and an actual item was selected. + let attachment = sourceItem.attachment; + + function getShaders() { + return promise.all([ + attachment.vs || (attachment.vs = attachment.programActor.getVertexShader()), + attachment.fs || (attachment.fs = attachment.programActor.getFragmentShader()) + ]); + } + function getSources([vertexShaderActor, fragmentShaderActor]) { + return promise.all([ + vertexShaderActor.getText(), + fragmentShaderActor.getText() + ]); + } + function showSources([vertexShaderText, fragmentShaderText]) { + ShadersEditorsView.setText({ + vs: vertexShaderText, + fs: fragmentShaderText + }); + } + + getShaders().then(getSources).then(showSources).then(null, Cu.reportError); + }, + + /** + * The check listener for the sources container. + */ + _onShaderCheck: function({ detail: { checked }, target }) { + let sourceItem = this.getItemForElement(target); + let attachment = sourceItem.attachment; + attachment.isBlackBoxed = !checked; + attachment.programActor[checked ? "unhighlight" : "highlight"](BLACKBOX_COLOR); + }, + + /** + * The mouseenter listener for the sources container. + */ + _onShaderMouseEnter: function(e) { + let sourceItem = this.getItemForElement(e.target, { noSiblings: true }); + if (sourceItem && !sourceItem.attachment.isBlackBoxed) { + sourceItem.attachment.programActor.highlight(HIGHLIGHT_COLOR); + + if (e instanceof Event) { + e.preventDefault(); + e.stopPropagation(); + } + } + }, + + /** + * The mouseleave listener for the sources container. + */ + _onShaderMouseLeave: function(e) { + let sourceItem = this.getItemForElement(e.target, { noSiblings: true }); + if (sourceItem && !sourceItem.attachment.isBlackBoxed) { + sourceItem.attachment.programActor.unhighlight(); + + if (e instanceof Event) { + e.preventDefault(); + e.stopPropagation(); + } + } + } +}); + +/** + * Functions handling the editors displaying the vertex and fragment shaders. + */ +let ShadersEditorsView = { + /** + * Initialization function, called when the tool is started. + */ + initialize: function() { + XPCOMUtils.defineLazyGetter(this, "_editorPromises", () => new Map()); + this._vsFocused = this._onFocused.bind(this, "vs", "fs"); + this._fsFocused = this._onFocused.bind(this, "fs", "vs"); + this._vsChanged = this._onChanged.bind(this, "vs"); + this._fsChanged = this._onChanged.bind(this, "fs"); + }, + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function() { + this._toggleListeners("off"); + }, + + /** + * Sets the text displayed in the vertex and fragment shader editors. + * + * @param object sources + * An object containing the following properties + * - vs: the vertex shader source code + * - fs: the fragment shader source code + */ + setText: function(sources) { + function setTextAndClearHistory(editor, text) { + editor.setText(text); + editor.clearHistory(); + } + + this._toggleListeners("off"); + this._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)); + this._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs)); + this._toggleListeners("on"); + + window.emit(EVENTS.SOURCES_SHOWN, sources); + }, + + /** + * Lazily initializes and returns a promise for an Editor instance. + * + * @param string type + * Specifies for which shader type should an editor be retrieved, + * either are "vs" for a vertex, or "fs" for a fragment shader. + */ + _getEditor: function(type) { + if ($("#content").hidden) { + return promise.reject(null); + } + if (this._editorPromises.has(type)) { + return this._editorPromises.get(type); + } + + let deferred = promise.defer(); + this._editorPromises.set(type, deferred.promise); + + // Initialize the source editor and store the newly created instance + // in the ether of a resolved promise's value. + let parent = $("#" + type +"-editor"); + let editor = new Editor(DEFAULT_EDITOR_CONFIG); + editor.appendTo(parent).then(() => deferred.resolve(editor)); + + return deferred.promise; + }, + + /** + * Toggles all the event listeners for the editors either on or off. + * + * @param string flag + * Either "on" to enable the event listeners, "off" to disable them. + */ + _toggleListeners: function(flag) { + ["vs", "fs"].forEach(type => { + this._getEditor(type).then(editor => { + editor[flag]("focus", this["_" + type + "Focused"]); + editor[flag]("change", this["_" + type + "Changed"]); + }); + }); + }, + + /** + * The focus listener for a source editor. + * + * @param string focused + * The corresponding shader type for the focused editor (e.g. "vs"). + * @param string focused + * The corresponding shader type for the other editor (e.g. "fs"). + */ + _onFocused: function(focused, unfocused) { + $("#" + focused + "-editor-label").setAttribute("selected", ""); + $("#" + unfocused + "-editor-label").removeAttribute("selected"); + }, + + /** + * The change listener for a source editor. + * + * @param string type + * The corresponding shader type for the focused editor (e.g. "vs"). + */ + _onChanged: function(type) { + setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type)); + }, + + /** + * Recompiles the source code for the shader being edited. + * This function is fired at a certain delay after the user stops typing. + * + * @param string type + * The corresponding shader type for the focused editor (e.g. "vs"). + */ + _doCompile: function(type) { + Task.spawn(function() { + let editor = yield this._getEditor(type); + let shaderActor = yield ShadersListView.selectedAttachment[type]; + + try { + yield shaderActor.compile(editor.getText()); + window.emit(EVENTS.SHADER_COMPILED, null); + // TODO: remove error gutter markers, after bug 919709 lands. + } catch (error) { + window.emit(EVENTS.SHADER_COMPILED, error); + // TODO: add error gutter markers, after bug 919709 lands. + } + }.bind(this)); + } +}; + +/** + * Localization convenience methods. + */ +let L10N = new ViewHelpers.L10N(STRINGS_URI); + +/** + * Convenient way of emitting events from the panel window. + */ +EventEmitter.decorate(this); + +/** + * DOM query helper. + */ +function $(selector, target = document) target.querySelector(selector); diff --git a/browser/devtools/shadereditor/shadereditor.xul b/browser/devtools/shadereditor/shadereditor.xul new file mode 100644 index 00000000000..5fbf52304d1 --- /dev/null +++ b/browser/devtools/shadereditor/shadereditor.xul @@ -0,0 +1,64 @@ + + + + + + + + + %debuggerDTD; +]> + + +