Adds an overlay window that covers the scripts panel and
[WebKit-https.git] / WebCore / page / inspector / ScriptsPanel.js
1 /*
2  * Copyright (C) 2008 Apple Inc. All Rights Reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.ScriptsPanel = function()
27 {
28     WebInspector.Panel.call(this);
29
30     this.element.addStyleClass("scripts");
31
32     this.topStatusBar = document.createElement("div");
33     this.topStatusBar.className = "status-bar";
34     this.topStatusBar.id = "scripts-status-bar";
35     this.element.appendChild(this.topStatusBar);
36
37     this.backButton = document.createElement("button");
38     this.backButton.className = "status-bar-item";
39     this.backButton.id = "scripts-back";
40     this.backButton.title = WebInspector.UIString("Show the previous script resource.");
41     this.backButton.disabled = true;
42     this.backButton.appendChild(document.createElement("img"));
43     this.topStatusBar.appendChild(this.backButton);
44
45     this.forwardButton = document.createElement("button");
46     this.forwardButton.className = "status-bar-item";
47     this.forwardButton.id = "scripts-forward";
48     this.forwardButton.title = WebInspector.UIString("Show the next script resource.");
49     this.forwardButton.disabled = true;
50     this.forwardButton.appendChild(document.createElement("img"));
51     this.topStatusBar.appendChild(this.forwardButton);
52
53     this.filesSelectElement = document.createElement("select");
54     this.filesSelectElement.className = "status-bar-item";
55     this.filesSelectElement.id = "scripts-files";
56     this.filesSelectElement.addEventListener("change", this._changeVisibleFile.bind(this), false);
57     this.topStatusBar.appendChild(this.filesSelectElement);
58
59     this.functionsSelectElement = document.createElement("select");
60     this.functionsSelectElement.className = "status-bar-item";
61     this.functionsSelectElement.id = "scripts-functions";
62     this.topStatusBar.appendChild(this.functionsSelectElement);
63
64     this.sidebarButtonsElement = document.createElement("div");
65     this.sidebarButtonsElement.id = "scripts-sidebar-buttons";
66     this.topStatusBar.appendChild(this.sidebarButtonsElement);
67
68     this.pauseButton = document.createElement("button");
69     this.pauseButton.className = "status-bar-item";
70     this.pauseButton.id = "scripts-pause";
71     this.pauseButton.title = WebInspector.UIString("Pause script execution.");
72     this.pauseButton.disabled = true;
73     this.pauseButton.appendChild(document.createElement("img"));
74     this.pauseButton.addEventListener("click", this._togglePause.bind(this), false);
75     this.sidebarButtonsElement.appendChild(this.pauseButton);
76
77     this.stepOverButton = document.createElement("button");
78     this.stepOverButton.className = "status-bar-item";
79     this.stepOverButton.id = "scripts-step-over";
80     this.stepOverButton.title = WebInspector.UIString("Step over next function call.");
81     this.stepOverButton.disabled = true;
82     this.stepOverButton.addEventListener("click", this._stepOverClicked.bind(this), false);
83     this.stepOverButton.appendChild(document.createElement("img"));
84     this.sidebarButtonsElement.appendChild(this.stepOverButton);
85
86     this.stepIntoButton = document.createElement("button");
87     this.stepIntoButton.className = "status-bar-item";
88     this.stepIntoButton.id = "scripts-step-into";
89     this.stepIntoButton.title = WebInspector.UIString("Step into next function call.");
90     this.stepIntoButton.disabled = true;
91     this.stepIntoButton.addEventListener("click", this._stepIntoClicked.bind(this), false);
92     this.stepIntoButton.appendChild(document.createElement("img"));
93     this.sidebarButtonsElement.appendChild(this.stepIntoButton);
94
95     this.stepOutButton = document.createElement("button");
96     this.stepOutButton.className = "status-bar-item";
97     this.stepOutButton.id = "scripts-step-out";
98     this.stepOutButton.title = WebInspector.UIString("Step out of current function.");
99     this.stepOutButton.disabled = true;
100     this.stepOutButton.addEventListener("click", this._stepOutClicked.bind(this), false);
101     this.stepOutButton.appendChild(document.createElement("img"));
102     this.sidebarButtonsElement.appendChild(this.stepOutButton);
103
104     this.debuggerStatusElement = document.createElement("div");
105     this.debuggerStatusElement.id = "scripts-debugger-status";
106     this.sidebarButtonsElement.appendChild(this.debuggerStatusElement);
107
108     this.scriptResourceViews = document.createElement("div");
109     this.scriptResourceViews.id = "script-resource-views";
110
111     this.sidebarElement = document.createElement("div");
112     this.sidebarElement.id = "scripts-sidebar";
113
114     this.sidebarResizeElement = document.createElement("div");
115     this.sidebarResizeElement.className = "sidebar-resizer-vertical";
116     this.sidebarResizeElement.addEventListener("mousedown", this._startSidebarResizeDrag.bind(this), false);
117
118     this.sidebarResizeWidgetElement = document.createElement("div");
119     this.sidebarResizeWidgetElement.id = "scripts-sidebar-resizer-widget";
120     this.sidebarResizeWidgetElement.addEventListener("mousedown", this._startSidebarResizeDrag.bind(this), false);
121     this.topStatusBar.appendChild(this.sidebarResizeWidgetElement);
122
123     this.sidebarPanes = {};
124     this.sidebarPanes.callstack = new WebInspector.CallStackSidebarPane();
125     this.sidebarPanes.scopechain = new WebInspector.ScopeChainSidebarPane();
126     this.sidebarPanes.breakpoints = new WebInspector.BreakpointsSidebarPane();
127
128     for (var pane in this.sidebarPanes)
129         this.sidebarElement.appendChild(this.sidebarPanes[pane].element);
130
131     this.sidebarPanes.callstack.expanded = true;
132     this.sidebarPanes.callstack.addEventListener("call frame selected", this._callFrameSelected, this);
133
134     this.sidebarPanes.scopechain.expanded = true;
135
136     this.attachOverlayElement = document.createElement("div");
137     this.attachOverlayElement.id = "scripts-attach-overlay";
138
139     var headerElement = document.createElement("h1");
140     headerElement.textContent = WebInspector.UIString("Debugging scripts requires you to attach the debugger.");
141     this.attachOverlayElement.appendChild(headerElement);
142
143     var infoElement = document.createElement("span");
144     infoElement.textContent = WebInspector.UIString("Attaching will reload the inspected page.");
145     this.attachOverlayElement.appendChild(infoElement);
146
147     this.attachOverlayElement.appendChild(document.createElement("br"));
148     this.attachOverlayElement.appendChild(document.createElement("br"));
149
150     var attachButton = document.createElement("button");
151     attachButton.textContent = WebInspector.UIString("Attach Debugger");
152     attachButton.addEventListener("click", this._toggleDebugging.bind(this), false);
153     this.attachOverlayElement.appendChild(attachButton);
154
155     this.element.appendChild(this.attachOverlayElement);
156     this.element.appendChild(this.scriptResourceViews);
157     this.element.appendChild(this.sidebarElement);
158     this.element.appendChild(this.sidebarResizeElement);
159
160     this.debuggingButton = document.createElement("button");
161     this.debuggingButton.id = "scripts-debugging-status-bar-item";
162     this.debuggingButton.className = "status-bar-item";
163     this.debuggingButton.addEventListener("click", this._toggleDebugging.bind(this), false);
164
165     this._breakpointsURLMap = {};
166
167     this.reset();
168 }
169
170 WebInspector.ScriptsPanel.prototype = {
171     toolbarItemClass: "scripts",
172
173     get toolbarItemLabel()
174     {
175         return WebInspector.UIString("Scripts");
176     },
177
178     get statusBarItems()
179     {
180         return [this.debuggingButton];
181     },
182
183     show: function()
184     {
185         WebInspector.Panel.prototype.show.call(this);
186         this.sidebarResizeElement.style.right = (this.sidebarElement.offsetWidth - 3) + "px";
187
188         if (this.visibleView) {
189             if (this.visibleView instanceof WebInspector.ResourceView)
190                 this.visibleView.headersVisible = false;
191             this.visibleView.show(this.scriptResourceViews);
192         }
193     },
194
195     addScript: function(sourceID, sourceURL, source, startingLine, errorLine, errorMessage)
196     {
197         var script = new WebInspector.Script(sourceID, sourceURL, source, startingLine, errorLine, errorMessage);
198
199         if (sourceURL in WebInspector.resourceURLMap) {
200             var resource = WebInspector.resourceURLMap[sourceURL];
201             resource.addScript(script);
202         }
203
204         if (sourceURL in this._breakpointsURLMap && sourceID) {
205             var breakpoints = this._breakpointsURLMap[sourceURL];
206             var breakpointsLength = breakpoints.length;
207             for (var i = 0; i < breakpointsLength; ++i) {
208                 var breakpoint = breakpoints[i];
209                 if (startingLine <= breakpoint.line)
210                     breakpoint.sourceID = sourceID;
211             }
212
213             InspectorController.addBreakpoint(breakpoint.sourceID, breakpoint.line);
214         }
215
216         if (sourceID)
217             this._sourceIDMap[sourceID] = (resource || script);
218
219         this._addScriptToFilesMenu(script);
220     },
221
222     addBreakpoint: function(breakpoint)
223     {
224         this.sidebarPanes.breakpoints.addBreakpoint(breakpoint);
225
226         var sourceFrame;
227         if (breakpoint.url) {
228             if (!(breakpoint.url in this._breakpointsURLMap))
229                 this._breakpointsURLMap[breakpoint.url] = [];
230             this._breakpointsURLMap[breakpoint.url].unshift(breakpoint);
231
232             if (breakpoint.url in WebInspector.resourceURLMap) {
233                 var resource = WebInspector.resourceURLMap[breakpoint.url];
234                 sourceFrame = this._sourceFrameForScriptOrResource(resource);
235             }
236         }
237
238         if (breakpoint.sourceID && !sourceFrame) {
239             var object = this._sourceIDMap[breakpoint.sourceID]
240             sourceFrame = this._sourceFrameForScriptOrResource(object);
241         }
242
243         if (sourceFrame)
244             sourceFrame.addBreakpoint(breakpoint);
245     },
246
247     removeBreakpoint: function(breakpoint)
248     {
249         this.sidebarPanes.breakpoints.removeBreakpoint(breakpoint);
250
251         var sourceFrame;
252         if (breakpoint.url && breakpoint.url in this._breakpointsURLMap) {
253             var breakpoints = this._breakpointsURLMap[breakpoint.url];
254             breakpoints.remove(breakpoint);
255             if (!breakpoints.length)
256                 delete this._breakpointsURLMap[breakpoint.url];
257
258             if (breakpoint.url in WebInspector.resourceURLMap) {
259                 var resource = WebInspector.resourceURLMap[breakpoint.url];
260                 sourceFrame = this._sourceFrameForScriptOrResource(resource);
261             }
262         }
263
264         if (breakpoint.sourceID && !sourceFrame) {
265             var object = this._sourceIDMap[breakpoint.sourceID]
266             sourceFrame = this._sourceFrameForScriptOrResource(object);
267         }
268
269         if (sourceFrame)
270             sourceFrame.removeBreakpoint(breakpoint);
271     },
272
273     debuggerPaused: function()
274     {
275         this._paused = true;
276         this._waitingToPause = false;
277         this._stepping = false;
278
279         this._updateDebuggerButtons();
280
281         var callStackPane = this.sidebarPanes.callstack;
282         var currentFrame = InspectorController.currentCallFrame();
283         callStackPane.update(currentFrame);
284         callStackPane.selectedCallFrame = currentFrame;
285     },
286
287     reset: function()
288     {
289         this.visibleView = null;
290
291         this._clearCurrentExecutionLine();
292
293         if (!InspectorController.debuggerAttached()) {
294             this._paused = false;
295             this._waitingToPause = false;
296             this._stepping = false;
297         }
298
299         this.sidebarPanes.callstack.update(null);
300         this.sidebarPanes.scopechain.update(null);
301
302         this._updateDebuggerButtons();
303
304         this.filesSelectElement.removeChildren();
305         this.functionsSelectElement.removeChildren();
306         this.scriptResourceViews.removeChildren();
307
308         if (this._sourceIDMap) {
309             for (var sourceID in this._sourceIDMap) {
310                 var object = this._sourceIDMap[sourceID];
311                 if (object instanceof WebInspector.Resource)
312                     object.removeAllScripts();
313             }
314         }
315
316         this._sourceIDMap = {};
317     },
318
319     get visibleView()
320     {
321         return this._visibleView;
322     },
323
324     set visibleView(x)
325     {
326         if (this._visibleView === x)
327             return;
328
329         if (this._visibleView)
330             this._visibleView.hide();
331
332         this._visibleView = x;
333
334         if (x)
335             x.show(this.scriptResourceViews);
336     },
337
338     showScript: function(script, line)
339     {
340         this._showScriptOrResource(script, line);
341     },
342
343     showResource: function(resource, line)
344     {
345         this._showScriptOrResource(resource, line);
346     },
347
348     scriptViewForScript: function(script)
349     {
350         if (!script)
351             return null;
352         if (!script._scriptView)
353             script._scriptView = new WebInspector.ScriptView(script);
354         return script._scriptView;
355     },
356
357     sourceFrameForScript: function(script)
358     {
359         var view = this.scriptViewForScript(script);
360         if (!view)
361             return null;
362
363         // Setting up the source frame requires that we be attached.
364         if (!this.element.parentNode)
365             this.attach();
366
367         view.setupSourceFrameIfNeeded();
368         return view.sourceFrame;
369     },
370
371     _sourceFrameForScriptOrResource: function(scriptOrResource)
372     {
373         if (scriptOrResource instanceof WebInspector.Resource)
374             return WebInspector.panels.resources.sourceFrameForResource(scriptOrResource);
375         if (scriptOrResource instanceof WebInspector.Script)
376             return this.sourceFrameForScript(scriptOrResource);
377     },
378
379     _showScriptOrResource: function(scriptOrResource, line)
380     {
381         if (!scriptOrResource)
382             return;
383
384         var view;
385         if (scriptOrResource instanceof WebInspector.Resource) {
386             view = WebInspector.panels.resources.resourceViewForResource(scriptOrResource);
387             view.headersVisible = false;
388
389             if (scriptOrResource.url in this._breakpointsURLMap) {
390                 var sourceFrame = this._sourceFrameForScriptOrResource(scriptOrResource);
391                 if (sourceFrame && !sourceFrame.breakpoints.length) {
392                     var breakpoints = this._breakpointsURLMap[scriptOrResource.url];
393                     var breakpointsLength = breakpoints.length;
394                     for (var i = 0; i < breakpointsLength; ++i)
395                         sourceFrame.addBreakpoint(breakpoints[i]);
396                 }
397             }
398         } else if (scriptOrResource instanceof WebInspector.Script)
399             view = this.scriptViewForScript(scriptOrResource);
400
401         if (!view)
402             return;
403
404         this.visibleView = view;
405
406         if (line && view.revealLine)
407             view.revealLine(line);
408
409         var select = this.filesSelectElement;
410         var options = select.options;
411         for (var i = 0; i < options.length; ++i) {
412             if (options[i].representedObject === scriptOrResource)
413                 break;
414         }
415
416         select.selectedIndex = i;
417     },
418
419     _addScriptToFilesMenu: function(script)
420     {
421         var select = this.filesSelectElement;
422         var options = select.options;
423         for (var i = 0; i < options.length; ++i) {
424             var option = options[i];
425             if (option.representedObject === (script.resource || script))
426                 return;
427         }
428
429         // FIXME: Append in some meaningful order.
430         var option = document.createElement("option");
431         option.representedObject = (script.resource || script);
432         option.text = (script.sourceURL ? script.sourceURL.trimURL(WebInspector.mainResource ? WebInspector.mainResource.domain : "") : "(eval script)");
433         select.appendChild(option);
434
435         // Call _showScriptOrResource if the option we just appended ended up being selected.
436         // This will happen for the first item added to the menu.
437         if (options[select.selectedIndex] === option)
438             this._showScriptOrResource(option.representedObject);
439     },
440
441     _removeScriptFromFilesMenu: function(script)
442     {
443         // This function assumes it is called before removeScript is called on the resource.
444         if (script.resource && script.resource.scripts.length > 1)
445             return; // The resource has more than one script, so keep the resource in the menu.
446
447         var select = this.filesSelectElement;
448         var options = select.options;
449         for (var i = 0; i < options.length; ++i) {
450             if (option.representedObject !== script.resource && option.representedObject !== script)
451                 continue;
452
453             if (select.selectedIndex === i) {
454                 // Pick the next selectedIndex. If we're at the end of the list, loop back to beginning.
455                 var nextSelectedIndex = ((select.selectedIndex + 1) >= select.options.length ? 0 : select.selectedIndex);
456             }
457
458             // Remove the option from the select
459             select.options[select.selectedIndex] = null;
460
461             if (nextSelectedIndex)
462                 select.selectedIndex = nextSelectedIndex;
463         }
464     },
465
466     _clearCurrentExecutionLine: function()
467     {
468         if (this._executionSourceFrame)
469             this._executionSourceFrame.executionLine = 0;
470         delete this._executionSourceFrame;
471     },
472
473     _callFrameSelected: function()
474     {
475         this._clearCurrentExecutionLine();
476
477         var callStackPane = this.sidebarPanes.callstack;
478         var currentFrame = callStackPane.selectedCallFrame;
479         if (!currentFrame)
480             return;
481
482         this.sidebarPanes.scopechain.update(currentFrame);
483
484         var scriptOrResource = this._sourceIDMap[currentFrame.sourceIdentifier];
485         this._showScriptOrResource(scriptOrResource, currentFrame.line);
486
487         this._executionSourceFrame = this._sourceFrameForScriptOrResource(scriptOrResource);
488         if (this._executionSourceFrame)
489             this._executionSourceFrame.executionLine = currentFrame.line;
490     },
491
492     _changeVisibleFile: function(event)
493     {
494         var select = this.filesSelectElement;
495         this._showScriptOrResource(select.options[select.selectedIndex].representedObject);
496     },
497
498     _startSidebarResizeDrag: function(event)
499     {
500         WebInspector.elementDragStart(this.sidebarElement, this._sidebarResizeDrag.bind(this), this._endSidebarResizeDrag.bind(this), event, "col-resize");
501
502         if (event.target === this.sidebarResizeWidgetElement)
503             this._dragOffset = (event.target.offsetWidth - (event.pageX - event.target.totalOffsetLeft));
504         else
505             this._dragOffset = 0;
506     },
507
508     _endSidebarResizeDrag: function(event)
509     {
510         WebInspector.elementDragEnd(event);
511
512         delete this._dragOffset;
513     },
514
515     _sidebarResizeDrag: function(event)
516     {
517         var x = event.pageX + this._dragOffset;
518         var newWidth = Number.constrain(window.innerWidth - x, Preferences.minScriptsSidebarWidth, window.innerWidth * 0.66);
519
520         this.sidebarElement.style.width = newWidth + "px";
521         this.sidebarButtonsElement.style.width = newWidth + "px";
522         this.scriptResourceViews.style.right = newWidth + "px";
523         this.sidebarResizeWidgetElement.style.right = newWidth + "px";
524         this.sidebarResizeElement.style.right = (newWidth - 3) + "px";
525
526         event.preventDefault();
527     },
528
529     _updateDebuggerButtons: function()
530     {
531         if (InspectorController.debuggerAttached()) {
532             this.debuggingButton.title = WebInspector.UIString("Stop debugging.");
533             this.debuggingButton.addStyleClass("toggled-on");
534             this.pauseButton.disabled = false;
535         } else {
536             this.debuggingButton.title = WebInspector.UIString("Start debugging and reload inspected page.");
537             this.debuggingButton.removeStyleClass("toggled-on");
538             this.pauseButton.disabled = true;
539         }
540
541         if (this._paused) {
542             this.pauseButton.addStyleClass("paused");
543
544             this.pauseButton.disabled = false;
545             this.stepOverButton.disabled = false;
546             this.stepIntoButton.disabled = false;
547             this.stepOutButton.disabled = false;
548
549             this.debuggerStatusElement.textContent = WebInspector.UIString("Paused");
550         } else {
551             this.pauseButton.removeStyleClass("paused");
552
553             this.pauseButton.disabled = this._waitingToPause;
554             this.stepOverButton.disabled = true;
555             this.stepIntoButton.disabled = true;
556             this.stepOutButton.disabled = true;
557
558             if (this._waitingToPause)
559                 this.debuggerStatusElement.textContent = WebInspector.UIString("Pausing");
560             else if (this._stepping)
561                 this.debuggerStatusElement.textContent = WebInspector.UIString("Stepping");
562             else
563                 this.debuggerStatusElement.textContent = "";
564         }
565     },
566
567     _toggleDebugging: function()
568     {
569         this._paused = false;
570         this._waitingToPause = false;
571         this._stepping = false;
572
573         if (InspectorController.debuggerAttached()) {
574             this.element.appendChild(this.attachOverlayElement);
575             InspectorController.stopDebugging();
576         } else {
577             this.attachOverlayElement.parentNode.removeChild(this.attachOverlayElement);
578             InspectorController.startDebuggingAndReloadInspectedPage();
579         }
580
581         this.sidebarPanes.callstack.update(null);
582         this.sidebarPanes.scopechain.update(null);
583
584         this._clearCurrentExecutionLine();
585         this._updateDebuggerButtons();
586     },
587
588     _togglePause: function()
589     {
590         if (this._paused) {
591             this._paused = false;
592             this._waitingToPause = false;
593             InspectorController.resumeDebugger();
594         } else {
595             this._stepping = false;
596             this._waitingToPause = true;
597             InspectorController.pauseInDebugger();
598         }
599
600         this.sidebarPanes.callstack.update(null);
601         this.sidebarPanes.scopechain.update(null);
602
603         this._clearCurrentExecutionLine();
604         this._updateDebuggerButtons();
605     },
606
607     _stepOverClicked: function()
608     {
609         this._paused = false;
610         this._stepping = true;
611
612         InspectorController.stepOverStatementInDebugger();
613     },
614
615     _stepIntoClicked: function()
616     {
617         this._paused = false;
618         this._stepping = true;
619
620         InspectorController.stepIntoStatementInDebugger();
621     },
622
623     _stepOutClicked: function()
624     {
625         this._paused = false;
626         this._stepping = true;
627
628         InspectorController.stepOutOfFunctionInDebugger();
629     }
630 }
631
632 WebInspector.ScriptsPanel.prototype.__proto__ = WebInspector.Panel.prototype;