Wed, 31 Dec 2014 06:09:35 +0100
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);