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(); } + } + } +}