Remember the docked state of the Web Inspector, so it can be reopened docked if it...
[WebKit-https.git] / WebCore / page / inspector / inspector.js
1 /*
2  * Copyright (C) 2006, 2007, 2008 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     minConsoleHeight: 75,
36     minSidebarWidth: 100,
37     minElementsSidebarWidth: 200,
38     minScriptsSidebarWidth: 200,
39     showInheritedComputedStyleProperties: false,
40     showMissingLocalizedStrings: false
41 }
42
43 var WebInspector = {
44     resources: [],
45     resourceURLMap: {},
46     searchResultsHeight: 100,
47     missingLocalizedStrings: {},
48
49     get previousFocusElement()
50     {
51         return this._previousFocusElement;
52     },
53
54     get currentFocusElement()
55     {
56         return this._currentFocusElement;
57     },
58
59     set currentFocusElement(x)
60     {
61         if (!x || this._currentFocusElement === x)
62             return;
63
64         this._previousFocusElement = this._currentFocusElement;
65         this._currentFocusElement = x;
66
67         if (this._currentFocusElement) {
68             this._currentFocusElement.focus();
69
70             // Make a caret selection inside the new element if there isn't a range selection and
71             // there isn't already a caret selection inside.
72             var selection = window.getSelection();
73             if (selection.isCollapsed && !this._currentFocusElement.isInsertionCaretInside()) {
74                 var selectionRange = document.createRange();
75                 selectionRange.setStart(this._currentFocusElement, 0);
76                 selectionRange.setEnd(this._currentFocusElement, 0);
77
78                 selection.removeAllRanges();
79                 selection.addRange(selectionRange);
80             }
81         } else if (this._previousFocusElement)
82             this._previousFocusElement.blur();
83     },
84
85     get currentPanel()
86     {
87         return this._currentPanel;
88     },
89
90     set currentPanel(x)
91     {
92         if (this._currentPanel === x)
93             return;
94
95         if (this._currentPanel)
96             this._currentPanel.hide();
97
98         this._currentPanel = x;
99
100         if (x)
101             x.show();
102     },
103
104     get attached()
105     {
106         return this._attached;
107     },
108
109     set attached(x)
110     {
111         if (this._attached === x)
112             return;
113
114         this._attached = x;
115
116         var dockToggleButton = document.getElementById("dock-status-bar-item");
117         var body = document.body;
118
119         if (x) {
120             InspectorController.attach();
121             body.removeStyleClass("detached");
122             body.addStyleClass("attached");
123             dockToggleButton.title = WebInspector.UIString("Undock into separate window.");
124         } else {
125             InspectorController.detach();
126             body.removeStyleClass("attached");
127             body.addStyleClass("detached");
128             dockToggleButton.title = WebInspector.UIString("Dock to main window.");
129         }
130     },
131
132     get showingSearchResults()
133     {
134         return this._showingSearchResults;
135     },
136
137     set showingSearchResults(x)
138     {
139         if (this._showingSearchResults === x)
140             return;
141
142         this._showingSearchResults = x;
143
144         var resultsContainer = document.getElementById("searchResults");
145         var searchResultsResizer = document.getElementById("searchResultsResizer");
146
147         if (x) {
148             resultsContainer.removeStyleClass("hidden");
149             searchResultsResizer.removeStyleClass("hidden");
150
151             var animations = [
152                 {element: resultsContainer, end: {top: 0}},
153                 {element: searchResultsResizer, end: {top: WebInspector.searchResultsHeight - 3}},
154                 {element: document.getElementById("main-panels"), end: {top: WebInspector.searchResultsHeight}}
155             ];
156
157             WebInspector.animateStyle(animations, 250);
158         } else {
159             searchResultsResizer.addStyleClass("hidden");
160
161             var animations = [
162                 {element: resultsContainer, end: {top: -WebInspector.searchResultsHeight}},
163                 {element: searchResultsResizer, end: {top: 0}},
164                 {element: document.getElementById("main-panels"), end: {top: 0}}
165             ];
166
167             var animationFinished = function()
168             {
169                 resultsContainer.addStyleClass("hidden");
170                 resultsContainer.removeChildren();
171                 delete this.searchResultsTree;
172             };
173
174             WebInspector.animateStyle(animations, 250, animationFinished);
175         }
176     },
177
178     get errors()
179     {
180         return this._errors || 0;
181     },
182
183     set errors(x)
184     {
185         x = Math.max(x, 0);
186
187         if (this._errors === x)
188             return;
189         this._errors = x;
190         this._updateErrorAndWarningCounts();
191     },
192
193     get warnings()
194     {
195         return this._warnings || 0;
196     },
197
198     set warnings(x)
199     {
200         x = Math.max(x, 0);
201
202         if (this._warnings === x)
203             return;
204         this._warnings = x;
205         this._updateErrorAndWarningCounts();
206     },
207
208     _updateErrorAndWarningCounts: function()
209     {
210         var errorWarningElement = document.getElementById("error-warning-count");
211         if (!errorWarningElement)
212             return;
213
214         if (!this.errors && !this.warnings) {
215             errorWarningElement.addStyleClass("hidden");
216             return;
217         }
218
219         errorWarningElement.removeStyleClass("hidden");
220
221         errorWarningElement.removeChildren();
222
223         if (this.errors) {
224             var errorElement = document.createElement("span");
225             errorElement.id = "error-count";
226             errorElement.textContent = this.errors;
227             errorWarningElement.appendChild(errorElement);
228         }
229
230         if (this.warnings) {
231             var warningsElement = document.createElement("span");
232             warningsElement.id = "warning-count";
233             warningsElement.textContent = this.warnings;
234             errorWarningElement.appendChild(warningsElement);
235         }
236
237         if (this.errors) {
238             if (this.warnings) {
239                 if (this.errors == 1) {
240                     if (this.warnings == 1)
241                         errorWarningElement.title = WebInspector.UIString("%d error, %d warning", this.errors, this.warnings);
242                     else
243                         errorWarningElement.title = WebInspector.UIString("%d error, %d warnings", this.errors, this.warnings);
244                 } else if (this.warnings == 1)
245                     errorWarningElement.title = WebInspector.UIString("%d errors, %d warning", this.errors, this.warnings);
246                 else
247                     errorWarningElement.title = WebInspector.UIString("%d errors, %d warnings", this.errors, this.warnings);
248             } else if (this.errors == 1)
249                 errorWarningElement.title = WebInspector.UIString("%d error", this.errors);
250             else
251                 errorWarningElement.title = WebInspector.UIString("%d errors", this.errors);
252         } else if (this.warnings == 1)
253             errorWarningElement.title = WebInspector.UIString("%d warning", this.warnings);
254         else if (this.warnings)
255             errorWarningElement.title = WebInspector.UIString("%d warnings", this.warnings);
256         else
257             errorWarningElement.title = null;
258     }
259 }
260
261 WebInspector.loaded = function()
262 {
263     var platform = InspectorController.platform();
264     document.body.addStyleClass("platform-" + platform);
265
266     this.console = new WebInspector.Console();
267     this.panels = {
268         elements: new WebInspector.ElementsPanel(),
269         resources: new WebInspector.ResourcesPanel(),
270         scripts: new WebInspector.ScriptsPanel(),
271         profiles: new WebInspector.ProfilesPanel(),
272         databases: new WebInspector.DatabasesPanel()
273     };
274
275     var toolbarElement = document.getElementById("toolbar");
276     var previousToolbarItem = toolbarElement.children[0];
277
278     for (var panelName in this.panels) {
279         var panel = this.panels[panelName];
280         var panelToolbarItem = panel.toolbarItem;
281         panelToolbarItem.addEventListener("click", this._toolbarItemClicked.bind(this));
282         if (previousToolbarItem)
283             toolbarElement.insertBefore(panelToolbarItem, previousToolbarItem.nextSibling);
284         else
285             toolbarElement.insertBefore(panelToolbarItem, toolbarElement.firstChild);
286         previousToolbarItem = panelToolbarItem;
287     }
288
289     this.currentPanel = this.panels.elements;
290
291     this.resourceCategories = {
292         documents: new WebInspector.ResourceCategory(WebInspector.UIString("Documents"), "documents"),
293         stylesheets: new WebInspector.ResourceCategory(WebInspector.UIString("Stylesheets"), "stylesheets"),
294         images: new WebInspector.ResourceCategory(WebInspector.UIString("Images"), "images"),
295         scripts: new WebInspector.ResourceCategory(WebInspector.UIString("Scripts"), "scripts"),
296         xhr: new WebInspector.ResourceCategory(WebInspector.UIString("XHR"), "xhr"),
297         fonts: new WebInspector.ResourceCategory(WebInspector.UIString("Fonts"), "fonts"),
298         other: new WebInspector.ResourceCategory(WebInspector.UIString("Other"), "other")
299     };
300
301     this.Tips = {
302         ResourceNotCompressed: {id: 0, message: WebInspector.UIString("You could save bandwidth by having your web server compress this transfer with gzip or zlib.")}
303     };
304
305     this.Warnings = {
306         IncorrectMIMEType: {id: 0, message: WebInspector.UIString("Resource interpreted as %s but transferred with MIME type %s.")}
307     };
308
309     this.addMainEventListeners(document);
310
311     window.addEventListener("unload", this.windowUnload.bind(this), true);
312     window.addEventListener("resize", this.windowResize.bind(this), true);
313
314     document.addEventListener("focus", this.focusChanged.bind(this), true);
315     document.addEventListener("keydown", this.documentKeyDown.bind(this), true);
316     document.addEventListener("keyup", this.documentKeyUp.bind(this), true);
317     document.addEventListener("beforecopy", this.documentCanCopy.bind(this), true);
318     document.addEventListener("copy", this.documentCopy.bind(this), true);
319
320     document.getElementById("searchResultsResizer").addEventListener("mousedown", this.searchResultsResizerDragStart, true);
321
322     var mainPanelsElement = document.getElementById("main-panels");
323     mainPanelsElement.handleKeyEvent = this.mainKeyDown.bind(this);
324     mainPanelsElement.handleKeyUpEvent = this.mainKeyUp.bind(this);
325     mainPanelsElement.handleCopyEvent = this.mainCopy.bind(this);
326
327     // Focus the mainPanelsElement in a timeout so it happens after the initial focus,
328     // so it doesn't get reset to the first toolbar button. This initial focus happens
329     // on Mac when the window is made key and the WebHTMLView becomes the first responder.
330     setTimeout(function() { WebInspector.currentFocusElement = mainPanelsElement }, 0);
331
332     var dockToggleButton = document.getElementById("dock-status-bar-item");
333     dockToggleButton.addEventListener("click", this.toggleAttach.bind(this), false);
334
335     if (this.attached)
336         dockToggleButton.title = WebInspector.UIString("Undock into separate window.");
337     else
338         dockToggleButton.title = WebInspector.UIString("Dock to main window.");
339
340     var errorWarningCount = document.getElementById("error-warning-count");
341     errorWarningCount.addEventListener("click", this.console.show.bind(this.console), false);
342     this._updateErrorAndWarningCounts();
343
344     document.getElementById("search-toolbar-label").textContent = WebInspector.UIString("Search");
345     var searchField = document.getElementById("search");
346     searchField.addEventListener("keyup", this.performSearch.bind(this), false);
347
348     document.getElementById("toolbar").addEventListener("mousedown", this.toolbarDragStart, true);
349     document.getElementById("close-button").addEventListener("click", this.close, true);
350
351     InspectorController.loaded();
352 }
353
354 var windowLoaded = function()
355 {
356     var localizedStringsURL = InspectorController.localizedStringsURL();
357     if (localizedStringsURL) {
358         var localizedStringsScriptElement = document.createElement("script");
359         localizedStringsScriptElement.addEventListener("load", WebInspector.loaded.bind(WebInspector), false);
360         localizedStringsScriptElement.type = "text/javascript";
361         localizedStringsScriptElement.src = localizedStringsURL;
362         document.getElementsByTagName("head").item(0).appendChild(localizedStringsScriptElement);
363     } else
364         WebInspector.loaded();
365
366     window.removeEventListener("load", windowLoaded, false);
367     delete windowLoaded;
368 };
369
370 window.addEventListener("load", windowLoaded, false);
371
372 WebInspector.windowUnload = function(event)
373 {
374     InspectorController.windowUnloading();
375 }
376
377 WebInspector.windowResize = function(event)
378 {
379     if (this.currentPanel && this.currentPanel.resize)
380         this.currentPanel.resize();
381 }
382
383 WebInspector.windowFocused = function(event)
384 {
385     if (event.target.nodeType === Node.DOCUMENT_NODE)
386         document.body.removeStyleClass("inactive");
387 }
388
389 WebInspector.windowBlured = function(event)
390 {
391     if (event.target.nodeType === Node.DOCUMENT_NODE)
392         document.body.addStyleClass("inactive");
393 }
394
395 WebInspector.focusChanged = function(event)
396 {
397     this.currentFocusElement = event.target;
398 }
399
400 WebInspector.setAttachedWindow = function(attached)
401 {
402     this.attached = attached;
403 }
404
405 WebInspector.close = function(event)
406 {
407     InspectorController.closeWindow();
408 }
409
410 WebInspector.documentClick = function(event)
411 {
412     var anchor = event.target.enclosingNodeOrSelfWithNodeName("a");
413     if (!anchor)
414         return;
415
416     // Prevent the link from navigating, since we don't do any navigation by following links normally.
417     event.preventDefault();
418
419     function followLink()
420     {
421         // FIXME: support webkit-html-external-link links here.
422         if (anchor.href in WebInspector.resourceURLMap) {
423             if (anchor.hasStyleClass("webkit-html-external-link")) {
424                 anchor.removeStyleClass("webkit-html-external-link");
425                 anchor.addStyleClass("webkit-html-resource-link");
426             }
427
428             WebInspector.showResourceForURL(anchor.href, anchor.lineNumber, anchor.preferredPanel);
429         }
430     }
431
432     if (WebInspector.followLinkTimeout)
433         clearTimeout(WebInspector.followLinkTimeout);
434
435     if (anchor.preventFollowOnDoubleClick) {
436         // Start a timeout if this is the first click, if the timeout is canceled
437         // before it fires, then a double clicked happened or another link was clicked.
438         if (event.detail === 1)
439             WebInspector.followLinkTimeout = setTimeout(followLink, 333);
440         return;
441     }
442
443     followLink();
444 }
445
446 WebInspector.documentKeyDown = function(event)
447 {
448     if (!this.currentFocusElement)
449         return;
450     if (this.currentFocusElement.handleKeyEvent)
451         this.currentFocusElement.handleKeyEvent(event);
452     else if (this.currentFocusElement.id && this.currentFocusElement.id.length && WebInspector[this.currentFocusElement.id + "KeyDown"])
453         WebInspector[this.currentFocusElement.id + "KeyDown"](event);
454
455     if (!event.handled) {
456         switch (event.keyIdentifier) {
457             case "U+001B": // Escape key
458                 this.console.visible = !this.console.visible;
459                 event.preventDefault();
460                 break;
461         }
462     }
463 }
464
465 WebInspector.documentKeyUp = function(event)
466 {
467     if (!this.currentFocusElement || !this.currentFocusElement.handleKeyUpEvent)
468         return;
469     this.currentFocusElement.handleKeyUpEvent(event);
470 }
471
472 WebInspector.documentCanCopy = function(event)
473 {
474     if (!this.currentFocusElement)
475         return;
476     // Calling preventDefault() will say "we support copying, so enable the Copy menu".
477     if (this.currentFocusElement.handleCopyEvent)
478         event.preventDefault();
479     else if (this.currentFocusElement.id && this.currentFocusElement.id.length && WebInspector[this.currentFocusElement.id + "Copy"])
480         event.preventDefault();
481 }
482
483 WebInspector.documentCopy = function(event)
484 {
485     if (!this.currentFocusElement)
486         return;
487     if (this.currentFocusElement.handleCopyEvent)
488         this.currentFocusElement.handleCopyEvent(event);
489     else if (this.currentFocusElement.id && this.currentFocusElement.id.length && WebInspector[this.currentFocusElement.id + "Copy"])
490         WebInspector[this.currentFocusElement.id + "Copy"](event);
491 }
492
493 WebInspector.mainKeyDown = function(event)
494 {
495     if (this.currentPanel && this.currentPanel.handleKeyEvent)
496         this.currentPanel.handleKeyEvent(event);
497 }
498
499 WebInspector.mainKeyUp = function(event)
500 {
501     if (this.currentPanel && this.currentPanel.handleKeyUpEvent)
502         this.currentPanel.handleKeyUpEvent(event);
503 }
504
505 WebInspector.mainCopy = function(event)
506 {
507     if (this.currentPanel && this.currentPanel.handleCopyEvent)
508         this.currentPanel.handleCopyEvent(event);
509 }
510
511 WebInspector.searchResultsKeyDown = function(event)
512 {
513     if (this.searchResultsTree)
514         this.searchResultsTree.handleKeyEvent(event);
515 }
516
517 WebInspector.animateStyle = function(animations, duration, callback, complete)
518 {
519     if (complete === undefined)
520         complete = 0;
521     var slice = (1000 / 30); // 30 frames per second
522
523     var defaultUnit = "px";
524     var propertyUnit = {opacity: ""};
525
526     for (var i = 0; i < animations.length; ++i) {
527         var animation = animations[i];
528         var element = null;
529         var start = null;
530         var current = null;
531         var end = null;
532         for (key in animation) {
533             if (key === "element")
534                 element = animation[key];
535             else if (key === "start")
536                 start = animation[key];
537             else if (key === "current")
538                 current = animation[key];
539             else if (key === "end")
540                 end = animation[key];
541         }
542
543         if (!element || !end)
544             continue;
545
546         var computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
547         if (!start) {
548             start = {};
549             for (key in end)
550                 start[key] = parseInt(computedStyle.getPropertyValue(key));
551             animation.start = start;
552         } else if (complete == 0)
553             for (key in start)
554                 element.style.setProperty(key, start[key] + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
555
556         if (!current) {
557             current = {};
558             for (key in start)
559                 current[key] = start[key];
560             animation.current = current;
561         }
562
563         function cubicInOut(t, b, c, d)
564         {
565             if ((t/=d/2) < 1) return c/2*t*t*t + b;
566             return c/2*((t-=2)*t*t + 2) + b;
567         }
568
569         var style = element.style;
570         for (key in end) {
571             var startValue = start[key];
572             var currentValue = current[key];
573             var endValue = end[key];
574             if ((complete + slice) < duration) {
575                 var delta = (endValue - startValue) / (duration / slice);
576                 var newValue = cubicInOut(complete, startValue, endValue - startValue, duration);
577                 style.setProperty(key, newValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
578                 current[key] = newValue;
579             } else {
580                 style.setProperty(key, endValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
581             }
582         }
583     }
584
585     if (complete < duration)
586         setTimeout(WebInspector.animateStyle, slice, animations, duration, callback, complete + slice);
587     else if (callback)
588         callback();
589 }
590
591 WebInspector.toggleAttach = function()
592 {
593     this.attached = !this.attached;
594 }
595
596 WebInspector.toolbarDragStart = function(event)
597 {
598     if (!WebInspector.attached && InspectorController.platform() !== "mac-leopard")
599         return;
600
601     var target = event.target;
602     if (target.hasStyleClass("toolbar-item") && target.hasStyleClass("toggleable"))
603         return;
604
605     var toolbar = document.getElementById("toolbar");
606     if (target !== toolbar && !target.hasStyleClass("toolbar-item"))
607         return;
608
609     toolbar.lastScreenX = event.screenX;
610     toolbar.lastScreenY = event.screenY;
611
612     WebInspector.elementDragStart(toolbar, WebInspector.toolbarDrag, WebInspector.toolbarDragEnd, event, (WebInspector.attached ? "row-resize" : "default"));
613 }
614
615 WebInspector.toolbarDragEnd = function(event)
616 {
617     var toolbar = document.getElementById("toolbar");
618
619     WebInspector.elementDragEnd(event);
620
621     delete toolbar.lastScreenX;
622     delete toolbar.lastScreenY;
623 }
624
625 WebInspector.toolbarDrag = function(event)
626 {
627     var toolbar = document.getElementById("toolbar");
628
629     if (WebInspector.attached) {
630         var height = window.innerHeight - (event.screenY - toolbar.lastScreenY);
631
632         InspectorController.setAttachedWindowHeight(height);
633     } else {
634         var x = event.screenX - toolbar.lastScreenX;
635         var y = event.screenY - toolbar.lastScreenY;
636
637         // We cannot call window.moveBy here because it restricts the movement
638         // of the window at the edges.
639         InspectorController.moveByUnrestricted(x, y);
640     }
641
642     toolbar.lastScreenX = event.screenX;
643     toolbar.lastScreenY = event.screenY;
644
645     event.preventDefault();
646 }
647
648 WebInspector.searchResultsResizerDragStart = function(event)
649 {
650     WebInspector.elementDragStart(document.getElementById("searchResults"), WebInspector.searchResultsResizerDrag, WebInspector.searchResultsResizerDragEnd, event, "row-resize");
651 }
652
653 WebInspector.searchResultsResizerDragEnd = function(event)
654 {
655     WebInspector.elementDragEnd(event);
656 }
657
658 WebInspector.searchResultsResizerDrag = function(event)
659 {
660     var y = event.pageY - document.getElementById("main").offsetTop;
661     var newHeight = Number.constrain(y, 100, window.innerHeight - 100);
662
663     WebInspector.searchResultsHeight = newHeight;
664
665     document.getElementById("searchResults").style.height = WebInspector.searchResultsHeight + "px";
666     document.getElementById("main-panels").style.top = newHeight + "px";
667     document.getElementById("searchResultsResizer").style.top = (newHeight - 3) + "px";
668
669     event.preventDefault();
670 }
671
672 WebInspector.elementDragStart = function(element, dividerDrag, elementDragEnd, event, cursor) 
673 {
674     if (this._elementDraggingEventListener || this._elementEndDraggingEventListener)
675         this.elementDragEnd(event);
676
677     this._elementDraggingEventListener = dividerDrag;
678     this._elementEndDraggingEventListener = elementDragEnd;
679
680     document.addEventListener("mousemove", dividerDrag, true);
681     document.addEventListener("mouseup", elementDragEnd, true);
682
683     document.body.style.cursor = cursor;
684
685     event.preventDefault();
686 }
687
688 WebInspector.elementDragEnd = function(event)
689 {
690     document.removeEventListener("mousemove", this._elementDraggingEventListener, true);
691     document.removeEventListener("mouseup", this._elementEndDraggingEventListener, true);
692
693     document.body.style.removeProperty("cursor");
694
695     delete this._elementDraggingEventListener;
696     delete this._elementEndDraggingEventListener;
697
698     event.preventDefault();
699 }
700
701 WebInspector.showConsole = function()
702 {
703     this.console.show();
704 }
705
706 WebInspector.showElementsPanel = function()
707 {
708     this.currentPanel = this.panels.elements;
709 }
710
711 WebInspector.showResourcesPanel = function()
712 {
713     this.currentPanel = this.panels.resources;
714 }
715
716 WebInspector.showScriptsPanel = function()
717 {
718     this.currentPanel = this.panels.scripts;
719 }
720
721 WebInspector.showProfilesPanel = function()
722 {
723     this.currentPanel = this.panels.profiles;
724 }
725
726 WebInspector.showDatabasesPanel = function()
727 {
728     this.currentPanel = this.panels.databases;
729 }
730
731 WebInspector.addResource = function(resource)
732 {
733     this.resources.push(resource);
734     this.resourceURLMap[resource.url] = resource;
735
736     if (resource.mainResource) {
737         this.mainResource = resource;
738         this.panels.elements.reset();
739     }
740
741     this.panels.resources.addResource(resource);
742 }
743
744 WebInspector.removeResource = function(resource)
745 {
746     resource.category.removeResource(resource);
747     delete this.resourceURLMap[resource.url];
748
749     var resourcesLength = this.resources.length;
750     for (var i = 0; i < resourcesLength; ++i) {
751         if (this.resources[i] === resource) {
752             this.resources.splice(i, 1);
753             break;
754         }
755     }
756
757     this.panels.resources.removeResource(resource);
758 }
759
760 WebInspector.addDatabase = function(database)
761 {
762     this.panels.databases.addDatabase(database);
763 }
764
765 WebInspector.debuggerAttached = function()
766 {
767     this.panels.scripts.debuggerAttached();
768 }
769
770 WebInspector.debuggerDetached = function()
771 {
772     this.panels.scripts.debuggerDetached();
773 }
774
775 WebInspector.parsedScriptSource = function(sourceID, sourceURL, source, startingLine)
776 {
777     this.panels.scripts.addScript(sourceID, sourceURL, source, startingLine);
778 }
779
780 WebInspector.failedToParseScriptSource = function(sourceURL, source, startingLine, errorLine, errorMessage)
781 {
782     this.panels.scripts.addScript(null, sourceURL, source, startingLine, errorLine, errorMessage);
783 }
784
785 WebInspector.pausedScript = function()
786 {
787     this.panels.scripts.debuggerPaused();
788 }
789
790 WebInspector.populateInterface = function()
791 {
792     for (var panelName in this.panels) {
793         var panel = this.panels[panelName];
794         if ("populateInterface" in panel)
795             panel.populateInterface();
796     }
797 }
798
799 WebInspector.reset = function()
800 {
801     for (var panelName in this.panels) {
802         var panel = this.panels[panelName];
803         if ("reset" in panel)
804             panel.reset();
805     }
806
807     for (var category in this.resourceCategories)
808         this.resourceCategories[category].removeAllResources();
809
810     this.resources = [];
811     this.resourceURLMap = {};
812
813     delete this.mainResource;
814
815     this.console.clearMessages();
816 }
817
818 WebInspector.inspectedWindowCleared = function(inspectedWindow)
819 {
820     this.panels.elements.inspectedWindowCleared(inspectedWindow);
821 }
822
823 WebInspector.resourceURLChanged = function(resource, oldURL)
824 {
825     delete this.resourceURLMap[oldURL];
826     this.resourceURLMap[resource.url] = resource;
827 }
828
829 WebInspector.addMessageToConsole = function(msg)
830 {
831     this.console.addMessage(msg);
832 }
833
834 WebInspector.startGroupInConsole = function()
835 {
836     this.console.startGroup();
837 }
838
839 WebInspector.endGroupInConsole = function()
840 {
841     this.console.endGroup();
842 }
843
844 WebInspector.addProfile = function(profile)
845 {
846     this.panels.profiles.addProfile(profile);
847 }
848
849 WebInspector.setRecordingProfile = function(isProfiling)
850 {
851     this.panels.profiles.setRecordingProfile(isProfiling);
852 }
853
854 WebInspector.drawLoadingPieChart = function(canvas, percent) {
855     var g = canvas.getContext("2d");
856     var darkColor = "rgb(122, 168, 218)";
857     var lightColor = "rgb(228, 241, 251)";
858     var cx = 8;
859     var cy = 8;
860     var r = 7;
861
862     g.beginPath();
863     g.arc(cx, cy, r, 0, Math.PI * 2, false); 
864     g.closePath();
865
866     g.lineWidth = 1;
867     g.strokeStyle = darkColor;
868     g.fillStyle = lightColor;
869     g.fill();
870     g.stroke();
871
872     var startangle = -Math.PI / 2;
873     var endangle = startangle + (percent * Math.PI * 2);
874
875     g.beginPath();
876     g.moveTo(cx, cy);
877     g.arc(cx, cy, r, startangle, endangle, false); 
878     g.closePath();
879
880     g.fillStyle = darkColor;
881     g.fill();
882 }
883
884 WebInspector.updateFocusedNode = function(node)
885 {
886     if (!node)
887         // FIXME: Should we deselect if null is passed in?
888         return;
889
890     this.currentPanel = this.panels.elements;
891     this.panels.elements.focusedDOMNode = node;
892 }
893
894 WebInspector.displayNameForURL = function(url)
895 {
896     var resource = this.resourceURLMap[url];
897     if (resource)
898         return resource.displayName;
899     return url.trimURL(WebInspector.mainResource ? WebInspector.mainResource.domain : "");
900 }
901
902 WebInspector.resourceForURL = function(url)
903 {
904     if (url in this.resourceURLMap)
905         return this.resourceURLMap[url];
906
907     // No direct match found. Search for resources that contain
908     // a substring of the URL.
909     for (var resourceURL in this.resourceURLMap) {
910         if (resourceURL.hasSubstring(url))
911             return this.resourceURLMap[resourceURL];
912     }
913
914     return null;
915 }
916
917 WebInspector.showResourceForURL = function(url, line, preferredPanel)
918 {
919     var resource = this.resourceForURL(url);
920     if (!resource)
921         return false;
922
923     if (preferredPanel && preferredPanel in WebInspector.panels) {
924         var panel = this.panels[preferredPanel];
925         if (!("showResource" in panel))
926             panel = null;
927         else if ("canShowResource" in panel && !panel.canShowResource(resource))
928             panel = null;
929     }
930
931     this.currentPanel = panel || this.panels.resources;
932     this.currentPanel.showResource(resource, line);
933     return true;
934 }
935
936 WebInspector.linkifyStringAsFragment = function(string)
937 {
938     var container = document.createDocumentFragment();
939     var linkStringRegEx = new RegExp("(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}://|www\\.)[\\w$\\-_+*'=\\|/\\\\(){}[\\]%@&#~,:;.!?]{4,}[\\w$\\-_+*=\\|/\\\\({%@&#~]");
940
941     while (string) {
942         var linkString = linkStringRegEx.exec(string);
943         if (!linkString)
944             break;
945
946         linkString = linkString[0];
947         var linkIndex = string.indexOf(linkString);
948         var nonLink = string.substring(0, linkIndex);
949         container.appendChild(document.createTextNode(nonLink));
950         var realURL = (linkString.indexOf("www.") === 0 ? "http://" + linkString : linkString);
951         container.appendChild(WebInspector.linkifyURLAsNode(realURL, linkString, null, (realURL in WebInspector.resourceURLMap)));
952         string = string.substring(linkIndex + linkString.length, string.length);
953     }
954
955     if (string)
956         container.appendChild(document.createTextNode(string));
957     
958     return container;
959 }
960
961 WebInspector.linkifyURLAsNode = function(url, linkText, classes, isExternal)
962 {
963     if (!linkText)
964         linkText = url;
965     classes = (classes ? classes + " " : "");
966     classes += isExternal ? "webkit-html-external-link" : "webkit-html-resource-link";
967
968     var a = document.createElement("a");
969     a.href = url;
970     a.className = classes;
971     a.title = url;
972     a.target = "_blank";
973     a.textContent = linkText;
974     
975     return a;
976 }
977
978 WebInspector.linkifyURL = function(url, linkText, classes, isExternal)
979 {
980     if (!linkText)
981         linkText = url.escapeHTML();
982     classes = (classes ? classes + " " : "");
983     classes += isExternal ? "webkit-html-external-link" : "webkit-html-resource-link";
984     var link = "<a href=\"" + url + "\" class=\"" + classes + "\" title=\"" + url + "\" target=\"_blank\">" + linkText + "</a>";
985     return link;
986 }
987
988 WebInspector.addMainEventListeners = function(doc)
989 {
990     doc.defaultView.addEventListener("focus", this.windowFocused.bind(this), true);
991     doc.defaultView.addEventListener("blur", this.windowBlured.bind(this), true);
992     doc.addEventListener("click", this.documentClick.bind(this), true);
993 }
994
995 WebInspector.performSearch = function(event)
996 {
997     var query = event.target.value;
998
999     if (!query || !query.length) {
1000         this.showingSearchResults = false;
1001         return;
1002     }
1003
1004     var forceSearch = event.keyIdentifier === "Enter";
1005     if(!forceSearch && query.length < 3)
1006         return;
1007
1008     if (!forceSearch && this.lastQuery && this.lastQuery === query)
1009         return;
1010     this.lastQuery = query;
1011
1012     var resultsContainer = document.getElementById("searchResults");
1013     resultsContainer.removeChildren();
1014
1015     var isXPath = query.indexOf("/") !== -1;
1016
1017     var xpathQuery;
1018     if (isXPath)
1019         xpathQuery = query;
1020     else {
1021         var escapedQuery = query.escapeCharacters("'");
1022         xpathQuery = "//*[contains(name(),'" + escapedQuery + "') or contains(@*,'" + escapedQuery + "')] | //text()[contains(.,'" + escapedQuery + "')] | //comment()[contains(.,'" + escapedQuery + "')]";
1023     }
1024
1025     var resourcesToSearch = [].concat(this.resourceCategories.documents.resources, this.resourceCategories.stylesheets.resources, this.resourceCategories.scripts.resources, this.resourceCategories.other.resources);
1026
1027     var files = [];
1028     for (var i = 0; i < resourcesToSearch.length; ++i) {
1029         var resource = resourcesToSearch[i];
1030
1031         var sourceResults = [];
1032         if (!isXPath) {
1033             var sourceFrame = this.panels.resources.sourceFrameForResource(resource);
1034             if (sourceFrame)
1035                 sourceResults = InspectorController.search(sourceFrame.element.contentDocument, query);
1036         }
1037
1038         var domResults = [];
1039         const searchResultsProperty = "__includedInInspectorSearchResults";
1040         function addNodesToDOMResults(nodes, length, getItem)
1041         {
1042             for (var i = 0; i < length; ++i) {
1043                 var node = getItem(nodes, i);
1044                 if (searchResultsProperty in node)
1045                     continue;
1046                 node[searchResultsProperty] = true;
1047                 domResults.push(node);
1048             }
1049         }
1050
1051         function cleanUpDOMResultsNodes()
1052         {
1053             for (var i = 0; i < domResults.length; ++i)
1054                 delete domResults[i][searchResultsProperty];
1055         }
1056
1057         if (resource.category === this.resourceCategories.documents) {
1058             var doc = resource.documentNode;
1059             try {
1060                 var result = InspectorController.inspectedWindow().Document.prototype.evaluate.call(doc, xpathQuery, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
1061                 addNodesToDOMResults(result, result.snapshotLength, function(l, i) { return l.snapshotItem(i); });
1062             } catch(err) {
1063                 // ignore any exceptions. the query might be malformed, but we allow that.
1064             }
1065
1066             try {
1067                 var result = InspectorController.inspectedWindow().Document.prototype.querySelectorAll.call(doc, query);
1068                 addNodesToDOMResults(result, result.length, function(l, i) { return l.item(i); });
1069             } catch(err) {
1070                 // ignore any exceptions. the query isn't necessarily a valid selector.
1071             }
1072
1073             cleanUpDOMResultsNodes();
1074         }
1075
1076         if ((!sourceResults || !sourceResults.length) && !domResults.length)
1077             continue;
1078
1079         files.push({resource: resource, sourceResults: sourceResults, domResults: domResults});
1080     }
1081
1082     if (!files.length)
1083         return;
1084
1085     this.showingSearchResults = true;
1086
1087     var fileList = document.createElement("ol");
1088     fileList.className = "outline-disclosure";
1089     resultsContainer.appendChild(fileList);
1090
1091     this.searchResultsTree = new TreeOutline(fileList);
1092     this.searchResultsTree.expandTreeElementsWhenArrowing = true;
1093
1094     var sourceResultSelected = function(element)
1095     {
1096         var selection = window.getSelection();
1097         selection.removeAllRanges();
1098         selection.addRange(element.representedObject.range);
1099
1100         var oldFocusElement = this.currentFocusElement;
1101         this.currentPanel = this.panels.resources;
1102         this.currentFocusElement = oldFocusElement;
1103
1104         this.panels.resources.showResource(element.representedObject.resource);
1105
1106         element.representedObject.line.scrollIntoViewIfNeeded(true);
1107         element.listItemElement.scrollIntoViewIfNeeded(false);
1108     }
1109
1110     var domResultSelected = function(element)
1111     {
1112         var oldFocusElement = this.currentFocusElement;
1113         this.currentPanel = this.panels.elements;
1114         this.currentFocusElement = oldFocusElement;
1115
1116         this.panels.elements.focusedDOMNode = element.representedObject.node;
1117         element.listItemElement.scrollIntoViewIfNeeded(false);
1118     }
1119
1120     for (var i = 0; i < files.length; ++i) {
1121         var file = files[i];
1122
1123         var fileItem = new TreeElement(file.resource.displayName, {}, true);
1124         fileItem.expanded = true;
1125         fileItem.selectable = false;
1126         this.searchResultsTree.appendChild(fileItem);
1127
1128         if (file.sourceResults && file.sourceResults.length) {
1129             for (var j = 0; j < file.sourceResults.length; ++j) {
1130                 var range = file.sourceResults[j];
1131                 var sourceDocument = range.startContainer.ownerDocument;
1132
1133                 var line = range.startContainer;
1134                 while (line.parentNode && line.nodeName.toLowerCase() != "tr")
1135                     line = line.parentNode;
1136                 var lineRange = sourceDocument.createRange();
1137                 lineRange.selectNodeContents(line);
1138
1139                 // Don't include any error bubbles in the search result
1140                 var end = line.lastChild.lastChild;
1141                 if (end.nodeName.toLowerCase() == "div" && end.hasStyleClass("webkit-html-message-bubble")) {
1142                     while (end && end.nodeName.toLowerCase() == "div" && end.hasStyleClass("webkit-html-message-bubble"))
1143                         end = end.previousSibling;
1144                     lineRange.setEndAfter(end);
1145                 }
1146
1147                 var beforeRange = sourceDocument.createRange();
1148                 beforeRange.setStart(lineRange.startContainer, lineRange.startOffset);
1149                 beforeRange.setEnd(range.startContainer, range.startOffset);
1150
1151                 var afterRange = sourceDocument.createRange();
1152                 afterRange.setStart(range.endContainer, range.endOffset);
1153                 afterRange.setEnd(lineRange.endContainer, lineRange.endOffset);
1154
1155                 var beforeText = beforeRange.toString().trimLeadingWhitespace();
1156                 var text = range.toString();
1157                 var afterText = afterRange.toString().trimTrailingWhitespace();
1158
1159                 var length = beforeText.length + text.length + afterText.length;
1160                 if (length > Preferences.maxTextSearchResultLength) {
1161                     var beforeAfterLength = (Preferences.maxTextSearchResultLength - text.length) / 2;
1162                     if (beforeText.length > beforeAfterLength)
1163                         beforeText = "\u2026" + beforeText.substr(-beforeAfterLength);
1164                     if (afterText.length > beforeAfterLength)
1165                         afterText = afterText.substr(0, beforeAfterLength) + "\u2026";
1166                 }
1167
1168                 var title = "<div class=\"selection selected\"></div>";
1169                 if (j == 0)
1170                     title += "<div class=\"search-results-section\">" + WebInspector.UIString("Source") + "</div>";
1171                 title += beforeText.escapeHTML() + "<span class=\"search-matched-string\">" + text.escapeHTML() + "</span>" + afterText.escapeHTML();
1172                 var item = new TreeElement(title, {resource: file.resource, line: line, range: range}, false);
1173                 item.onselect = sourceResultSelected.bind(this);
1174                 fileItem.appendChild(item);
1175             }
1176         }
1177
1178         if (file.domResults.length) {
1179             for (var j = 0; j < file.domResults.length; ++j) {
1180                 var node = file.domResults[j];
1181                 var title = "<div class=\"selection selected\"></div>";
1182                 if (j == 0)
1183                     title += "<div class=\"search-results-section\">" + WebInspector.UIString("DOM") + "</div>";
1184                 title += nodeTitleInfo.call(node).title;
1185                 var item = new TreeElement(title, {resource: file.resource, node: node}, false);
1186                 item.onselect = domResultSelected.bind(this);
1187                 fileItem.appendChild(item);
1188             }
1189         }
1190     }
1191 }
1192
1193 WebInspector.UIString = function(string)
1194 {
1195     if (window.localizedStrings && string in window.localizedStrings)
1196         string = window.localizedStrings[string];
1197     else {
1198         if (!(string in this.missingLocalizedStrings)) {
1199             console.error("Localized string \"" + string + "\" not found.");
1200             this.missingLocalizedStrings[string] = true;
1201         }
1202
1203         if (Preferences.showMissingLocalizedStrings)
1204             string += " (not localized)";
1205     }
1206
1207     return String.vsprintf(string, Array.prototype.slice.call(arguments, 1));
1208 }
1209
1210 WebInspector.isBeingEdited = function(element)
1211 {
1212     return element.__editing;
1213 }
1214
1215 WebInspector.startEditing = function(element, committedCallback, cancelledCallback, context)
1216 {
1217     if (element.__editing)
1218         return;
1219     element.__editing = true;
1220
1221     var oldText = element.textContent;
1222     var oldHandleKeyEvent = element.handleKeyEvent;
1223
1224     element.addStyleClass("editing");
1225
1226     var oldTabIndex = element.tabIndex;
1227     if (element.tabIndex < 0)
1228         element.tabIndex = 0;
1229
1230     function blurEventListener() {
1231         editingCommitted.call(element);
1232     }
1233
1234     function cleanUpAfterEditing() {
1235         delete this.__editing;
1236
1237         this.removeStyleClass("editing");
1238         this.tabIndex = oldTabIndex;
1239         this.scrollTop = 0;
1240         this.scrollLeft = 0;
1241
1242         this.handleKeyEvent = oldHandleKeyEvent;
1243         element.removeEventListener("blur", blurEventListener, false);
1244
1245         if (element === WebInspector.currentFocusElement || element.isAncestor(WebInspector.currentFocusElement))
1246             WebInspector.currentFocusElement = WebInspector.previousFocusElement;
1247     }
1248
1249     function editingCancelled() {
1250         this.innerText = oldText;
1251
1252         cleanUpAfterEditing.call(this);
1253
1254         cancelledCallback(this, context);
1255     }
1256
1257     function editingCommitted() {
1258         cleanUpAfterEditing.call(this);
1259
1260         committedCallback(this, this.textContent, oldText, context);
1261     }
1262
1263     element.handleKeyEvent = function(event) {
1264         if (oldHandleKeyEvent)
1265             oldHandleKeyEvent(event);
1266         if (event.handled)
1267             return;
1268
1269         if (event.keyIdentifier === "Enter") {
1270             editingCommitted.call(element);
1271             event.preventDefault();
1272         } else if (event.keyCode === 27) { // Escape key
1273             editingCancelled.call(element);
1274             event.preventDefault();
1275             event.handled = true;
1276         }
1277     }
1278
1279     element.addEventListener("blur", blurEventListener, false);
1280
1281     WebInspector.currentFocusElement = element;
1282 }
1283
1284 WebInspector._toolbarItemClicked = function(event)
1285 {
1286     var toolbarItem = event.currentTarget;
1287     this.currentPanel = toolbarItem.panel;
1288 }
1289
1290 // This table maps MIME types to the Resource.Types which are valid for them.
1291 // The following line:
1292 //    "text/html":                {0: 1},
1293 // means that text/html is a valid MIME type for resources that have type
1294 // WebInspector.Resource.Type.Document (which has a value of 0).
1295 WebInspector.MIMETypes = {
1296     "text/html":                   {0: true},
1297     "text/xml":                    {0: true},
1298     "text/plain":                  {0: true},
1299     "application/xhtml+xml":       {0: true},
1300     "text/css":                    {1: true},
1301     "text/xsl":                    {1: true},
1302     "image/jpeg":                  {2: true},
1303     "image/png":                   {2: true},
1304     "image/gif":                   {2: true},
1305     "image/bmp":                   {2: true},
1306     "image/x-icon":                {2: true},
1307     "image/x-xbitmap":             {2: true},
1308     "font/ttf":                    {3: true},
1309     "font/opentype":               {3: true},
1310     "application/x-font-type1":    {3: true},
1311     "application/x-font-ttf":      {3: true},
1312     "application/x-truetype-font": {3: true},
1313     "text/javascript":             {4: true},
1314     "text/ecmascript":             {4: true},
1315     "application/javascript":      {4: true},
1316     "application/ecmascript":      {4: true},
1317     "application/x-javascript":    {4: true},
1318     "text/javascript1.1":          {4: true},
1319     "text/javascript1.2":          {4: true},
1320     "text/javascript1.3":          {4: true},
1321     "text/jscript":                {4: true},
1322     "text/livescript":             {4: true},
1323 }