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