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
@@ -2,42 +2,34 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Cthangover.Tools.Common;
|
||||
using Cthangover.Tools.Services;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
/// <summary>State of one open scenario file tab: identity, saved text and dirty flag.</summary>
|
||||
public sealed class TabData
|
||||
{
|
||||
public string Key;
|
||||
public string ModId;
|
||||
public string RelativePath;
|
||||
public string FileName;
|
||||
public string SavedText;
|
||||
public bool IsDirty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tabbed editor area of the scenario editor: a permanent graph tab plus
|
||||
/// one text tab per opened .scenario file, with dirty tracking and a
|
||||
/// monospace syntax-highlighted code editor.
|
||||
/// monospace syntax-highlighted code editor with line numbers. Only one
|
||||
/// <see cref="CodeEdit"/> exists; switching tabs swaps its text with the
|
||||
/// tab's <see cref="TabData.Buffer"/>, which therefore holds unsaved edits
|
||||
/// of background tabs.
|
||||
/// </summary>
|
||||
public partial class ScenarioEditTabs : VBoxContainer
|
||||
{
|
||||
private const int GraphTabIndex = 0;
|
||||
|
||||
private TabBar _tabBar;
|
||||
private TextEdit _textEdit;
|
||||
private CodeEdit _textEdit;
|
||||
private ScenarioGraphView _graphView;
|
||||
private Font _monospaceFont;
|
||||
|
||||
private readonly List<TabData> _tabs = new();
|
||||
private int _activeTabIndex = -1;
|
||||
private bool _suppressTextChanged;
|
||||
|
||||
public TextEdit CodeEditor => _textEdit;
|
||||
public CodeEdit CodeEditor => _textEdit;
|
||||
public string CurrentText => _textEdit.Text;
|
||||
public string CurrentKey =>
|
||||
_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count ? _tabs[_activeTabIndex].Key : null;
|
||||
@@ -54,10 +46,12 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
public event Action<bool> DirtyChanged;
|
||||
public event Action<string> GraphFileRequested;
|
||||
|
||||
public ScenarioEditTabs(ScenarioGraphView graphView, Font monospaceFont)
|
||||
/// <summary>Raised on every user edit of the active text tab.</summary>
|
||||
public event Action BufferChanged;
|
||||
|
||||
public ScenarioEditTabs(ScenarioGraphView graphView)
|
||||
{
|
||||
_graphView = graphView;
|
||||
_monospaceFont = monospaceFont;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill;
|
||||
@@ -69,15 +63,8 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_tabBar.AddTab(TranslationServer.Translate("tools/scenario_editor/tab_graph"));
|
||||
AddChild(_tabBar);
|
||||
|
||||
_textEdit = new TextEdit
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
Editable = true,
|
||||
SyntaxHighlighter = ScenarioSyntaxService.CreateHighlighter(),
|
||||
WrapMode = TextEdit.LineWrappingMode.None
|
||||
};
|
||||
_textEdit.AddThemeFontOverride("font", _monospaceFont);
|
||||
_textEdit = ToolUi.Code();
|
||||
_textEdit.SyntaxHighlighter = ScenarioSyntaxService.CreateHighlighter();
|
||||
_textEdit.TextChanged += OnTextChanged;
|
||||
AddChild(_textEdit);
|
||||
|
||||
@@ -115,12 +102,13 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
ModId = modId,
|
||||
RelativePath = relativePath,
|
||||
FileName = Path.GetFileNameWithoutExtension(relativePath),
|
||||
SavedText = text,
|
||||
Buffer = text,
|
||||
IsDirty = false
|
||||
};
|
||||
|
||||
_tabs.Add(tab);
|
||||
_tabBar.AddTab(tab.FileName);
|
||||
_tabBar.SetTabTooltip(_tabBar.TabCount - 1, ScenarioFileService.GetDisplayPath(modId, relativePath));
|
||||
|
||||
SwitchToTextTab(_tabs.Count - 1);
|
||||
}
|
||||
@@ -137,7 +125,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
return;
|
||||
|
||||
var tab = _tabs[_activeTabIndex];
|
||||
tab.SavedText = _textEdit.Text;
|
||||
tab.Buffer = _textEdit.Text;
|
||||
if (tab.IsDirty)
|
||||
{
|
||||
tab.IsDirty = false;
|
||||
@@ -218,16 +206,16 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
return;
|
||||
|
||||
if (_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count)
|
||||
_tabs[_activeTabIndex].SavedText = _textEdit.Text;
|
||||
_tabs[_activeTabIndex].Buffer = _textEdit.Text;
|
||||
|
||||
_activeTabIndex = index;
|
||||
_tabBar.CurrentTab = index + 1;
|
||||
|
||||
var tab = _tabs[index];
|
||||
|
||||
SetTextSilent(tab.SavedText);
|
||||
SetTextSilent(tab.Buffer);
|
||||
|
||||
TextTabSelected?.Invoke(tab.SavedText);
|
||||
TextTabSelected?.Invoke(tab.Buffer);
|
||||
}
|
||||
|
||||
private void CloseTextTab(int index)
|
||||
@@ -296,6 +284,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
UpdateTabTitle(_activeTabIndex);
|
||||
UpdateGlobalDirty();
|
||||
}
|
||||
BufferChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void UpdateTabTitle(int index)
|
||||
@@ -315,7 +304,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
private void SaveActiveTextEditContent()
|
||||
{
|
||||
if (_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count)
|
||||
_tabs[_activeTabIndex].SavedText = _textEdit.Text;
|
||||
_tabs[_activeTabIndex].Buffer = _textEdit.Text;
|
||||
}
|
||||
|
||||
private void OnGraphScenarioFileRequested(string filePath)
|
||||
|
||||
@@ -2,32 +2,38 @@ using System;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
using Cthangover.Tools.Common;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Tool window hosting the scenario editor: a file tree of every mod's
|
||||
/// .scenario files, tabbed text/graph editors, metadata and thumbnail
|
||||
/// panels, plus a play-test mode that runs the current scenario in-game.
|
||||
/// Tool window hosting the scenario editor: a searchable tree of every
|
||||
/// mod's .scenario files, tabbed code/graph editors, a metadata panel that
|
||||
/// follows the text as it is typed, background thumbnails, plus a
|
||||
/// play-test mode that runs the current scenario in-game.
|
||||
/// <para/>
|
||||
/// Shortcuts: Ctrl+S saves the active file, F5 play-tests it.
|
||||
/// </summary>
|
||||
public partial class ScenarioEditorWindow : ToolWindow
|
||||
{
|
||||
private const double InfoRefreshDelay = 0.4;
|
||||
|
||||
private Control _editLayout;
|
||||
private Control _playLayout;
|
||||
|
||||
private ScenarioFileTreePanel _fileTreePanel;
|
||||
private ScenarioEditTabs _editTabs;
|
||||
private ScenarioInfoPanel _infoPanel;
|
||||
private ScenarioThumbnailPanel _thumbPanel;
|
||||
private Timer _infoTimer;
|
||||
|
||||
private Button _saveBtn;
|
||||
private Button _openFileBtn;
|
||||
private Button _playBtn;
|
||||
private Label _filePathLabel;
|
||||
private Label _statusLabel;
|
||||
|
||||
private bool _playActive;
|
||||
private CanvasLayer _stopOverlay;
|
||||
@@ -36,6 +42,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
BuildUI();
|
||||
SetHint(ToolUi.Tr("tools/scenario_editor/hint"));
|
||||
}
|
||||
|
||||
/// <summary>Opens with the graph view so the editor starts on the cross-scene map.</summary>
|
||||
@@ -44,10 +51,6 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_editTabs.SetViewToGraph();
|
||||
}
|
||||
|
||||
protected override void Cleanup()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Returns from play-test mode as soon as the in-game test play ends.</summary>
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
@@ -60,8 +63,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_editLayout = new Control
|
||||
{
|
||||
AnchorRight = 1f,
|
||||
AnchorBottom = 1f,
|
||||
Visible = true
|
||||
AnchorBottom = 1f
|
||||
};
|
||||
AddChild(_editLayout);
|
||||
|
||||
@@ -71,111 +73,70 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
var toolbar = CreateToolbar();
|
||||
outerVBox.AddChild(toolbar);
|
||||
|
||||
_saveBtn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/save"),
|
||||
Disabled = true
|
||||
};
|
||||
_saveBtn.Pressed += OnSavePressed;
|
||||
_saveBtn = ToolUi.Button("tools/common/save", OnSavePressed, ButtonKind.Primary, "tools/common/save_tip");
|
||||
toolbar.AddChild(_saveBtn);
|
||||
|
||||
_openFileBtn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/open_file"),
|
||||
Disabled = true
|
||||
};
|
||||
_openFileBtn.Pressed += OnOpenFilePressed;
|
||||
toolbar.AddChild(_openFileBtn);
|
||||
|
||||
_playBtn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/play"),
|
||||
TooltipText = TranslationServer.Translate("tools/scenario_editor/play_tooltip"),
|
||||
Disabled = true
|
||||
};
|
||||
_playBtn.Pressed += OnPlayPressed;
|
||||
_playBtn = ToolUi.Button("tools/scenario_editor/play", OnPlayPressed, ButtonKind.Secondary, "tools/scenario_editor/play_tooltip");
|
||||
toolbar.AddChild(_playBtn);
|
||||
_openFileBtn = ToolUi.Button("tools/scenario_editor/open_file", OnOpenFilePressed, ButtonKind.Secondary, "tools/scenario_editor/open_file_tip");
|
||||
toolbar.AddChild(_openFileBtn);
|
||||
toolbar.AddChild(ToolUi.Divider());
|
||||
toolbar.AddChild(ToolUi.Button("tools/scenario_editor/rescan", OnRescanPressed, ButtonKind.Secondary, "tools/scenario_editor/rescan_tip"));
|
||||
|
||||
_filePathLabel = new Label { Text = "" };
|
||||
_filePathLabel.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
_filePathLabel = new Label
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextOverrunBehavior = TextServer.OverrunBehavior.TrimEllipsis
|
||||
};
|
||||
_filePathLabel.AddThemeFontOverride("font", ToolUi.Mono());
|
||||
_filePathLabel.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
_filePathLabel.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
|
||||
toolbar.AddChild(_filePathLabel);
|
||||
|
||||
var mainSplit = new HSplitContainer();
|
||||
mainSplit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
mainSplit.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
outerVBox.AddChild(mainSplit);
|
||||
|
||||
var leftPanel = new PanelContainer
|
||||
var mainSplit = new HSplitContainer
|
||||
{
|
||||
CustomMinimumSize = new Vector2(340, 0),
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
outerVBox.AddChild(mainSplit);
|
||||
|
||||
var leftVBox = new VBoxContainer { SizeFlagsVertical = Control.SizeFlags.ExpandFill };
|
||||
var leftPanel = CreateSidebar(leftVBox, 320);
|
||||
mainSplit.AddChild(leftPanel);
|
||||
|
||||
var leftVBox = new VBoxContainer();
|
||||
leftPanel.AddChild(leftVBox);
|
||||
|
||||
var tabsContainer = new TabContainer();
|
||||
tabsContainer.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
tabsContainer.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
var tabsContainer = new TabContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill,
|
||||
CustomMinimumSize = new Vector2(0, 420)
|
||||
};
|
||||
leftVBox.AddChild(tabsContainer);
|
||||
|
||||
_fileTreePanel = (ScenarioFileTreePanel)TscnScenes.LoadAndBuild("scenes/scenario_file_tree_panel.tscn");
|
||||
_fileTreePanel.FileSelected += OnFileSelected;
|
||||
tabsContainer.AddChild(_fileTreePanel);
|
||||
tabsContainer.SetTabTitle(0, TranslationServer.Translate("tools/scenario_editor/tab_files"));
|
||||
tabsContainer.SetTabTitle(0, ToolUi.Tr("tools/scenario_editor/tab_files"));
|
||||
|
||||
_infoPanel = (ScenarioInfoPanel)TscnScenes.LoadAndBuild("scenes/scenario_info_panel.tscn");
|
||||
tabsContainer.AddChild(_infoPanel);
|
||||
tabsContainer.SetTabTitle(1, TranslationServer.Translate("tools/scenario_editor/tab_info"));
|
||||
|
||||
leftVBox.AddChild(new Label { Text = TranslationServer.Translate("tools/scenario_editor/bg_previews") + ":" });
|
||||
tabsContainer.SetTabTitle(1, ToolUi.Tr("tools/scenario_editor/tab_info"));
|
||||
|
||||
leftVBox.AddChild(ToolUi.Section("tools/scenario_editor/bg_previews"));
|
||||
_thumbPanel = (ScenarioThumbnailPanel)TscnScenes.LoadAndBuild("scenes/scenario_thumbnail_panel.tscn");
|
||||
leftVBox.AddChild(_thumbPanel);
|
||||
|
||||
var rightPanel = new PanelContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
mainSplit.AddChild(rightPanel);
|
||||
|
||||
_editTabs = new ScenarioEditTabs(new ScenarioGraphView(), GetMonospaceFont());
|
||||
_editTabs = new ScenarioEditTabs(new ScenarioGraphView());
|
||||
_editTabs.TextTabSelected += OnTextTabSelected;
|
||||
_editTabs.GraphTabSelected += OnGraphTabSelected;
|
||||
_editTabs.DirtyChanged += OnDirtyChanged;
|
||||
_editTabs.GraphFileRequested += OnGraphFileRequested;
|
||||
rightPanel.AddChild(_editTabs);
|
||||
_editTabs.BufferChanged += () => _infoTimer.Start();
|
||||
mainSplit.AddChild(_editTabs);
|
||||
|
||||
var statusBar = new HBoxContainer();
|
||||
outerVBox.AddChild(statusBar);
|
||||
|
||||
_statusLabel = new Label { Text = "" };
|
||||
statusBar.AddChild(_statusLabel);
|
||||
|
||||
_playLayout = new Control
|
||||
{
|
||||
AnchorRight = 1f,
|
||||
AnchorBottom = 1f,
|
||||
Visible = false
|
||||
};
|
||||
AddChild(_playLayout);
|
||||
|
||||
var stopContainer = new HBoxContainer();
|
||||
stopContainer.Alignment = BoxContainer.AlignmentMode.Center;
|
||||
stopContainer.AnchorRight = 0.5f;
|
||||
stopContainer.AnchorLeft = 0.5f;
|
||||
stopContainer.OffsetTop = 8;
|
||||
_playLayout.AddChild(stopContainer);
|
||||
|
||||
var stopBtn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/stop"),
|
||||
CustomMinimumSize = new Vector2(120, 40)
|
||||
};
|
||||
stopBtn.Pressed += OnStopPressed;
|
||||
stopContainer.AddChild(stopBtn);
|
||||
_infoTimer = new Timer { OneShot = true, WaitTime = InfoRefreshDelay };
|
||||
_infoTimer.Timeout += () => _infoPanel.Update(_editTabs.CurrentText);
|
||||
AddChild(_infoTimer);
|
||||
}
|
||||
|
||||
private void OnFileSelected(string filePath)
|
||||
@@ -188,15 +149,19 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_fileTreePanel.SelectFile(filePath);
|
||||
}
|
||||
|
||||
private void OnRescanPressed()
|
||||
{
|
||||
_fileTreePanel.Reload();
|
||||
_editTabs.RefreshGraph();
|
||||
SetStatus(ToolUi.Tr("tools/scenario_editor/rescanned"));
|
||||
}
|
||||
|
||||
private void OnTextTabSelected(string text)
|
||||
{
|
||||
_filePathLabel.Text = _editTabs.CurrentKey != null
|
||||
var hasFile = _editTabs.CurrentKey != null;
|
||||
_filePathLabel.Text = hasFile
|
||||
? ScenarioFileService.GetDisplayPath(_editTabs.CurrentModId, _editTabs.CurrentRelativePath) : "";
|
||||
_saveBtn.Disabled = false;
|
||||
_openFileBtn.Disabled = false;
|
||||
_playBtn.Disabled = false;
|
||||
_statusLabel.Text = _editTabs.IsCurrentDirty
|
||||
? TranslationServer.Translate("tools/scenario_editor/status_modified") : "";
|
||||
SetFileButtonsEnabled(hasFile);
|
||||
_infoPanel.Update(text);
|
||||
_thumbPanel.Load(text);
|
||||
}
|
||||
@@ -204,26 +169,38 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
private void OnGraphTabSelected()
|
||||
{
|
||||
_filePathLabel.Text = "";
|
||||
_saveBtn.Disabled = true;
|
||||
_openFileBtn.Disabled = true;
|
||||
_playBtn.Disabled = true;
|
||||
_statusLabel.Text = "";
|
||||
SetFileButtonsEnabled(false);
|
||||
_infoPanel.Clear();
|
||||
_thumbPanel.Clear();
|
||||
_editTabs.RefreshGraph();
|
||||
}
|
||||
|
||||
private void SetFileButtonsEnabled(bool enabled)
|
||||
{
|
||||
_saveBtn.Disabled = !enabled;
|
||||
_openFileBtn.Disabled = !enabled;
|
||||
_playBtn.Disabled = !enabled;
|
||||
}
|
||||
|
||||
private void OnDirtyChanged(bool anyDirty)
|
||||
{
|
||||
if (anyDirty)
|
||||
{
|
||||
SetDirty();
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/status_modified");
|
||||
}
|
||||
else
|
||||
{
|
||||
MarkClean();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ctrl+S.</summary>
|
||||
protected override void OnSaveShortcut() => OnSavePressed();
|
||||
|
||||
/// <summary>F5 play-tests the active scenario.</summary>
|
||||
protected override bool HandleKey(InputEventKey key)
|
||||
{
|
||||
if (key.Keycode != Key.F5 || !_editTabs.HasActiveFile)
|
||||
return false;
|
||||
|
||||
OnPlayPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSavePressed()
|
||||
@@ -238,22 +215,20 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
if (!ScenarioFileService.WriteAllText(modId, relativePath, text))
|
||||
{
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/readonly_zip");
|
||||
SetStatus(ToolUi.Tr("tools/scenario_editor/readonly_zip"), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
_editTabs.SaveCurrentFile();
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/status_saved");
|
||||
SetStatus(string.Format(ToolUi.Tr("tools/common/saved"), ScenarioFileService.GetDisplayPath(modId, relativePath)), ToolStatusKind.Ok);
|
||||
_infoPanel.Update(text);
|
||||
_thumbPanel.Load(text);
|
||||
_editTabs.RefreshGraph();
|
||||
GameLogger.Log("SCENARIO_EDITOR", $"Saved: {modId}/{relativePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GameLogger.Log("SCENARIO_EDITOR", $"Save failed: {ex.Message}", LogLevel.Error);
|
||||
_statusLabel.Text = string.Format(
|
||||
TranslationServer.Translate("tools/scenario_editor/save_error"), ex.Message);
|
||||
SetStatus(string.Format(ToolUi.Tr("tools/scenario_editor/save_error"), ex.Message), ToolStatusKind.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,14 +240,16 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
var path = ScenarioFileService.GetAbsolutePath(_editTabs.CurrentModId, _editTabs.CurrentRelativePath);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/readonly_zip");
|
||||
SetStatus(ToolUi.Tr("tools/scenario_editor/readonly_zip"), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
GD.Print($"[ScenarioEditor] Opening: {path}");
|
||||
var err = OS.ShellOpen(path);
|
||||
if (err != Error.Ok)
|
||||
GameLogger.Log("SCENARIO_EDITOR", $"ShellOpen error: {err}", LogLevel.Error);
|
||||
{
|
||||
GameLogger.Log("SCENARIO_EDITOR", $"ShellOpen '{path}' failed: {err}", LogLevel.Error);
|
||||
SetStatus($"{ToolUi.Tr("tools/scenario_editor/open_file_error")}: {err}", ToolStatusKind.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayPressed()
|
||||
@@ -285,7 +262,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
if (metadata.Scene == "-")
|
||||
{
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/no_scene_error");
|
||||
SetStatus(ToolUi.Tr("tools/scenario_editor/no_scene_error"), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -299,7 +276,6 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
}
|
||||
else
|
||||
{
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/readonly_zip");
|
||||
GameLogger.Log("SCENARIO_EDITOR",
|
||||
$"Test play: '{modId}/{relativePath}' is read-only, playing from the editor buffer",
|
||||
LogLevel.Warning);
|
||||
@@ -307,14 +283,10 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_statusLabel.Text = string.Format(
|
||||
TranslationServer.Translate("tools/scenario_editor/save_error"), ex.Message);
|
||||
SetStatus(string.Format(ToolUi.Tr("tools/scenario_editor/save_error"), ex.Message), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
_editLayout.Visible = false;
|
||||
_playLayout.Visible = true;
|
||||
|
||||
SceneManager.IsTestPlayActive = true;
|
||||
SceneManager.TestScenarioText = text;
|
||||
|
||||
@@ -348,8 +320,6 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
private void ReturnFromPlay()
|
||||
{
|
||||
_playActive = false;
|
||||
_playLayout.Visible = false;
|
||||
_editLayout.Visible = true;
|
||||
Visible = true;
|
||||
|
||||
if (_stopOverlay != null)
|
||||
@@ -360,22 +330,21 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
var sceneService = GetNodeOrNull<GodotSceneService>("/root/GodotSceneService");
|
||||
sceneService?.SwitchToMenu();
|
||||
SetStatus(ToolUi.Tr("tools/scenario_editor/play_stopped"));
|
||||
}
|
||||
|
||||
/// <summary>Stop button shown over the running game while the scenario is play-tested.</summary>
|
||||
private CanvasLayer CreateStopOverlay()
|
||||
{
|
||||
var layer = new CanvasLayer { Layer = 128 };
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/stop"),
|
||||
CustomMinimumSize = new Vector2(120, 40)
|
||||
};
|
||||
btn.Pressed += OnStopPressed;
|
||||
var btn = ToolUi.Button("tools/scenario_editor/stop", OnStopPressed, ButtonKind.Danger);
|
||||
btn.Theme = ToolTheme.Get();
|
||||
btn.CustomMinimumSize = new Vector2(120, 36);
|
||||
|
||||
var margin = new MarginContainer();
|
||||
margin.AddThemeConstantOverride("margin_top", 8);
|
||||
margin.AddThemeConstantOverride("margin_left", 8);
|
||||
margin.AddThemeConstantOverride("margin_top", UiTokens.Space3);
|
||||
margin.AddThemeConstantOverride("margin_left", UiTokens.Space3);
|
||||
margin.AddChild(btn);
|
||||
|
||||
layer.AddChild(margin);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Layered left-to-right layout of the scene graph: a scene's column is
|
||||
/// its distance (in scene switches) from the start scenes — the
|
||||
/// <c>start_scene</c> node, or every scene nothing switches to when there
|
||||
/// is none — so the story reads from left to right. Scenes no start
|
||||
/// reaches are stacked in one extra column on the right. Inside a column
|
||||
/// scenes are sorted by name, which keeps the picture stable between
|
||||
/// rebuilds.
|
||||
/// </summary>
|
||||
public static class ScenarioGraphLayout
|
||||
{
|
||||
private const float ColumnWidth = 300f;
|
||||
private const float RowHeight = 130f;
|
||||
private const float Margin = 40f;
|
||||
|
||||
/// <summary>Canvas position per lower-cased scene name.</summary>
|
||||
public static Dictionary<string, Vector2> Compute(SceneGraphData data)
|
||||
{
|
||||
var names = data.Nodes.Select(n => n.SceneName.ToLowerInvariant()).Distinct().ToList();
|
||||
var outgoing = names.ToDictionary(n => n, _ => new List<string>());
|
||||
var incoming = new HashSet<string>();
|
||||
foreach (var edge in data.Edges)
|
||||
{
|
||||
var from = edge.FromScene.ToLowerInvariant();
|
||||
var to = edge.ToScene.ToLowerInvariant();
|
||||
if (!outgoing.ContainsKey(from) || !outgoing.ContainsKey(to) || from == to)
|
||||
continue;
|
||||
outgoing[from].Add(to);
|
||||
incoming.Add(to);
|
||||
}
|
||||
|
||||
var starts = data.Nodes.Where(n => n.IsStartScene).Select(n => n.SceneName.ToLowerInvariant()).ToList();
|
||||
if (starts.Count == 0)
|
||||
starts = names.Where(n => !incoming.Contains(n)).ToList();
|
||||
|
||||
var depth = new Dictionary<string, int>();
|
||||
var queue = new Queue<string>();
|
||||
foreach (var start in starts)
|
||||
{
|
||||
depth[start] = 0;
|
||||
queue.Enqueue(start);
|
||||
}
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
foreach (var next in outgoing[current])
|
||||
{
|
||||
if (depth.ContainsKey(next))
|
||||
continue;
|
||||
depth[next] = depth[current] + 1;
|
||||
queue.Enqueue(next);
|
||||
}
|
||||
}
|
||||
|
||||
var unreachableColumn = depth.Count > 0 ? depth.Values.Max() + 1 : 0;
|
||||
var result = new Dictionary<string, Vector2>();
|
||||
foreach (var column in names.GroupBy(n => depth.TryGetValue(n, out var d) ? d : unreachableColumn).OrderBy(g => g.Key))
|
||||
{
|
||||
var row = 0;
|
||||
foreach (var name in column.OrderBy(n => n, StringComparer.Ordinal))
|
||||
result[name] = new Vector2(Margin + column.Key * ColumnWidth, Margin + row++ * RowHeight);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Linq;
|
||||
using Cthangover.Tools.Services;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
@@ -14,26 +16,59 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
private Label _infoLocaleKeys;
|
||||
private Label _infoQuestRefs;
|
||||
|
||||
public ScenarioInfoPanel()
|
||||
/// <summary>Binds the rows once the panel enters the tree (see <see cref="Bind"/>).</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
GetNode<Label>("Scroll/VBox/HintScene").Text = TranslationServer.Translate("tools/scenario_editor/info_scene") + ":";
|
||||
GetNode<Label>("Scroll/VBox/HintPriority").Text = TranslationServer.Translate("tools/scenario_editor/info_priority") + ":";
|
||||
GetNode<Label>("Scroll/VBox/HintCondition").Text = TranslationServer.Translate("tools/scenario_editor/info_condition") + ":";
|
||||
GetNode<Label>("Scroll/VBox/HintSwitchTargets").Text = TranslationServer.Translate("tools/scenario_editor/info_switch_targets") + ":";
|
||||
GetNode<Label>("Scroll/VBox/HintLocaleKeys").Text = TranslationServer.Translate("tools/scenario_editor/info_locale_keys") + ":";
|
||||
GetNode<Label>("Scroll/VBox/HintQuestRefs").Text = TranslationServer.Translate("tools/scenario_editor/info_quest_refs") + ":";
|
||||
Bind();
|
||||
}
|
||||
|
||||
_infoScene = GetNode<Label>("Scroll/VBox/InfoScene");
|
||||
_infoPriority = GetNode<Label>("Scroll/VBox/InfoPriority");
|
||||
_infoCondition = GetNode<Label>("Scroll/VBox/InfoCondition");
|
||||
_infoSwitchTargets = GetNode<Label>("Scroll/VBox/InfoSwitchTargets");
|
||||
_infoLocaleKeys = GetNode<Label>("Scroll/VBox/InfoLocaleKeys");
|
||||
_infoQuestRefs = GetNode<Label>("Scroll/VBox/InfoQuestRefs");
|
||||
/// <summary>
|
||||
/// Looks the rows up lazily: the tscn builder runs the constructor before
|
||||
/// the child nodes exist, so lookups there would fail.
|
||||
/// </summary>
|
||||
private void Bind()
|
||||
{
|
||||
if (_infoScene != null)
|
||||
return;
|
||||
|
||||
SetHint("HintScene", "tools/scenario_editor/info_scene");
|
||||
SetHint("HintPriority", "tools/scenario_editor/info_priority");
|
||||
SetHint("HintCondition", "tools/scenario_editor/info_condition");
|
||||
SetHint("HintSwitchTargets", "tools/scenario_editor/info_switch_targets");
|
||||
SetHint("HintLocaleKeys", "tools/scenario_editor/info_locale_keys");
|
||||
SetHint("HintQuestRefs", "tools/scenario_editor/info_quest_refs");
|
||||
|
||||
_infoScene = Value("InfoScene");
|
||||
_infoPriority = Value("InfoPriority");
|
||||
_infoCondition = Value("InfoCondition");
|
||||
_infoSwitchTargets = Value("InfoSwitchTargets");
|
||||
_infoLocaleKeys = Value("InfoLocaleKeys");
|
||||
_infoQuestRefs = Value("InfoQuestRefs");
|
||||
}
|
||||
|
||||
private void SetHint(string node, string key)
|
||||
{
|
||||
var label = GetNode<Label>("Scroll/VBox/" + node);
|
||||
label.Text = TranslationServer.Translate(key).ToString().ToUpperInvariant();
|
||||
label.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
label.AddThemeFontSizeOverride("font_size", UiTokens.FontSmall);
|
||||
}
|
||||
|
||||
private Label Value(string node)
|
||||
{
|
||||
var label = GetNode<Label>("Scroll/VBox/" + node);
|
||||
label.AutowrapMode = TextServer.AutowrapMode.WordSmart;
|
||||
label.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
label.CustomMinimumSize = new Vector2(120, 0);
|
||||
label.AddThemeFontOverride("font", ToolWindow.GetMonospaceFont());
|
||||
label.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
|
||||
return label;
|
||||
}
|
||||
|
||||
/// <summary>Parses the given scenario text and refreshes every metadata row.</summary>
|
||||
public void Update(string text)
|
||||
{
|
||||
Bind();
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
Clear();
|
||||
@@ -54,6 +89,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
/// <summary>Resets every metadata row to the "-" placeholder.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
Bind();
|
||||
_infoScene.Text = "-";
|
||||
_infoPriority.Text = "-";
|
||||
_infoCondition.Text = "-";
|
||||
|
||||
@@ -9,10 +9,11 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
private HBoxContainer _thumbContainer;
|
||||
|
||||
public ScenarioThumbnailPanel()
|
||||
{
|
||||
_thumbContainer = GetNode<HBoxContainer>("ThumbContainer");
|
||||
}
|
||||
/// <summary>
|
||||
/// Container lookup is lazy: the tscn builder runs the constructor
|
||||
/// before the child nodes exist.
|
||||
/// </summary>
|
||||
private HBoxContainer Thumbs => _thumbContainer ??= GetNode<HBoxContainer>("ThumbContainer");
|
||||
|
||||
/// <summary>Loads the background thumbnails referenced by the given scenario text (missing ones render as placeholders).</summary>
|
||||
public void Load(string text)
|
||||
@@ -26,7 +27,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
if (refs.BackgroundRefs.Count == 0)
|
||||
{
|
||||
_thumbContainer.AddChild(new Label
|
||||
Thumbs.AddChild(new Label
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/no_backgrounds")
|
||||
});
|
||||
@@ -39,7 +40,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
if (tex != null)
|
||||
{
|
||||
_thumbContainer.AddChild(new TextureRect
|
||||
Thumbs.AddChild(new TextureRect
|
||||
{
|
||||
Texture = tex,
|
||||
ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize,
|
||||
@@ -50,10 +51,10 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
}
|
||||
else
|
||||
{
|
||||
_thumbContainer.AddChild(new Label
|
||||
Thumbs.AddChild(new Label
|
||||
{
|
||||
Text = $"[? {id}]",
|
||||
TooltipText = $"Not found: {id}"
|
||||
TooltipText = string.Format(TranslationServer.Translate("tools/scenario_editor/bg_missing"), id)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -62,7 +63,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
/// <summary>Removes every thumbnail from the row.</summary>
|
||||
public new void Clear()
|
||||
{
|
||||
foreach (var c in _thumbContainer.GetChildren())
|
||||
foreach (var c in Thumbs.GetChildren())
|
||||
c.QueueFree();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// State of one open scenario file tab: identity, the current text buffer
|
||||
/// (including unsaved edits while the tab is in the background) and the
|
||||
/// dirty flag.
|
||||
/// </summary>
|
||||
public sealed class TabData
|
||||
{
|
||||
public string Key;
|
||||
public string ModId;
|
||||
public string RelativePath;
|
||||
public string FileName;
|
||||
public string Buffer;
|
||||
public bool IsDirty;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user