browser/devtools/shadereditor/shadereditor.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     4 "use strict";
     6 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
     8 Cu.import("resource://gre/modules/Services.jsm");
     9 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    10 Cu.import("resource://gre/modules/Task.jsm");
    11 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
    12 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
    14 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
    15 const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
    16 const EventEmitter = require("devtools/toolkit/event-emitter");
    17 const {Tooltip} = require("devtools/shared/widgets/Tooltip");
    18 const Editor = require("devtools/sourceeditor/editor");
    20 // The panel's window global is an EventEmitter firing the following events:
    21 const EVENTS = {
    22   // When new programs are received from the server.
    23   NEW_PROGRAM: "ShaderEditor:NewProgram",
    24   PROGRAMS_ADDED: "ShaderEditor:ProgramsAdded",
    26   // When the vertex and fragment sources were shown in the editor.
    27   SOURCES_SHOWN: "ShaderEditor:SourcesShown",
    29   // When a shader's source was edited and compiled via the editor.
    30   SHADER_COMPILED: "ShaderEditor:ShaderCompiled",
    32   // When the UI is reset from tab navigation
    33   UI_RESET: "ShaderEditor:UIReset",
    35   // When the editor's error markers are all removed
    36   EDITOR_ERROR_MARKERS_REMOVED: "ShaderEditor:EditorCleaned"
    37 };
    39 const STRINGS_URI = "chrome://browser/locale/devtools/shadereditor.properties"
    40 const HIGHLIGHT_TINT = [1, 0, 0.25, 1]; // rgba
    41 const TYPING_MAX_DELAY = 500; // ms
    42 const SHADERS_AUTOGROW_ITEMS = 4;
    43 const GUTTER_ERROR_PANEL_OFFSET_X = 7; // px
    44 const GUTTER_ERROR_PANEL_DELAY = 100; // ms
    45 const DEFAULT_EDITOR_CONFIG = {
    46   gutters: ["errors"],
    47   lineNumbers: true,
    48   showAnnotationRuler: true
    49 };
    51 /**
    52  * The current target and the WebGL Editor front, set by this tool's host.
    53  */
    54 let gToolbox, gTarget, gFront;
    56 /**
    57  * Initializes the shader editor controller and views.
    58  */
    59 function startupShaderEditor() {
    60   return promise.all([
    61     EventsHandler.initialize(),
    62     ShadersListView.initialize(),
    63     ShadersEditorsView.initialize()
    64   ]);
    65 }
    67 /**
    68  * Destroys the shader editor controller and views.
    69  */
    70 function shutdownShaderEditor() {
    71   return promise.all([
    72     EventsHandler.destroy(),
    73     ShadersListView.destroy(),
    74     ShadersEditorsView.destroy()
    75   ]);
    76 }
    78 /**
    79  * Functions handling target-related lifetime events.
    80  */
    81 let EventsHandler = {
    82   /**
    83    * Listen for events emitted by the current tab target.
    84    */
    85   initialize: function() {
    86     this._onHostChanged = this._onHostChanged.bind(this);
    87     this._onTabNavigated = this._onTabNavigated.bind(this);
    88     this._onProgramLinked = this._onProgramLinked.bind(this);
    89     this._onProgramsAdded = this._onProgramsAdded.bind(this);
    90     gToolbox.on("host-changed", this._onHostChanged);
    91     gTarget.on("will-navigate", this._onTabNavigated);
    92     gTarget.on("navigate", this._onTabNavigated);
    93     gFront.on("program-linked", this._onProgramLinked);
    95   },
    97   /**
    98    * Remove events emitted by the current tab target.
    99    */
   100   destroy: function() {
   101     gToolbox.off("host-changed", this._onHostChanged);
   102     gTarget.off("will-navigate", this._onTabNavigated);
   103     gTarget.off("navigate", this._onTabNavigated);
   104     gFront.off("program-linked", this._onProgramLinked);
   105   },
   107   /**
   108    * Handles a host change event on the parent toolbox.
   109    */
   110   _onHostChanged: function() {
   111     if (gToolbox.hostType == "side") {
   112       $("#shaders-pane").removeAttribute("height");
   113     }
   114   },
   116   /**
   117    * Called for each location change in the debugged tab.
   118    */
   119   _onTabNavigated: function(event) {
   120     switch (event) {
   121       case "will-navigate": {
   122         Task.spawn(function() {
   123           // Make sure the backend is prepared to handle WebGL contexts.
   124           gFront.setup({ reload: false });
   126           // Reset UI.
   127           ShadersListView.empty();
   128           $("#reload-notice").hidden = true;
   129           $("#waiting-notice").hidden = false;
   130           yield ShadersEditorsView.setText({ vs: "", fs: "" });
   131           $("#content").hidden = true;
   132         }).then(() => window.emit(EVENTS.UI_RESET));
   133         break;
   134       }
   135       case "navigate": {
   136         // Manually retrieve the list of program actors known to the server,
   137         // because the backend won't emit "program-linked" notifications
   138         // in the case of a bfcache navigation (since no new programs are
   139         // actually linked).
   140         gFront.getPrograms().then(this._onProgramsAdded);
   141         break;
   142       }
   143     }
   144   },
   146   /**
   147    * Called every time a program was linked in the debugged tab.
   148    */
   149   _onProgramLinked: function(programActor) {
   150     this._addProgram(programActor);
   151     window.emit(EVENTS.NEW_PROGRAM);
   152   },
   154   /**
   155    * Callback for the front's getPrograms() method.
   156    */
   157   _onProgramsAdded: function(programActors) {
   158     programActors.forEach(this._addProgram);
   159     window.emit(EVENTS.PROGRAMS_ADDED);
   160   },
   162   /**
   163    * Adds a program to the shaders list and unhides any modal notices.
   164    */
   165   _addProgram: function(programActor) {
   166     $("#waiting-notice").hidden = true;
   167     $("#reload-notice").hidden = true;
   168     $("#content").hidden = false;
   169     ShadersListView.addProgram(programActor);
   170   }
   171 };
   173 /**
   174  * Functions handling the sources UI.
   175  */
   176 let ShadersListView = Heritage.extend(WidgetMethods, {
   177   /**
   178    * Initialization function, called when the tool is started.
   179    */
   180   initialize: function() {
   181     this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), {
   182       showArrows: true,
   183       showItemCheckboxes: true
   184     });
   186     this._onProgramSelect = this._onProgramSelect.bind(this);
   187     this._onProgramCheck = this._onProgramCheck.bind(this);
   188     this._onProgramMouseEnter = this._onProgramMouseEnter.bind(this);
   189     this._onProgramMouseLeave = this._onProgramMouseLeave.bind(this);
   191     this.widget.addEventListener("select", this._onProgramSelect, false);
   192     this.widget.addEventListener("check", this._onProgramCheck, false);
   193     this.widget.addEventListener("mouseenter", this._onProgramMouseEnter, true);
   194     this.widget.addEventListener("mouseleave", this._onProgramMouseLeave, true);
   195   },
   197   /**
   198    * Destruction function, called when the tool is closed.
   199    */
   200   destroy: function() {
   201     this.widget.removeEventListener("select", this._onProgramSelect, false);
   202     this.widget.removeEventListener("check", this._onProgramCheck, false);
   203     this.widget.removeEventListener("mouseenter", this._onProgramMouseEnter, true);
   204     this.widget.removeEventListener("mouseleave", this._onProgramMouseLeave, true);
   205   },
   207   /**
   208    * Adds a program to this programs container.
   209    *
   210    * @param object programActor
   211    *        The program actor coming from the active thread.
   212    */
   213   addProgram: function(programActor) {
   214     if (this.hasProgram(programActor)) {
   215       return;
   216     }
   218     // Currently, there's no good way of differentiating between programs
   219     // in a way that helps humans. It will be a good idea to implement a
   220     // standard of allowing debuggees to add some identifiable metadata to their
   221     // program sources or instances.
   222     let label = L10N.getFormatStr("shadersList.programLabel", this.itemCount);
   223     let contents = document.createElement("label");
   224     contents.className = "plain program-item";
   225     contents.setAttribute("value", label);
   226     contents.setAttribute("crop", "start");
   227     contents.setAttribute("flex", "1");
   229     // Append a program item to this container.
   230     this.push([contents], {
   231       index: -1, /* specifies on which position should the item be appended */
   232       attachment: {
   233         label: label,
   234         programActor: programActor,
   235         checkboxState: true,
   236         checkboxTooltip: L10N.getStr("shadersList.blackboxLabel")
   237       }
   238     });
   240     // Make sure there's always a selected item available.
   241     if (!this.selectedItem) {
   242       this.selectedIndex = 0;
   243     }
   245     // Prevent this container from growing indefinitely in height when the
   246     // toolbox is docked to the side.
   247     if (gToolbox.hostType == "side" && this.itemCount == SHADERS_AUTOGROW_ITEMS) {
   248       this._pane.setAttribute("height", this._pane.getBoundingClientRect().height);
   249     }
   250   },
   252   /**
   253    * Returns whether a program was already added to this programs container.
   254    *
   255    * @param object programActor
   256    *        The program actor coming from the active thread.
   257    * @param boolean
   258    *        True if the program was added, false otherwise.
   259    */
   260   hasProgram: function(programActor) {
   261     return !!this.attachments.filter(e => e.programActor == programActor).length;
   262   },
   264   /**
   265    * The select listener for the programs container.
   266    */
   267   _onProgramSelect: function({ detail: sourceItem }) {
   268     if (!sourceItem) {
   269       return;
   270     }
   271     // The container is not empty and an actual item was selected.
   272     let attachment = sourceItem.attachment;
   274     function getShaders() {
   275       return promise.all([
   276         attachment.vs || (attachment.vs = attachment.programActor.getVertexShader()),
   277         attachment.fs || (attachment.fs = attachment.programActor.getFragmentShader())
   278       ]);
   279     }
   280     function getSources([vertexShaderActor, fragmentShaderActor]) {
   281       return promise.all([
   282         vertexShaderActor.getText(),
   283         fragmentShaderActor.getText()
   284       ]);
   285     }
   286     function showSources([vertexShaderText, fragmentShaderText]) {
   287       return ShadersEditorsView.setText({
   288         vs: vertexShaderText,
   289         fs: fragmentShaderText
   290       });
   291     }
   293     getShaders()
   294       .then(getSources)
   295       .then(showSources)
   296       .then(null, Cu.reportError);
   297   },
   299   /**
   300    * The check listener for the programs container.
   301    */
   302   _onProgramCheck: function({ detail: { checked }, target }) {
   303     let sourceItem = this.getItemForElement(target);
   304     let attachment = sourceItem.attachment;
   305     attachment.isBlackBoxed = !checked;
   306     attachment.programActor[checked ? "unblackbox" : "blackbox"]();
   307   },
   309   /**
   310    * The mouseenter listener for the programs container.
   311    */
   312   _onProgramMouseEnter: function(e) {
   313     let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
   314     if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
   315       sourceItem.attachment.programActor.highlight(HIGHLIGHT_TINT);
   317       if (e instanceof Event) {
   318         e.preventDefault();
   319         e.stopPropagation();
   320       }
   321     }
   322   },
   324   /**
   325    * The mouseleave listener for the programs container.
   326    */
   327   _onProgramMouseLeave: function(e) {
   328     let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
   329     if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
   330       sourceItem.attachment.programActor.unhighlight();
   332       if (e instanceof Event) {
   333         e.preventDefault();
   334         e.stopPropagation();
   335       }
   336     }
   337   }
   338 });
   340 /**
   341  * Functions handling the editors displaying the vertex and fragment shaders.
   342  */
   343 let ShadersEditorsView = {
   344   /**
   345    * Initialization function, called when the tool is started.
   346    */
   347   initialize: function() {
   348     XPCOMUtils.defineLazyGetter(this, "_editorPromises", () => new Map());
   349     this._vsFocused = this._onFocused.bind(this, "vs", "fs");
   350     this._fsFocused = this._onFocused.bind(this, "fs", "vs");
   351     this._vsChanged = this._onChanged.bind(this, "vs");
   352     this._fsChanged = this._onChanged.bind(this, "fs");
   353   },
   355   /**
   356    * Destruction function, called when the tool is closed.
   357    */
   358   destroy: function() {
   359     this._toggleListeners("off");
   360   },
   362   /**
   363    * Sets the text displayed in the vertex and fragment shader editors.
   364    *
   365    * @param object sources
   366    *        An object containing the following properties
   367    *          - vs: the vertex shader source code
   368    *          - fs: the fragment shader source code
   369    * @return object
   370    *        A promise resolving upon completion of text setting.
   371    */
   372   setText: function(sources) {
   373     let view = this;
   374     function setTextAndClearHistory(editor, text) {
   375       editor.setText(text);
   376       editor.clearHistory();
   377     }
   379     return Task.spawn(function() {
   380       yield view._toggleListeners("off");
   381       yield promise.all([
   382         view._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)),
   383         view._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs))
   384       ]);
   385       yield view._toggleListeners("on");
   386     }).then(() => window.emit(EVENTS.SOURCES_SHOWN, sources));
   387   },
   389   /**
   390    * Lazily initializes and returns a promise for an Editor instance.
   391    *
   392    * @param string type
   393    *        Specifies for which shader type should an editor be retrieved,
   394    *        either are "vs" for a vertex, or "fs" for a fragment shader.
   395    * @return object
   396    *        Returns a promise that resolves to an editor instance
   397    */
   398   _getEditor: function(type) {
   399     if ($("#content").hidden) {
   400       return promise.reject(new Error("Shader Editor is still waiting for a WebGL context to be created."));
   401     }
   402     if (this._editorPromises.has(type)) {
   403       return this._editorPromises.get(type);
   404     }
   406     let deferred = promise.defer();
   407     this._editorPromises.set(type, deferred.promise);
   409     // Initialize the source editor and store the newly created instance
   410     // in the ether of a resolved promise's value.
   411     let parent = $("#" + type +"-editor");
   412     let editor = new Editor(DEFAULT_EDITOR_CONFIG);
   413     editor.config.mode = Editor.modes[type];
   414     editor.appendTo(parent).then(() => deferred.resolve(editor));
   416     return deferred.promise;
   417   },
   419   /**
   420    * Toggles all the event listeners for the editors either on or off.
   421    *
   422    * @param string flag
   423    *        Either "on" to enable the event listeners, "off" to disable them.
   424    * @return object
   425    *        A promise resolving upon completion of toggling the listeners.
   426    */
   427   _toggleListeners: function(flag) {
   428     return promise.all(["vs", "fs"].map(type => {
   429       return this._getEditor(type).then(editor => {
   430         editor[flag]("focus", this["_" + type + "Focused"]);
   431         editor[flag]("change", this["_" + type + "Changed"]);
   432       });
   433     }));
   434   },
   436   /**
   437    * The focus listener for a source editor.
   438    *
   439    * @param string focused
   440    *        The corresponding shader type for the focused editor (e.g. "vs").
   441    * @param string focused
   442    *        The corresponding shader type for the other editor (e.g. "fs").
   443    */
   444   _onFocused: function(focused, unfocused) {
   445     $("#" + focused + "-editor-label").setAttribute("selected", "");
   446     $("#" + unfocused + "-editor-label").removeAttribute("selected");
   447   },
   449   /**
   450    * The change listener for a source editor.
   451    *
   452    * @param string type
   453    *        The corresponding shader type for the focused editor (e.g. "vs").
   454    */
   455   _onChanged: function(type) {
   456     setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type));
   458     // Remove all the gutter markers and line classes from the editor.
   459     this._cleanEditor(type);
   460   },
   462   /**
   463    * Recompiles the source code for the shader being edited.
   464    * This function is fired at a certain delay after the user stops typing.
   465    *
   466    * @param string type
   467    *        The corresponding shader type for the focused editor (e.g. "vs").
   468    */
   469   _doCompile: function(type) {
   470     Task.spawn(function() {
   471       let editor = yield this._getEditor(type);
   472       let shaderActor = yield ShadersListView.selectedAttachment[type];
   474       try {
   475         yield shaderActor.compile(editor.getText());
   476         this._onSuccessfulCompilation();
   477       } catch (e) {
   478         this._onFailedCompilation(type, editor, e);
   479       }
   480     }.bind(this));
   481   },
   483   /**
   484    * Called uppon a successful shader compilation.
   485    */
   486   _onSuccessfulCompilation: function() {
   487     // Signal that the shader was compiled successfully.
   488     window.emit(EVENTS.SHADER_COMPILED, null);
   489   },
   491   /**
   492    * Called uppon an unsuccessful shader compilation.
   493    */
   494   _onFailedCompilation: function(type, editor, errors) {
   495     let lineCount = editor.lineCount();
   496     let currentLine = editor.getCursor().line;
   497     let listeners = { mouseenter: this._onMarkerMouseEnter };
   499     function matchLinesAndMessages(string) {
   500       return {
   501         // First number that is not equal to 0.
   502         lineMatch: string.match(/\d{2,}|[1-9]/),
   503         // The string after all the numbers, semicolons and spaces.
   504         textMatch: string.match(/[^\s\d:][^\r\n|]*/)
   505       };
   506     }
   507     function discardInvalidMatches(e) {
   508       // Discard empty line and text matches.
   509       return e.lineMatch && e.textMatch;
   510     }
   511     function sanitizeValidMatches(e) {
   512       return {
   513         // Drivers might yield confusing line numbers under some obscure
   514         // circumstances. Don't throw the errors away in those cases,
   515         // just display them on the currently edited line.
   516         line: e.lineMatch[0] > lineCount ? currentLine : e.lineMatch[0] - 1,
   517         // Trim whitespace from the beginning and the end of the message,
   518         // and replace all other occurences of double spaces to a single space.
   519         text: e.textMatch[0].trim().replace(/\s{2,}/g, " ")
   520       };
   521     }
   522     function sortByLine(first, second) {
   523       // Sort all the errors ascending by their corresponding line number.
   524       return first.line > second.line ? 1 : -1;
   525     }
   526     function groupSameLineMessages(accumulator, current) {
   527       // Group errors corresponding to the same line number to a single object.
   528       let previous = accumulator[accumulator.length - 1];
   529       if (!previous || previous.line != current.line) {
   530         return [...accumulator, {
   531           line: current.line,
   532           messages: [current.text]
   533         }];
   534       } else {
   535         previous.messages.push(current.text);
   536         return accumulator;
   537       }
   538     }
   539     function displayErrors({ line, messages }) {
   540       // Add gutter markers and line classes for every error in the source.
   541       editor.addMarker(line, "errors", "error");
   542       editor.setMarkerListeners(line, "errors", "error", listeners, messages);
   543       editor.addLineClass(line, "error-line");
   544     }
   546     (this._errors[type] = errors.link
   547       .split("ERROR")
   548       .map(matchLinesAndMessages)
   549       .filter(discardInvalidMatches)
   550       .map(sanitizeValidMatches)
   551       .sort(sortByLine)
   552       .reduce(groupSameLineMessages, []))
   553       .forEach(displayErrors);
   555     // Signal that the shader wasn't compiled successfully.
   556     window.emit(EVENTS.SHADER_COMPILED, errors);
   557   },
   559   /**
   560    * Event listener for the 'mouseenter' event on a marker in the editor gutter.
   561    */
   562   _onMarkerMouseEnter: function(line, node, messages) {
   563     if (node._markerErrorsTooltip) {
   564       return;
   565     }
   567     let tooltip = node._markerErrorsTooltip = new Tooltip(document);
   568     tooltip.defaultOffsetX = GUTTER_ERROR_PANEL_OFFSET_X;
   569     tooltip.setTextContent({ messages: messages });
   570     tooltip.startTogglingOnHover(node, () => true, GUTTER_ERROR_PANEL_DELAY);
   571   },
   573   /**
   574    * Removes all the gutter markers and line classes from the editor.
   575    */
   576   _cleanEditor: function(type) {
   577     this._getEditor(type).then(editor => {
   578       editor.removeAllMarkers("errors");
   579       this._errors[type].forEach(e => editor.removeLineClass(e.line));
   580       this._errors[type].length = 0;
   581       window.emit(EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
   582     });
   583   },
   585   _errors: {
   586     vs: [],
   587     fs: []
   588   }
   589 };
   591 /**
   592  * Localization convenience methods.
   593  */
   594 let L10N = new ViewHelpers.L10N(STRINGS_URI);
   596 /**
   597  * Convenient way of emitting events from the panel window.
   598  */
   599 EventEmitter.decorate(this);
   601 /**
   602  * DOM query helper.
   603  */
   604 function $(selector, target = document) target.querySelector(selector);

mercurial