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:
cthangover committed 2026-10-17 14:29:14 +00:00
1 parent 578ba7c5d5
commit 8742aa3da5
45 files changed
+4718 -3463

No files matched your search

+23 -29
View File
@@ -41,23 +41,22 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
_graph.NodeDeselected += OnNodeDeselected;
AddChild(_graph);
var panel = new VBoxContainer { CustomMinimumSize = new Vector2(250, 0) };
var panel = new VBoxContainer { CustomMinimumSize = new Vector2(260, 0) };
panel.SizeFlagsVertical = SizeFlags.ExpandFill;
AddChild(panel);
panel.AddChild(new Label
{
Text = TranslationServer.Translate("tools/scenario_editor/graph_select_node"),
AutowrapMode = TextServer.AutowrapMode.WordSmart
});
var hint = UiKit.Hint(TranslationServer.Translate("tools/scenario_editor/graph_select_node"));
hint.AutowrapMode = TextServer.AutowrapMode.WordSmart;
panel.AddChild(hint);
_infoLabel = new RichTextLabel
{
BbcodeEnabled = true,
FitContent = true,
ScrollFollowing = true,
SizeFlagsVertical = SizeFlags.ExpandFill
};
_infoLabel.AddThemeFontSizeOverride("normal_font_size", UiTokens.FontBody);
_infoLabel.AddThemeFontSizeOverride("bold_font_size", UiTokens.FontBodyLarge);
_infoLabel.AddThemeColorOverride("default_color", UiTokens.Body);
_infoLabel.MetaClicked += OnLinkClicked;
panel.AddChild(_infoLabel);
}
@@ -71,13 +70,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
_nodes = new Dictionary<string, GraphNode>();
_lookup = new Dictionary<string, SceneGraphNode>();
int x = 40, y = 40;
foreach (var pos in savedPositions.Values)
{
int bottom = (int)pos.Y + 160 + 20;
if (bottom > y)
y = bottom;
}
var layout = ScenarioGraphLayout.Compute(data);
foreach (var nodeData in data.Nodes)
{
@@ -90,7 +83,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
var position = savedPositions.TryGetValue(key, out var saved)
? saved
: new Vector2(x, y);
: layout[key];
var gn = new GraphNode
{
@@ -99,12 +92,15 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
PositionOffset = position
};
gn.AddChild(new Label
var nodeLabel = new Label
{
Text = $"Mod: {nodeData.ModId}\nScenarios: {nodeData.ScenarioCount}" +
(bgCount > 0 ? $"\nBg: {firstBg}" +
Text = string.Format(Tr("tools/scenario_editor/graph_node"), nodeData.ModId, nodeData.ScenarioCount) +
(bgCount > 0 ? "\n" + string.Format(Tr("tools/scenario_editor/graph_node_bg"), firstBg) +
(bgCount > 1 ? $" +{bgCount - 1}" : "") : "")
});
};
nodeLabel.AddThemeColorOverride("font_color", UiTokens.Body);
nodeLabel.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
gn.AddChild(nodeLabel);
var color = UiTokens.White;
if (nodeData.IsStartScene)
@@ -123,11 +119,6 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
_nodes[key] = gn;
_lookup[key] = nodeData;
if (!savedPositions.ContainsKey(key))
{
x += 280;
if (x > 1000) { x = 40; y += 160; }
}
}
foreach (var edge in data.Edges)
@@ -138,6 +129,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
continue;
_graph.ConnectNode(_nodes[srcK].Name, 0, _nodes[dstK].Name, 0);
}
}
private Dictionary<string, Vector2> SaveNodePositions()
@@ -181,12 +173,12 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
return;
var sb = new System.Text.StringBuilder();
sb.AppendLine($"Scene: [b]{nodeData.SceneName}[/b]");
sb.AppendLine($"Mod: {nodeData.ModId}");
sb.AppendLine($"[b]{nodeData.SceneName}[/b]");
sb.AppendLine(string.Format(Tr("tools/scenario_editor/graph_detail_mod"), nodeData.ModId));
if (nodeData.Backgrounds.Count > 0)
sb.AppendLine($"Backgrounds: {string.Join(", ", nodeData.Backgrounds)}");
sb.AppendLine(string.Format(Tr("tools/scenario_editor/graph_node_bg"), string.Join(", ", nodeData.Backgrounds)));
sb.AppendLine();
sb.AppendLine($"Scenarios ({nodeData.Scenarios.Count}):");
sb.AppendLine(string.Format(Tr("tools/scenario_editor/graph_detail_scenarios"), nodeData.Scenarios.Count));
foreach (var sc in nodeData.Scenarios.OrderBy(s => s.Priority))
{
@@ -202,6 +194,8 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
_infoLabel.Text = "";
}
private static string Tr(string key) => TranslationServer.Translate(key);
private void OnLinkClicked(Variant meta)
{
var path = Uri.UnescapeDataString(meta.AsString());