using System; using System.Collections.Generic; using Cthangover.Core.UI.Theming; using Cthangover.Tools.Services; using Godot; namespace Cthangover.Core.UI.Tool.ScenarioEditor { /// /// Left-hand tree of every mod's scenario files, grouped by mod and /// subdirectory, with a search field that keeps only matching files (and /// expands their groups). Raises with the /// scenario key on picks. /// /// Built from scenario_file_tree_panel.tscn: the tscn builder creates /// the script node through its constructor before the child nodes exist, /// so children are looked up lazily () — not in the /// constructor, which is what used to crash the whole editor on open. /// public partial class ScenarioFileTreePanel : VBoxContainer { private Tree _fileTree; private LineEdit _search; private List _mods = new(); private readonly Dictionary _itemToKey = new(); private readonly Dictionary _keyToItem = new(); private bool _isProgrammaticSelection; private string _selectedKey; public event Action FileSelected; /// Binds child nodes and fills the tree once the panel enters the tree. public override void _Ready() { Reload(); } /// Re-scans the mods' scenario files and rebuilds the tree. public void Reload() { Bind(); _mods = ScenarioFileService.ScanScenarioFiles(); Populate(); } private void Bind() { if (_fileTree != null) return; _fileTree = GetNode("FileTree"); _fileTree.CellSelected += OnCellSelected; _search = GetNode("Search"); _search.PlaceholderText = TranslationServer.Translate("tools/scenario_editor/search_files"); _search.TextChanged += _ => Populate(); } private void Populate() { _fileTree.Clear(); _itemToKey.Clear(); _keyToItem.Clear(); var filter = _search.Text.Trim(); var root = _fileTree.CreateItem(); foreach (var mod in _mods) { TreeItem modItem = null; foreach (var group in mod.Groups) { TreeItem groupItem = null; foreach (var file in group.Files) { if (filter.Length > 0 && file.RelativePath.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) continue; if (modItem == null) { modItem = _fileTree.CreateItem(root); modItem.SetText(0, mod.ModId); modItem.SetSelectable(0, false); modItem.SetCustomColor(0, UiTokens.AccentBright); } if (groupItem == null) { if (string.IsNullOrEmpty(group.DirectoryPath)) { groupItem = modItem; } else { groupItem = _fileTree.CreateItem(modItem); groupItem.SetText(0, group.DirectoryPath + "/"); groupItem.SetSelectable(0, false); groupItem.SetCustomColor(0, UiTokens.Muted); groupItem.Collapsed = filter.Length == 0; } } var fileItem = _fileTree.CreateItem(groupItem); fileItem.SetText(0, file.Name); fileItem.SetTooltipText(0, ScenarioFileService.GetDisplayPath(file.ModId, file.RelativePath)); _itemToKey[fileItem] = file.Key; _keyToItem[file.Key] = fileItem; } } } if (_selectedKey != null && _keyToItem.TryGetValue(_selectedKey, out var selected)) SelectSilently(selected); } /// Selects the tree row of a scenario key and raises . public void SelectFile(string key) { Bind(); _selectedKey = key; if (!_keyToItem.TryGetValue(key, out var item)) { _search.Text = string.Empty; Populate(); if (!_keyToItem.TryGetValue(key, out item)) return; } SelectSilently(item); FileSelected?.Invoke(key); } private void SelectSilently(TreeItem item) { for (var parent = item.GetParent(); parent != null; parent = parent.GetParent()) parent.Collapsed = false; _isProgrammaticSelection = true; item.Select(0); _fileTree.ScrollToItem(item); _isProgrammaticSelection = false; } private void OnCellSelected() { if (_isProgrammaticSelection) return; var item = _fileTree.GetSelected(); if (item == null || !_itemToKey.TryGetValue(item, out var key)) return; _selectedKey = key; FileSelected?.Invoke(key); } } }