gecko/browser/devtools/shadereditor/shadereditor.js

470 lines
14 KiB
JavaScript

/* 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 new programs are received from the server.
NEW_PROGRAM: "ShaderEditor:NewProgram",
PROGRAMS_ADDED: "ShaderEditor:ProgramsAdded",
// 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 gToolbox, 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._onHostChanged = this._onHostChanged.bind(this);
this._onTabNavigated = this._onTabNavigated.bind(this);
this._onProgramLinked = this._onProgramLinked.bind(this);
this._onProgramsAdded = this._onProgramsAdded.bind(this);
gToolbox.on("host-changed", this._onHostChanged);
gTarget.on("will-navigate", this._onTabNavigated);
gTarget.on("navigate", this._onTabNavigated);
gFront.on("program-linked", this._onProgramLinked);
},
/**
* Remove events emitted by the current tab target.
*/
destroy: function() {
gToolbox.off("host-changed", this._onHostChanged);
gTarget.off("will-navigate", this._onTabNavigated);
gTarget.off("navigate", this._onTabNavigated);
gFront.off("program-linked", this._onProgramLinked);
},
/**
* Handles a host change event on the parent toolbox.
*/
_onHostChanged: function() {
if (gToolbox.hostType == "side") {
$("#shaders-pane").removeAttribute("height");
}
},
/**
* Called for each location change in the debugged tab.
*/
_onTabNavigated: function(event) {
switch (event) {
case "will-navigate": {
// Make sure the backend is prepared to handle WebGL contexts.
gFront.setup({ reload: false });
// Reset UI.
ShadersListView.empty();
ShadersEditorsView.setText({ vs: "", fs: "" });
$("#reload-notice").hidden = true;
$("#waiting-notice").hidden = false;
$("#content").hidden = true;
break;
}
case "navigate": {
// Manually retrieve the list of program actors known to the server,
// because the backend won't emit "program-linked" notifications
// in the case of a bfcache navigation (since no new programs are
// actually linked).
gFront.getPrograms().then(this._onProgramsAdded);
break;
}
}
},
/**
* Called every time a program was linked in the debugged tab.
*/
_onProgramLinked: function(programActor) {
this._addProgram(programActor);
window.emit(EVENTS.NEW_PROGRAM);
},
/**
* Callback for the front's getPrograms() method.
*/
_onProgramsAdded: function(programActors) {
programActors.forEach(this._addProgram);
window.emit(EVENTS.PROGRAMS_ADDED);
},
/**
* Adds a program to the shaders list and unhides any modal notices.
*/
_addProgram: 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) {
if (this.hasProgram(programActor)) {
return;
}
// 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;
}
// Prevent this container from growing indefinitely in height when the
// toolbox is docked to the side.
if (gToolbox.hostType == "side" && this.itemCount == SHADERS_AUTOGROW_ITEMS) {
this._pane.setAttribute("height", this._pane.getBoundingClientRect().height);
}
},
/**
* Returns whether a program was already added to this programs container.
*
* @param object programActor
* The program actor coming from the active thread.
* @param boolean
* True if the program was added, false otherwise.
*/
hasProgram: function(programActor) {
return !!this.attachments.filter(e => e.programActor == programActor).length;
},
/**
* 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);