Highlight JSON, TSCN and scenario fields across editor tools

This commit is contained in:
cthangover committed 2026-10-26 19:44:26 +03:00
1 parent 84894b1e98
commit cbc9472738
7 files changed
+70 -29

No files matched your search

+2 -1
View File
@@ -119,12 +119,13 @@ namespace Cthangover.Tools.Common
/// <summary>Monospace font for IDs, numbers and code (style guide <c>--ct-mono</c>).</summary> /// <summary>Monospace font for IDs, numbers and code (style guide <c>--ct-mono</c>).</summary>
public static Font Mono() => ToolWindow.GetMonospaceFont(); public static Font Mono() => ToolWindow.GetMonospaceFont();
/// <summary>Code editor preset: monospace, line numbers, current-line highlight, no wrap.</summary> /// <summary>Code editor preset: monospace, line numbers, JSON highlighting and no wrap; scenario/scene callers select their syntax.</summary>
public static CodeEdit Code(bool editable = true) public static CodeEdit Code(bool editable = true)
{ {
var edit = new CodeEdit var edit = new CodeEdit
{ {
Editable = editable, Editable = editable,
SyntaxHighlighter = SourceSyntax.CreateJsonHighlighter(),
GuttersDrawLineNumbers = true, GuttersDrawLineNumbers = true,
HighlightCurrentLine = true, HighlightCurrentLine = true,
WrapMode = TextEdit.LineWrappingMode.None, WrapMode = TextEdit.LineWrappingMode.None,
+1
View File
@@ -110,6 +110,7 @@ namespace Cthangover.Core.UI.Tool.LightEditor
canvasPanel.AddChild(_canvas); canvasPanel.AddChild(_canvas);
_lineEdit = ToolUi.Code(editable: false); _lineEdit = ToolUi.Code(editable: false);
_lineEdit.SyntaxHighlighter = ScenarioSyntaxService.CreateHighlighter();
_lineEdit.CustomMinimumSize = new Vector2(0, 70); _lineEdit.CustomMinimumSize = new Vector2(0, 70);
_lineEdit.WrapMode = TextEdit.LineWrappingMode.Boundary; _lineEdit.WrapMode = TextEdit.LineWrappingMode.Boundary;
_lineEdit.Visible = false; _lineEdit.Visible = false;
+1
View File
@@ -249,6 +249,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
_sideTabs.SetTabTitle(1, ToolUi.Tr("tools/scene_builder/tab_tscn")); _sideTabs.SetTabTitle(1, ToolUi.Tr("tools/scene_builder/tab_tscn"));
_tscnEditor = ToolUi.Code(); _tscnEditor = ToolUi.Code();
_tscnEditor.SyntaxHighlighter = SourceSyntax.CreateTscnHighlighter();
_tscnEditor.TextChanged += OnTscnTextChanged; _tscnEditor.TextChanged += OnTscnTextChanged;
tscnTab.AddChild(_tscnEditor); tscnTab.AddChild(_tscnEditor);
tscnTab.AddChild(ToolUi.Hint(ToolUi.Tr("tools/scene_builder/tscn_hint"))); tscnTab.AddChild(ToolUi.Hint(ToolUi.Tr("tools/scene_builder/tscn_hint")));
+19 -2
View File
@@ -19,7 +19,8 @@ namespace Cthangover.Tools.Schemas
/// the kind (by name), of player/party/characters;</item> /// the kind (by name), of player/party/characters;</item>
/// <item><c>ref_list</c> — a comma list with a drop-down that appends an entity;</item> /// <item><c>ref_list</c> — a comma list with a drop-down that appends an entity;</item>
/// <item><c>picture</c> — a path with the picture grid of the editors;</item> /// <item><c>picture</c> — a path with the picture grid of the editors;</item>
/// <item>anything else — a text box (monospace for formulas, conditions and JSON).</item> /// <item><c>json</c> — a multiline source editor with syntax highlighting;</item>
/// <item>anything else — a text box (monospace for formulas and conditions).</item>
/// </list> /// </list>
/// A current value the options do not know is kept and shown first, so opening and /// 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. /// saving never loses data the schema did not foresee.
@@ -28,6 +29,7 @@ namespace Cthangover.Tools.Schemas
{ {
private readonly Action changed; private readonly Action changed;
private readonly LineEdit text; private readonly LineEdit text;
private readonly CodeEdit json;
private readonly OptionButton picker; private readonly OptionButton picker;
private readonly CheckBox check; private readonly CheckBox check;
private readonly List<string> pickerValues = new(); private readonly List<string> pickerValues = new();
@@ -74,10 +76,19 @@ namespace Cthangover.Tools.Schemas
} }
Editor = ToolUi.Field(parent, label, row, tip); Editor = ToolUi.Field(parent, label, row, tip);
break; 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: default:
text = ToolUi.Text(field.Default ?? string.Empty, OnTyped); text = ToolUi.Text(field.Default ?? string.Empty, OnTyped);
text.Name = "Text_" + field.Key; text.Name = "Text_" + field.Key;
if (Type is "formula" or "condition" or "json") if (Type is "formula" or "condition")
text.AddThemeFontOverride("font", ToolUi.Mono()); text.AddThemeFontOverride("font", ToolUi.Mono());
Editor = ToolUi.Field(parent, label, text, tip); Editor = ToolUi.Field(parent, label, text, tip);
break; break;
@@ -103,6 +114,8 @@ namespace Cthangover.Tools.Schemas
return check.ButtonPressed ? "1" : string.Empty; return check.ButtonPressed ? "1" : string.Empty;
if (text != null) if (text != null)
return text.Text.Trim(); 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; return picker != null && picker.Selected >= 0 && picker.Selected < pickerValues.Count ? pickerValues[picker.Selected] : string.Empty;
} }
set set
@@ -112,6 +125,8 @@ namespace Cthangover.Tools.Schemas
check.ButtonPressed = value is "1" or "true" or "True" or "yes"; check.ButtonPressed = value is "1" or "true" or "True" or "yes";
else if (text != null) else if (text != null)
text.Text = value ?? string.Empty; text.Text = value ?? string.Empty;
else if (json != null)
json.Text = value ?? string.Empty;
else else
FillPicker(value ?? string.Empty); FillPicker(value ?? string.Empty);
suppress = false; suppress = false;
@@ -155,6 +170,8 @@ namespace Cthangover.Tools.Schemas
} }
} }
private void OnJsonChanged() => OnTyped(json.Text);
private void OnTyped(string value) private void OnTyped(string value)
{ {
if (!suppress) if (!suppress)
+3 -26
View File
@@ -3,6 +3,7 @@ using Cthangover.Mods.Dialog.Scenarios;
using Cthangover.Core.Scenarios; using Cthangover.Core.Scenarios;
using Cthangover.Core.Factories.Impls; using Cthangover.Core.Factories.Impls;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Cthangover.Mods.Interface.Theming;
using Godot; using Godot;
namespace Cthangover.Tools.Services namespace Cthangover.Tools.Services
@@ -20,36 +21,12 @@ namespace Cthangover.Tools.Services
{ {
/// <summary> /// <summary>
/// Builds a <see cref="CodeHighlighter"/> for scenario DSL syntax. /// Builds a <see cref="CodeHighlighter"/> for scenario DSL syntax.
/// Comments (lines starting with <c>#</c>) are green, strings are bright /// Comments (lines starting with <c>#</c>) are faint, strings are bright
/// gold, command names are blue, and argument keywords (e.g. <c>key</c>, /// gold, command names are blue, and argument keywords (e.g. <c>key</c>,
/// <c>wait</c>, <c>quest_id</c>) are amber — all from <see cref="UiTokens"/>. /// <c>wait</c>, <c>quest_id</c>) are amber — all from <see cref="UiTokens"/>.
/// </summary> /// </summary>
public static CodeHighlighter CreateHighlighter() public static CodeHighlighter CreateHighlighter()
{ => SourceSyntax.CreateScenarioHighlighter(ScenarioCommandStrategyFactory.GetAllCommandNames());
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;
}
/// <summary> /// <summary>
/// Splits a line into tokens, handling quoted strings (with backslash /// Splits a line into tokens, handling quoted strings (with backslash
@@ -25,6 +25,8 @@ namespace Cthangover.Tools.Tests.Windows
/// <summary>Runs the checks for <paramref name="toolId"/>; <paramref name="check"/> records each result.</summary> /// <summary>Runs the checks for <paramref name="toolId"/>; <paramref name="check"/> records each result.</summary>
public static void Run(string toolId, Window window, Action<bool, string> check) public static void Run(string toolId, Window window, Action<bool, string> check)
{ {
foreach (var editor in window.FindChildren("*", "", true, false).OfType<CodeEdit>())
check(editor.SyntaxHighlighter != null, toolId + ": source field has syntax highlighting");
switch (toolId) switch (toolId)
{ {
case "map_editor": case "map_editor":
+42
View File
@@ -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;
/// <summary>JSON schema fields preserve structured values through their highlighted multiline editor.</summary>
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<string>(), restored["text"].GetValue<string>(), "text survives the field");
ModAssert.True(restored["active"].GetValue<bool>(), "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<int>(), "edited JSON is returned by the form");
}
finally { parent.Free(); }
}
}
}