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 * @constructor
31 * @extends {WebInspector.VBox}
32 */
33WebInspector.NavigatorView = function()
34{
35    WebInspector.VBox.call(this);
36    this.registerRequiredCSS("navigatorView.css");
37
38    this.element.classList.add("navigator-container");
39    var scriptsOutlineElement = this.element.createChild("div", "outline-disclosure navigator");
40    var scriptsTreeElement = scriptsOutlineElement.createChild("ol");
41    this._scriptsTree = new WebInspector.NavigatorTreeOutline(scriptsTreeElement);
42
43    this.setDefaultFocusedElement(this._scriptsTree.element);
44
45    /** @type {!Map.<!WebInspector.UISourceCode, !WebInspector.NavigatorUISourceCodeTreeNode>} */
46    this._uiSourceCodeNodes = new Map();
47    /** @type {!Map.<!WebInspector.NavigatorTreeNode, !StringMap.<!WebInspector.NavigatorFolderTreeNode>>} */
48    this._subfolderNodes = new Map();
49
50    this._rootNode = new WebInspector.NavigatorRootTreeNode(this);
51    this._rootNode.populate();
52
53    this.element.addEventListener("contextmenu", this.handleContextMenu.bind(this), false);
54}
55
56WebInspector.NavigatorView.Events = {
57    ItemSelected: "ItemSelected",
58    ItemRenamed: "ItemRenamed",
59}
60
61/**
62 * @param {string} type
63 * @return {string}
64 */
65WebInspector.NavigatorView.iconClassForType = function(type)
66{
67    if (type === WebInspector.NavigatorTreeOutline.Types.Domain)
68        return "navigator-domain-tree-item";
69    if (type === WebInspector.NavigatorTreeOutline.Types.FileSystem)
70        return "navigator-folder-tree-item";
71    return "navigator-folder-tree-item";
72}
73
74WebInspector.NavigatorView.prototype = {
75    setWorkspace: function(workspace)
76    {
77        this._workspace = workspace;
78        this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAdded, this);
79        this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeRemoved, this._uiSourceCodeRemoved, this);
80        this._workspace.addEventListener(WebInspector.Workspace.Events.ProjectRemoved, this._projectRemoved.bind(this), this);
81    },
82
83    wasShown: function()
84    {
85        if (this._loaded)
86            return;
87        this._loaded = true;
88        this._workspace.uiSourceCodes().forEach(this._addUISourceCode.bind(this));
89    },
90
91    /**
92     * @param {!WebInspector.UISourceCode} uiSourceCode
93     * @return {boolean}
94     */
95    accept: function(uiSourceCode)
96    {
97        return !uiSourceCode.project().isServiceProject();
98    },
99
100    /**
101     * @param {!WebInspector.UISourceCode} uiSourceCode
102     */
103    _addUISourceCode: function(uiSourceCode)
104    {
105        if (!this.accept(uiSourceCode))
106            return;
107        var projectNode = this._projectNode(uiSourceCode.project());
108        var folderNode = this._folderNode(projectNode, uiSourceCode.parentPath());
109        var uiSourceCodeNode = new WebInspector.NavigatorUISourceCodeTreeNode(this, uiSourceCode);
110        this._uiSourceCodeNodes.set(uiSourceCode, uiSourceCodeNode);
111        folderNode.appendChild(uiSourceCodeNode);
112    },
113
114    /**
115     * @param {!WebInspector.Event} event
116     */
117    _uiSourceCodeAdded: function(event)
118    {
119        var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data);
120        this._addUISourceCode(uiSourceCode);
121    },
122
123    /**
124     * @param {!WebInspector.Event} event
125     */
126    _uiSourceCodeRemoved: function(event)
127    {
128        var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data);
129        this._removeUISourceCode(uiSourceCode);
130    },
131
132    /**
133     * @param {!WebInspector.Event} event
134     */
135    _projectRemoved: function(event)
136    {
137        var project = /** @type {!WebInspector.Project} */ (event.data);
138        project.removeEventListener(WebInspector.Project.Events.DisplayNameUpdated, this._updateProjectNodeTitle, this);
139        var uiSourceCodes = project.uiSourceCodes();
140        for (var i = 0; i < uiSourceCodes.length; ++i)
141            this._removeUISourceCode(uiSourceCodes[i]);
142    },
143
144    /**
145     * @param {!WebInspector.Project} project
146     * @return {!WebInspector.NavigatorTreeNode}
147     */
148    _projectNode: function(project)
149    {
150        if (!project.displayName())
151            return this._rootNode;
152
153        var projectNode = this._rootNode.child(project.id());
154        if (!projectNode) {
155            projectNode = this._createProjectNode(project);
156            this._rootNode.appendChild(projectNode);
157        }
158        return projectNode;
159    },
160
161    /**
162     * @param {!WebInspector.Project} project
163     * @return {!WebInspector.NavigatorTreeNode}
164     */
165    _createProjectNode: function(project)
166    {
167        var type = project.type() === WebInspector.projectTypes.FileSystem ? WebInspector.NavigatorTreeOutline.Types.FileSystem : WebInspector.NavigatorTreeOutline.Types.Domain;
168        var projectNode = new WebInspector.NavigatorFolderTreeNode(this, project, project.id(), type, "", project.displayName());
169        project.addEventListener(WebInspector.Project.Events.DisplayNameUpdated, this._updateProjectNodeTitle, this);
170        return projectNode;
171    },
172
173    /**
174     * @param {!WebInspector.Event} event
175     */
176    _updateProjectNodeTitle: function(event)
177    {
178        var project = /** @type {!WebInspector.Project} */(event.target);
179        var projectNode = this._rootNode.child(project.id());
180        if (!projectNode)
181            return;
182        projectNode.treeElement().titleText = project.displayName();
183    },
184
185    /**
186     * @param {!WebInspector.NavigatorTreeNode} projectNode
187     * @param {string} folderPath
188     * @return {!WebInspector.NavigatorTreeNode}
189     */
190    _folderNode: function(projectNode, folderPath)
191    {
192        if (!folderPath)
193            return projectNode;
194
195        var subfolderNodes = this._subfolderNodes.get(projectNode);
196        if (!subfolderNodes) {
197            subfolderNodes = /** @type {!StringMap.<!WebInspector.NavigatorFolderTreeNode>} */ (new StringMap());
198            this._subfolderNodes.set(projectNode, subfolderNodes);
199        }
200
201        var folderNode = subfolderNodes.get(folderPath);
202        if (folderNode)
203            return folderNode;
204
205        var parentNode = projectNode;
206        var index = folderPath.lastIndexOf("/");
207        if (index !== -1)
208            parentNode = this._folderNode(projectNode, folderPath.substring(0, index));
209
210        var name = folderPath.substring(index + 1);
211        folderNode = new WebInspector.NavigatorFolderTreeNode(this, null, name, WebInspector.NavigatorTreeOutline.Types.Folder, folderPath, name);
212        subfolderNodes.set(folderPath, folderNode);
213        parentNode.appendChild(folderNode);
214        return folderNode;
215    },
216
217    /**
218     * @param {!WebInspector.UISourceCode} uiSourceCode
219     * @param {boolean=} select
220     */
221    revealUISourceCode: function(uiSourceCode, select)
222    {
223        var node = this._uiSourceCodeNodes.get(uiSourceCode);
224        if (!node)
225            return;
226        if (this._scriptsTree.selectedTreeElement)
227            this._scriptsTree.selectedTreeElement.deselect();
228        this._lastSelectedUISourceCode = uiSourceCode;
229        node.reveal(select);
230    },
231
232    /**
233     * @param {!WebInspector.UISourceCode} uiSourceCode
234     * @param {boolean} focusSource
235     */
236    _sourceSelected: function(uiSourceCode, focusSource)
237    {
238        this._lastSelectedUISourceCode = uiSourceCode;
239        var data = { uiSourceCode: uiSourceCode, focusSource: focusSource};
240        this.dispatchEventToListeners(WebInspector.NavigatorView.Events.ItemSelected, data);
241    },
242
243    /**
244     * @param {!WebInspector.UISourceCode} uiSourceCode
245     */
246    sourceDeleted: function(uiSourceCode)
247    {
248    },
249
250    /**
251     * @param {!WebInspector.UISourceCode} uiSourceCode
252     */
253    _removeUISourceCode: function(uiSourceCode)
254    {
255        var node = this._uiSourceCodeNodes.get(uiSourceCode);
256        if (!node)
257            return;
258
259        var projectNode = this._projectNode(uiSourceCode.project());
260        var subfolderNodes = this._subfolderNodes.get(projectNode);
261        var parentNode = node.parent;
262        this._uiSourceCodeNodes.remove(uiSourceCode);
263        parentNode.removeChild(node);
264        node = parentNode;
265
266        while (node) {
267            parentNode = node.parent;
268            if (!parentNode || !node.isEmpty())
269                break;
270            if (subfolderNodes)
271                subfolderNodes.remove(node._folderPath);
272            parentNode.removeChild(node);
273            node = parentNode;
274        }
275    },
276
277    /**
278     * @param {!WebInspector.UISourceCode} uiSourceCode
279     */
280    _updateIcon: function(uiSourceCode)
281    {
282        var node = this._uiSourceCodeNodes.get(uiSourceCode);
283        node.updateIcon();
284    },
285
286    reset: function()
287    {
288        var nodes = this._uiSourceCodeNodes.values();
289        for (var i = 0; i < nodes.length; ++i)
290            nodes[i].dispose();
291
292        this._scriptsTree.removeChildren();
293        this._uiSourceCodeNodes.clear();
294        this._subfolderNodes.clear();
295        this._rootNode.reset();
296    },
297
298    /**
299     * @param {!Event} event
300     */
301    handleContextMenu: function(event)
302    {
303        var contextMenu = new WebInspector.ContextMenu(event);
304        this._appendAddFolderItem(contextMenu);
305        contextMenu.show();
306    },
307
308    /**
309     * @param {!WebInspector.ContextMenu} contextMenu
310     */
311    _appendAddFolderItem: function(contextMenu)
312    {
313        function addFolder()
314        {
315            WebInspector.isolatedFileSystemManager.addFileSystem();
316        }
317
318        var addFolderLabel = WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add folder to workspace" : "Add Folder to Workspace");
319        contextMenu.appendItem(addFolderLabel, addFolder);
320    },
321
322    /**
323     * @param {!WebInspector.Project} project
324     * @param {string} path
325     */
326    _handleContextMenuRefresh: function(project, path)
327    {
328        project.refresh(path);
329    },
330
331    /**
332     * @param {!WebInspector.Project} project
333     * @param {string} path
334     * @param {!WebInspector.UISourceCode=} uiSourceCode
335     */
336    _handleContextMenuCreate: function(project, path, uiSourceCode)
337    {
338        this.create(project, path, uiSourceCode);
339    },
340
341    /**
342     * @param {!WebInspector.UISourceCode} uiSourceCode
343     */
344    _handleContextMenuRename: function(uiSourceCode)
345    {
346        this.rename(uiSourceCode, false);
347    },
348
349    /**
350     * @param {!WebInspector.Project} project
351     * @param {string} path
352     */
353    _handleContextMenuExclude: function(project, path)
354    {
355        var shouldExclude = window.confirm(WebInspector.UIString("Are you sure you want to exclude this folder?"));
356        if (shouldExclude) {
357            WebInspector.startBatchUpdate();
358            project.excludeFolder(path);
359            WebInspector.endBatchUpdate();
360        }
361    },
362
363    /**
364     * @param {!WebInspector.UISourceCode} uiSourceCode
365     */
366    _handleContextMenuDelete: function(uiSourceCode)
367    {
368        var shouldDelete = window.confirm(WebInspector.UIString("Are you sure you want to delete this file?"));
369        if (shouldDelete)
370            uiSourceCode.project().deleteFile(uiSourceCode.path());
371    },
372
373    /**
374     * @param {!Event} event
375     * @param {!WebInspector.UISourceCode} uiSourceCode
376     */
377    handleFileContextMenu: function(event, uiSourceCode)
378    {
379        var contextMenu = new WebInspector.ContextMenu(event);
380        contextMenu.appendApplicableItems(uiSourceCode);
381        contextMenu.appendSeparator();
382
383        var project = uiSourceCode.project();
384        if (project.type() === WebInspector.projectTypes.FileSystem) {
385            var path = uiSourceCode.parentPath();
386            contextMenu.appendItem(WebInspector.UIString("Rename\u2026"), this._handleContextMenuRename.bind(this, uiSourceCode));
387            contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Make a copy\u2026" : "Make a Copy\u2026"), this._handleContextMenuCreate.bind(this, project, path, uiSourceCode));
388            contextMenu.appendItem(WebInspector.UIString("Delete"), this._handleContextMenuDelete.bind(this, uiSourceCode));
389            contextMenu.appendSeparator();
390        }
391
392        this._appendAddFolderItem(contextMenu);
393        contextMenu.show();
394    },
395
396    /**
397     * @param {!Event} event
398     * @param {!WebInspector.NavigatorFolderTreeNode} node
399     */
400    handleFolderContextMenu: function(event, node)
401    {
402        var contextMenu = new WebInspector.ContextMenu(event);
403        var path = "/";
404        var projectNode = node;
405        while (projectNode.parent !== this._rootNode) {
406            path = "/" + projectNode.id + path;
407            projectNode = projectNode.parent;
408        }
409
410        var project = projectNode._project;
411
412        if (project.type() === WebInspector.projectTypes.FileSystem) {
413            contextMenu.appendItem(WebInspector.UIString("Refresh"), this._handleContextMenuRefresh.bind(this, project, path));
414            contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "New file" : "New File"), this._handleContextMenuCreate.bind(this, project, path));
415            contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Exclude folder" : "Exclude Folder"), this._handleContextMenuExclude.bind(this, project, path));
416        }
417        contextMenu.appendSeparator();
418        this._appendAddFolderItem(contextMenu);
419
420        function removeFolder()
421        {
422            var shouldRemove = window.confirm(WebInspector.UIString("Are you sure you want to remove this folder?"));
423            if (shouldRemove)
424                project.remove();
425        }
426
427        if (project.type() === WebInspector.projectTypes.FileSystem && node === projectNode) {
428            var removeFolderLabel = WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Remove folder from workspace" : "Remove Folder from Workspace");
429            contextMenu.appendItem(removeFolderLabel, removeFolder);
430        }
431
432        contextMenu.show();
433    },
434
435    /**
436     * @param {!WebInspector.UISourceCode} uiSourceCode
437     * @param {boolean} deleteIfCanceled
438     */
439    rename: function(uiSourceCode, deleteIfCanceled)
440    {
441        var node = this._uiSourceCodeNodes.get(uiSourceCode);
442        console.assert(node);
443        node.rename(callback.bind(this));
444
445        /**
446         * @this {WebInspector.NavigatorView}
447         * @param {boolean} committed
448         */
449        function callback(committed)
450        {
451            if (!committed) {
452                if (deleteIfCanceled)
453                    uiSourceCode.remove();
454                return;
455            }
456
457            this.dispatchEventToListeners(WebInspector.NavigatorView.Events.ItemRenamed, uiSourceCode);
458            this._updateIcon(uiSourceCode);
459            this._sourceSelected(uiSourceCode, true)
460        }
461    },
462
463    /**
464     * @param {!WebInspector.Project} project
465     * @param {string} path
466     * @param {!WebInspector.UISourceCode=} uiSourceCodeToCopy
467     */
468    create: function(project, path, uiSourceCodeToCopy)
469    {
470        var filePath;
471        var uiSourceCode;
472
473        /**
474         * @this {WebInspector.NavigatorView}
475         * @param {?string} content
476         */
477        function contentLoaded(content)
478        {
479            createFile.call(this, content || "");
480        }
481
482        if (uiSourceCodeToCopy)
483            uiSourceCodeToCopy.requestContent(contentLoaded.bind(this));
484        else
485            createFile.call(this);
486
487        /**
488         * @this {WebInspector.NavigatorView}
489         * @param {string=} content
490         */
491        function createFile(content)
492        {
493            project.createFile(path, null, content || "", fileCreated.bind(this));
494        }
495
496        /**
497         * @this {WebInspector.NavigatorView}
498         * @param {?string} path
499         */
500        function fileCreated(path)
501        {
502            if (!path)
503                return;
504            filePath = path;
505            uiSourceCode = project.uiSourceCode(filePath);
506            if (!uiSourceCode) {
507                console.assert(uiSourceCode)
508                return;
509            }
510            this._sourceSelected(uiSourceCode, false);
511            this.revealUISourceCode(uiSourceCode, true);
512            this.rename(uiSourceCode, true);
513        }
514    },
515
516    __proto__: WebInspector.VBox.prototype
517}
518
519/**
520 * @constructor
521 * @extends {WebInspector.NavigatorView}
522 */
523WebInspector.SourcesNavigatorView = function()
524{
525    WebInspector.NavigatorView.call(this);
526    WebInspector.targetManager.addEventListener(WebInspector.TargetManager.Events.InspectedURLChanged, this._inspectedURLChanged, this);
527}
528
529WebInspector.SourcesNavigatorView.prototype = {
530    /**
531     * @override
532     * @param {!WebInspector.UISourceCode} uiSourceCode
533     * @return {boolean}
534     */
535    accept: function(uiSourceCode)
536    {
537        if (!WebInspector.NavigatorView.prototype.accept(uiSourceCode))
538            return false;
539        return uiSourceCode.project().type() !== WebInspector.projectTypes.ContentScripts && uiSourceCode.project().type() !== WebInspector.projectTypes.Snippets;
540
541    },
542
543    /**
544     * @param {!WebInspector.Event} event
545     */
546    _inspectedURLChanged: function(event)
547    {
548       var nodes = this._uiSourceCodeNodes.values();
549       for (var i = 0; i < nodes.length; ++i) {
550           var uiSourceCode = nodes[i].uiSourceCode();
551           var inspectedPageURL = WebInspector.targetManager.inspectedPageURL();
552           if (inspectedPageURL && uiSourceCode.url === inspectedPageURL)
553              this.revealUISourceCode(uiSourceCode, true);
554       }
555    },
556
557    /**
558     * @param {!WebInspector.UISourceCode} uiSourceCode
559     */
560    _addUISourceCode: function(uiSourceCode)
561    {
562        WebInspector.NavigatorView.prototype._addUISourceCode.call(this, uiSourceCode);
563        var inspectedPageURL = WebInspector.targetManager.inspectedPageURL();
564        if (inspectedPageURL && uiSourceCode.url === inspectedPageURL)
565            this.revealUISourceCode(uiSourceCode, true);
566     },
567
568    __proto__: WebInspector.NavigatorView.prototype
569}
570
571/**
572 * @constructor
573 * @extends {WebInspector.NavigatorView}
574 */
575WebInspector.ContentScriptsNavigatorView = function()
576{
577    WebInspector.NavigatorView.call(this);
578}
579
580WebInspector.ContentScriptsNavigatorView.prototype = {
581    /**
582     * @override
583     * @param {!WebInspector.UISourceCode} uiSourceCode
584     * @return {boolean}
585     */
586    accept: function(uiSourceCode)
587    {
588        if (!WebInspector.NavigatorView.prototype.accept(uiSourceCode))
589            return false;
590        return uiSourceCode.project().type() === WebInspector.projectTypes.ContentScripts;
591    },
592
593    __proto__: WebInspector.NavigatorView.prototype
594}
595
596/**
597 * @constructor
598 * @extends {TreeOutline}
599 * @param {!Element} element
600 */
601WebInspector.NavigatorTreeOutline = function(element)
602{
603    TreeOutline.call(this, element);
604    this.element = element;
605
606    this.comparator = WebInspector.NavigatorTreeOutline._treeElementsCompare;
607}
608
609WebInspector.NavigatorTreeOutline.Types = {
610    Root: "Root",
611    Domain: "Domain",
612    Folder: "Folder",
613    UISourceCode: "UISourceCode",
614    FileSystem: "FileSystem"
615}
616
617/**
618 * @param {!TreeElement} treeElement1
619 * @param {!TreeElement} treeElement2
620 * @return {number}
621 */
622WebInspector.NavigatorTreeOutline._treeElementsCompare = function compare(treeElement1, treeElement2)
623{
624    // Insert in the alphabetical order, first domains, then folders, then scripts.
625    function typeWeight(treeElement)
626    {
627        var type = treeElement.type();
628        if (type === WebInspector.NavigatorTreeOutline.Types.Domain) {
629            if (treeElement.titleText === WebInspector.targetManager.inspectedPageDomain())
630                return 1;
631            return 2;
632        }
633        if (type === WebInspector.NavigatorTreeOutline.Types.FileSystem)
634            return 3;
635        if (type === WebInspector.NavigatorTreeOutline.Types.Folder)
636            return 4;
637        return 5;
638    }
639
640    var typeWeight1 = typeWeight(treeElement1);
641    var typeWeight2 = typeWeight(treeElement2);
642
643    var result;
644    if (typeWeight1 > typeWeight2)
645        result = 1;
646    else if (typeWeight1 < typeWeight2)
647        result = -1;
648    else {
649        var title1 = treeElement1.titleText;
650        var title2 = treeElement2.titleText;
651        result = title1.compareTo(title2);
652    }
653    return result;
654}
655
656WebInspector.NavigatorTreeOutline.prototype = {
657   /**
658    * @return {!Array.<!WebInspector.UISourceCode>}
659    */
660   scriptTreeElements: function()
661   {
662       var result = [];
663       if (this.children.length) {
664           for (var treeElement = this.children[0]; treeElement; treeElement = treeElement.traverseNextTreeElement(false, this, true)) {
665               if (treeElement instanceof WebInspector.NavigatorSourceTreeElement)
666                   result.push(treeElement.uiSourceCode);
667           }
668       }
669       return result;
670   },
671
672    __proto__: TreeOutline.prototype
673}
674
675/**
676 * @constructor
677 * @extends {TreeElement}
678 * @param {string} type
679 * @param {string} title
680 * @param {!Array.<string>} iconClasses
681 * @param {boolean} hasChildren
682 * @param {boolean=} noIcon
683 */
684WebInspector.BaseNavigatorTreeElement = function(type, title, iconClasses, hasChildren, noIcon)
685{
686    this._type = type;
687    TreeElement.call(this, "", null, hasChildren);
688    this._titleText = title;
689    this._iconClasses = iconClasses;
690    this._noIcon = noIcon;
691}
692
693WebInspector.BaseNavigatorTreeElement.prototype = {
694    onattach: function()
695    {
696        this.listItemElement.removeChildren();
697        if (this._iconClasses) {
698            for (var i = 0; i < this._iconClasses.length; ++i)
699                this.listItemElement.classList.add(this._iconClasses[i]);
700        }
701
702        this.listItemElement.createChild("div", "selection");
703
704        if (!this._noIcon)
705            this.imageElement = this.listItemElement.createChild("img", "icon");
706
707        this.titleElement = this.listItemElement.createChild("div", "base-navigator-tree-element-title");
708        this.titleElement.textContent = this._titleText;
709    },
710
711    /**
712     * @param {!Array.<string>} iconClasses
713     */
714    updateIconClasses: function(iconClasses)
715    {
716        for (var i = 0; i < this._iconClasses.length; ++i)
717            this.listItemElement.classList.remove(this._iconClasses[i]);
718        this._iconClasses = iconClasses;
719        for (var i = 0; i < this._iconClasses.length; ++i)
720            this.listItemElement.classList.add(this._iconClasses[i]);
721    },
722
723    onreveal: function()
724    {
725        if (this.listItemElement)
726            this.listItemElement.scrollIntoViewIfNeeded(true);
727    },
728
729    /**
730     * @return {string}
731     */
732    get titleText()
733    {
734        return this._titleText;
735    },
736
737    set titleText(titleText)
738    {
739        if (this._titleText === titleText)
740            return;
741        this._titleText = titleText || "";
742        if (this.titleElement) {
743            this.titleElement.textContent = this._titleText;
744            this.titleElement.title = this._titleText;
745        }
746    },
747
748    /**
749     * @return {string}
750     */
751    type: function()
752    {
753        return this._type;
754    },
755
756    __proto__: TreeElement.prototype
757}
758
759/**
760 * @constructor
761 * @extends {WebInspector.BaseNavigatorTreeElement}
762 * @param {!WebInspector.NavigatorView} navigatorView
763 * @param {string} type
764 * @param {string} title
765 */
766WebInspector.NavigatorFolderTreeElement = function(navigatorView, type, title)
767{
768    var iconClass = WebInspector.NavigatorView.iconClassForType(type);
769    WebInspector.BaseNavigatorTreeElement.call(this, type, title, [iconClass], true);
770    this._navigatorView = navigatorView;
771}
772
773WebInspector.NavigatorFolderTreeElement.prototype = {
774    onpopulate: function()
775    {
776        this._node.populate();
777    },
778
779    onattach: function()
780    {
781        WebInspector.BaseNavigatorTreeElement.prototype.onattach.call(this);
782        this.collapse();
783        this.listItemElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), false);
784    },
785
786    /**
787     * @param {!WebInspector.NavigatorFolderTreeNode} node
788     */
789    setNode: function(node)
790    {
791        this._node = node;
792        var paths = [];
793        while (node && !node.isRoot()) {
794            paths.push(node._title);
795            node = node.parent;
796        }
797        paths.reverse();
798        this.tooltip = paths.join("/");
799    },
800
801    /**
802     * @param {!Event} event
803     */
804    _handleContextMenuEvent: function(event)
805    {
806        if (!this._node)
807            return;
808        this.select();
809        this._navigatorView.handleFolderContextMenu(event, this._node);
810    },
811
812    __proto__: WebInspector.BaseNavigatorTreeElement.prototype
813}
814
815/**
816 * @constructor
817 * @extends {WebInspector.BaseNavigatorTreeElement}
818 * @param {!WebInspector.NavigatorView} navigatorView
819 * @param {!WebInspector.UISourceCode} uiSourceCode
820 * @param {string} title
821 */
822WebInspector.NavigatorSourceTreeElement = function(navigatorView, uiSourceCode, title)
823{
824    this._navigatorView = navigatorView;
825    this._uiSourceCode = uiSourceCode;
826    WebInspector.BaseNavigatorTreeElement.call(this, WebInspector.NavigatorTreeOutline.Types.UISourceCode, title, this._calculateIconClasses(), false);
827    this.tooltip = uiSourceCode.originURL();
828}
829
830WebInspector.NavigatorSourceTreeElement.prototype = {
831    /**
832     * @return {!WebInspector.UISourceCode}
833     */
834    get uiSourceCode()
835    {
836        return this._uiSourceCode;
837    },
838
839    /**
840     * @return {!Array.<string>}
841     */
842    _calculateIconClasses: function()
843    {
844        return ["navigator-" + this._uiSourceCode.contentType().name() + "-tree-item"];
845    },
846
847    updateIcon: function()
848    {
849        this.updateIconClasses(this._calculateIconClasses());
850    },
851
852    onattach: function()
853    {
854        WebInspector.BaseNavigatorTreeElement.prototype.onattach.call(this);
855        this.listItemElement.draggable = true;
856        this.listItemElement.addEventListener("click", this._onclick.bind(this), false);
857        this.listItemElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), false);
858        this.listItemElement.addEventListener("mousedown", this._onmousedown.bind(this), false);
859        this.listItemElement.addEventListener("dragstart", this._ondragstart.bind(this), false);
860    },
861
862    _onmousedown: function(event)
863    {
864        if (event.which === 1) // Warm-up data for drag'n'drop
865            this._uiSourceCode.requestContent(callback.bind(this));
866        /**
867         * @param {?string} content
868         * @this {WebInspector.NavigatorSourceTreeElement}
869         */
870        function callback(content)
871        {
872            this._warmedUpContent = content;
873        }
874    },
875
876    _shouldRenameOnMouseDown: function()
877    {
878        if (!this._uiSourceCode.canRename())
879            return false;
880        var isSelected = this === this.treeOutline.selectedTreeElement;
881        var isFocused = this.treeOutline.childrenListElement.isSelfOrAncestor(document.activeElement);
882        return isSelected && isFocused && !WebInspector.isBeingEdited(this.treeOutline.element);
883    },
884
885    selectOnMouseDown: function(event)
886    {
887        if (event.which !== 1 || !this._shouldRenameOnMouseDown()) {
888            TreeElement.prototype.selectOnMouseDown.call(this, event);
889            return;
890        }
891        setTimeout(rename.bind(this), 300);
892
893        /**
894         * @this {WebInspector.NavigatorSourceTreeElement}
895         */
896        function rename()
897        {
898            if (this._shouldRenameOnMouseDown())
899                this._navigatorView.rename(this.uiSourceCode, false);
900        }
901    },
902
903    _ondragstart: function(event)
904    {
905        event.dataTransfer.setData("text/plain", this._warmedUpContent);
906        event.dataTransfer.effectAllowed = "copy";
907        return true;
908    },
909
910    /**
911     * @return {boolean}
912     */
913    onspace: function()
914    {
915        this._navigatorView._sourceSelected(this.uiSourceCode, true);
916        return true;
917    },
918
919    /**
920     * @param {!Event} event
921     */
922    _onclick: function(event)
923    {
924        this._navigatorView._sourceSelected(this.uiSourceCode, false);
925    },
926
927    /**
928     * @override
929     * @return {boolean}
930     */
931    ondblclick: function(event)
932    {
933        var middleClick = event.button === 1;
934        this._navigatorView._sourceSelected(this.uiSourceCode, !middleClick);
935        return false;
936    },
937
938    /**
939     * @override
940     * @return {boolean}
941     */
942    onenter: function()
943    {
944        this._navigatorView._sourceSelected(this.uiSourceCode, true);
945        return true;
946    },
947
948    /**
949     * @override
950     * @return {boolean}
951     */
952    ondelete: function()
953    {
954        this._navigatorView.sourceDeleted(this.uiSourceCode);
955        return true;
956    },
957
958    /**
959     * @param {!Event} event
960     */
961    _handleContextMenuEvent: function(event)
962    {
963        this.select();
964        this._navigatorView.handleFileContextMenu(event, this._uiSourceCode);
965    },
966
967    __proto__: WebInspector.BaseNavigatorTreeElement.prototype
968}
969
970/**
971 * @constructor
972 * @param {string} id
973 */
974WebInspector.NavigatorTreeNode = function(id)
975{
976    this.id = id;
977    /** @type {!StringMap.<!WebInspector.NavigatorTreeNode>} */
978    this._children = new StringMap();
979}
980
981WebInspector.NavigatorTreeNode.prototype = {
982    /**
983     * @return {!TreeElement}
984     */
985    treeElement: function() { throw "Not implemented"; },
986
987    dispose: function() { },
988
989    /**
990     * @return {boolean}
991     */
992    isRoot: function()
993    {
994        return false;
995    },
996
997    /**
998     * @return {boolean}
999     */
1000    hasChildren: function()
1001    {
1002        return true;
1003    },
1004
1005    populate: function()
1006    {
1007        if (this.isPopulated())
1008            return;
1009        if (this.parent)
1010            this.parent.populate();
1011        this._populated = true;
1012        this.wasPopulated();
1013    },
1014
1015    wasPopulated: function()
1016    {
1017        var children = this.children();
1018        for (var i = 0; i < children.length; ++i)
1019            this.treeElement().appendChild(children[i].treeElement());
1020    },
1021
1022    /**
1023     * @param {!WebInspector.NavigatorTreeNode} node
1024     */
1025    didAddChild: function(node)
1026    {
1027        if (this.isPopulated())
1028            this.treeElement().appendChild(node.treeElement());
1029    },
1030
1031    /**
1032     * @param {!WebInspector.NavigatorTreeNode} node
1033     */
1034    willRemoveChild: function(node)
1035    {
1036        if (this.isPopulated())
1037            this.treeElement().removeChild(node.treeElement());
1038    },
1039
1040    /**
1041     * @return {boolean}
1042     */
1043    isPopulated: function()
1044    {
1045        return this._populated;
1046    },
1047
1048    /**
1049     * @return {boolean}
1050     */
1051    isEmpty: function()
1052    {
1053        return !this._children.size;
1054    },
1055
1056    /**
1057     * @param {string} id
1058     * @return {?WebInspector.NavigatorTreeNode}
1059     */
1060    child: function(id)
1061    {
1062        return this._children.get(id) || null;
1063    },
1064
1065    /**
1066     * @return {!Array.<!WebInspector.NavigatorTreeNode>}
1067     */
1068    children: function()
1069    {
1070        return this._children.values();
1071    },
1072
1073    /**
1074     * @param {!WebInspector.NavigatorTreeNode} node
1075     */
1076    appendChild: function(node)
1077    {
1078        this._children.set(node.id, node);
1079        node.parent = this;
1080        this.didAddChild(node);
1081    },
1082
1083    /**
1084     * @param {!WebInspector.NavigatorTreeNode} node
1085     */
1086    removeChild: function(node)
1087    {
1088        this.willRemoveChild(node);
1089        this._children.remove(node.id);
1090        delete node.parent;
1091        node.dispose();
1092    },
1093
1094    reset: function()
1095    {
1096        this._children.clear();
1097    }
1098}
1099
1100/**
1101 * @constructor
1102 * @extends {WebInspector.NavigatorTreeNode}
1103 * @param {!WebInspector.NavigatorView} navigatorView
1104 */
1105WebInspector.NavigatorRootTreeNode = function(navigatorView)
1106{
1107    WebInspector.NavigatorTreeNode.call(this, "");
1108    this._navigatorView = navigatorView;
1109}
1110
1111WebInspector.NavigatorRootTreeNode.prototype = {
1112    /**
1113     * @return {boolean}
1114     */
1115    isRoot: function()
1116    {
1117        return true;
1118    },
1119
1120    /**
1121     * @return {!TreeOutline}
1122     */
1123    treeElement: function()
1124    {
1125        return this._navigatorView._scriptsTree;
1126    },
1127
1128    __proto__: WebInspector.NavigatorTreeNode.prototype
1129}
1130
1131/**
1132 * @constructor
1133 * @extends {WebInspector.NavigatorTreeNode}
1134 * @param {!WebInspector.NavigatorView} navigatorView
1135 * @param {!WebInspector.UISourceCode} uiSourceCode
1136 */
1137WebInspector.NavigatorUISourceCodeTreeNode = function(navigatorView, uiSourceCode)
1138{
1139    WebInspector.NavigatorTreeNode.call(this, uiSourceCode.name());
1140    this._navigatorView = navigatorView;
1141    this._uiSourceCode = uiSourceCode;
1142    this._treeElement = null;
1143}
1144
1145WebInspector.NavigatorUISourceCodeTreeNode.prototype = {
1146    /**
1147     * @return {!WebInspector.UISourceCode}
1148     */
1149    uiSourceCode: function()
1150    {
1151        return this._uiSourceCode;
1152    },
1153
1154    updateIcon: function()
1155    {
1156        if (this._treeElement)
1157            this._treeElement.updateIcon();
1158    },
1159
1160    /**
1161     * @return {!TreeElement}
1162     */
1163    treeElement: function()
1164    {
1165        if (this._treeElement)
1166            return this._treeElement;
1167
1168        this._treeElement = new WebInspector.NavigatorSourceTreeElement(this._navigatorView, this._uiSourceCode, "");
1169        this.updateTitle();
1170
1171        this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.TitleChanged, this._titleChanged, this);
1172        this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this);
1173        this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
1174
1175        return this._treeElement;
1176    },
1177
1178    /**
1179     * @param {boolean=} ignoreIsDirty
1180     */
1181    updateTitle: function(ignoreIsDirty)
1182    {
1183        if (!this._treeElement)
1184            return;
1185
1186        var titleText = this._uiSourceCode.displayName();
1187        if (!ignoreIsDirty && (this._uiSourceCode.isDirty() || this._uiSourceCode.hasUnsavedCommittedChanges()))
1188            titleText = "*" + titleText;
1189        this._treeElement.titleText = titleText;
1190    },
1191
1192    /**
1193     * @return {boolean}
1194     */
1195    hasChildren: function()
1196    {
1197        return false;
1198    },
1199
1200    dispose: function()
1201    {
1202        if (!this._treeElement)
1203            return;
1204        this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.TitleChanged, this._titleChanged, this);
1205        this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this);
1206        this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
1207    },
1208
1209    _titleChanged: function(event)
1210    {
1211        this.updateTitle();
1212    },
1213
1214    _workingCopyChanged: function(event)
1215    {
1216        this.updateTitle();
1217    },
1218
1219    _workingCopyCommitted: function(event)
1220    {
1221        this.updateTitle();
1222    },
1223
1224    /**
1225     * @param {boolean=} select
1226     */
1227    reveal: function(select)
1228    {
1229        this.parent.populate();
1230        this.parent.treeElement().expand();
1231        this._treeElement.reveal();
1232        if (select)
1233            this._treeElement.select(true);
1234    },
1235
1236    /**
1237     * @param {function(boolean)=} callback
1238     */
1239    rename: function(callback)
1240    {
1241        if (!this._treeElement)
1242            return;
1243
1244        // Tree outline should be marked as edited as well as the tree element to prevent search from starting.
1245        var treeOutlineElement = this._treeElement.treeOutline.element;
1246        WebInspector.markBeingEdited(treeOutlineElement, true);
1247
1248        /**
1249         * @param {!Element} element
1250         * @param {string} newTitle
1251         * @param {string} oldTitle
1252         * @this {WebInspector.NavigatorUISourceCodeTreeNode}
1253         */
1254        function commitHandler(element, newTitle, oldTitle)
1255        {
1256            if (newTitle !== oldTitle) {
1257                this._treeElement.titleText = newTitle;
1258                this._uiSourceCode.rename(newTitle, renameCallback.bind(this));
1259                return;
1260            }
1261            afterEditing.call(this, true);
1262        }
1263
1264        /**
1265         * @param {boolean} success
1266         * @this {WebInspector.NavigatorUISourceCodeTreeNode}
1267         */
1268        function renameCallback(success)
1269        {
1270            if (!success) {
1271                WebInspector.markBeingEdited(treeOutlineElement, false);
1272                this.updateTitle();
1273                this.rename(callback);
1274                return;
1275            }
1276            afterEditing.call(this, true);
1277        }
1278
1279        /**
1280         * @this {WebInspector.NavigatorUISourceCodeTreeNode}
1281         */
1282        function cancelHandler()
1283        {
1284            afterEditing.call(this, false);
1285        }
1286
1287        /**
1288         * @param {boolean} committed
1289         * @this {WebInspector.NavigatorUISourceCodeTreeNode}
1290         */
1291        function afterEditing(committed)
1292        {
1293            WebInspector.markBeingEdited(treeOutlineElement, false);
1294            this.updateTitle();
1295            this._treeElement.treeOutline.childrenListElement.focus();
1296            if (callback)
1297                callback(committed);
1298        }
1299
1300        var editingConfig = new WebInspector.InplaceEditor.Config(commitHandler.bind(this), cancelHandler.bind(this));
1301        this.updateTitle(true);
1302        WebInspector.InplaceEditor.startEditing(this._treeElement.titleElement, editingConfig);
1303        window.getSelection().setBaseAndExtent(this._treeElement.titleElement, 0, this._treeElement.titleElement, 1);
1304    },
1305
1306    __proto__: WebInspector.NavigatorTreeNode.prototype
1307}
1308
1309/**
1310 * @constructor
1311 * @extends {WebInspector.NavigatorTreeNode}
1312 * @param {!WebInspector.NavigatorView} navigatorView
1313 * @param {?WebInspector.Project} project
1314 * @param {string} id
1315 * @param {string} type
1316 * @param {string} folderPath
1317 * @param {string} title
1318 */
1319WebInspector.NavigatorFolderTreeNode = function(navigatorView, project, id, type, folderPath, title)
1320{
1321    WebInspector.NavigatorTreeNode.call(this, id);
1322    this._navigatorView = navigatorView;
1323    this._project = project;
1324    this._type = type;
1325    this._folderPath = folderPath;
1326    this._title = title;
1327}
1328
1329WebInspector.NavigatorFolderTreeNode.prototype = {
1330    /**
1331     * @return {!TreeElement}
1332     */
1333    treeElement: function()
1334    {
1335        if (this._treeElement)
1336            return this._treeElement;
1337        this._treeElement = this._createTreeElement(this._title, this);
1338        return this._treeElement;
1339    },
1340
1341    /**
1342     * @return {!TreeElement}
1343     */
1344    _createTreeElement: function(title, node)
1345    {
1346        var treeElement = new WebInspector.NavigatorFolderTreeElement(this._navigatorView, this._type, title);
1347        treeElement.setNode(node);
1348        return treeElement;
1349    },
1350
1351    wasPopulated: function()
1352    {
1353        if (!this._treeElement || this._treeElement._node !== this)
1354            return;
1355        this._addChildrenRecursive();
1356    },
1357
1358    _addChildrenRecursive: function()
1359    {
1360        var children = this.children();
1361        for (var i = 0; i < children.length; ++i) {
1362            var child = children[i];
1363            this.didAddChild(child);
1364            if (child instanceof WebInspector.NavigatorFolderTreeNode)
1365                child._addChildrenRecursive();
1366        }
1367    },
1368
1369    _shouldMerge: function(node)
1370    {
1371        return this._type !== WebInspector.NavigatorTreeOutline.Types.Domain && node instanceof WebInspector.NavigatorFolderTreeNode;
1372    },
1373
1374    didAddChild: function(node)
1375    {
1376        function titleForNode(node)
1377        {
1378            return node._title;
1379        }
1380
1381        if (!this._treeElement)
1382            return;
1383
1384        var children = this.children();
1385
1386        if (children.length === 1 && this._shouldMerge(node)) {
1387            node._isMerged = true;
1388            this._treeElement.titleText = this._treeElement.titleText + "/" + node._title;
1389            node._treeElement = this._treeElement;
1390            this._treeElement.setNode(node);
1391            return;
1392        }
1393
1394        var oldNode;
1395        if (children.length === 2)
1396            oldNode = children[0] !== node ? children[0] : children[1];
1397        if (oldNode && oldNode._isMerged) {
1398            delete oldNode._isMerged;
1399            var mergedToNodes = [];
1400            mergedToNodes.push(this);
1401            var treeNode = this;
1402            while (treeNode._isMerged) {
1403                treeNode = treeNode.parent;
1404                mergedToNodes.push(treeNode);
1405            }
1406            mergedToNodes.reverse();
1407            var titleText = mergedToNodes.map(titleForNode).join("/");
1408
1409            var nodes = [];
1410            treeNode = oldNode;
1411            do {
1412                nodes.push(treeNode);
1413                children = treeNode.children();
1414                treeNode = children.length === 1 ? children[0] : null;
1415            } while (treeNode && treeNode._isMerged);
1416
1417            if (!this.isPopulated()) {
1418                this._treeElement.titleText = titleText;
1419                this._treeElement.setNode(this);
1420                for (var i = 0; i < nodes.length; ++i) {
1421                    delete nodes[i]._treeElement;
1422                    delete nodes[i]._isMerged;
1423                }
1424                return;
1425            }
1426            var oldTreeElement = this._treeElement;
1427            var treeElement = this._createTreeElement(titleText, this);
1428            for (var i = 0; i < mergedToNodes.length; ++i)
1429                mergedToNodes[i]._treeElement = treeElement;
1430            oldTreeElement.parent.appendChild(treeElement);
1431
1432            oldTreeElement.setNode(nodes[nodes.length - 1]);
1433            oldTreeElement.titleText = nodes.map(titleForNode).join("/");
1434            oldTreeElement.parent.removeChild(oldTreeElement);
1435            this._treeElement.appendChild(oldTreeElement);
1436            if (oldTreeElement.expanded)
1437                treeElement.expand();
1438        }
1439        if (this.isPopulated())
1440            this._treeElement.appendChild(node.treeElement());
1441    },
1442
1443    willRemoveChild: function(node)
1444    {
1445        if (node._isMerged || !this.isPopulated())
1446            return;
1447        this._treeElement.removeChild(node._treeElement);
1448    },
1449
1450    __proto__: WebInspector.NavigatorTreeNode.prototype
1451}
1452