Reviewed by Anders.
[WebKit-https.git] / WebCore / page / inspector / inspector.js
1 /*
2  * Copyright (C) 2006, 2007 Apple Inc.  All rights reserved.
3  * Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1.  Redistributions of source code must retain the above copyright
10  *     notice, this list of conditions and the following disclaimer. 
11  * 2.  Redistributions in binary form must reproduce the above copyright
12  *     notice, this list of conditions and the following disclaimer in the
13  *     documentation and/or other materials provided with the distribution. 
14  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15  *     its contributors may be used to endorse or promote products derived
16  *     from this software without specific prior written permission. 
17  *
18  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 var Preferences = {
31     ignoreWhitespace: true,
32     showUserAgentStyles: true,
33     maxInlineTextChildLength: 80,
34     maxTextSearchResultLength: 80,
35     showInheritedComputedStyleProperties: false,
36     showMissingLocalizedStrings: false
37 }
38
39 var WebInspector = {
40     resources: [],
41     resourceURLMap: {},
42     backForwardList: [],
43     searchResultsHeight: 100,
44     localizedStrings: {},
45     missingLocalizedStrings: {},
46
47     get consolePanel()
48     {
49         if (!this._consolePanel)
50             this._consolePanel = new WebInspector.ConsolePanel();
51
52         return this._consolePanel;
53     },
54
55     get networkPanel()
56     {
57         if (!this._networkPanel)
58             this._networkPanel = new WebInspector.NetworkPanel();
59
60         return this._networkPanel;
61     },
62
63     get currentBackForwardIndex()
64     {
65         if (this._currentBackForwardIndex === undefined)
66             this._currentBackForwardIndex = -1;
67
68         return this._currentBackForwardIndex;
69     },
70
71     set currentBackForwardIndex(x)
72     {
73         if (this._currentBackForwardIndex === x)
74             return;
75
76         this._currentBackForwardIndex = x;
77         this.updateBackForwardButtons();
78     },
79
80     get currentFocusElement()
81     {
82         return this._currentFocusElement;
83     },
84
85     set currentFocusElement(x)
86     {
87         if (!x || this._currentFocusElement === x)
88             return;
89
90         if (this._currentFocusElement) {
91             this._currentFocusElement.removeStyleClass("focused");
92             this._currentFocusElement.addStyleClass("blurred");
93             if (this._currentFocusElement.blurred)
94                 this._currentFocusElement.blurred();
95         }
96
97         this._currentFocusElement = x;
98
99         if (x) {
100             x.addStyleClass("focused");
101             x.removeStyleClass("blurred");
102             if (this._currentFocusElement.focused)
103                 this._currentFocusElement.focused();
104         }
105     },
106
107     get currentPanel()
108     {
109         return this._currentPanel;
110     },
111
112     set currentPanel(x)
113     {
114         if (this._currentPanel === x)
115             return;
116
117         if (this._currentPanel)
118             this._currentPanel.hide();
119
120         this._currentPanel = x;
121
122         if (x)
123             x.show();
124     },
125
126     get attached()
127     {
128         return this._attached;
129     },
130
131     set attached(x)
132     {
133         if (this._attached === x)
134             return;
135
136         this._attached = x;
137
138         var body = document.body;
139         if (x) {
140             InspectorController.attach();
141             body.removeStyleClass("detached");
142             body.addStyleClass("attached");
143         } else {
144             InspectorController.detach();
145             body.removeStyleClass("attached");
146             body.addStyleClass("detached");
147         }
148     },
149
150     get showingStatusArea()
151     {
152         return this._showingStatusArea;
153     },
154
155     set showingStatusArea(x)
156     {
157         if (this._showingStatusArea === x)
158             return;
159
160         this._showingStatusArea = x;
161
162         var list = document.getElementById("list");
163         var status = document.getElementById("status");
164         var statusButton = document.getElementById("statusToggle");
165
166         if (x) {
167             statusButton.addStyleClass("hide");
168             list.addStyleClass("status-visible");
169             status.addStyleClass("visible");
170         } else {
171             statusButton.removeStyleClass("hide");
172             list.removeStyleClass("status-visible");
173             status.removeStyleClass("visible");
174         }
175     },
176
177     get showingSearchResults()
178     {
179         return this._showingSearchResults;
180     },
181
182     set showingSearchResults(x)
183     {
184         if (this._showingSearchResults === x)
185             return;
186
187         this._showingSearchResults = x;
188
189         var resultsContainer = document.getElementById("searchResults");
190         var searchResultsResizer = document.getElementById("searchResultsResizer");
191
192         if (x) {
193             resultsContainer.removeStyleClass("hidden");
194             searchResultsResizer.removeStyleClass("hidden");
195
196             var animations = [
197                 {element: resultsContainer, end: {top: 0}},
198                 {element: searchResultsResizer, end: {top: WebInspector.searchResultsHeight - 3}},
199                 {element: document.getElementById("panels"), end: {top: WebInspector.searchResultsHeight}}
200             ];
201
202             WebInspector.animateStyle(animations, 250);
203         } else {
204             searchResultsResizer.addStyleClass("hidden");
205
206             var animations = [
207                 {element: resultsContainer, end: {top: -WebInspector.searchResultsHeight}},
208                 {element: searchResultsResizer, end: {top: 0}},
209                 {element: document.getElementById("panels"), end: {top: 0}}
210             ];
211
212             var animationFinished = function()
213             {
214                 resultsContainer.addStyleClass("hidden");
215                 resultsContainer.removeChildren();
216                 delete this.searchResultsTree;
217             };
218
219             WebInspector.animateStyle(animations, 250, animationFinished);
220         }
221     }
222 }
223
224 WebInspector.loaded = function()
225 {
226     var platform = InspectorController.platform();
227     document.body.addStyleClass("platform-" + platform);
228
229     this.fileOutline = new TreeOutline(document.getElementById("list"));
230     this.fileOutline.expandTreeElementsWhenArrowing = true;
231
232     this.statusOutline = new TreeOutline(document.getElementById("status"));
233     this.statusOutline.expandTreeElementsWhenArrowing = true;
234
235     this.resourceCategories = {
236         documents: new WebInspector.ResourceCategory(WebInspector.UIString("documents"), "documents"),
237         stylesheets: new WebInspector.ResourceCategory(WebInspector.UIString("stylesheets"), "stylesheets"),
238         images: new WebInspector.ResourceCategory(WebInspector.UIString("images"), "images"),
239         scripts: new WebInspector.ResourceCategory(WebInspector.UIString("scripts"), "scripts"),
240         fonts: new WebInspector.ResourceCategory(WebInspector.UIString("fonts"), "fonts"),
241         databases: new WebInspector.ResourceCategory(WebInspector.UIString("databases"), "databases"),
242         other: new WebInspector.ResourceCategory(WebInspector.UIString("other"), "other")
243     };
244
245     this.Tips = {
246         ResourceNotCompressed: {id: 0, message: WebInspector.UIString("You could save bandwidth by having your web server compress this transfer with gzip or zlib.")}
247     };
248
249     this.Warnings = {
250         IncorrectMIMEType: {id: 0, message: WebInspector.UIString("Resource interpreted as %s but transferred with MIME type %s.")}
251     };
252
253     this.consoleListItem = new WebInspector.ConsoleStatusTreeElement(WebInspector.consolePanel);
254     this.statusOutline.appendChild(this.consoleListItem);
255
256     this.networkListItem = new WebInspector.StatusTreeElement(WebInspector.UIString("Network"), "network", WebInspector.networkPanel);
257     this.statusOutline.appendChild(this.networkListItem);
258
259     this.resourceCategories.documents.listItem.expand();
260
261     this.currentFocusElement = document.getElementById("sidebar");
262
263     this.addMainEventListeners(document);
264
265     window.addEventListener("unload", this.windowUnload.bind(this), true);
266     window.addEventListener("resize", this.windowResize.bind(this), true);
267
268     document.addEventListener("mousedown", this.changeFocus.bind(this), true);
269     document.addEventListener("focus", this.changeFocus.bind(this), true);
270     document.addEventListener("keydown", this.documentKeyDown.bind(this), true);
271     document.addEventListener("beforecopy", this.documentCanCopy.bind(this), true);
272     document.addEventListener("copy", this.documentCopy.bind(this), true);
273
274     document.getElementById("back").title = WebInspector.UIString("Show previous panel.");
275     document.getElementById("forward").title = WebInspector.UIString("Show next panel.");
276
277     document.getElementById("search").setAttribute("placeholder", WebInspector.UIString("Search"));
278
279     document.getElementById("back").addEventListener("click", this.back.bind(this), true);
280     document.getElementById("forward").addEventListener("click", this.forward.bind(this), true);
281     this.updateBackForwardButtons();
282
283     document.getElementById("attachToggle").addEventListener("click", this.toggleAttach.bind(this), true);
284     document.getElementById("statusToggle").addEventListener("click", this.toggleStatusArea.bind(this), true);
285
286     document.getElementById("sidebarResizeWidget").addEventListener("mousedown", this.sidebarResizerDragStart, true);
287     document.getElementById("sidebarResizer").addEventListener("mousedown", this.sidebarResizerDragStart, true);
288     document.getElementById("searchResultsResizer").addEventListener("mousedown", this.searchResultsResizerDragStart, true);
289
290     if (platform === "mac-leopard")
291         document.getElementById("toolbar").addEventListener("mousedown", this.toolbarDragStart, true);
292
293     document.body.addStyleClass("detached");
294
295     InspectorController.loaded();
296 }
297
298 var windowLoaded = function()
299 {
300     var localizedStringsURL = InspectorController.localizedStringsURL();
301     if (localizedStringsURL) {
302         var localizedStringsScriptElement = document.createElement("script");
303         localizedStringsScriptElement.addEventListener("load", WebInspector.loaded.bind(WebInspector), false);
304         localizedStringsScriptElement.type = "text/javascript";
305         localizedStringsScriptElement.src = localizedStringsURL;
306         document.getElementsByTagName("head").item(0).appendChild(localizedStringsScriptElement);
307     } else
308         WebInspector.loaded();
309
310     window.removeEventListener("load", windowLoaded, false);
311     delete windowLoaded;
312 };
313
314 window.addEventListener("load", windowLoaded, false);
315
316 WebInspector.windowUnload = function(event)
317 {
318     InspectorController.windowUnloading();
319 }
320
321 WebInspector.windowResize = function(event)
322 {
323     if (this.currentPanel && this.currentPanel.resize)
324         this.currentPanel.resize();
325 }
326
327 WebInspector.windowFocused = function(event)
328 {
329     if (event.target.nodeType === Node.DOCUMENT_NODE)
330         document.body.removeStyleClass("inactive");
331 }
332
333 WebInspector.windowBlured = function(event)
334 {
335     if (event.target.nodeType === Node.DOCUMENT_NODE)
336         document.body.addStyleClass("inactive");
337 }
338
339 WebInspector.changeFocus = function(event)
340 {
341     var nextFocusElement;
342
343     var current = event.target;
344     while (current) {
345         if (current.nodeName.toLowerCase() === "input")
346             nextFocusElement = current;
347         current = current.parentNode;
348     }
349
350     if (!nextFocusElement)
351         nextFocusElement = event.target.firstParentWithClass("focusable");
352
353     this.currentFocusElement = nextFocusElement;
354 }
355
356 WebInspector.documentClick = function(event)
357 {
358     var anchor = event.target.firstParentOrSelfWithNodeName("a");
359     if (!anchor || !anchor.hasStyleClass("webkit-html-resource-link"))
360         return;
361
362     if (WebInspector.showResourceForURL(anchor.getAttribute("href"))) {
363         event.preventDefault();
364         event.stopPropagation();
365     }
366 }
367
368 WebInspector.documentKeyDown = function(event)
369 {
370     if (!this.currentFocusElement)
371         return;
372     if (this.currentFocusElement.handleKeyEvent)
373         this.currentFocusElement.handleKeyEvent(event);
374     else if (this.currentFocusElement.id && this.currentFocusElement.id.length && WebInspector[this.currentFocusElement.id + "KeyDown"])
375         WebInspector[this.currentFocusElement.id + "KeyDown"](event);
376 }
377
378 WebInspector.documentCanCopy = function(event)
379 {
380     if (!this.currentFocusElement)
381         return;
382     // Calling preventDefault() will say "we support copying, so enable the Copy menu".
383     if (this.currentFocusElement.handleCopyEvent)
384         event.preventDefault();
385     else if (this.currentFocusElement.id && this.currentFocusElement.id.length && WebInspector[this.currentFocusElement.id + "Copy"])
386         event.preventDefault();
387 }
388
389 WebInspector.documentCopy = function(event)
390 {
391     if (!this.currentFocusElement)
392         return;
393     if (this.currentFocusElement.handleCopyEvent)
394         this.currentFocusElement.handleCopyEvent(event);
395     else if (this.currentFocusElement.id && this.currentFocusElement.id.length && WebInspector[this.currentFocusElement.id + "Copy"])
396         WebInspector[this.currentFocusElement.id + "Copy"](event);
397 }
398
399 WebInspector.sidebarKeyDown = function(event)
400 {
401     var nextSelectedElement;
402
403     if (this.fileOutline.selectedTreeElement) {
404         if (!this.fileOutline.handleKeyEvent(event) && event.keyIdentifier === "Down" && !event.altKey && this.showingStatusArea) {
405             var nextSelectedElement = this.statusOutline.children[0];
406             while (nextSelectedElement && !nextSelectedElement.selectable)
407                 nextSelectedElement = nextSelectedElement.traverseNextTreeElement(false);
408         }
409     } else if (this.statusOutline.selectedTreeElement) {
410         if (!this.showingStatusArea || (!this.statusOutline.handleKeyEvent(event) && event.keyIdentifier === "Up" && !event.altKey)) {
411             var nextSelectedElement = this.fileOutline.children[0];
412             var lastSelectable = null;
413
414             while (nextSelectedElement) {
415                 if (nextSelectedElement.selectable)
416                     lastSelectable = nextSelectedElement;
417                 nextSelectedElement = nextSelectedElement.traverseNextTreeElement(false);
418             }
419
420             nextSelectedElement = lastSelectable;
421         }
422     }
423
424     if (nextSelectedElement) {
425         nextSelectedElement.reveal();
426         nextSelectedElement.select();
427
428         event.preventDefault();
429         event.stopPropagation();
430     }
431 }
432
433 WebInspector.sidebarCopy = function(event)
434 {
435     event.clipboardData.clearData();
436     event.preventDefault();
437
438     var selectedElement = this.fileOutline.selectedTreeElement;
439     if (!selectedElement || !selectedElement.representedObject || !selectedElement.representedObject.url)
440         return;
441
442     event.clipboardData.setData("URL", this.fileOutline.selectedTreeElement.representedObject.url);
443 }
444
445 WebInspector.mainKeyDown = function(event)
446 {
447     if (this.currentPanel && this.currentPanel.handleKeyEvent)
448         this.currentPanel.handleKeyEvent(event);
449 }
450
451 WebInspector.mainCopy = function(event)
452 {
453     if (this.currentPanel && this.currentPanel.handleCopyEvent)
454         this.currentPanel.handleCopyEvent(event);
455 }
456
457 WebInspector.searchResultsKeyDown = function(event)
458 {
459     if (this.searchResultsTree)
460         this.searchResultsTree.handleKeyEvent(event);
461 }
462
463 WebInspector.animateStyle = function(animations, duration, callback, complete)
464 {
465     if (complete === undefined)
466         complete = 0;
467     var slice = (1000 / 30); // 30 frames per second
468
469     var defaultUnit = "px";
470     var propertyUnit = {opacity: ""};
471
472     for (var i = 0; i < animations.length; ++i) {
473         var animation = animations[i];
474         var element = null;
475         var start = null;
476         var current = null;
477         var end = null;
478         for (key in animation) {
479             if (key === "element")
480                 element = animation[key];
481             else if (key === "start")
482                 start = animation[key];
483             else if (key === "current")
484                 current = animation[key];
485             else if (key === "end")
486                 end = animation[key];
487         }
488
489         if (!element || !end)
490             continue;
491
492         var computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
493         if (!start) {
494             start = {};
495             for (key in end)
496                 start[key] = parseInt(computedStyle.getPropertyValue(key));
497             animation.start = start;
498         } else if (complete == 0)
499             for (key in start)
500                 element.style.setProperty(key, start[key] + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
501
502         if (!current) {
503             current = {};
504             for (key in start)
505                 current[key] = start[key];
506             animation.current = current;
507         }
508
509         function cubicInOut(t, b, c, d)
510         {
511             if ((t/=d/2) < 1) return c/2*t*t*t + b;
512             return c/2*((t-=2)*t*t + 2) + b;
513         }
514
515         var style = element.style;
516         for (key in end) {
517             var startValue = start[key];
518             var currentValue = current[key];
519             var endValue = end[key];
520             if ((complete + slice) < duration) {
521                 var delta = (endValue - startValue) / (duration / slice);
522                 var newValue = cubicInOut(complete, startValue, endValue - startValue, duration);
523                 style.setProperty(key, newValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
524                 current[key] = newValue;
525             } else {
526                 style.setProperty(key, endValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
527             }
528         }
529     }
530
531     if (complete < duration)
532         setTimeout(WebInspector.animateStyle, slice, animations, duration, callback, complete + slice);
533     else if (callback)
534         callback();
535 }
536
537 WebInspector.toggleAttach = function()
538 {
539     this.attached = !this.attached;
540 }
541
542 WebInspector.toggleStatusArea = function()
543 {
544     this.showingStatusArea = !this.showingStatusArea;
545 }
546
547 WebInspector.toolbarDragStart = function(event)
548 {
549     var toolbar = document.getElementById("toolbar");
550     if (event.target !== toolbar || WebInspector.attached)
551         return;
552
553     toolbar.lastScreenX = event.screenX;
554     toolbar.lastScreenY = event.screenY;
555
556     document.addEventListener("mousemove", WebInspector.toolbarDrag, true);
557     document.addEventListener("mouseup", WebInspector.toolbarDragEnd, true);
558     document.body.style.cursor = "default";
559
560     event.preventDefault();
561 }
562
563 WebInspector.toolbarDragEnd = function(event)
564 {
565     var toolbar = document.getElementById("toolbar");
566     delete toolbar.lastScreenX;
567     delete toolbar.lastScreenY;
568
569     document.removeEventListener("mousemove", WebInspector.toolbarDrag, true);
570     document.removeEventListener("mouseup", WebInspector.toolbarDragEnd, true);
571     document.body.style.removeProperty("cursor");
572
573     event.preventDefault();
574 }
575
576 WebInspector.toolbarDrag = function(event)
577 {
578     var toolbar = document.getElementById("toolbar");
579
580     var x = event.screenX - toolbar.lastScreenX;
581     var y = event.screenY - toolbar.lastScreenY;
582
583     toolbar.lastScreenX = event.screenX;
584     toolbar.lastScreenY = event.screenY;
585
586     // We cannot call window.moveBy here becuase it restricts the movement of the window
587     // at the edges.
588     InspectorController.moveByUnrestricted(x, y);
589
590     event.preventDefault();
591 }
592
593 WebInspector.sidebarResizerDragStart = function(event)
594 {
595     WebInspector.elementDragStart(document.getElementById("sidebar"), WebInspector.sidebarResizerDrag, WebInspector.sidebarResizerDragEnd, event, "col-resize");
596 }
597
598 WebInspector.sidebarResizerDragEnd = function(event)
599 {
600     WebInspector.elementDragEnd(document.getElementById("sidebar"), WebInspector.sidebarResizerDrag, WebInspector.sidebarResizerDragEnd, event);
601 }
602
603 WebInspector.sidebarResizerDrag = function(event)
604 {
605     var x = event.pageX;
606
607     // FIXME: We can should come up with a better hueristic for constraining the size of the sidebar.
608     var newWidth = Number.constrain(x, 100, window.innerWidth - 100);
609
610     document.getElementById("sidebar").style.width = newWidth + "px";
611     document.getElementById("sidebarResizer").style.left = (newWidth - 3) + "px";
612     document.getElementById("main").style.left = newWidth + "px";
613     document.getElementById("toolbarButtons").style.left = newWidth + "px";
614
615     if (WebInspector.currentPanel && WebInspector.currentPanel.resize)
616         WebInspector.currentPanel.resize();
617
618     event.preventDefault();
619 }
620
621 WebInspector.searchResultsResizerDragStart = function(event)
622 {
623     WebInspector.elementDragStart(document.getElementById("searchResults"), WebInspector.searchResultsResizerDrag, WebInspector.searchResultsResizerDragEnd, event, "row-resize");
624 }
625
626 WebInspector.searchResultsResizerDragEnd = function(event)
627 {
628     WebInspector.elementDragEnd(document.getElementById("searchResults"), WebInspector.searchResultsResizerDrag, WebInspector.searchResultsResizerDragEnd, event);
629 }
630
631 WebInspector.searchResultsResizerDrag = function(event)
632 {
633     var y = event.pageY - document.getElementById("main").offsetTop;
634     var newHeight = Number.constrain(y, 100, window.innerHeight - 100);
635
636     WebInspector.searchResultsHeight = newHeight;
637
638     document.getElementById("searchResults").style.height = WebInspector.searchResultsHeight + "px";
639     document.getElementById("panels").style.top = newHeight + "px";
640     document.getElementById("searchResultsResizer").style.top = (newHeight - 3) + "px";
641
642     event.preventDefault();
643 }
644
645 WebInspector.elementDragStart = function(element, dividerDrag, elementDragEnd, event, cursor) 
646 {
647     if (WebInspector.draggingElement)
648         return elementDragEnd(event);
649
650     WebInspector.draggingElement = true;
651
652     document.addEventListener("mousemove", dividerDrag, true);
653     document.addEventListener("mouseup", elementDragEnd, true);
654     document.body.style.cursor = cursor;
655
656     event.preventDefault();
657 }
658
659 WebInspector.elementDragEnd = function(element, dividerDrag, elementDragEnd, event) 
660 {
661     document.removeEventListener("mousemove", dividerDrag, true);
662     document.removeEventListener("mouseup", elementDragEnd, true);
663     document.body.style.removeProperty("cursor");
664
665     delete WebInspector.draggingElement;
666
667     event.preventDefault();
668 }
669
670 WebInspector.back = function()
671 {
672     if (this.currentBackForwardIndex <= 0) {
673         console.error("Can't go back from index " + this.currentBackForwardIndex);
674         return;
675     }
676
677     this.navigateToPanel(this.backForwardList[--this.currentBackForwardIndex], null, true);
678 }
679
680 WebInspector.forward = function()
681 {
682     if (this.currentBackForwardIndex >= this.backForwardList.length - 1) {
683         console.error("Can't go forward from index " + this.currentBackForwardIndex);
684         return;
685     }
686
687     this.navigateToPanel(this.backForwardList[++this.currentBackForwardIndex], null, true);
688 }
689
690 WebInspector.updateBackForwardButtons = function()
691 {
692     var index = this.currentBackForwardIndex;
693
694     document.getElementById("back").disabled = index <= 0;
695     document.getElementById("forward").disabled = index >= this.backForwardList.length - 1;
696 }
697
698 WebInspector.showConsole = function()
699 {
700     this.showingStatusArea = true;
701     this.navigateToPanel(WebInspector.consolePanel);
702 }
703
704 WebInspector.showTimeline = function()
705 {
706     this.showingStatusArea = true;
707     this.navigateToPanel(WebInspector.networkPanel);
708 }
709
710 WebInspector.addResource = function(resource)
711 {
712     this.resources.push(resource);
713
714     if (resource.mainResource)
715         this.mainResource = resource;
716
717     if (resource.url) {
718         this.resourceURLMap[resource.url] = resource;
719         this.networkPanel.addResourceToTimeline(resource);
720     }
721 }
722
723 WebInspector.removeResource = function(resource)
724 {
725     resource.detach();
726
727     resource.category.removeResource(resource);
728
729     if (resource.url)
730         delete this.resourceURLMap[resource.url];
731
732     var resourcesLength = this.resources.length;
733     for (var i = 0; i < resourcesLength; ++i) {
734         if (this.resources[i] === resource) {
735             this.resources.splice(i, 1);
736             break;
737         }
738     }
739 }
740
741 WebInspector.clearResources = function()
742 {
743     for (var category in this.resourceCategories)
744         this.resourceCategories[category].removeAllResources();
745     this.resources = [];
746     this.backForwardList = [];
747     this.currentBackForwardIndex = -1;
748     delete this.mainResource;
749 }
750
751 WebInspector.clearDatabaseResources = function()
752 {
753     this.resourceCategories.databases.removeAllResources();
754 }
755
756 WebInspector.resourceURLChanged = function(resource, oldURL)
757 {
758     delete this.resourceURLMap[oldURL];
759     this.resourceURLMap[resource.url] = resource;
760 }
761
762 WebInspector.addMessageToConsole = function(msg)
763 {
764     this.consolePanel.addMessage(msg);
765     switch (msg.level) {
766         case WebInspector.ConsoleMessage.MessageLevel.Warning:
767             ++this.consoleListItem.warnings;
768             this.showingStatusArea = true;
769             break;
770         case WebInspector.ConsoleMessage.MessageLevel.Error:
771             ++this.consoleListItem.errors;
772             this.showingStatusArea = true;
773             break;
774     }
775 }
776
777 WebInspector.clearConsoleMessages = function()
778 {
779     this.consolePanel.clearMessages();
780     this.consoleListItem.warnings = this.consoleListItem.errors = 0;
781 }
782
783 WebInspector.clearNetworkTimeline = function()
784 {
785     if (this._networkPanel)
786         this._networkPanel.clearTimeline();
787 }
788
789 WebInspector.drawLoadingPieChart = function(canvas, percent) {
790     var g = canvas.getContext("2d");
791     var darkColor = "rgb(122, 168, 218)";
792     var lightColor = "rgb(228, 241, 251)";
793     var cx = 8;
794     var cy = 8;
795     var r = 7;
796
797     g.beginPath();
798     g.arc(cx, cy, r, 0, Math.PI * 2, false); 
799     g.closePath();
800
801     g.lineWidth = 1;
802     g.strokeStyle = darkColor;
803     g.fillStyle = lightColor;
804     g.fill();
805     g.stroke();
806
807     var startangle = -Math.PI / 2;
808     var endangle = startangle + (percent * Math.PI * 2);
809
810     g.beginPath();
811     g.moveTo(cx, cy);
812     g.arc(cx, cy, r, startangle, endangle, false); 
813     g.closePath();
814
815     g.fillStyle = darkColor;
816     g.fill();
817 }
818
819 WebInspector.updateFocusedNode = function(node)
820 {
821     if (!node)
822         // FIXME: Should we deselect if null is passed in?
823         return;
824
825     for (var i = 0; i < this.resourceCategories.documents.resources.length; ++i) {
826         var resource = this.resourceCategories.documents.resources[i];
827         if (resource.documentNode !== node.ownerDocument)
828             continue;
829
830         this.navigateToPanel(resource.panel, "dom");
831         resource.panel.focusedDOMNode = node;
832
833         this.currentFocusElement = document.getElementById("main");
834
835         break;
836     }
837 }
838
839 WebInspector.resourceForURL = function(url)
840 {
841     for (var resourceURL in this.resourceURLMap) {
842         if (resourceURL.hasSubstring(url))
843             return this.resourceURLMap[resourceURL];
844     }
845
846     return null;
847 }
848
849 WebInspector.showResourceForURL = function(url)
850 {
851     var resource = this.resourceForURL(url);
852     if (!resource)
853         return false;
854
855     this.navigateToResource(resource);
856     return true;
857 }
858
859 WebInspector.linkifyURL = function(url, linkText, classes, isExternal)
860 {
861     if (linkText === undefined)
862         linkText = url.escapeHTML();
863     classes = (classes === undefined) ? "" : classes + " ";
864     classes += isExternal ? "webkit-html-external-link" : "webkit-html-resource-link";
865     var link = "<a href=\"" + url + "\" class=\"" + classes + "\" target=\"_blank\">" + linkText + "</a>";
866     return link;
867 }
868
869 WebInspector.addMainEventListeners = function(doc)
870 {
871     doc.defaultView.addEventListener("focus", function(event) { WebInspector.windowFocused(event) }, true);
872     doc.defaultView.addEventListener("blur", function(event) { WebInspector.windowBlured(event) }, true);
873     doc.addEventListener("click", function(event) { WebInspector.documentClick(event) }, true);
874 }
875
876 WebInspector.performSearch = function(query)
877 {
878     if (!query || !query.length) {
879         this.showingSearchResults = false;
880         return;
881     }
882
883     var resultsContainer = document.getElementById("searchResults");
884     resultsContainer.removeChildren();
885
886     var isXPath = query.indexOf("/") !== -1;
887
888     var xpathQuery;
889     if (isXPath)
890         xpathQuery = query;
891     else {
892         var escapedQuery = query.escapeCharacters("'");
893         xpathQuery = "//*[contains(name(),'" + escapedQuery + "') or contains(@*,'" + escapedQuery + "')] | //text()[contains(.,'" + escapedQuery + "')] | //comment()[contains(.,'" + escapedQuery + "')]";
894     }
895
896     var resourcesToSearch = [].concat(this.resourceCategories.documents.resources, this.resourceCategories.stylesheets.resources, this.resourceCategories.scripts.resources, this.resourceCategories.other.resources);
897
898     var files = [];
899     for (var i = 0; i < resourcesToSearch.length; ++i) {
900         var resource = resourcesToSearch[i];
901
902         var sourceResults = [];
903         if (!isXPath && "source" in resource.panel.views) {
904             resource.panel.setupSourceFrameIfNeeded();
905             sourceResults = InspectorController.search(resource.panel.views.source.frameElement.contentDocument, query);
906         }
907
908         var domResults = [];
909         if (resource.category === this.resourceCategories.documents) {
910             try {
911                 var doc = resource.documentNode;
912                 var nodeList = doc.evaluate(xpathQuery, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
913                 for (var j = 0; j < nodeList.snapshotLength; ++j)
914                     domResults.push(nodeList.snapshotItem(i));
915             } catch(err) {
916                 // ignore any exceptions. the query might be malformed, but we allow that.
917             }
918         }
919
920         if ((!sourceResults || !sourceResults.length) && !domResults.length)
921             continue;
922
923         files.push({resource: resource, sourceResults: sourceResults, domResults: domResults});
924     }
925
926     if (!files.length)
927         return;
928
929     this.showingSearchResults = true;
930
931     var fileList = document.createElement("ol");
932     fileList.className = "outline-disclosure";
933     resultsContainer.appendChild(fileList);
934
935     this.searchResultsTree = new TreeOutline(fileList);
936     this.searchResultsTree.expandTreeElementsWhenArrowing = true;
937
938     var sourceResultSelected = function(element)
939     {
940         var selection = window.getSelection();
941         selection.removeAllRanges();
942         selection.addRange(element.representedObject.range);
943
944         WebInspector.navigateToPanel(element.representedObject.panel, "source");
945         element.representedObject.line.scrollIntoView(true);
946         resultsContainer.scrollToElement(element.listItemElement);
947     }
948
949     var domResultSelected = function(element)
950     {
951         WebInspector.navigateToPanel(element.representedObject.panel, "dom");
952         element.representedObject.panel.focusedDOMNode = element.representedObject.node;
953         resultsContainer.scrollToElement(element.listItemElement);
954     }
955
956     for (var i = 0; i < files.length; ++i) {
957         var file = files[i];
958
959         var fileItem = new TreeElement(file.resource.displayName, {}, true);
960         fileItem.expanded = true;
961         fileItem.selectable = false;
962         this.searchResultsTree.appendChild(fileItem);
963
964         if (file.sourceResults && file.sourceResults.length) {
965             for (var j = 0; j < file.sourceResults.length; ++j) {
966                 var range = file.sourceResults[j];
967
968                 var line = range.startContainer;
969                 while (line.parentNode && line.nodeName.toLowerCase() != "tr")
970                     line = line.parentNode;
971                 var lineRange = file.resource.panel.views.source.frameElement.contentDocument.createRange();
972                 lineRange.selectNodeContents(line);
973
974                 // Don't include any error bubbles in the search result
975                 var end = line.lastChild.lastChild;
976                 if (end.nodeName.toLowerCase() == "div" && end.hasStyleClass("webkit-html-message-bubble")) {
977                     while (end && end.nodeName.toLowerCase() == "div" && end.hasStyleClass("webkit-html-message-bubble"))
978                         end = end.previousSibling;
979                     lineRange.setEndAfter(end);
980                 }
981
982                 var beforeRange = file.resource.panel.views.source.frameElement.contentDocument.createRange();
983                 beforeRange.setStart(lineRange.startContainer, lineRange.startOffset);
984                 beforeRange.setEnd(range.startContainer, range.startOffset);
985
986                 var afterRange = file.resource.panel.views.source.frameElement.contentDocument.createRange();
987                 afterRange.setStart(range.endContainer, range.endOffset);
988                 afterRange.setEnd(lineRange.endContainer, lineRange.endOffset);
989
990                 var beforeText = beforeRange.toString().trimLeadingWhitespace();
991                 var text = range.toString();
992                 var afterText = afterRange.toString().trimTrailingWhitespace();
993
994                 var length = beforeText.length + text.length + afterText.length;
995                 if (length > Preferences.maxTextSearchResultLength) {
996                     var beforeAfterLength = (Preferences.maxTextSearchResultLength - text.length) / 2;
997                     if (beforeText.length > beforeAfterLength)
998                         beforeText = "\u2026" + beforeText.substr(-beforeAfterLength);
999                     if (afterText.length > beforeAfterLength)
1000                         afterText = afterText.substr(0, beforeAfterLength) + "\u2026";
1001                 }
1002
1003                 var title = "<div class=\"selection selected\"></div>";
1004                 if (j == 0)
1005                     title += "<div class=\"search-results-section\">" + WebInspector.UIString("Source") + "</div>";
1006                 title += beforeText.escapeHTML() + "<span class=\"search-matched-string\">" + text.escapeHTML() + "</span>" + afterText.escapeHTML();
1007                 var item = new TreeElement(title, {panel: file.resource.panel, line: line, range: range}, false);
1008                 item.onselect = sourceResultSelected;
1009                 fileItem.appendChild(item);
1010             }
1011         }
1012
1013         if (file.domResults.length) {
1014             for (var j = 0; j < file.domResults.length; ++j) {
1015                 var node = file.domResults[j];
1016                 var title = "<div class=\"selection selected\"></div>";
1017                 if (j == 0)
1018                     title += "<div class=\"search-results-section\">" + WebInspector.UIString("DOM") + "</div>";
1019                 title += nodeTitleInfo.call(node).title;
1020                 var item = new TreeElement(title, {panel: file.resource.panel, node: node}, false);
1021                 item.onselect = domResultSelected;
1022                 fileItem.appendChild(item);
1023             }
1024         }
1025     }
1026 }
1027
1028 WebInspector.navigateToResource = function(resource)
1029 {
1030     this.navigateToPanel(resource.panel);
1031 }
1032
1033 WebInspector.navigateToPanel = function(panel, view, fromBackForwardAction)
1034 {
1035     if (this.currentPanel === panel) {
1036         if (panel && view)
1037             panel.currentView = view;
1038         return;
1039     }
1040
1041     if (!fromBackForwardAction) {
1042         var oldIndex = this.currentBackForwardIndex;
1043         if (oldIndex >= 0)
1044             this.backForwardList.splice(oldIndex + 1, this.backForwardList.length - oldIndex);
1045         this.currentBackForwardIndex++;
1046         this.backForwardList.push(panel);
1047     }
1048
1049     this.currentPanel = panel;
1050     if (panel && view)
1051         panel.currentView = view;
1052 }
1053
1054 WebInspector.UIString = function(string)
1055 {
1056     if (string in this.localizedStrings)
1057         string = this.localizedStrings[string];
1058     else {
1059         if (!(string in this.missingLocalizedStrings)) {
1060             console.error("Localized string \"" + string + "\" not found.");
1061             this.missingLocalizedStrings[string] = true;
1062         }
1063
1064         if (Preferences.showMissingLocalizedStrings)
1065             string += " (not localized)";
1066     }
1067
1068     return String.vsprintf(string, Array.prototype.slice.call(arguments, 1));
1069 }
1070
1071 WebInspector.StatusTreeElement = function(title, iconClass, panel)
1072 {
1073     TreeElement.call(this, "<span class=\"title only\">" + title + "</span><span class=\"icon " + iconClass + "\"></span>", null, false);
1074     this.panel = panel;
1075 }
1076
1077 WebInspector.StatusTreeElement.prototype = {
1078     onselect: function()
1079     {
1080         var selectedElement = WebInspector.fileOutline.selectedTreeElement;
1081         if (selectedElement)
1082             selectedElement.deselect();
1083         if (this.panel)
1084             WebInspector.navigateToPanel(this.panel);
1085     },
1086
1087     ondeselect: function()
1088     {
1089         if (this.panel)
1090             this.panel.hide();
1091     }
1092 }
1093
1094 WebInspector.StatusTreeElement.prototype.__proto__ = TreeElement.prototype;
1095
1096 WebInspector.ConsoleStatusTreeElement = function(panel)
1097 {
1098     WebInspector.StatusTreeElement.call(this, WebInspector.UIString("Console"), "console", panel);
1099 }
1100
1101 WebInspector.ConsoleStatusTreeElement.prototype = {
1102     get warnings()
1103     {
1104         if (!("_warnings" in this))
1105             this._warnings = 0;
1106
1107         return this._warnings;
1108     },
1109
1110     set warnings(x)
1111     {
1112         if (this._warnings === x)
1113             return;
1114
1115         this._warnings = x;
1116
1117         this._updateTitle();
1118     },
1119
1120     get errors()
1121     {
1122         if (!("_errors" in this))
1123             this._errors = 0;
1124
1125         return this._errors;
1126     },
1127
1128     set errors(x)
1129     {
1130         if (this._errors === x)
1131             return;
1132
1133         this._errors = x;
1134
1135         this._updateTitle();
1136     },
1137
1138     _updateTitle: function()
1139     {
1140         var title = "<span class=\"title";
1141         if (!this.warnings && !this.errors)
1142             title += " only";
1143         title += "\">" + WebInspector.UIString("Console") + "</span><span class=\"icon console\"></span>";
1144
1145         if (this.warnings || this.errors) {
1146             title += "<span class=\"info\">";
1147             if (this.errors) {
1148                 title += this.errors + " error";
1149                 if (this.errors > 1)
1150                     title += "s";
1151             }
1152             if (this.warnings) {
1153                 if (this.errors)
1154                     title += ", ";
1155                 title += this.warnings + " warning";
1156                 if (this.warnings > 1)
1157                     title += "s";
1158             }
1159             title += "</span>";
1160         }
1161
1162         this.title = title;
1163     }
1164 }
1165
1166 WebInspector.ConsoleStatusTreeElement.prototype.__proto__ = WebInspector.StatusTreeElement.prototype;
1167
1168 // This table maps MIME types to the Resource.Types which are valid for them.
1169 // The following line:
1170 //    "text/html":                {0: 1},
1171 // means that text/html is a valid MIME type for resources that have type
1172 // WebInspector.Resource.Type.Document (which has a value of 0).
1173 WebInspector.MIMETypes = {
1174     "text/html":                   {0: true},
1175     "text/xml":                    {0: true},
1176     "text/plain":                  {0: true},
1177     "application/xhtml+xml":       {0: true},
1178     "text/css":                    {1: true},
1179     "text/xsl":                    {1: true},
1180     "image/jpeg":                  {2: true},
1181     "image/png":                   {2: true},
1182     "image/gif":                   {2: true},
1183     "image/bmp":                   {2: true},
1184     "image/x-icon":                {2: true},
1185     "image/x-xbitmap":             {2: true},
1186     "font/ttf":                    {3: true},
1187     "font/opentype":               {3: true},
1188     "application/x-font-type1":    {3: true},
1189     "application/x-font-ttf":      {3: true},
1190     "application/x-truetype-font": {3: true},
1191     "text/javascript":             {4: true},
1192     "text/ecmascript":             {4: true},
1193     "application/javascript":      {4: true},
1194     "application/ecmascript":      {4: true},
1195     "application/x-javascript":    {4: true},
1196     "text/javascript1.1":          {4: true},
1197     "text/javascript1.2":          {4: true},
1198     "text/javascript1.3":          {4: true},
1199     "text/jscript":                {4: true},
1200     "text/livescript":             {4: true},
1201 }