36b5284d69937fff9a42628912450ba1ddedfa27
[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     document.body.addStyleClass("platform-" + InspectorController.platform());
227
228     this.fileOutline = new TreeOutline(document.getElementById("list"));
229     this.fileOutline.expandTreeElementsWhenArrowing = true;
230
231     this.statusOutline = new TreeOutline(document.getElementById("status"));
232     this.statusOutline.expandTreeElementsWhenArrowing = true;
233
234     this.resourceCategories = {
235         documents: new WebInspector.ResourceCategory(WebInspector.UIString("documents"), "documents"),
236         stylesheets: new WebInspector.ResourceCategory(WebInspector.UIString("stylesheets"), "stylesheets"),
237         images: new WebInspector.ResourceCategory(WebInspector.UIString("images"), "images"),
238         scripts: new WebInspector.ResourceCategory(WebInspector.UIString("scripts"), "scripts"),
239         fonts: new WebInspector.ResourceCategory(WebInspector.UIString("fonts"), "fonts"),
240         databases: new WebInspector.ResourceCategory(WebInspector.UIString("databases"), "databases"),
241         other: new WebInspector.ResourceCategory(WebInspector.UIString("other"), "other")
242     };
243
244     this.Tips = {
245         ResourceNotCompressed: {id: 0, message: WebInspector.UIString("You could save bandwidth by having your web server compress this transfer with gzip or zlib.")}
246     };
247
248     this.Warnings = {
249         IncorrectMIMEType: {id: 0, message: WebInspector.UIString("Resource interpreted as %s but transferred with MIME type %s.")}
250     };
251
252     this.consoleListItem = new WebInspector.ConsoleStatusTreeElement(WebInspector.consolePanel);
253     this.statusOutline.appendChild(this.consoleListItem);
254
255     this.networkListItem = new WebInspector.StatusTreeElement(WebInspector.UIString("Network"), "network", WebInspector.networkPanel);
256     this.statusOutline.appendChild(this.networkListItem);
257
258     this.resourceCategories.documents.listItem.expand();
259
260     this.currentFocusElement = document.getElementById("sidebar");
261
262     this.addMainEventListeners(document);
263
264     window.addEventListener("unload", this.windowUnload.bind(this), true);
265     window.addEventListener("resize", this.windowResize.bind(this), true);
266
267     document.addEventListener("mousedown", this.changeFocus.bind(this), true);
268     document.addEventListener("focus", this.changeFocus.bind(this), true);
269     document.addEventListener("keypress", this.documentKeypress.bind(this), true);
270     document.addEventListener("beforecopy", this.documentCanCopy.bind(this), true);
271     document.addEventListener("copy", this.documentCopy.bind(this), true);
272
273     document.getElementById("back").title = WebInspector.UIString("Show previous panel.");
274     document.getElementById("forward").title = WebInspector.UIString("Show next panel.");
275
276     document.getElementById("search").setAttribute("placeholder", WebInspector.UIString("Search"));
277
278     document.getElementById("back").addEventListener("click", this.back.bind(this), true);
279     document.getElementById("forward").addEventListener("click", this.forward.bind(this), true);
280     this.updateBackForwardButtons();
281
282     document.getElementById("attachToggle").addEventListener("click", this.toggleAttach.bind(this), true);
283     document.getElementById("statusToggle").addEventListener("click", this.toggleStatusArea.bind(this), true);
284
285     document.getElementById("sidebarResizeWidget").addEventListener("mousedown", this.sidebarResizerDragStart, true);
286     document.getElementById("sidebarResizer").addEventListener("mousedown", this.sidebarResizerDragStart, true);
287     document.getElementById("searchResultsResizer").addEventListener("mousedown", this.searchResultsResizerDragStart, true);
288
289     document.body.addStyleClass("detached");
290
291     InspectorController.loaded();
292 }
293
294 var windowLoaded = function()
295 {
296     var localizedStringsURL = InspectorController.localizedStringsURL();
297     if (localizedStringsURL) {
298         var localizedStringsScriptElement = document.createElement("script");
299         localizedStringsScriptElement.addEventListener("load", WebInspector.loaded.bind(WebInspector), false);
300         localizedStringsScriptElement.type = "text/javascript";
301         localizedStringsScriptElement.src = localizedStringsURL;
302         document.getElementsByTagName("head").item(0).appendChild(localizedStringsScriptElement);
303     } else
304         WebInspector.loaded();
305
306     delete windowLoaded;
307     window.removeEventListener("load", windowLoaded, false);
308 };
309
310 window.addEventListener("load", windowLoaded, false);
311
312 WebInspector.windowUnload = function(event)
313 {
314     InspectorController.windowUnloading();
315 }
316
317 WebInspector.windowResize = function(event)
318 {
319     if (this.currentPanel && this.currentPanel.resize)
320         this.currentPanel.resize();
321 }
322
323 WebInspector.windowFocused = function(event)
324 {
325     if (event.target.nodeType === Node.DOCUMENT_NODE)
326         document.body.removeStyleClass("inactive");
327 }
328
329 WebInspector.windowBlured = function(event)
330 {
331     if (event.target.nodeType === Node.DOCUMENT_NODE)
332         document.body.addStyleClass("inactive");
333 }
334
335 WebInspector.changeFocus = function(event)
336 {
337     var nextFocusElement;
338
339     var current = event.target;
340     while (current) {
341         if (current.nodeName.toLowerCase() === "input")
342             nextFocusElement = current;
343         current = current.parentNode;
344     }
345
346     if (!nextFocusElement)
347         nextFocusElement = event.target.firstParentWithClass("focusable");
348
349     this.currentFocusElement = nextFocusElement;
350 }
351
352 WebInspector.documentClick = function(event)
353 {
354     var anchor = event.target.firstParentOrSelfWithNodeName("a");
355     if (!anchor || !anchor.hasStyleClass("webkit-html-resource-link"))
356         return;
357
358     if (WebInspector.showResourceForURL(anchor.getAttribute("href"))) {
359         event.preventDefault();
360         event.stopPropagation();
361     }
362 }
363
364 WebInspector.documentKeypress = function(event)
365 {
366     if (!this.currentFocusElement)
367         return;
368     if (this.currentFocusElement.handleKeyEvent)
369         this.currentFocusElement.handleKeyEvent(event);
370     else if (this.currentFocusElement.id && this.currentFocusElement.id.length && WebInspector[this.currentFocusElement.id + "Keypress"])
371         WebInspector[this.currentFocusElement.id + "Keypress"](event);
372 }
373
374 WebInspector.documentCanCopy = function(event)
375 {
376     if (!this.currentFocusElement)
377         return;
378     // Calling preventDefault() will say "we support copying, so enable the Copy menu".
379     if (this.currentFocusElement.handleCopyEvent)
380         event.preventDefault();
381     else if (this.currentFocusElement.id && this.currentFocusElement.id.length && WebInspector[this.currentFocusElement.id + "Copy"])
382         event.preventDefault();
383 }
384
385 WebInspector.documentCopy = function(event)
386 {
387     if (!this.currentFocusElement)
388         return;
389     if (this.currentFocusElement.handleCopyEvent)
390         this.currentFocusElement.handleCopyEvent(event);
391     else if (this.currentFocusElement.id && this.currentFocusElement.id.length && WebInspector[this.currentFocusElement.id + "Copy"])
392         WebInspector[this.currentFocusElement.id + "Copy"](event);
393 }
394
395 WebInspector.sidebarKeypress = function(event)
396 {
397     var nextSelectedElement;
398
399     if (this.fileOutline.selectedTreeElement) {
400         if (!this.fileOutline.handleKeyEvent(event) && event.keyIdentifier === "Down" && !event.altKey && this.showingStatusArea) {
401             var nextSelectedElement = this.statusOutline.children[0];
402             while (nextSelectedElement && !nextSelectedElement.selectable)
403                 nextSelectedElement = nextSelectedElement.traverseNextTreeElement(false);
404         }
405     } else if (this.statusOutline.selectedTreeElement) {
406         if (!this.showingStatusArea || (!this.statusOutline.handleKeyEvent(event) && event.keyIdentifier === "Up" && !event.altKey)) {
407             var nextSelectedElement = this.fileOutline.children[0];
408             var lastSelectable = null;
409
410             while (nextSelectedElement) {
411                 if (nextSelectedElement.selectable)
412                     lastSelectable = nextSelectedElement;
413                 nextSelectedElement = nextSelectedElement.traverseNextTreeElement(false);
414             }
415
416             nextSelectedElement = lastSelectable;
417         }
418     }
419
420     if (nextSelectedElement) {
421         nextSelectedElement.reveal();
422         nextSelectedElement.select();
423
424         event.preventDefault();
425         event.stopPropagation();
426     }
427 }
428
429 WebInspector.sidebarCopy = function(event)
430 {
431     event.clipboardData.clearData();
432     event.preventDefault();
433
434     var selectedElement = this.fileOutline.selectedTreeElement;
435     if (!selectedElement || !selectedElement.representedObject || !selectedElement.representedObject.url)
436         return;
437
438     event.clipboardData.setData("URL", this.fileOutline.selectedTreeElement.representedObject.url);
439 }
440
441 WebInspector.mainKeypress = function(event)
442 {
443     if (this.currentPanel && this.currentPanel.handleKeyEvent)
444         this.currentPanel.handleKeyEvent(event);
445 }
446
447 WebInspector.mainCopy = function(event)
448 {
449     if (this.currentPanel && this.currentPanel.handleCopyEvent)
450         this.currentPanel.handleCopyEvent(event);
451 }
452
453 WebInspector.searchResultsKeypress = function(event)
454 {
455     if (this.searchResultsTree)
456         this.searchResultsTree.handleKeyEvent(event);
457 }
458
459 WebInspector.animateStyle = function(animations, duration, callback, complete)
460 {
461     if (complete === undefined)
462         complete = 0;
463     var slice = (1000 / 30); // 30 frames per second
464
465     var defaultUnit = "px";
466     var propertyUnit = {opacity: ""};
467
468     for (var i = 0; i < animations.length; ++i) {
469         var animation = animations[i];
470         var element = null;
471         var start = null;
472         var current = null;
473         var end = null;
474         for (key in animation) {
475             if (key === "element")
476                 element = animation[key];
477             else if (key === "start")
478                 start = animation[key];
479             else if (key === "current")
480                 current = animation[key];
481             else if (key === "end")
482                 end = animation[key];
483         }
484
485         if (!element || !end)
486             continue;
487
488         var computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
489         if (!start) {
490             start = {};
491             for (key in end)
492                 start[key] = parseInt(computedStyle.getPropertyValue(key));
493             animation.start = start;
494         } else if (complete == 0)
495             for (key in start)
496                 element.style.setProperty(key, start[key] + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
497
498         if (!current) {
499             current = {};
500             for (key in start)
501                 current[key] = start[key];
502             animation.current = current;
503         }
504
505         function cubicInOut(t, b, c, d)
506         {
507             if ((t/=d/2) < 1) return c/2*t*t*t + b;
508             return c/2*((t-=2)*t*t + 2) + b;
509         }
510
511         var style = element.style;
512         for (key in end) {
513             var startValue = start[key];
514             var currentValue = current[key];
515             var endValue = end[key];
516             if ((complete + slice) < duration) {
517                 var delta = (endValue - startValue) / (duration / slice);
518                 var newValue = cubicInOut(complete, startValue, endValue - startValue, duration);
519                 style.setProperty(key, newValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
520                 current[key] = newValue;
521             } else {
522                 style.setProperty(key, endValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
523             }
524         }
525     }
526
527     if (complete < duration)
528         setTimeout(WebInspector.animateStyle, slice, animations, duration, callback, complete + slice);
529     else if (callback)
530         callback();
531 }
532
533 WebInspector.toggleAttach = function()
534 {
535     this.attached = !this.attached;
536 }
537
538 WebInspector.toggleStatusArea = function()
539 {
540     this.showingStatusArea = !this.showingStatusArea;
541 }
542
543 WebInspector.sidebarResizerDragStart = function(event)
544 {
545     WebInspector.dividerDragStart(document.getElementById("sidebar"), WebInspector.sidebarResizerDrag, WebInspector.sidebarResizerDragEnd, event, "col-resize");
546 }
547
548 WebInspector.sidebarResizerDragEnd = function(event)
549 {
550     WebInspector.dividerDragEnd(document.getElementById("sidebar"), WebInspector.sidebarResizerDrag, WebInspector.sidebarResizerDragEnd, event);
551 }
552
553 WebInspector.sidebarResizerDrag = function(event)
554 {
555     var sidebar = document.getElementById("sidebar");
556     if (sidebar.dragging == true) {
557         var x = event.clientX + window.scrollX;
558
559         // FIXME: We can should come up with a better hueristic for constraining the size
560         // of the sidebar.
561         var newWidth = Number.constrain(x, 100, window.innerWidth - 100);
562
563         if (x == newWidth)
564             sidebar.dragLastX = x;
565
566         sidebar.style.width = newWidth + "px";
567         document.getElementById("sidebarResizer").style.left = (newWidth - 3) + "px";
568         document.getElementById("main").style.left = newWidth + "px";
569         document.getElementById("toolbarButtons").style.left = newWidth + "px";
570
571         if (WebInspector.currentPanel && WebInspector.currentPanel.resize)
572             WebInspector.currentPanel.resize();
573
574         event.preventDefault();
575     }
576 }
577
578 WebInspector.searchResultsResizerDragStart = function(event)
579 {
580     WebInspector.dividerDragStart(document.getElementById("searchResults"), WebInspector.searchResultsResizerDrag, WebInspector.searchResultsResizerDragEnd, event, "row-resize");
581 }
582
583 WebInspector.searchResultsResizerDragEnd = function(event)
584 {
585     WebInspector.dividerDragEnd(document.getElementById("searchResults"), WebInspector.searchResultsResizerDrag, WebInspector.searchResultsResizerDragEnd, event);
586 }
587
588 WebInspector.searchResultsResizerDrag = function(event)
589 {
590     var searchResults = document.getElementById("searchResults");
591     if (searchResults.dragging == true) {
592         var y = event.clientY - document.getElementById("main").offsetTop;
593         var newHeight = Number.constrain(y, 100, window.innerHeight - 100);
594
595         if (y == newHeight)
596             searchResults.dragLastY = y;
597
598         WebInspector.searchResultsHeight = newHeight;
599         searchResults.style.height = WebInspector.searchResultsHeight + "px";
600         document.getElementById("panels").style.top = newHeight + "px";
601         document.getElementById("searchResultsResizer").style.top = (newHeight - 3) + "px";
602         event.preventDefault();
603     }
604 }
605
606 WebInspector.dividerDragStart = function(element, dividerDrag, dividerDragEnd, event, cursor) 
607 {
608     element.dragging = true;
609     element.dragLastY = event.clientY + window.scrollY;
610     element.dragLastX = event.clientX + window.scrollX;
611     document.addEventListener("mousemove", dividerDrag, true);
612     document.addEventListener("mouseup", dividerDragEnd, true);
613     document.body.style.cursor = cursor;
614     event.preventDefault();
615 }
616
617 WebInspector.dividerDragEnd = function(element, dividerDrag, dividerDragEnd, event) 
618 {
619     element.dragging = false;
620     document.removeEventListener("mousemove", dividerDrag, true);
621     document.removeEventListener("mouseup", dividerDragEnd, true);
622     document.body.style.removeProperty("cursor");
623 }
624
625 WebInspector.back = function()
626 {
627     if (this.currentBackForwardIndex <= 0) {
628         console.error("Can't go back from index " + this.currentBackForwardIndex);
629         return;
630     }
631
632     this.navigateToPanel(this.backForwardList[--this.currentBackForwardIndex], null, true);
633 }
634
635 WebInspector.forward = function()
636 {
637     if (this.currentBackForwardIndex >= this.backForwardList.length - 1) {
638         console.error("Can't go forward from index " + this.currentBackForwardIndex);
639         return;
640     }
641
642     this.navigateToPanel(this.backForwardList[++this.currentBackForwardIndex], null, true);
643 }
644
645 WebInspector.updateBackForwardButtons = function()
646 {
647     var index = this.currentBackForwardIndex;
648
649     document.getElementById("back").disabled = index <= 0;
650     document.getElementById("forward").disabled = index >= this.backForwardList.length - 1;
651 }
652
653 WebInspector.showConsole = function()
654 {
655     this.showingStatusArea = true;
656     this.navigateToPanel(WebInspector.consolePanel);
657 }
658
659 WebInspector.showTimeline = function()
660 {
661     this.showingStatusArea = true;
662     this.navigateToPanel(WebInspector.networkPanel);
663 }
664
665 WebInspector.addResource = function(resource)
666 {
667     this.resources.push(resource);
668
669     if (resource.mainResource)
670         this.mainResource = resource;
671
672     if (resource.url) {
673         this.resourceURLMap[resource.url] = resource;
674         this.networkPanel.addResourceToTimeline(resource);
675     }
676 }
677
678 WebInspector.removeResource = function(resource)
679 {
680     resource.detach();
681
682     resource.category.removeResource(resource);
683
684     if (resource.url)
685         delete this.resourceURLMap[resource.url];
686
687     var resourcesLength = this.resources.length;
688     for (var i = 0; i < resourcesLength; ++i) {
689         if (this.resources[i] === resource) {
690             this.resources.splice(i, 1);
691             break;
692         }
693     }
694 }
695
696 WebInspector.clearResources = function()
697 {
698     for (var category in this.resourceCategories)
699         this.resourceCategories[category].removeAllResources();
700     this.resources = [];
701     this.backForwardList = [];
702     this.currentBackForwardIndex = -1;
703     delete this.mainResource;
704 }
705
706 WebInspector.clearDatabaseResources = function()
707 {
708     this.resourceCategories.databases.removeAllResources();
709 }
710
711 WebInspector.resourceURLChanged = function(resource, oldURL)
712 {
713     delete this.resourceURLMap[oldURL];
714     this.resourceURLMap[resource.url] = resource;
715 }
716
717 WebInspector.addMessageToConsole = function(msg)
718 {
719     this.consolePanel.addMessage(msg);
720     switch (msg.level) {
721         case WebInspector.ConsoleMessage.MessageLevel.Warning:
722             ++this.consoleListItem.warnings;
723             this.showingStatusArea = true;
724             break;
725         case WebInspector.ConsoleMessage.MessageLevel.Error:
726             ++this.consoleListItem.errors;
727             this.showingStatusArea = true;
728             break;
729     }
730 }
731
732 WebInspector.clearConsoleMessages = function()
733 {
734     this.consolePanel.clearMessages();
735     this.consoleListItem.warnings = this.consoleListItem.errors = 0;
736 }
737
738 WebInspector.clearNetworkTimeline = function()
739 {
740     if (this._networkPanel)
741         this._networkPanel.clearTimeline();
742 }
743
744 WebInspector.drawLoadingPieChart = function(canvas, percent) {
745     var g = canvas.getContext("2d");
746     var darkColor = "rgb(122, 168, 218)";
747     var lightColor = "rgb(228, 241, 251)";
748     var cx = 8;
749     var cy = 8;
750     var r = 7;
751
752     g.beginPath();
753     g.arc(cx, cy, r, 0, Math.PI * 2, false); 
754     g.closePath();
755
756     g.lineWidth = 1;
757     g.strokeStyle = darkColor;
758     g.fillStyle = lightColor;
759     g.fill();
760     g.stroke();
761
762     var startangle = -Math.PI / 2;
763     var endangle = startangle + (percent * Math.PI * 2);
764
765     g.beginPath();
766     g.moveTo(cx, cy);
767     g.arc(cx, cy, r, startangle, endangle, false); 
768     g.closePath();
769
770     g.fillStyle = darkColor;
771     g.fill();
772 }
773
774 WebInspector.updateFocusedNode = function(node)
775 {
776     if (!node)
777         // FIXME: Should we deselect if null is passed in?
778         return;
779
780     for (var i = 0; i < this.resourceCategories.documents.resources.length; ++i) {
781         var resource = this.resourceCategories.documents.resources[i];
782         if (resource.documentNode !== node.ownerDocument)
783             continue;
784
785         this.navigateToPanel(resource.panel, "dom");
786         resource.panel.focusedDOMNode = node;
787
788         this.currentFocusElement = document.getElementById("main");
789
790         break;
791     }
792 }
793
794 WebInspector.resourceForURL = function(url)
795 {
796     for (var resourceURL in this.resourceURLMap) {
797         if (resourceURL.hasSubstring(url))
798             return this.resourceURLMap[resourceURL];
799     }
800
801     return null;
802 }
803
804 WebInspector.showResourceForURL = function(url)
805 {
806     var resource = this.resourceForURL(url);
807     if (!resource)
808         return false;
809
810     this.navigateToResource(resource);
811     return true;
812 }
813
814 WebInspector.linkifyURL = function(url, linkText, classes, isExternal)
815 {
816     if (linkText === undefined)
817         linkText = url.escapeHTML();
818     classes = (classes === undefined) ? "" : classes + " ";
819     classes += isExternal ? "webkit-html-external-link" : "webkit-html-resource-link";
820     var link = "<a href=\"" + url + "\" class=\"" + classes + "\" target=\"_blank\">" + linkText + "</a>";
821     return link;
822 }
823
824 WebInspector.addMainEventListeners = function(doc)
825 {
826     doc.defaultView.addEventListener("focus", function(event) { WebInspector.windowFocused(event) }, true);
827     doc.defaultView.addEventListener("blur", function(event) { WebInspector.windowBlured(event) }, true);
828     doc.addEventListener("click", function(event) { WebInspector.documentClick(event) }, true);
829 }
830
831 WebInspector.performSearch = function(query)
832 {
833     if (!query || !query.length) {
834         this.showingSearchResults = false;
835         return;
836     }
837
838     var resultsContainer = document.getElementById("searchResults");
839     resultsContainer.removeChildren();
840
841     var isXPath = query.indexOf("/") !== -1;
842
843     var xpathQuery;
844     if (isXPath)
845         xpathQuery = query;
846     else {
847         var escapedQuery = query.escapeCharacters("'");
848         xpathQuery = "//*[contains(name(),'" + escapedQuery + "') or contains(@*,'" + escapedQuery + "')] | //text()[contains(.,'" + escapedQuery + "')] | //comment()[contains(.,'" + escapedQuery + "')]";
849     }
850
851     var resourcesToSearch = [].concat(this.resourceCategories.documents.resources, this.resourceCategories.stylesheets.resources, this.resourceCategories.scripts.resources, this.resourceCategories.other.resources);
852
853     var files = [];
854     for (var i = 0; i < resourcesToSearch.length; ++i) {
855         var resource = resourcesToSearch[i];
856
857         var sourceResults = [];
858         if (!isXPath && "source" in resource.panel.views) {
859             resource.panel.setupSourceFrameIfNeeded();
860             sourceResults = InspectorController.search(resource.panel.views.source.frameElement.contentDocument, query);
861         }
862
863         var domResults = [];
864         if (resource.category === this.resourceCategories.documents) {
865             try {
866                 var doc = resource.documentNode;
867                 var nodeList = doc.evaluate(xpathQuery, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
868                 for (var j = 0; j < nodeList.snapshotLength; ++j)
869                     domResults.push(nodeList.snapshotItem(i));
870             } catch(err) {
871                 // ignore any exceptions. the query might be malformed, but we allow that.
872             }
873         }
874
875         if ((!sourceResults || !sourceResults.length) && !domResults.length)
876             continue;
877
878         files.push({resource: resource, sourceResults: sourceResults, domResults: domResults});
879     }
880
881     if (!files.length)
882         return;
883
884     this.showingSearchResults = true;
885
886     var fileList = document.createElement("ol");
887     fileList.className = "outline-disclosure";
888     resultsContainer.appendChild(fileList);
889
890     this.searchResultsTree = new TreeOutline(fileList);
891     this.searchResultsTree.expandTreeElementsWhenArrowing = true;
892
893     var sourceResultSelected = function(element)
894     {
895         var selection = window.getSelection();
896         selection.removeAllRanges();
897         selection.addRange(element.representedObject.range);
898
899         WebInspector.navigateToPanel(element.representedObject.panel, "source");
900         element.representedObject.line.scrollIntoView(true);
901         resultsContainer.scrollToElement(element.listItemElement);
902     }
903
904     var domResultSelected = function(element)
905     {
906         WebInspector.navigateToPanel(element.representedObject.panel, "dom");
907         element.representedObject.panel.focusedDOMNode = element.representedObject.node;
908         resultsContainer.scrollToElement(element.listItemElement);
909     }
910
911     for (var i = 0; i < files.length; ++i) {
912         var file = files[i];
913
914         var fileItem = new TreeElement(file.resource.displayName, {}, true);
915         fileItem.expanded = true;
916         fileItem.selectable = false;
917         this.searchResultsTree.appendChild(fileItem);
918
919         if (file.sourceResults && file.sourceResults.length) {
920             for (var j = 0; j < file.sourceResults.length; ++j) {
921                 var range = file.sourceResults[j];
922
923                 var line = range.startContainer;
924                 while (line.parentNode && line.nodeName.toLowerCase() != "tr")
925                     line = line.parentNode;
926                 var lineRange = file.resource.panel.views.source.frameElement.contentDocument.createRange();
927                 lineRange.selectNodeContents(line);
928
929                 // Don't include any error bubbles in the search result
930                 var end = line.lastChild.lastChild;
931                 if (end.nodeName.toLowerCase() == "div" && end.hasStyleClass("webkit-html-message-bubble")) {
932                     while (end && end.nodeName.toLowerCase() == "div" && end.hasStyleClass("webkit-html-message-bubble"))
933                         end = end.previousSibling;
934                     lineRange.setEndAfter(end);
935                 }
936
937                 var beforeRange = file.resource.panel.views.source.frameElement.contentDocument.createRange();
938                 beforeRange.setStart(lineRange.startContainer, lineRange.startOffset);
939                 beforeRange.setEnd(range.startContainer, range.startOffset);
940
941                 var afterRange = file.resource.panel.views.source.frameElement.contentDocument.createRange();
942                 afterRange.setStart(range.endContainer, range.endOffset);
943                 afterRange.setEnd(lineRange.endContainer, lineRange.endOffset);
944
945                 var beforeText = beforeRange.toString().trimLeadingWhitespace();
946                 var text = range.toString();
947                 var afterText = afterRange.toString().trimTrailingWhitespace();
948
949                 var length = beforeText.length + text.length + afterText.length;
950                 if (length > Preferences.maxTextSearchResultLength) {
951                     var beforeAfterLength = (Preferences.maxTextSearchResultLength - text.length) / 2;
952                     if (beforeText.length > beforeAfterLength)
953                         beforeText = "\u2026" + beforeText.substr(-beforeAfterLength);
954                     if (afterText.length > beforeAfterLength)
955                         afterText = afterText.substr(0, beforeAfterLength) + "\u2026";
956                 }
957
958                 var title = "<div class=\"selection selected\"></div>";
959                 if (j == 0)
960                     title += "<div class=\"search-results-section\">" + WebInspector.UIString("Source") + "</div>";
961                 title += beforeText.escapeHTML() + "<span class=\"search-matched-string\">" + text.escapeHTML() + "</span>" + afterText.escapeHTML();
962                 var item = new TreeElement(title, {panel: file.resource.panel, line: line, range: range}, false);
963                 item.onselect = sourceResultSelected;
964                 fileItem.appendChild(item);
965             }
966         }
967
968         if (file.domResults.length) {
969             for (var j = 0; j < file.domResults.length; ++j) {
970                 var node = file.domResults[j];
971                 var title = "<div class=\"selection selected\"></div>";
972                 if (j == 0)
973                     title += "<div class=\"search-results-section\">" + WebInspector.UIString("DOM") + "</div>";
974                 title += nodeTitleInfo.call(node).title;
975                 var item = new TreeElement(title, {panel: file.resource.panel, node: node}, false);
976                 item.onselect = domResultSelected;
977                 fileItem.appendChild(item);
978             }
979         }
980     }
981 }
982
983 WebInspector.navigateToResource = function(resource)
984 {
985     this.navigateToPanel(resource.panel);
986 }
987
988 WebInspector.navigateToPanel = function(panel, view, fromBackForwardAction)
989 {
990     if (this.currentPanel === panel) {
991         if (panel && view)
992             panel.currentView = view;
993         return;
994     }
995
996     if (!fromBackForwardAction) {
997         var oldIndex = this.currentBackForwardIndex;
998         if (oldIndex >= 0)
999             this.backForwardList.splice(oldIndex + 1, this.backForwardList.length - oldIndex);
1000         this.currentBackForwardIndex++;
1001         this.backForwardList.push(panel);
1002     }
1003
1004     this.currentPanel = panel;
1005     if (panel && view)
1006         panel.currentView = view;
1007 }
1008
1009 WebInspector.UIString = function(string)
1010 {
1011     if (string in this.localizedStrings)
1012         string = this.localizedStrings[string];
1013     else {
1014         if (!(string in this.missingLocalizedStrings)) {
1015             console.error("Localized string \"" + string + "\" not found.");
1016             this.missingLocalizedStrings[string] = true;
1017         }
1018
1019         if (Preferences.showMissingLocalizedStrings)
1020             string += " (not localized)";
1021     }
1022
1023     return String.vsprintf(string, Array.prototype.slice.call(arguments, 1));
1024 }
1025
1026 WebInspector.StatusTreeElement = function(title, iconClass, panel)
1027 {
1028     TreeElement.call(this, "<span class=\"title only\">" + title + "</span><span class=\"icon " + iconClass + "\"></span>", null, false);
1029     this.panel = panel;
1030 }
1031
1032 WebInspector.StatusTreeElement.prototype = {
1033     onselect: function()
1034     {
1035         var selectedElement = WebInspector.fileOutline.selectedTreeElement;
1036         if (selectedElement)
1037             selectedElement.deselect();
1038         if (this.panel)
1039             WebInspector.navigateToPanel(this.panel);
1040     },
1041
1042     ondeselect: function()
1043     {
1044         if (this.panel)
1045             this.panel.hide();
1046     }
1047 }
1048
1049 WebInspector.StatusTreeElement.prototype.__proto__ = TreeElement.prototype;
1050
1051 WebInspector.ConsoleStatusTreeElement = function(panel)
1052 {
1053     WebInspector.StatusTreeElement.call(this, WebInspector.UIString("Console"), "console", panel);
1054 }
1055
1056 WebInspector.ConsoleStatusTreeElement.prototype = {
1057     get warnings()
1058     {
1059         if (!("_warnings" in this))
1060             this._warnings = 0;
1061
1062         return this._warnings;
1063     },
1064
1065     set warnings(x)
1066     {
1067         if (this._warnings === x)
1068             return;
1069
1070         this._warnings = x;
1071
1072         this._updateTitle();
1073     },
1074
1075     get errors()
1076     {
1077         if (!("_errors" in this))
1078             this._errors = 0;
1079
1080         return this._errors;
1081     },
1082
1083     set errors(x)
1084     {
1085         if (this._errors === x)
1086             return;
1087
1088         this._errors = x;
1089
1090         this._updateTitle();
1091     },
1092
1093     _updateTitle: function()
1094     {
1095         var title = "<span class=\"title";
1096         if (!this.warnings && !this.errors)
1097             title += " only";
1098         title += "\">" + WebInspector.UIString("Console") + "</span><span class=\"icon console\"></span>";
1099
1100         if (this.warnings || this.errors) {
1101             title += "<span class=\"info\">";
1102             if (this.errors) {
1103                 title += this.errors + " error";
1104                 if (this.errors > 1)
1105                     title += "s";
1106             }
1107             if (this.warnings) {
1108                 if (this.errors)
1109                     title += ", ";
1110                 title += this.warnings + " warning";
1111                 if (this.warnings > 1)
1112                     title += "s";
1113             }
1114             title += "</span>";
1115         }
1116
1117         this.title = title;
1118     }
1119 }
1120
1121 WebInspector.ConsoleStatusTreeElement.prototype.__proto__ = WebInspector.StatusTreeElement.prototype;
1122
1123 // This table maps MIME types to the Resource.Types which are valid for them.
1124 // The following line:
1125 //    "text/html":                {0: 1},
1126 // means that text/html is a valid MIME type for resources that have type
1127 // WebInspector.Resource.Type.Document (which has a value of 0).
1128 WebInspector.MIMETypes = {
1129     "text/html":                   {0: true},
1130     "text/xml":                    {0: true},
1131     "text/plain":                  {0: true},
1132     "application/xhtml+xml":       {0: true},
1133     "text/css":                    {1: true},
1134     "text/xsl":                    {1: true},
1135     "image/jpeg":                  {2: true},
1136     "image/png":                   {2: true},
1137     "image/gif":                   {2: true},
1138     "image/bmp":                   {2: true},
1139     "image/x-icon":                {2: true},
1140     "image/x-xbitmap":             {2: true},
1141     "font/ttf":                    {3: true},
1142     "font/opentype":               {3: true},
1143     "application/x-font-type1":    {3: true},
1144     "application/x-font-ttf":      {3: true},
1145     "application/x-truetype-font": {3: true},
1146     "text/javascript":             {4: true},
1147     "text/ecmascript":             {4: true},
1148     "application/javascript":      {4: true},
1149     "application/ecmascript":      {4: true},
1150     "application/x-javascript":    {4: true},
1151     "text/javascript1.1":          {4: true},
1152     "text/javascript1.2":          {4: true},
1153     "text/javascript1.3":          {4: true},
1154     "text/jscript":                {4: true},
1155     "text/livescript":             {4: true},
1156 }