|
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 "use strict"; |
|
7 |
|
8 const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes |
|
9 const SOURCE_URL_DEFAULT_MAX_LENGTH = 64; // chars |
|
10 const STACK_FRAMES_SOURCE_URL_MAX_LENGTH = 15; // chars |
|
11 const STACK_FRAMES_SOURCE_URL_TRIM_SECTION = "center"; |
|
12 const STACK_FRAMES_SCROLL_DELAY = 100; // ms |
|
13 const BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH = 1000; // chars |
|
14 const BREAKPOINT_CONDITIONAL_POPUP_POSITION = "before_start"; |
|
15 const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X = 7; // px |
|
16 const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y = -3; // px |
|
17 const RESULTS_PANEL_POPUP_POSITION = "before_end"; |
|
18 const RESULTS_PANEL_MAX_RESULTS = 10; |
|
19 const FILE_SEARCH_ACTION_MAX_DELAY = 300; // ms |
|
20 const GLOBAL_SEARCH_EXPAND_MAX_RESULTS = 50; |
|
21 const GLOBAL_SEARCH_LINE_MAX_LENGTH = 300; // chars |
|
22 const GLOBAL_SEARCH_ACTION_MAX_DELAY = 1500; // ms |
|
23 const FUNCTION_SEARCH_ACTION_MAX_DELAY = 400; // ms |
|
24 const SEARCH_GLOBAL_FLAG = "!"; |
|
25 const SEARCH_FUNCTION_FLAG = "@"; |
|
26 const SEARCH_TOKEN_FLAG = "#"; |
|
27 const SEARCH_LINE_FLAG = ":"; |
|
28 const SEARCH_VARIABLE_FLAG = "*"; |
|
29 const SEARCH_AUTOFILL = [SEARCH_GLOBAL_FLAG, SEARCH_FUNCTION_FLAG, SEARCH_TOKEN_FLAG]; |
|
30 const EDITOR_VARIABLE_HOVER_DELAY = 350; // ms |
|
31 const EDITOR_VARIABLE_POPUP_POSITION = "topcenter bottomleft"; |
|
32 const TOOLBAR_ORDER_POPUP_POSITION = "topcenter bottomleft"; |
|
33 |
|
34 /** |
|
35 * Object defining the debugger view components. |
|
36 */ |
|
37 let DebuggerView = { |
|
38 /** |
|
39 * Initializes the debugger view. |
|
40 * |
|
41 * @return object |
|
42 * A promise that is resolved when the view finishes initializing. |
|
43 */ |
|
44 initialize: function() { |
|
45 if (this._startup) { |
|
46 return this._startup; |
|
47 } |
|
48 |
|
49 let deferred = promise.defer(); |
|
50 this._startup = deferred.promise; |
|
51 |
|
52 this._initializePanes(); |
|
53 this.Toolbar.initialize(); |
|
54 this.Options.initialize(); |
|
55 this.Filtering.initialize(); |
|
56 this.FilteredSources.initialize(); |
|
57 this.FilteredFunctions.initialize(); |
|
58 this.StackFrames.initialize(); |
|
59 this.StackFramesClassicList.initialize(); |
|
60 this.Sources.initialize(); |
|
61 this.VariableBubble.initialize(); |
|
62 this.Tracer.initialize(); |
|
63 this.WatchExpressions.initialize(); |
|
64 this.EventListeners.initialize(); |
|
65 this.GlobalSearch.initialize(); |
|
66 this._initializeVariablesView(); |
|
67 this._initializeEditor(deferred.resolve); |
|
68 |
|
69 document.title = L10N.getStr("DebuggerWindowTitle"); |
|
70 |
|
71 return deferred.promise; |
|
72 }, |
|
73 |
|
74 /** |
|
75 * Destroys the debugger view. |
|
76 * |
|
77 * @return object |
|
78 * A promise that is resolved when the view finishes destroying. |
|
79 */ |
|
80 destroy: function() { |
|
81 if (this._shutdown) { |
|
82 return this._shutdown; |
|
83 } |
|
84 |
|
85 let deferred = promise.defer(); |
|
86 this._shutdown = deferred.promise; |
|
87 |
|
88 this.Toolbar.destroy(); |
|
89 this.Options.destroy(); |
|
90 this.Filtering.destroy(); |
|
91 this.FilteredSources.destroy(); |
|
92 this.FilteredFunctions.destroy(); |
|
93 this.StackFrames.destroy(); |
|
94 this.StackFramesClassicList.destroy(); |
|
95 this.Sources.destroy(); |
|
96 this.VariableBubble.destroy(); |
|
97 this.Tracer.destroy(); |
|
98 this.WatchExpressions.destroy(); |
|
99 this.EventListeners.destroy(); |
|
100 this.GlobalSearch.destroy(); |
|
101 this._destroyPanes(); |
|
102 this._destroyEditor(deferred.resolve); |
|
103 |
|
104 return deferred.promise; |
|
105 }, |
|
106 |
|
107 /** |
|
108 * Initializes the UI for all the displayed panes. |
|
109 */ |
|
110 _initializePanes: function() { |
|
111 dumpn("Initializing the DebuggerView panes"); |
|
112 |
|
113 this._body = document.getElementById("body"); |
|
114 this._editorDeck = document.getElementById("editor-deck"); |
|
115 this._sourcesPane = document.getElementById("sources-pane"); |
|
116 this._instrumentsPane = document.getElementById("instruments-pane"); |
|
117 this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle"); |
|
118 |
|
119 this.showEditor = this.showEditor.bind(this); |
|
120 this.showBlackBoxMessage = this.showBlackBoxMessage.bind(this); |
|
121 this.showProgressBar = this.showProgressBar.bind(this); |
|
122 this.maybeShowBlackBoxMessage = this.maybeShowBlackBoxMessage.bind(this); |
|
123 |
|
124 this._onTabSelect = this._onInstrumentsPaneTabSelect.bind(this); |
|
125 this._instrumentsPane.tabpanels.addEventListener("select", this._onTabSelect); |
|
126 |
|
127 this._collapsePaneString = L10N.getStr("collapsePanes"); |
|
128 this._expandPaneString = L10N.getStr("expandPanes"); |
|
129 |
|
130 this._sourcesPane.setAttribute("width", Prefs.sourcesWidth); |
|
131 this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth); |
|
132 this.toggleInstrumentsPane({ visible: Prefs.panesVisibleOnStartup }); |
|
133 |
|
134 // Side hosts requires a different arrangement of the debugger widgets. |
|
135 if (gHostType == "side") { |
|
136 this.handleHostChanged(gHostType); |
|
137 } |
|
138 }, |
|
139 |
|
140 /** |
|
141 * Destroys the UI for all the displayed panes. |
|
142 */ |
|
143 _destroyPanes: function() { |
|
144 dumpn("Destroying the DebuggerView panes"); |
|
145 |
|
146 if (gHostType != "side") { |
|
147 Prefs.sourcesWidth = this._sourcesPane.getAttribute("width"); |
|
148 Prefs.instrumentsWidth = this._instrumentsPane.getAttribute("width"); |
|
149 } |
|
150 |
|
151 this._sourcesPane = null; |
|
152 this._instrumentsPane = null; |
|
153 this._instrumentsPaneToggleButton = null; |
|
154 }, |
|
155 |
|
156 /** |
|
157 * Initializes the VariablesView instance and attaches a controller. |
|
158 */ |
|
159 _initializeVariablesView: function() { |
|
160 this.Variables = new VariablesView(document.getElementById("variables"), { |
|
161 searchPlaceholder: L10N.getStr("emptyVariablesFilterText"), |
|
162 emptyText: L10N.getStr("emptyVariablesText"), |
|
163 onlyEnumVisible: Prefs.variablesOnlyEnumVisible, |
|
164 searchEnabled: Prefs.variablesSearchboxVisible, |
|
165 eval: (variable, value) => { |
|
166 let string = variable.evaluationMacro(variable, value); |
|
167 DebuggerController.StackFrames.evaluate(string); |
|
168 }, |
|
169 lazyEmpty: true |
|
170 }); |
|
171 |
|
172 // Attach the current toolbox to the VView so it can link DOMNodes to |
|
173 // the inspector/highlighter |
|
174 this.Variables.toolbox = DebuggerController._toolbox; |
|
175 |
|
176 // Attach a controller that handles interfacing with the debugger protocol. |
|
177 VariablesViewController.attach(this.Variables, { |
|
178 getEnvironmentClient: aObject => gThreadClient.environment(aObject), |
|
179 getObjectClient: aObject => { |
|
180 return aObject instanceof DebuggerController.Tracer.WrappedObject |
|
181 ? DebuggerController.Tracer.syncGripClient(aObject.object) |
|
182 : gThreadClient.pauseGrip(aObject) |
|
183 } |
|
184 }); |
|
185 |
|
186 // Relay events from the VariablesView. |
|
187 this.Variables.on("fetched", (aEvent, aType) => { |
|
188 switch (aType) { |
|
189 case "scopes": |
|
190 window.emit(EVENTS.FETCHED_SCOPES); |
|
191 break; |
|
192 case "variables": |
|
193 window.emit(EVENTS.FETCHED_VARIABLES); |
|
194 break; |
|
195 case "properties": |
|
196 window.emit(EVENTS.FETCHED_PROPERTIES); |
|
197 break; |
|
198 } |
|
199 }); |
|
200 }, |
|
201 |
|
202 /** |
|
203 * Initializes the Editor instance. |
|
204 * |
|
205 * @param function aCallback |
|
206 * Called after the editor finishes initializing. |
|
207 */ |
|
208 _initializeEditor: function(aCallback) { |
|
209 dumpn("Initializing the DebuggerView editor"); |
|
210 |
|
211 let extraKeys = {}; |
|
212 bindKey("_doTokenSearch", "tokenSearchKey"); |
|
213 bindKey("_doGlobalSearch", "globalSearchKey", { alt: true }); |
|
214 bindKey("_doFunctionSearch", "functionSearchKey"); |
|
215 extraKeys[Editor.keyFor("jumpToLine")] = false; |
|
216 |
|
217 function bindKey(func, key, modifiers = {}) { |
|
218 let key = document.getElementById(key).getAttribute("key"); |
|
219 let shortcut = Editor.accel(key, modifiers); |
|
220 extraKeys[shortcut] = () => DebuggerView.Filtering[func](); |
|
221 } |
|
222 |
|
223 this.editor = new Editor({ |
|
224 mode: Editor.modes.text, |
|
225 readOnly: true, |
|
226 lineNumbers: true, |
|
227 showAnnotationRuler: true, |
|
228 gutters: [ "breakpoints" ], |
|
229 extraKeys: extraKeys, |
|
230 contextMenu: "sourceEditorContextMenu" |
|
231 }); |
|
232 |
|
233 this.editor.appendTo(document.getElementById("editor")).then(() => { |
|
234 this.editor.extend(DebuggerEditor); |
|
235 this._loadingText = L10N.getStr("loadingText"); |
|
236 this._onEditorLoad(aCallback); |
|
237 }); |
|
238 |
|
239 this.editor.on("gutterClick", (ev, line) => { |
|
240 if (this.editor.hasBreakpoint(line)) { |
|
241 this.editor.removeBreakpoint(line); |
|
242 } else { |
|
243 this.editor.addBreakpoint(line); |
|
244 } |
|
245 }); |
|
246 }, |
|
247 |
|
248 /** |
|
249 * The load event handler for the source editor, also executing any necessary |
|
250 * post-load operations. |
|
251 * |
|
252 * @param function aCallback |
|
253 * Called after the editor finishes loading. |
|
254 */ |
|
255 _onEditorLoad: function(aCallback) { |
|
256 dumpn("Finished loading the DebuggerView editor"); |
|
257 |
|
258 DebuggerController.Breakpoints.initialize().then(() => { |
|
259 window.emit(EVENTS.EDITOR_LOADED, this.editor); |
|
260 aCallback(); |
|
261 }); |
|
262 }, |
|
263 |
|
264 /** |
|
265 * Destroys the Editor instance and also executes any necessary |
|
266 * post-unload operations. |
|
267 * |
|
268 * @param function aCallback |
|
269 * Called after the editor finishes destroying. |
|
270 */ |
|
271 _destroyEditor: function(aCallback) { |
|
272 dumpn("Destroying the DebuggerView editor"); |
|
273 |
|
274 DebuggerController.Breakpoints.destroy().then(() => { |
|
275 window.emit(EVENTS.EDITOR_UNLOADED, this.editor); |
|
276 this.editor.destroy(); |
|
277 this.editor = null; |
|
278 aCallback(); |
|
279 }); |
|
280 }, |
|
281 |
|
282 /** |
|
283 * Display the source editor. |
|
284 */ |
|
285 showEditor: function() { |
|
286 this._editorDeck.selectedIndex = 0; |
|
287 }, |
|
288 |
|
289 /** |
|
290 * Display the black box message. |
|
291 */ |
|
292 showBlackBoxMessage: function() { |
|
293 this._editorDeck.selectedIndex = 1; |
|
294 }, |
|
295 |
|
296 /** |
|
297 * Display the progress bar. |
|
298 */ |
|
299 showProgressBar: function() { |
|
300 this._editorDeck.selectedIndex = 2; |
|
301 }, |
|
302 |
|
303 /** |
|
304 * Show or hide the black box message vs. source editor depending on if the |
|
305 * selected source is black boxed or not. |
|
306 */ |
|
307 maybeShowBlackBoxMessage: function() { |
|
308 let { source } = DebuggerView.Sources.selectedItem.attachment; |
|
309 if (gThreadClient.source(source).isBlackBoxed) { |
|
310 this.showBlackBoxMessage(); |
|
311 } else { |
|
312 this.showEditor(); |
|
313 } |
|
314 }, |
|
315 |
|
316 /** |
|
317 * Sets the currently displayed text contents in the source editor. |
|
318 * This resets the mode and undo stack. |
|
319 * |
|
320 * @param string aTextContent |
|
321 * The source text content. |
|
322 */ |
|
323 _setEditorText: function(aTextContent = "") { |
|
324 this.editor.setMode(Editor.modes.text); |
|
325 this.editor.setText(aTextContent); |
|
326 this.editor.clearDebugLocation(); |
|
327 this.editor.clearHistory(); |
|
328 }, |
|
329 |
|
330 /** |
|
331 * Sets the proper editor mode (JS or HTML) according to the specified |
|
332 * content type, or by determining the type from the url or text content. |
|
333 * |
|
334 * @param string aUrl |
|
335 * The source url. |
|
336 * @param string aContentType [optional] |
|
337 * The source content type. |
|
338 * @param string aTextContent [optional] |
|
339 * The source text content. |
|
340 */ |
|
341 _setEditorMode: function(aUrl, aContentType = "", aTextContent = "") { |
|
342 // Avoid setting the editor mode for very large files. |
|
343 // Is this still necessary? See bug 929225. |
|
344 if (aTextContent.length >= SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) { |
|
345 return void this.editor.setMode(Editor.modes.text); |
|
346 } |
|
347 |
|
348 // Use JS mode for files with .js and .jsm extensions. |
|
349 if (SourceUtils.isJavaScript(aUrl, aContentType)) { |
|
350 return void this.editor.setMode(Editor.modes.js); |
|
351 } |
|
352 |
|
353 // Use HTML mode for files in which the first non whitespace character is |
|
354 // <, regardless of extension. |
|
355 if (aTextContent.match(/^\s*</)) { |
|
356 return void this.editor.setMode(Editor.modes.html); |
|
357 } |
|
358 |
|
359 // Unknown language, use text. |
|
360 this.editor.setMode(Editor.modes.text); |
|
361 }, |
|
362 |
|
363 /** |
|
364 * Sets the currently displayed source text in the editor. |
|
365 * |
|
366 * You should use DebuggerView.updateEditor instead. It updates the current |
|
367 * caret and debug location based on a requested url and line. |
|
368 * |
|
369 * @param object aSource |
|
370 * The source object coming from the active thread. |
|
371 * @param object aFlags |
|
372 * Additional options for setting the source. Supported options: |
|
373 * - force: boolean allowing whether we can get the selected url's |
|
374 * text again. |
|
375 * @return object |
|
376 * A promise that is resolved after the source text has been set. |
|
377 */ |
|
378 _setEditorSource: function(aSource, aFlags={}) { |
|
379 // Avoid setting the same source text in the editor again. |
|
380 if (this._editorSource.url == aSource.url && !aFlags.force) { |
|
381 return this._editorSource.promise; |
|
382 } |
|
383 let transportType = gClient.localTransport ? "_LOCAL" : "_REMOTE"; |
|
384 let histogramId = "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE" + transportType + "_MS"; |
|
385 let histogram = Services.telemetry.getHistogramById(histogramId); |
|
386 let startTime = Date.now(); |
|
387 |
|
388 let deferred = promise.defer(); |
|
389 |
|
390 this._setEditorText(L10N.getStr("loadingText")); |
|
391 this._editorSource = { url: aSource.url, promise: deferred.promise }; |
|
392 |
|
393 DebuggerController.SourceScripts.getText(aSource).then(([, aText, aContentType]) => { |
|
394 // Avoid setting an unexpected source. This may happen when switching |
|
395 // very fast between sources that haven't been fetched yet. |
|
396 if (this._editorSource.url != aSource.url) { |
|
397 return; |
|
398 } |
|
399 |
|
400 this._setEditorText(aText); |
|
401 this._setEditorMode(aSource.url, aContentType, aText); |
|
402 |
|
403 // Synchronize any other components with the currently displayed source. |
|
404 DebuggerView.Sources.selectedValue = aSource.url; |
|
405 DebuggerController.Breakpoints.updateEditorBreakpoints(); |
|
406 |
|
407 histogram.add(Date.now() - startTime); |
|
408 |
|
409 // Resolve and notify that a source file was shown. |
|
410 window.emit(EVENTS.SOURCE_SHOWN, aSource); |
|
411 deferred.resolve([aSource, aText, aContentType]); |
|
412 }, |
|
413 ([, aError]) => { |
|
414 let msg = L10N.getStr("errorLoadingText") + DevToolsUtils.safeErrorString(aError); |
|
415 this._setEditorText(msg); |
|
416 Cu.reportError(msg); |
|
417 dumpn(msg); |
|
418 |
|
419 // Reject and notify that there was an error showing the source file. |
|
420 window.emit(EVENTS.SOURCE_ERROR_SHOWN, aSource); |
|
421 deferred.reject([aSource, aError]); |
|
422 }); |
|
423 |
|
424 return deferred.promise; |
|
425 }, |
|
426 |
|
427 /** |
|
428 * Update the source editor's current caret and debug location based on |
|
429 * a requested url and line. |
|
430 * |
|
431 * @param string aUrl |
|
432 * The target source url. |
|
433 * @param number aLine [optional] |
|
434 * The target line in the source. |
|
435 * @param object aFlags [optional] |
|
436 * Additional options for showing the source. Supported options: |
|
437 * - charOffset: character offset for the caret or debug location |
|
438 * - lineOffset: line offset for the caret or debug location |
|
439 * - columnOffset: column offset for the caret or debug location |
|
440 * - noCaret: don't set the caret location at the specified line |
|
441 * - noDebug: don't set the debug location at the specified line |
|
442 * - align: string specifying whether to align the specified line |
|
443 * at the "top", "center" or "bottom" of the editor |
|
444 * - force: boolean allowing whether we can get the selected url's |
|
445 * text again |
|
446 * @return object |
|
447 * A promise that is resolved after the source text has been set. |
|
448 */ |
|
449 setEditorLocation: function(aUrl, aLine = 0, aFlags = {}) { |
|
450 // Avoid trying to set a source for a url that isn't known yet. |
|
451 if (!this.Sources.containsValue(aUrl)) { |
|
452 return promise.reject(new Error("Unknown source for the specified URL.")); |
|
453 } |
|
454 |
|
455 // If the line is not specified, default to the current frame's position, |
|
456 // if available and the frame's url corresponds to the requested url. |
|
457 if (!aLine) { |
|
458 let cachedFrames = DebuggerController.activeThread.cachedFrames; |
|
459 let currentDepth = DebuggerController.StackFrames.currentFrameDepth; |
|
460 let frame = cachedFrames[currentDepth]; |
|
461 if (frame && frame.where.url == aUrl) { |
|
462 aLine = frame.where.line; |
|
463 } |
|
464 } |
|
465 |
|
466 let sourceItem = this.Sources.getItemByValue(aUrl); |
|
467 let sourceForm = sourceItem.attachment.source; |
|
468 |
|
469 // Make sure the requested source client is shown in the editor, then |
|
470 // update the source editor's caret position and debug location. |
|
471 return this._setEditorSource(sourceForm, aFlags).then(([,, aContentType]) => { |
|
472 // Record the contentType learned from fetching |
|
473 sourceForm.contentType = aContentType; |
|
474 // Line numbers in the source editor should start from 1. If invalid |
|
475 // or not specified, then don't do anything. |
|
476 if (aLine < 1) { |
|
477 window.emit(EVENTS.EDITOR_LOCATION_SET); |
|
478 return; |
|
479 } |
|
480 if (aFlags.charOffset) { |
|
481 aLine += this.editor.getPosition(aFlags.charOffset).line; |
|
482 } |
|
483 if (aFlags.lineOffset) { |
|
484 aLine += aFlags.lineOffset; |
|
485 } |
|
486 if (!aFlags.noCaret) { |
|
487 let location = { line: aLine -1, ch: aFlags.columnOffset || 0 }; |
|
488 this.editor.setCursor(location, aFlags.align); |
|
489 } |
|
490 if (!aFlags.noDebug) { |
|
491 this.editor.setDebugLocation(aLine - 1); |
|
492 } |
|
493 window.emit(EVENTS.EDITOR_LOCATION_SET); |
|
494 }).then(null, console.error); |
|
495 }, |
|
496 |
|
497 /** |
|
498 * Gets the visibility state of the instruments pane. |
|
499 * @return boolean |
|
500 */ |
|
501 get instrumentsPaneHidden() |
|
502 this._instrumentsPane.hasAttribute("pane-collapsed"), |
|
503 |
|
504 /** |
|
505 * Gets the currently selected tab in the instruments pane. |
|
506 * @return string |
|
507 */ |
|
508 get instrumentsPaneTab() |
|
509 this._instrumentsPane.selectedTab.id, |
|
510 |
|
511 /** |
|
512 * Sets the instruments pane hidden or visible. |
|
513 * |
|
514 * @param object aFlags |
|
515 * An object containing some of the following properties: |
|
516 * - visible: true if the pane should be shown, false to hide |
|
517 * - animated: true to display an animation on toggle |
|
518 * - delayed: true to wait a few cycles before toggle |
|
519 * - callback: a function to invoke when the toggle finishes |
|
520 * @param number aTabIndex [optional] |
|
521 * The index of the intended selected tab in the details pane. |
|
522 */ |
|
523 toggleInstrumentsPane: function(aFlags, aTabIndex) { |
|
524 let pane = this._instrumentsPane; |
|
525 let button = this._instrumentsPaneToggleButton; |
|
526 |
|
527 ViewHelpers.togglePane(aFlags, pane); |
|
528 |
|
529 if (aFlags.visible) { |
|
530 button.removeAttribute("pane-collapsed"); |
|
531 button.setAttribute("tooltiptext", this._collapsePaneString); |
|
532 } else { |
|
533 button.setAttribute("pane-collapsed", ""); |
|
534 button.setAttribute("tooltiptext", this._expandPaneString); |
|
535 } |
|
536 |
|
537 if (aTabIndex !== undefined) { |
|
538 pane.selectedIndex = aTabIndex; |
|
539 } |
|
540 }, |
|
541 |
|
542 /** |
|
543 * Sets the instruments pane visible after a short period of time. |
|
544 * |
|
545 * @param function aCallback |
|
546 * A function to invoke when the toggle finishes. |
|
547 */ |
|
548 showInstrumentsPane: function(aCallback) { |
|
549 DebuggerView.toggleInstrumentsPane({ |
|
550 visible: true, |
|
551 animated: true, |
|
552 delayed: true, |
|
553 callback: aCallback |
|
554 }, 0); |
|
555 }, |
|
556 |
|
557 /** |
|
558 * Handles a tab selection event on the instruments pane. |
|
559 */ |
|
560 _onInstrumentsPaneTabSelect: function() { |
|
561 if (this._instrumentsPane.selectedTab.id == "events-tab") { |
|
562 DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch(); |
|
563 } |
|
564 }, |
|
565 |
|
566 /** |
|
567 * Handles a host change event issued by the parent toolbox. |
|
568 * |
|
569 * @param string aType |
|
570 * The host type, either "bottom", "side" or "window". |
|
571 */ |
|
572 handleHostChanged: function(aType) { |
|
573 let newLayout = ""; |
|
574 |
|
575 if (aType == "side") { |
|
576 newLayout = "vertical"; |
|
577 this._enterVerticalLayout(); |
|
578 } else { |
|
579 newLayout = "horizontal"; |
|
580 this._enterHorizontalLayout(); |
|
581 } |
|
582 |
|
583 this._hostType = aType; |
|
584 this._body.setAttribute("layout", newLayout); |
|
585 window.emit(EVENTS.LAYOUT_CHANGED, newLayout); |
|
586 }, |
|
587 |
|
588 /** |
|
589 * Switches the debugger widgets to a horizontal layout. |
|
590 */ |
|
591 _enterVerticalLayout: function() { |
|
592 let normContainer = document.getElementById("debugger-widgets"); |
|
593 let vertContainer = document.getElementById("vertical-layout-panes-container"); |
|
594 |
|
595 // Move the soruces and instruments panes in a different container. |
|
596 let splitter = document.getElementById("sources-and-instruments-splitter"); |
|
597 vertContainer.insertBefore(this._sourcesPane, splitter); |
|
598 vertContainer.appendChild(this._instrumentsPane); |
|
599 |
|
600 // Make sure the vertical layout container's height doesn't repeatedly |
|
601 // grow or shrink based on the displayed sources, variables etc. |
|
602 vertContainer.setAttribute("height", |
|
603 vertContainer.getBoundingClientRect().height); |
|
604 }, |
|
605 |
|
606 /** |
|
607 * Switches the debugger widgets to a vertical layout. |
|
608 */ |
|
609 _enterHorizontalLayout: function() { |
|
610 let normContainer = document.getElementById("debugger-widgets"); |
|
611 let vertContainer = document.getElementById("vertical-layout-panes-container"); |
|
612 |
|
613 // The sources and instruments pane need to be inserted at their |
|
614 // previous locations in their normal container. |
|
615 let splitter = document.getElementById("sources-and-editor-splitter"); |
|
616 normContainer.insertBefore(this._sourcesPane, splitter); |
|
617 normContainer.appendChild(this._instrumentsPane); |
|
618 |
|
619 // Revert to the preferred sources and instruments widths, because |
|
620 // they flexed in the vertical layout. |
|
621 this._sourcesPane.setAttribute("width", Prefs.sourcesWidth); |
|
622 this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth); |
|
623 }, |
|
624 |
|
625 /** |
|
626 * Handles any initialization on a tab navigation event issued by the client. |
|
627 */ |
|
628 handleTabNavigation: function() { |
|
629 dumpn("Handling tab navigation in the DebuggerView"); |
|
630 |
|
631 this.Filtering.clearSearch(); |
|
632 this.FilteredSources.clearView(); |
|
633 this.FilteredFunctions.clearView(); |
|
634 this.GlobalSearch.clearView(); |
|
635 this.StackFrames.empty(); |
|
636 this.Sources.empty(); |
|
637 this.Variables.empty(); |
|
638 this.EventListeners.empty(); |
|
639 |
|
640 if (this.editor) { |
|
641 this.editor.setMode(Editor.modes.text); |
|
642 this.editor.setText(""); |
|
643 this.editor.clearHistory(); |
|
644 this._editorSource = {}; |
|
645 } |
|
646 |
|
647 this.Sources.emptyText = L10N.getStr("loadingSourcesText"); |
|
648 }, |
|
649 |
|
650 _startup: null, |
|
651 _shutdown: null, |
|
652 Toolbar: null, |
|
653 Options: null, |
|
654 Filtering: null, |
|
655 FilteredSources: null, |
|
656 FilteredFunctions: null, |
|
657 GlobalSearch: null, |
|
658 StackFrames: null, |
|
659 Sources: null, |
|
660 Tracer: null, |
|
661 Variables: null, |
|
662 VariableBubble: null, |
|
663 WatchExpressions: null, |
|
664 EventListeners: null, |
|
665 editor: null, |
|
666 _editorSource: {}, |
|
667 _loadingText: "", |
|
668 _body: null, |
|
669 _editorDeck: null, |
|
670 _sourcesPane: null, |
|
671 _instrumentsPane: null, |
|
672 _instrumentsPaneToggleButton: null, |
|
673 _collapsePaneString: "", |
|
674 _expandPaneString: "" |
|
675 }; |
|
676 |
|
677 /** |
|
678 * A custom items container, used for displaying views like the |
|
679 * FilteredSources, FilteredFunctions etc., inheriting the generic WidgetMethods. |
|
680 */ |
|
681 function ResultsPanelContainer() { |
|
682 } |
|
683 |
|
684 ResultsPanelContainer.prototype = Heritage.extend(WidgetMethods, { |
|
685 /** |
|
686 * Sets the anchor node for this container panel. |
|
687 * @param nsIDOMNode aNode |
|
688 */ |
|
689 set anchor(aNode) { |
|
690 this._anchor = aNode; |
|
691 |
|
692 // If the anchor node is not null, create a panel to attach to the anchor |
|
693 // when showing the popup. |
|
694 if (aNode) { |
|
695 if (!this._panel) { |
|
696 this._panel = document.createElement("panel"); |
|
697 this._panel.id = "results-panel"; |
|
698 this._panel.setAttribute("level", "top"); |
|
699 this._panel.setAttribute("noautofocus", "true"); |
|
700 this._panel.setAttribute("consumeoutsideclicks", "false"); |
|
701 document.documentElement.appendChild(this._panel); |
|
702 } |
|
703 if (!this.widget) { |
|
704 this.widget = new SimpleListWidget(this._panel); |
|
705 this.autoFocusOnFirstItem = false; |
|
706 this.autoFocusOnSelection = false; |
|
707 this.maintainSelectionVisible = false; |
|
708 } |
|
709 } |
|
710 // Cleanup the anchor and remove the previously created panel. |
|
711 else { |
|
712 this._panel.remove(); |
|
713 this._panel = null; |
|
714 this.widget = null; |
|
715 } |
|
716 }, |
|
717 |
|
718 /** |
|
719 * Gets the anchor node for this container panel. |
|
720 * @return nsIDOMNode |
|
721 */ |
|
722 get anchor() { |
|
723 return this._anchor; |
|
724 }, |
|
725 |
|
726 /** |
|
727 * Sets the container panel hidden or visible. It's hidden by default. |
|
728 * @param boolean aFlag |
|
729 */ |
|
730 set hidden(aFlag) { |
|
731 if (aFlag) { |
|
732 this._panel.hidden = true; |
|
733 this._panel.hidePopup(); |
|
734 } else { |
|
735 this._panel.hidden = false; |
|
736 this._panel.openPopup(this._anchor, this.position, this.left, this.top); |
|
737 } |
|
738 }, |
|
739 |
|
740 /** |
|
741 * Gets this container's visibility state. |
|
742 * @return boolean |
|
743 */ |
|
744 get hidden() |
|
745 this._panel.state == "closed" || |
|
746 this._panel.state == "hiding", |
|
747 |
|
748 /** |
|
749 * Removes all items from this container and hides it. |
|
750 */ |
|
751 clearView: function() { |
|
752 this.hidden = true; |
|
753 this.empty(); |
|
754 }, |
|
755 |
|
756 /** |
|
757 * Selects the next found item in this container. |
|
758 * Does not change the currently focused node. |
|
759 */ |
|
760 selectNext: function() { |
|
761 let nextIndex = this.selectedIndex + 1; |
|
762 if (nextIndex >= this.itemCount) { |
|
763 nextIndex = 0; |
|
764 } |
|
765 this.selectedItem = this.getItemAtIndex(nextIndex); |
|
766 }, |
|
767 |
|
768 /** |
|
769 * Selects the previously found item in this container. |
|
770 * Does not change the currently focused node. |
|
771 */ |
|
772 selectPrev: function() { |
|
773 let prevIndex = this.selectedIndex - 1; |
|
774 if (prevIndex < 0) { |
|
775 prevIndex = this.itemCount - 1; |
|
776 } |
|
777 this.selectedItem = this.getItemAtIndex(prevIndex); |
|
778 }, |
|
779 |
|
780 /** |
|
781 * Customization function for creating an item's UI. |
|
782 * |
|
783 * @param string aLabel |
|
784 * The item's label string. |
|
785 * @param string aBeforeLabel |
|
786 * An optional string shown before the label. |
|
787 * @param string aBelowLabel |
|
788 * An optional string shown underneath the label. |
|
789 */ |
|
790 _createItemView: function(aLabel, aBelowLabel, aBeforeLabel) { |
|
791 let container = document.createElement("vbox"); |
|
792 container.className = "results-panel-item"; |
|
793 |
|
794 let firstRowLabels = document.createElement("hbox"); |
|
795 let secondRowLabels = document.createElement("hbox"); |
|
796 |
|
797 if (aBeforeLabel) { |
|
798 let beforeLabelNode = document.createElement("label"); |
|
799 beforeLabelNode.className = "plain results-panel-item-label-before"; |
|
800 beforeLabelNode.setAttribute("value", aBeforeLabel); |
|
801 firstRowLabels.appendChild(beforeLabelNode); |
|
802 } |
|
803 |
|
804 let labelNode = document.createElement("label"); |
|
805 labelNode.className = "plain results-panel-item-label"; |
|
806 labelNode.setAttribute("value", aLabel); |
|
807 firstRowLabels.appendChild(labelNode); |
|
808 |
|
809 if (aBelowLabel) { |
|
810 let belowLabelNode = document.createElement("label"); |
|
811 belowLabelNode.className = "plain results-panel-item-label-below"; |
|
812 belowLabelNode.setAttribute("value", aBelowLabel); |
|
813 secondRowLabels.appendChild(belowLabelNode); |
|
814 } |
|
815 |
|
816 container.appendChild(firstRowLabels); |
|
817 container.appendChild(secondRowLabels); |
|
818 |
|
819 return container; |
|
820 }, |
|
821 |
|
822 _anchor: null, |
|
823 _panel: null, |
|
824 position: RESULTS_PANEL_POPUP_POSITION, |
|
825 left: 0, |
|
826 top: 0 |
|
827 }); |