Tool windows redesign and the Novel Editor workspace
Tool windows: - Every editor (scenario, scene builder, map, interactive objects, lighting, catalog export) is rebuilt on the interface ToolWindow chrome and the denser ToolTheme: header with title/close, status bar, Ctrl+S, dirty-state confirmation. - Shared building blocks in src/Common: ToolUi (rows, buttons, sections), CollapsibleSection, FilteredList, ModFileSelector, DragHandle and BackgroundCanvas (scene background with pan/zoom). - The map, interactive and light editors draw on canvases (MapEditorCanvas, InteractiveEditorCanvas, LightEditorCanvas) instead of one Control "handle" node per object; the *Handle classes are removed. - Scenario editor: tab data model (TabData), automatic graph layout (ScenarioGraphLayout), file tree/info/thumbnail panel updates. - Services: ModFileService (mod file listing and writing), SceneBackgroundService; MapFileService/ModResourceService follow. - Catalog export: per-language name/description editing. - Locale (en/ru) regrouped under tools/*; unused keys removed. Novel Editor: - collections/novel_editor.json: a "game" whose entry scene is an authoring workspace over the novel engine and the story content. - NovelEditorHub (scenes/novel_editor_hub.tscn) lists every installed tool through IToolProvider.AddMenuButton; ToolsInitializer registers it as the logical scene "novel_editor".
This commit is contained in:
1 parent
578ba7c5d5
commit
8742aa3da5
45 files changed
+4718
-3463
No files matched your search
@@ -6,82 +6,138 @@ using Godot;
|
||||
|
||||
namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
/// <summary>Left-hand tree of every mod's scenario files, grouped by mod and subdirectory; raises <see cref="FileSelected"/> on picks.</summary>
|
||||
/// <summary>
|
||||
/// 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 <see cref="FileSelected"/> with the
|
||||
/// scenario key on picks.
|
||||
/// <para/>
|
||||
/// Built from <c>scenario_file_tree_panel.tscn</c>: the tscn builder creates
|
||||
/// the script node through its constructor before the child nodes exist,
|
||||
/// so children are looked up lazily (<see cref="Bind"/>) — not in the
|
||||
/// constructor, which is what used to crash the whole editor on open.
|
||||
/// </summary>
|
||||
public partial class ScenarioFileTreePanel : VBoxContainer
|
||||
{
|
||||
private Tree _fileTree;
|
||||
private readonly Dictionary<string, string> _fileItemToKey = new();
|
||||
private readonly Dictionary<string, TreeItem> _keyToTreeItem = new();
|
||||
private LineEdit _search;
|
||||
private List<ModScenarioTree> _mods = new();
|
||||
private readonly Dictionary<TreeItem, string> _itemToKey = new();
|
||||
private readonly Dictionary<string, TreeItem> _keyToItem = new();
|
||||
private bool _isProgrammaticSelection;
|
||||
private string _selectedKey;
|
||||
|
||||
public event Action<string> FileSelected;
|
||||
|
||||
public ScenarioFileTreePanel()
|
||||
/// <summary>Binds child nodes and fills the tree once the panel enters the tree.</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
Reload();
|
||||
}
|
||||
|
||||
/// <summary>Re-scans the mods' scenario files and rebuilds the tree.</summary>
|
||||
public void Reload()
|
||||
{
|
||||
Bind();
|
||||
_mods = ScenarioFileService.ScanScenarioFiles();
|
||||
Populate();
|
||||
}
|
||||
|
||||
private void Bind()
|
||||
{
|
||||
if (_fileTree != null)
|
||||
return;
|
||||
|
||||
_fileTree = GetNode<Tree>("FileTree");
|
||||
_fileTree.CellSelected += OnCellSelected;
|
||||
|
||||
Populate();
|
||||
_search = GetNode<LineEdit>("Search");
|
||||
_search.PlaceholderText = TranslationServer.Translate("tools/scenario_editor/search_files");
|
||||
_search.TextChanged += _ => Populate();
|
||||
}
|
||||
|
||||
private void Populate()
|
||||
{
|
||||
_fileTree.Clear();
|
||||
_fileItemToKey.Clear();
|
||||
_keyToTreeItem.Clear();
|
||||
_itemToKey.Clear();
|
||||
_keyToItem.Clear();
|
||||
|
||||
var filter = _search.Text.Trim();
|
||||
var root = _fileTree.CreateItem();
|
||||
var mods = ScenarioFileService.ScanScenarioFiles();
|
||||
|
||||
foreach (var mod in mods)
|
||||
foreach (var mod in _mods)
|
||||
{
|
||||
var modItem = _fileTree.CreateItem(root);
|
||||
modItem.SetText(0, $"[{mod.ModId}]");
|
||||
modItem.SetSelectable(0, false);
|
||||
modItem.SetCustomColor(0, UiTokens.Info);
|
||||
|
||||
TreeItem modItem = null;
|
||||
foreach (var group in mod.Groups)
|
||||
{
|
||||
TreeItem parentItem;
|
||||
if (string.IsNullOrEmpty(group.DirectoryPath))
|
||||
{
|
||||
parentItem = modItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
parentItem = _fileTree.CreateItem(modItem);
|
||||
parentItem.SetText(0, group.DirectoryPath + "/");
|
||||
parentItem.SetSelectable(0, false);
|
||||
}
|
||||
|
||||
TreeItem groupItem = null;
|
||||
foreach (var file in group.Files)
|
||||
{
|
||||
var fileItem = _fileTree.CreateItem(parentItem);
|
||||
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.SetMetadata(0, Variant.From("scenario"));
|
||||
_fileItemToKey[GetItemKey(fileItem)] = file.Key;
|
||||
_keyToTreeItem[file.Key] = fileItem;
|
||||
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);
|
||||
}
|
||||
|
||||
private static string GetItemKey(TreeItem item)
|
||||
{
|
||||
return item.GetInstanceId().ToString();
|
||||
}
|
||||
|
||||
/// <summary>Programmatically selects the tree row for a scenario key (no selection event).</summary>
|
||||
/// <summary>Selects the tree row of a scenario key and raises <see cref="FileSelected"/>.</summary>
|
||||
public void SelectFile(string key)
|
||||
{
|
||||
if (!_keyToTreeItem.TryGetValue(key, out var item))
|
||||
return;
|
||||
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;
|
||||
|
||||
FileSelected?.Invoke(key);
|
||||
}
|
||||
|
||||
private void OnCellSelected()
|
||||
@@ -90,18 +146,11 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
return;
|
||||
|
||||
var item = _fileTree.GetSelected();
|
||||
if (item == null)
|
||||
if (item == null || !_itemToKey.TryGetValue(item, out var key))
|
||||
return;
|
||||
|
||||
var meta = item.GetMetadata(0);
|
||||
if (meta.VariantType != Variant.Type.String || meta.AsString() != "scenario")
|
||||
return;
|
||||
|
||||
var itemKey = GetItemKey(item);
|
||||
if (!_fileItemToKey.TryGetValue(itemKey, out var fileKey))
|
||||
return;
|
||||
|
||||
FileSelected?.Invoke(fileKey);
|
||||
_selectedKey = key;
|
||||
FileSelected?.Invoke(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user