diff --git a/src/Common/ToolUi.cs b/src/Common/ToolUi.cs
index e58e568..ee11882 100644
--- a/src/Common/ToolUi.cs
+++ b/src/Common/ToolUi.cs
@@ -119,12 +119,13 @@ namespace Cthangover.Tools.Common
/// Monospace font for IDs, numbers and code (style guide --ct-mono).
public static Font Mono() => ToolWindow.GetMonospaceFont();
- /// Code editor preset: monospace, line numbers, current-line highlight, no wrap.
+ /// Code editor preset: monospace, line numbers, JSON highlighting and no wrap; scenario/scene callers select their syntax.
public static CodeEdit Code(bool editable = true)
{
var edit = new CodeEdit
{
Editable = editable,
+ SyntaxHighlighter = SourceSyntax.CreateJsonHighlighter(),
GuttersDrawLineNumbers = true,
HighlightCurrentLine = true,
WrapMode = TextEdit.LineWrappingMode.None,
diff --git a/src/LightEditor/LightEditorWindow.cs b/src/LightEditor/LightEditorWindow.cs
index 5ef9eb2..2aa5664 100644
--- a/src/LightEditor/LightEditorWindow.cs
+++ b/src/LightEditor/LightEditorWindow.cs
@@ -110,6 +110,7 @@ namespace Cthangover.Core.UI.Tool.LightEditor
canvasPanel.AddChild(_canvas);
_lineEdit = ToolUi.Code(editable: false);
+ _lineEdit.SyntaxHighlighter = ScenarioSyntaxService.CreateHighlighter();
_lineEdit.CustomMinimumSize = new Vector2(0, 70);
_lineEdit.WrapMode = TextEdit.LineWrappingMode.Boundary;
_lineEdit.Visible = false;
diff --git a/src/SceneBuilder/SceneBuilderWindow.cs b/src/SceneBuilder/SceneBuilderWindow.cs
index 549611c..eb35047 100644
--- a/src/SceneBuilder/SceneBuilderWindow.cs
+++ b/src/SceneBuilder/SceneBuilderWindow.cs
@@ -249,6 +249,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
_sideTabs.SetTabTitle(1, ToolUi.Tr("tools/scene_builder/tab_tscn"));
_tscnEditor = ToolUi.Code();
+ _tscnEditor.SyntaxHighlighter = SourceSyntax.CreateTscnHighlighter();
_tscnEditor.TextChanged += OnTscnTextChanged;
tscnTab.AddChild(_tscnEditor);
tscnTab.AddChild(ToolUi.Hint(ToolUi.Tr("tools/scene_builder/tscn_hint")));
diff --git a/src/Schemas/SchemaFieldControl.cs b/src/Schemas/SchemaFieldControl.cs
index 98280c5..52ebad5 100644
--- a/src/Schemas/SchemaFieldControl.cs
+++ b/src/Schemas/SchemaFieldControl.cs
@@ -19,7 +19,8 @@ namespace Cthangover.Tools.Schemas
/// the kind (by name), of player/party/characters;
/// - ref_list — a comma list with a drop-down that appends an entity;
/// - picture — a path with the picture grid of the editors;
- /// - anything else — a text box (monospace for formulas, conditions and JSON).
+ /// - json — a multiline source editor with syntax highlighting;
+ /// - anything else — a text box (monospace for formulas and conditions).
///
/// A current value the options do not know is kept and shown first, so opening and
/// saving never loses data the schema did not foresee.
@@ -28,6 +29,7 @@ namespace Cthangover.Tools.Schemas
{
private readonly Action changed;
private readonly LineEdit text;
+ private readonly CodeEdit json;
private readonly OptionButton picker;
private readonly CheckBox check;
private readonly List pickerValues = new();
@@ -74,10 +76,19 @@ namespace Cthangover.Tools.Schemas
}
Editor = ToolUi.Field(parent, label, row, tip);
break;
+ case "json":
+ json = ToolUi.Code();
+ json.Name = "Text_" + field.Key;
+ json.Text = field.Default ?? string.Empty;
+ json.CustomMinimumSize = new Vector2(0, 90);
+ json.SizeFlagsVertical = Control.SizeFlags.Fill;
+ json.TextChanged += OnJsonChanged;
+ Editor = ToolUi.Field(parent, label, json, tip);
+ break;
default:
text = ToolUi.Text(field.Default ?? string.Empty, OnTyped);
text.Name = "Text_" + field.Key;
- if (Type is "formula" or "condition" or "json")
+ if (Type is "formula" or "condition")
text.AddThemeFontOverride("font", ToolUi.Mono());
Editor = ToolUi.Field(parent, label, text, tip);
break;
@@ -103,6 +114,8 @@ namespace Cthangover.Tools.Schemas
return check.ButtonPressed ? "1" : string.Empty;
if (text != null)
return text.Text.Trim();
+ if (json != null)
+ return json.Text.Trim();
return picker != null && picker.Selected >= 0 && picker.Selected < pickerValues.Count ? pickerValues[picker.Selected] : string.Empty;
}
set
@@ -112,6 +125,8 @@ namespace Cthangover.Tools.Schemas
check.ButtonPressed = value is "1" or "true" or "True" or "yes";
else if (text != null)
text.Text = value ?? string.Empty;
+ else if (json != null)
+ json.Text = value ?? string.Empty;
else
FillPicker(value ?? string.Empty);
suppress = false;
@@ -155,6 +170,8 @@ namespace Cthangover.Tools.Schemas
}
}
+ private void OnJsonChanged() => OnTyped(json.Text);
+
private void OnTyped(string value)
{
if (!suppress)
diff --git a/src/Services/ScenarioSyntaxService.cs b/src/Services/ScenarioSyntaxService.cs
index 63be17b..516ad01 100644
--- a/src/Services/ScenarioSyntaxService.cs
+++ b/src/Services/ScenarioSyntaxService.cs
@@ -3,6 +3,7 @@ using Cthangover.Mods.Dialog.Scenarios;
using Cthangover.Core.Scenarios;
using Cthangover.Core.Factories.Impls;
using Cthangover.Core.UI.Theming;
+using Cthangover.Mods.Interface.Theming;
using Godot;
namespace Cthangover.Tools.Services
@@ -20,36 +21,12 @@ namespace Cthangover.Tools.Services
{
///
/// Builds a for scenario DSL syntax.
- /// Comments (lines starting with #) are green, strings are bright
+ /// Comments (lines starting with #) are faint, strings are bright
/// gold, command names are blue, and argument keywords (e.g. key,
/// wait, quest_id) are amber — all from .
///
public static CodeHighlighter CreateHighlighter()
- {
- var hl = new CodeHighlighter();
- hl.AddColorRegion("#", null, UiTokens.Success, true);
- hl.AddColorRegion("\"", "\"", UiTokens.AccentBright, false);
- hl.NumberColor = UiTokens.Info;
- hl.SymbolColor = UiTokens.Muted;
-
- var cmdColor = UiTokens.Info;
- hl.KeywordColors = new Godot.Collections.Dictionary();
-
- foreach (var cmd in ScenarioCommandStrategyFactory.GetAllCommandNames())
- hl.KeywordColors[cmd] = cmdColor;
-
- foreach (var cmd in new[] {
- "option", "select"
- }) hl.KeywordColors[cmd] = cmdColor;
-
- var argColor = UiTokens.Accent;
- foreach (var kw in new[] {
- "key", "first", "second", "hide_color", "wait", "speed", "hidden",
- "sprites", "loop", "target", "quest_id", "show", "hide", "duration"
- }) hl.KeywordColors[kw] = argColor;
-
- return hl;
- }
+ => SourceSyntax.CreateScenarioHighlighter(ScenarioCommandStrategyFactory.GetAllCommandNames());
///
/// Splits a line into tokens, handling quoted strings (with backslash
diff --git a/test_suites/windows/src/ToolWindowsExercises.cs b/test_suites/windows/src/ToolWindowsExercises.cs
index a2fe88b..e2fd96f 100644
--- a/test_suites/windows/src/ToolWindowsExercises.cs
+++ b/test_suites/windows/src/ToolWindowsExercises.cs
@@ -25,6 +25,8 @@ namespace Cthangover.Tools.Tests.Windows
/// Runs the checks for ; records each result.
public static void Run(string toolId, Window window, Action check)
{
+ foreach (var editor in window.FindChildren("*", "", true, false).OfType())
+ check(editor.SyntaxHighlighter != null, toolId + ": source field has syntax highlighting");
switch (toolId)
{
case "map_editor":
diff --git a/tests/SchemaFieldControlTests.cs b/tests/SchemaFieldControlTests.cs
new file mode 100644
index 0000000..96768d6
--- /dev/null
+++ b/tests/SchemaFieldControlTests.cs
@@ -0,0 +1,42 @@
+namespace Cthangover.Tools.Tests
+{
+ using System.Text.Json;
+ using System.Text.Json.Nodes;
+ using Cthangover.Core.Mods.Testing;
+ using Cthangover.Mods.Schemas;
+ using Cthangover.Tools.Schemas;
+ using Godot;
+
+ /// JSON schema fields preserve structured values through their highlighted multiline editor.
+ public class SchemaFieldControlTests
+ {
+ [ModTest("schema/json_field_roundtrip")]
+ static void JsonFieldRoundtrip(ModTestContext ctx)
+ {
+ var parent = new VBoxContainer();
+ ((SceneTree)Engine.GetMainLoop()).Root.AddChild(parent);
+ try
+ {
+ var field = new SchemaFieldControl(parent, new FieldSchema { Key = "Extra", Type = "json" }, null);
+ ModAssert.True(field.Editor is CodeEdit, "JSON field has a multiline code editor");
+ var code = (CodeEdit)field.Editor;
+ ModAssert.True(code.SyntaxHighlighter != null, "JSON field has syntax highlighting");
+ var original = new JsonObject
+ {
+ ["text"] = "Unicode text: \u041f\u0440\u0438\u0432\u0435\u0442",
+ ["active"] = true,
+ ["nested"] = new JsonArray(1, null, 3),
+ };
+ field.Value = JsonSerializer.Serialize(original, new JsonSerializerOptions { WriteIndented = true });
+ var restored = JsonNode.Parse(field.Value);
+ ModAssert.Equal(original["text"].GetValue(), restored["text"].GetValue(), "text survives the field");
+ ModAssert.True(restored["active"].GetValue(), "boolean survives the field");
+ ModAssert.Equal(3, restored["nested"].AsArray().Count, "array survives the field");
+ ModAssert.True(restored["nested"][1] == null, "null survives the field");
+ code.Text = "{\n \"number\": 42\n}";
+ ModAssert.Equal(42, JsonNode.Parse(field.Value)["number"].GetValue(), "edited JSON is returned by the form");
+ }
+ finally { parent.Free(); }
+ }
+ }
+}