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 */
33WebInspector.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
64WebInspector.NavigatorView.Events = {
65    ItemSelected: "ItemSelected",
66    FileRenamed: "FileRenamed"
67}
68
69WebInspector.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
78WebInspector.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 */
302WebInspector.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
315WebInspector.NavigatorTreeOutline.Types = {
316    Root: "Root",
317    Domain: "Domain",
318    Folder: "Folder",
319    UISourceCode: "UISourceCode",
320    FileSystem: "FileSystem"
321}
322
323WebInspector.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
357WebInspector.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 */
397WebInspector.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
406WebInspector.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 */
481WebInspector.NavigatorFolderTreeElement = function(type, title)
482{
483    var iconClass = WebInspector.NavigatorView.iconClassForType(type);
484    WebInspector.BaseNavigatorTreeElement.call(this, type, title, [iconClass], true);
485}
486
487WebInspector.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 */
509WebInspector.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
517WebInspector.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 */
602WebInspector.NavigatorTreeNode = function(id)
603{
604    this.id = id;
605    this._children = {};
606}
607
608WebInspector.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 */
706WebInspector.NavigatorRootTreeNode = function(navigatorView)
707{
708    WebInspector.NavigatorTreeNode.call(this, "");
709    this._navigatorView = navigatorView;
710}
711
712WebInspector.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 */
756WebInspector.NavigatorUISourceCodeTreeNode = function(navigatorView, uiSourceCode)
757{
758    WebInspector.NavigatorTreeNode.call(this, uiSourceCode.name());
759    this._navigatorView = navigatorView;
760    this._uiSourceCode = uiSourceCode;
761}
762
763WebInspector.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 */
909WebInspector.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
917WebInspector.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}
1039