Web Inspector: Polish TabbedEditorContaner and ScriptsNavigator behavior.
[WebKit-https.git] / Source / WebCore / inspector / front-end / NavigatorView.js
1 /*
2  * Copyright (C) 2012 Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  * 1. Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *
11  * 2. Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *
16  * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. AND ITS CONTRIBUTORS
17  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE INC.
20  * OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 /**
30  * @extends {WebInspector.View}
31  * @constructor
32  */
33 WebInspector.NavigatorView = function()
34 {
35     WebInspector.View.call(this);
36     this.registerRequiredCSS("navigatorView.css");
37
38     this._treeSearchBoxElement = document.createElement("div");
39     this._treeSearchBoxElement.className = "navigator-tree-search-box";
40     this.element.appendChild(this._treeSearchBoxElement);
41
42     var scriptsTreeElement = document.createElement("ol");
43     this._scriptsTree = new WebInspector.NavigatorTreeOutline(this._treeSearchBoxElement, scriptsTreeElement);
44
45     var scriptsOutlineElement = document.createElement("div");
46     scriptsOutlineElement.addStyleClass("outline-disclosure");
47     scriptsOutlineElement.addStyleClass("navigator");
48     scriptsOutlineElement.appendChild(scriptsTreeElement);
49
50     this.element.addStyleClass("fill");
51     this.element.addStyleClass("navigator-container");
52     this.element.appendChild(scriptsOutlineElement);
53     this.setDefaultFocusedElement(this._scriptsTree.element);
54
55     /** @type {Object.<string, WebInspector.NavigatorUISourceCodeTreeNode>} */
56     this._uiSourceCodeNodes = {};
57
58     this._rootNode = new WebInspector.NavigatorRootTreeNode(this);
59     this._rootNode.populate();
60
61     WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.InspectedURLChanged, this._inspectedURLChanged, this);
62 }
63
64 WebInspector.NavigatorView.Events = {
65     ItemSelected: "ItemSelected",
66     FileRenamed: "FileRenamed"
67 }
68
69 WebInspector.NavigatorView.iconClassForType = function(type)
70 {
71     if (type === WebInspector.NavigatorTreeOutline.Types.Domain)
72         return "navigator-domain-tree-item";
73     if (type === WebInspector.NavigatorTreeOutline.Types.FileSystem)
74         return "navigator-folder-tree-item";
75     return "navigator-folder-tree-item";
76 }
77
78 WebInspector.NavigatorView.prototype = {
79     /**
80      * @param {WebInspector.UISourceCode} uiSourceCode
81      */
82     addUISourceCode: function(uiSourceCode)
83     {
84         var node = this._getOrCreateUISourceCodeParentNode(uiSourceCode);
85         var uiSourceCodeNode = new WebInspector.NavigatorUISourceCodeTreeNode(this, uiSourceCode);
86         this._uiSourceCodeNodes[uiSourceCode.uri()] = uiSourceCodeNode;
87         node.appendChild(uiSourceCodeNode);
88         if (uiSourceCode.url === WebInspector.inspectedPageURL)
89             this.revealUISourceCode(uiSourceCode);
90     },
91
92     /**
93      * @param {WebInspector.Event} event
94      */
95     _inspectedURLChanged: function(event)
96     {
97         var nodes = Object.values(this._uiSourceCodeNodes);
98         for (var i = 0; i < nodes.length; ++i) {
99             var uiSourceCode = nodes[i].uiSourceCode();
100             if (uiSourceCode.url === WebInspector.inspectedPageURL)
101                 this.revealUISourceCode(uiSourceCode);
102         }
103
104     },
105
106     /**
107      * @param {WebInspector.Project} project
108      * @return {WebInspector.NavigatorTreeNode}
109      */
110     _getProjectNode: function(project)
111     {
112         if (!project.displayName())
113             return this._rootNode;
114         return this._rootNode.child(project.id());
115     },
116
117     /**
118      * @param {WebInspector.Project} project
119      * @return {WebInspector.NavigatorFolderTreeNode}
120      */
121     _createProjectNode: function(project)
122     {
123         var type = project.type() === WebInspector.projectTypes.FileSystem ? WebInspector.NavigatorTreeOutline.Types.FileSystem : WebInspector.NavigatorTreeOutline.Types.Domain;
124         var projectNode = new WebInspector.NavigatorFolderTreeNode(this, project.id(), type, project.displayName());
125         this._rootNode.appendChild(projectNode);
126         return projectNode;
127     },
128
129     /**
130      * @param {WebInspector.Project} project
131      * @return {WebInspector.NavigatorTreeNode}
132      */
133     _getOrCreateProjectNode: function(project)
134     {
135         return this._getProjectNode(project) || this._createProjectNode(project);
136     },
137
138     /**
139      * @param {WebInspector.NavigatorTreeNode} parentNode
140      * @param {string} name
141      * @return {WebInspector.NavigatorFolderTreeNode}
142      */
143     _getFolderNode: function(parentNode, name)
144     {
145         return parentNode.child(name);
146     },
147
148     /**
149      * @param {WebInspector.NavigatorTreeNode} parentNode
150      * @param {string} name
151      * @return {WebInspector.NavigatorFolderTreeNode}
152      */
153     _createFolderNode: function(parentNode, name)
154     {
155         var folderNode = new WebInspector.NavigatorFolderTreeNode(this, name, WebInspector.NavigatorTreeOutline.Types.Folder, name);
156         parentNode.appendChild(folderNode);
157         return folderNode;
158     },
159
160     /**
161      * @param {WebInspector.NavigatorTreeNode} parentNode
162      * @param {string} name
163      * @return {WebInspector.NavigatorFolderTreeNode}
164      */
165     _getOrCreateFolderNode: function(parentNode, name)
166     {
167         return this._getFolderNode(parentNode, name) || this._createFolderNode(parentNode, name);
168     },
169
170     /**
171      * @param {WebInspector.UISourceCode} uiSourceCode
172      * @return {WebInspector.NavigatorTreeNode}
173      */
174     _getUISourceCodeParentNode: function(uiSourceCode)
175     {
176         var projectNode = this._getProjectNode(uiSourceCode.project());
177         if (!projectNode)
178             return null;
179         var path = uiSourceCode.path();
180         var parentNode = projectNode;
181         for (var i = 0; i < path.length - 1; ++i) {
182             parentNode = this._getFolderNode(parentNode, path[i]);
183             if (!parentNode)
184                 return null;
185         }
186         return parentNode;
187     },
188
189     /**
190      * @param {WebInspector.UISourceCode} uiSourceCode
191      * @return {WebInspector.NavigatorTreeNode}
192      */
193     _getOrCreateUISourceCodeParentNode: function(uiSourceCode)
194     {
195         var projectNode = this._getOrCreateProjectNode(uiSourceCode.project());
196         if (!projectNode)
197             return null;
198         var path = uiSourceCode.path();
199         var parentNode = projectNode;
200         for (var i = 0; i < path.length - 1; ++i) {
201             parentNode = this._getOrCreateFolderNode(parentNode, path[i]);
202             if (!parentNode)
203                 return null;
204         }
205         return parentNode;
206     },
207
208     /**
209      * @param {WebInspector.UISourceCode} uiSourceCode
210      * @param {boolean=} select
211      */
212     revealUISourceCode: function(uiSourceCode, select)
213     {
214         var node = this._uiSourceCodeNodes[uiSourceCode.uri()];
215         if (!node)
216             return null;
217         if (this._scriptsTree.selectedTreeElement)
218             this._scriptsTree.selectedTreeElement.deselect();
219         this._lastSelectedUISourceCode = uiSourceCode;
220         node.reveal(select);
221     },
222
223     /**
224      * @param {WebInspector.UISourceCode} uiSourceCode
225      * @param {boolean} focusSource
226      */
227     _scriptSelected: function(uiSourceCode, focusSource)
228     {
229         this._lastSelectedUISourceCode = uiSourceCode;
230         var data = { uiSourceCode: uiSourceCode, focusSource: focusSource};
231         this.dispatchEventToListeners(WebInspector.NavigatorView.Events.ItemSelected, data);
232     },
233
234     /**
235      * @param {WebInspector.UISourceCode} uiSourceCode
236      */
237     removeUISourceCode: function(uiSourceCode)
238     {
239         var parentNode = this._getUISourceCodeParentNode(uiSourceCode);
240         if (!parentNode)
241             return;
242         var node = this._uiSourceCodeNodes[uiSourceCode.uri()];
243         if (!node)
244             return;
245         delete this._uiSourceCodeNodes[uiSourceCode.uri()]
246         parentNode.removeChild(node);
247         node = parentNode;
248         while (node) {
249             parentNode = node.parent;
250             if (!parentNode || !node.isEmpty())
251                 break;
252             parentNode.removeChild(node);
253             node = parentNode;
254         }
255     },
256
257     _fileRenamed: function(uiSourceCode, newTitle)
258     {    
259         var data = { uiSourceCode: uiSourceCode, name: newTitle };
260         this.dispatchEventToListeners(WebInspector.NavigatorView.Events.FileRenamed, data);
261     },
262
263     /**
264      * @param {WebInspector.UISourceCode} uiSourceCode
265      * @param {function(boolean)=} callback
266      */
267     rename: function(uiSourceCode, callback)
268     {
269         var node = this._uiSourceCodeNodes[uiSourceCode.uri()];
270         if (!node)
271             return null;
272         node.rename(callback);
273     },
274
275     reset: function()
276     {
277         for (var uri in this._uiSourceCodeNodes)
278             this._uiSourceCodeNodes[uri].dispose();
279
280         this._scriptsTree.stopSearch();
281         this._scriptsTree.removeChildren();
282         this._uiSourceCodeNodes = {};
283         this._rootNode.reset();
284     },
285
286     handleContextMenu: function(event, uiSourceCode)
287     {
288         var contextMenu = new WebInspector.ContextMenu(event);
289         contextMenu.appendApplicableItems(uiSourceCode);
290         contextMenu.show();
291     },
292
293     __proto__: WebInspector.View.prototype
294 }
295
296 /**
297  * @constructor
298  * @extends {TreeOutline}
299  * @param {Element} treeSearchBoxElement
300  * @param {Element} element
301  */
302 WebInspector.NavigatorTreeOutline = function(treeSearchBoxElement, element)
303 {
304     TreeOutline.call(this, element);
305     this.element = element;
306
307     this._treeSearchBoxElement = treeSearchBoxElement;
308     
309     this.comparator = WebInspector.NavigatorTreeOutline._treeElementsCompare;
310
311     this.searchable = true;
312     this.searchInputElement = document.createElement("input");
313 }
314
315 WebInspector.NavigatorTreeOutline.Types = {
316     Root: "Root",
317     Domain: "Domain",
318     Folder: "Folder",
319     UISourceCode: "UISourceCode",
320     FileSystem: "FileSystem"
321 }
322
323 WebInspector.NavigatorTreeOutline._treeElementsCompare = function compare(treeElement1, treeElement2)
324 {
325     // Insert in the alphabetical order, first domains, then folders, then scripts.
326     function typeWeight(treeElement)
327     {
328         var type = treeElement.type();
329         if (type === WebInspector.NavigatorTreeOutline.Types.Domain) {
330             if (treeElement.titleText === WebInspector.inspectedPageDomain)
331                 return 1;
332             return 2;
333         }
334         if (type === WebInspector.NavigatorTreeOutline.Types.FileSystem)
335             return 3;
336         if (type === WebInspector.NavigatorTreeOutline.Types.Folder)
337             return 4;
338         return 5;
339     }
340
341     var typeWeight1 = typeWeight(treeElement1);
342     var typeWeight2 = typeWeight(treeElement2);
343
344     var result;
345     if (typeWeight1 > typeWeight2)
346         result = 1;
347     else if (typeWeight1 < typeWeight2)
348         result = -1;
349     else {
350         var title1 = treeElement1.titleText;
351         var title2 = treeElement2.titleText;
352         result = title1.compareTo(title2);
353     }
354     return result;
355 }
356
357 WebInspector.NavigatorTreeOutline.prototype = {
358    /**
359     * @return {Array.<WebInspector.UISourceCode>}
360     */
361    scriptTreeElements: function()
362    {
363        var result = [];
364        if (this.children.length) {
365            for (var treeElement = this.children[0]; treeElement; treeElement = treeElement.traverseNextTreeElement(false, this, true)) {
366                if (treeElement instanceof WebInspector.NavigatorSourceTreeElement)
367                    result.push(treeElement.uiSourceCode);
368            }
369        }
370        return result;
371    },
372
373    searchStarted: function()
374    {
375        this._treeSearchBoxElement.appendChild(this.searchInputElement);
376        this._treeSearchBoxElement.addStyleClass("visible");
377    },
378
379    searchFinished: function()
380    {
381        this._treeSearchBoxElement.removeChild(this.searchInputElement);
382        this._treeSearchBoxElement.removeStyleClass("visible");
383    },
384
385     __proto__: TreeOutline.prototype
386 }
387
388 /**
389  * @constructor
390  * @extends {TreeElement}
391  * @param {string} type
392  * @param {string} title
393  * @param {Array.<string>} iconClasses
394  * @param {boolean} hasChildren
395  * @param {boolean=} noIcon
396  */
397 WebInspector.BaseNavigatorTreeElement = function(type, title, iconClasses, hasChildren, noIcon)
398 {
399     this._type = type;
400     TreeElement.call(this, "", null, hasChildren);
401     this._titleText = title;
402     this._iconClasses = iconClasses;
403     this._noIcon = noIcon;
404 }
405
406 WebInspector.BaseNavigatorTreeElement.prototype = {
407     onattach: function()
408     {
409         this.listItemElement.removeChildren();
410         if (this._iconClasses) {
411             for (var i = 0; i < this._iconClasses.length; ++i)
412                 this.listItemElement.addStyleClass(this._iconClasses[i]);
413         }
414
415         var selectionElement = document.createElement("div");
416         selectionElement.className = "selection";
417         this.listItemElement.appendChild(selectionElement);
418
419         if (!this._noIcon) {
420             this.imageElement = document.createElement("img");
421             this.imageElement.className = "icon";
422             this.listItemElement.appendChild(this.imageElement);
423         }
424         
425         this.titleElement = document.createElement("div");
426         this.titleElement.className = "base-navigator-tree-element-title";
427         this._titleTextNode = document.createTextNode("");
428         this._titleTextNode.textContent = this._titleText;
429         this.titleElement.appendChild(this._titleTextNode);
430         this.listItemElement.appendChild(this.titleElement);
431     },
432
433     onreveal: function()
434     {
435         if (this.listItemElement)
436             this.listItemElement.scrollIntoViewIfNeeded(true);
437     },
438
439     /**
440      * @return {string}
441      */
442     get titleText()
443     {
444         return this._titleText;
445     },
446
447     set titleText(titleText)
448     {
449         if (this._titleText === titleText)
450             return;
451         this._titleText = titleText || "";
452         if (this.titleElement)
453             this.titleElement.textContent = this._titleText;
454     },
455     
456     /**
457      * @param {string} searchText
458      */
459     matchesSearchText: function(searchText)
460     {
461         return this.titleText.match(new RegExp("^" + searchText.escapeForRegExp(), "i"));
462     },
463
464     /**
465      * @return {string}
466      */
467     type: function()
468     {
469         return this._type;
470     },
471
472     __proto__: TreeElement.prototype
473 }
474
475 /**
476  * @constructor
477  * @extends {WebInspector.BaseNavigatorTreeElement}
478  * @param {string} type
479  * @param {string} title
480  */
481 WebInspector.NavigatorFolderTreeElement = function(type, title)
482 {
483     var iconClass = WebInspector.NavigatorView.iconClassForType(type);
484     WebInspector.BaseNavigatorTreeElement.call(this, type, title, [iconClass], true);
485 }
486
487 WebInspector.NavigatorFolderTreeElement.prototype = {
488     onpopulate: function()
489     {
490         this._node.populate();
491     },
492
493     onattach: function()
494     {
495         WebInspector.BaseNavigatorTreeElement.prototype.onattach.call(this);
496         this.collapse();
497     },
498
499     __proto__: WebInspector.BaseNavigatorTreeElement.prototype
500 }
501
502 /**
503  * @constructor
504  * @extends {WebInspector.BaseNavigatorTreeElement}
505  * @param {WebInspector.NavigatorView} navigatorView
506  * @param {WebInspector.UISourceCode} uiSourceCode
507  * @param {string} title
508  */
509 WebInspector.NavigatorSourceTreeElement = function(navigatorView, uiSourceCode, title)
510 {
511     WebInspector.BaseNavigatorTreeElement.call(this, WebInspector.NavigatorTreeOutline.Types.UISourceCode, title, ["navigator-" + uiSourceCode.contentType().name() + "-tree-item"], false);
512     this._navigatorView = navigatorView;
513     this._uiSourceCode = uiSourceCode;
514     this.tooltip = uiSourceCode.originURL();
515 }
516
517 WebInspector.NavigatorSourceTreeElement.prototype = {
518     /**
519      * @return {WebInspector.UISourceCode}
520      */
521     get uiSourceCode()
522     {
523         return this._uiSourceCode;
524     },
525
526     onattach: function()
527     {
528         WebInspector.BaseNavigatorTreeElement.prototype.onattach.call(this);
529         this.listItemElement.draggable = true;
530         this.listItemElement.addEventListener("click", this._onclick.bind(this), false);
531         this.listItemElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), false);
532         this.listItemElement.addEventListener("mousedown", this._onmousedown.bind(this), false);
533         this.listItemElement.addEventListener("dragstart", this._ondragstart.bind(this), false);
534     },
535
536     _onmousedown: function(event)
537     {
538         if (event.which === 1) // Warm-up data for drag'n'drop
539             this._uiSourceCode.requestContent(callback.bind(this));
540         /**
541          * @param {?string} content
542          * @param {boolean} contentEncoded
543          * @param {string} mimeType
544          */
545         function callback(content, contentEncoded, mimeType)
546         {
547             this._warmedUpContent = content;
548         }
549     },
550
551     _ondragstart: function(event)
552     {
553         event.dataTransfer.setData("text/plain", this._warmedUpContent);
554         event.dataTransfer.effectAllowed = "copy";
555         return true;
556     },
557
558     onspace: function()
559     {
560         this._navigatorView._scriptSelected(this.uiSourceCode, true);
561         return true;
562     },
563
564     /**
565      * @param {Event} event
566      */
567     _onclick: function(event)
568     {
569         this._navigatorView._scriptSelected(this.uiSourceCode, false);
570     },
571
572     /**
573      * @param {Event} event
574      */
575     ondblclick: function(event)
576     {
577         var middleClick = event.button === 1;
578         this._navigatorView._scriptSelected(this.uiSourceCode, !middleClick);
579     },
580
581     onenter: function()
582     {
583         this._navigatorView._scriptSelected(this.uiSourceCode, true);
584         return true;
585     },
586
587     /**
588      * @param {Event} event
589      */
590     _handleContextMenuEvent: function(event)
591     {
592         this._navigatorView.handleContextMenu(event, this._uiSourceCode);
593     },
594
595     __proto__: WebInspector.BaseNavigatorTreeElement.prototype
596 }
597
598 /**
599  * @constructor
600  * @param {string} id
601  */
602 WebInspector.NavigatorTreeNode = function(id)
603 {
604     this.id = id;
605     this._children = {};
606 }
607
608 WebInspector.NavigatorTreeNode.prototype = {
609     /**
610      * @return {TreeElement}
611      */
612     treeElement: function() { },
613
614     dispose: function() { },
615
616     /**
617      * @return {boolean}
618      */
619     isRoot: function()
620     {
621         return false;
622     },
623
624     /**
625      * @return {boolean}
626      */
627     hasChildren: function()
628     {
629         return true;
630     },
631
632     populate: function()
633     {
634         if (this.isPopulated())
635             return;
636         if (this.parent)
637             this.parent.populate();
638         this._populated = true;
639         this.wasPopulated();
640     },
641
642     wasPopulated: function()
643     {
644         for (var id in this._children)
645             this.treeElement().appendChild(this._children[id].treeElement());
646     },
647
648     didAddChild: function(node)
649     {
650         if (this.isPopulated())
651             this.treeElement().appendChild(node.treeElement());
652     },
653
654     willRemoveChild: function(node)
655     {
656         if (this.isPopulated())
657             this.treeElement().removeChild(node.treeElement());
658     },
659
660     isPopulated: function()
661     {
662         return this._populated;
663     },
664
665     isEmpty: function()
666     {
667         return this.children().length === 0;
668     },
669
670     child: function(id)
671     {
672         return this._children[id];
673     },
674
675     children: function()
676     {
677         return Object.values(this._children);
678     },
679
680     appendChild: function(node)
681     {
682         this._children[node.id] = node;
683         node.parent = this;
684         this.didAddChild(node);
685     },
686
687     removeChild: function(node)
688     {
689         this.willRemoveChild(node);
690         delete this._children[node.id];
691         delete node.parent;
692         node.dispose();
693     },
694
695     reset: function()
696     {
697         this._children = {};
698     }
699 }
700
701 /**
702  * @constructor
703  * @extends {WebInspector.NavigatorTreeNode}
704  * @param {WebInspector.NavigatorView} navigatorView
705  */
706 WebInspector.NavigatorRootTreeNode = function(navigatorView)
707 {
708     WebInspector.NavigatorTreeNode.call(this, "");
709     this._navigatorView = navigatorView;
710 }
711
712 WebInspector.NavigatorRootTreeNode.prototype = {
713     /**
714      * @return {boolean}
715      */
716     isRoot: function()
717     {
718         return true;
719     },
720
721     /**
722      * @return {TreeOutline}
723      */
724     treeElement: function()
725     {
726         return this._navigatorView._scriptsTree;
727     },
728
729     wasPopulated: function()
730     {
731         for (var id in this._children)
732             this.treeElement().appendChild(this._children[id].treeElement());
733     },
734
735     didAddChild: function(node)
736     {
737         if (this.isPopulated())
738             this.treeElement().appendChild(node.treeElement());
739     },
740
741     willRemoveChild: function(node)
742     {
743         if (this.isPopulated())
744             this.treeElement().removeChild(node.treeElement());
745     },
746
747     __proto__: WebInspector.NavigatorTreeNode.prototype
748 }
749
750 /**
751  * @constructor
752  * @extends {WebInspector.NavigatorTreeNode}
753  * @param {WebInspector.NavigatorView} navigatorView
754  * @param {WebInspector.UISourceCode} uiSourceCode
755  */
756 WebInspector.NavigatorUISourceCodeTreeNode = function(navigatorView, uiSourceCode)
757 {
758     WebInspector.NavigatorTreeNode.call(this, uiSourceCode.name());
759     this._navigatorView = navigatorView;
760     this._uiSourceCode = uiSourceCode;
761 }
762
763 WebInspector.NavigatorUISourceCodeTreeNode.prototype = {
764     /**
765      * @return {WebInspector.UISourceCode}
766      */
767     uiSourceCode: function()
768     {
769         return this._uiSourceCode;
770     },
771
772     /**
773      * @return {TreeElement}
774      */
775     treeElement: function()
776     {
777         if (this._treeElement)
778             return this._treeElement;
779
780         this._treeElement = new WebInspector.NavigatorSourceTreeElement(this._navigatorView, this._uiSourceCode, "");
781         this.updateTitle();
782
783         this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.TitleChanged, this._titleChanged, this);
784         this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this);
785         this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
786         this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.FormattedChanged, this._formattedChanged, this);
787
788         return this._treeElement;
789     },
790
791     /**
792      * @param {boolean=} ignoreIsDirty
793      */
794     updateTitle: function(ignoreIsDirty)
795     {
796         if (!this._treeElement)
797             return;
798
799         var titleText = this._uiSourceCode.name().trimEnd(100);
800         if (!titleText)
801             titleText = WebInspector.UIString("(program)");
802         if (!ignoreIsDirty && this._uiSourceCode.isDirty())
803             titleText = "*" + titleText;
804         this._treeElement.titleText = titleText;
805     },
806
807     /**
808      * @return {boolean}
809      */
810     hasChildren: function()
811     {
812         return false;
813     },
814
815     dispose: function()
816     {
817         if (!this._treeElement)
818             return;
819         this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.TitleChanged, this._titleChanged, this);
820         this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this);
821         this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
822         this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.FormattedChanged, this._formattedChanged, this);
823     },
824
825     _titleChanged: function(event)
826     {
827         this.updateTitle();
828     },
829
830     _workingCopyChanged: function(event)
831     {
832         this.updateTitle();
833     },
834
835     _workingCopyCommitted: function(event)
836     {
837         this.updateTitle();
838     },
839
840     _formattedChanged: function(event)
841     {
842         this.updateTitle();
843     },
844
845     /**
846      * @param {boolean=} select
847      */
848     reveal: function(select)
849     {
850         this.parent.populate();
851         this.parent.treeElement().expand();
852         this._treeElement.reveal();
853         if (select)
854             this._treeElement.select();
855     },
856
857     /**
858      * @param {function(boolean)=} callback
859      */
860     rename: function(callback)
861     {
862         if (!this._treeElement)
863             return;
864
865         // Tree outline should be marked as edited as well as the tree element to prevent search from starting.
866         var treeOutlineElement = this._treeElement.treeOutline.element;
867         WebInspector.markBeingEdited(treeOutlineElement, true);
868
869         function commitHandler(element, newTitle, oldTitle)
870         {
871             if (newTitle && newTitle !== oldTitle)
872                 this._navigatorView._fileRenamed(this._uiSourceCode, newTitle);
873             afterEditing.call(this, true);
874         }
875
876         function cancelHandler()
877         {
878             afterEditing.call(this, false);
879         }
880
881         /**
882          * @param {boolean} committed
883          */
884         function afterEditing(committed)
885         {
886             WebInspector.markBeingEdited(treeOutlineElement, false);
887             this.updateTitle();
888             if (callback)
889                 callback(committed);
890         }
891
892         var editingConfig = new WebInspector.EditingConfig(commitHandler.bind(this), cancelHandler.bind(this));
893         this.updateTitle(true);
894         WebInspector.startEditing(this._treeElement.titleElement, editingConfig);
895         window.getSelection().setBaseAndExtent(this._treeElement.titleElement, 0, this._treeElement.titleElement, 1);
896     },
897
898     __proto__: WebInspector.NavigatorTreeNode.prototype
899 }
900
901 /**
902  * @constructor
903  * @extends {WebInspector.NavigatorTreeNode}
904  * @param {WebInspector.NavigatorView} navigatorView
905  * @param {string} id
906  * @param {string} type
907  * @param {string} title
908  */
909 WebInspector.NavigatorFolderTreeNode = function(navigatorView, id, type, title)
910 {
911     WebInspector.NavigatorTreeNode.call(this, id);
912     this._navigatorView = navigatorView;
913     this._type = type;
914     this._title = title;
915 }
916
917 WebInspector.NavigatorFolderTreeNode.prototype = {
918     /**
919      * @return {TreeElement}
920      */
921     treeElement: function()
922     {
923         if (this._treeElement)
924             return this._treeElement;
925         this._treeElement = this._createTreeElement(this._title, this);
926         return this._treeElement;
927     },
928
929     /**
930      * @return {TreeElement}
931      */
932     _createTreeElement: function(title, node)
933     {
934         var treeElement = new WebInspector.NavigatorFolderTreeElement(this._type, title);
935         treeElement._node = node;
936         return treeElement;
937     },
938
939     wasPopulated: function()
940     {
941         if (!this._treeElement || this._treeElement._node !== this)
942             return;
943         this._addChildrenRecursive();
944     },
945
946     _addChildrenRecursive: function()
947     {
948         for (var id in this._children) {
949             var child = this._children[id];
950             this.didAddChild(child);
951             if (child instanceof WebInspector.NavigatorFolderTreeNode)
952                 child._addChildrenRecursive();
953         }
954     },
955
956     _shouldMerge: function(node)
957     {
958         return this._type !== WebInspector.NavigatorTreeOutline.Types.Domain && node instanceof WebInspector.NavigatorFolderTreeNode;
959     },
960
961     didAddChild: function(node)
962     {
963         function titleForNode(node)
964         {
965             return node._title;
966         }
967
968         if (!this._treeElement)
969             return;
970
971         var children = this.children();
972
973         if (children.length === 1 && this._shouldMerge(node)) {
974             node._isMerged = true;
975             this._treeElement.titleText = this._treeElement.titleText + "/" + node._title;
976             node._treeElement = this._treeElement;
977             this._treeElement._node = node;
978             return;
979         }
980
981         var oldNode;
982         if (children.length === 2)
983             oldNode = children[0] !== node ? children[0] : children[1];
984         if (oldNode && oldNode._isMerged) {
985             delete oldNode._isMerged;
986             var mergedToNodes = [];
987             mergedToNodes.push(this);
988             var treeNode = this;
989             while (treeNode._isMerged) {
990                 treeNode = treeNode.parent;
991                 mergedToNodes.push(treeNode);
992             }
993             mergedToNodes.reverse();
994             var titleText = mergedToNodes.map(titleForNode).join("/");
995
996             var nodes = [];
997             treeNode = oldNode;
998             do {
999                 nodes.push(treeNode);
1000                 children = treeNode.children();
1001                 treeNode = children.length === 1 ? children[0] : null;
1002             } while (treeNode && treeNode._isMerged);
1003
1004             if (!this.isPopulated()) {
1005                 this._treeElement.titleText = titleText;
1006                 this._treeElement._node = this;
1007                 for (var i = 0; i < nodes.length; ++i) {
1008                     delete nodes[i]._treeElement;
1009                     delete nodes[i]._isMerged;
1010                 }
1011                 return;
1012             }
1013             var oldTreeElement = this._treeElement;
1014             var treeElement = this._createTreeElement(titleText, this);
1015             for (var i = 0; i < mergedToNodes.length; ++i)
1016                 mergedToNodes[i]._treeElement = treeElement;
1017             oldTreeElement.parent.appendChild(treeElement);
1018
1019             oldTreeElement._node = nodes[nodes.length - 1];
1020             oldTreeElement.titleText = nodes.map(titleForNode).join("/");
1021             oldTreeElement.parent.removeChild(oldTreeElement);
1022             this._treeElement.appendChild(oldTreeElement);
1023             if (oldTreeElement.expanded)
1024                 treeElement.expand();
1025         }
1026         if (this.isPopulated())
1027             this._treeElement.appendChild(node.treeElement());
1028     },
1029
1030     willRemoveChild: function(node)
1031     {
1032         if (node._isMerged || !this.isPopulated())
1033             return;
1034         this._treeElement.removeChild(node._treeElement);
1035     },
1036
1037     __proto__: WebInspector.NavigatorTreeNode.prototype
1038 }