namespace Cthangover.Tools.Schemas { using Cthangover.Mods.Schemas; using System; using System.Collections.Generic; using System.Linq; using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Tool.ScenarioEditor; using Cthangover.Mods.Interface.Theming; using Cthangover.Mods.Interface.Tool; using Cthangover.Tools.Common; using Godot; /// /// The command palette: every scenario action and command some mod described, searchable /// and grouped, with a form built from its schema. The author fills the form (pickers /// for entities, check boxes, formula fields), sees the line it makes and what the /// validator would say about it, and inserts the line into the open scenario editor /// below the caret — or copies it. Nothing about an action is hard-coded here: a new /// mod with a schema file appears in the palette as soon as it is loaded. /// /// The text stays the source of truth: the palette writes exactly the DSL line an author /// would have typed, so a professional keeps typing and a beginner never has to. /// public partial class CommandPaletteWindow : ToolWindow { private const string ActionPrefix = "action:"; private const string CommandPrefix = "command:"; private readonly List controls = new(); private FilteredList list; private VBoxContainer form; private Label titleLabel; private Label helpLabel; private LineEdit arrowEdit; private LineEdit preview; private Label issuesLabel; private ActionSchema action; private CommandSchema command; /// Builds the window over the current schema catalog. public CommandPaletteWindow() : base("tools/schema/palette/title") { CthangoverTheme.Apply(this); BuildUi(); Fill(); SetHint(ToolUi.Tr("tools/schema/palette/hint")); } /// The line the form makes now (empty before anything is chosen). public string Line => preview?.Text ?? string.Empty; /// Chooses an entry: action:<id> or command:<word>. public void Choose(string entryId) { list.Select(entryId); OnChosen(entryId); } /// Sets a form value by parameter key (tests drive the form like a user). public void SetValue(string key, string value) { var control = controls.FirstOrDefault(c => string.Equals(c.Field.Key, key, StringComparison.OrdinalIgnoreCase)); if (control == null) return; control.Value = value; Rebuild(); } /// Inserts below the caret of the open scenario editor; false when none is open. public bool InsertIntoEditor() { var tabs = GetTree()?.Root.FindChildren("*", "", true, false).OfType().FirstOrDefault(t => t.HasActiveFile); if (tabs == null || string.IsNullOrEmpty(Line)) { SetStatus(ToolUi.Tr("tools/schema/palette/no_editor"), ToolStatusKind.Error); return false; } var code = tabs.CodeEditor; var at = Math.Min(code.GetCaretLine() + 1, code.GetLineCount()); code.InsertLineAt(at, Line); code.SetCaretLine(at); SetStatus(string.Format(ToolUi.Tr("tools/schema/palette/inserted"), tabs.CurrentKey), ToolStatusKind.Ok); return true; } private void BuildUi() { var outer = CreateFillContainer(); AddChild(outer); var toolbar = CreateToolbar(); toolbar.AddChild(ToolUi.Button("tools/schema/palette/insert", OnInsertPressed, ButtonKind.Primary, "tools/schema/palette/insert_tip")); toolbar.AddChild(ToolUi.Button("tools/schema/palette/copy", OnCopyPressed, ButtonKind.Secondary, "tools/schema/palette/copy_tip")); outer.AddChild(toolbar); var main = new HSplitContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, SizeFlagsVertical = Control.SizeFlags.ExpandFill }; outer.AddChild(main); var left = new VBoxContainer { SizeFlagsVertical = Control.SizeFlags.ExpandFill }; left.AddChild(ToolUi.Section("tools/schema/palette/section_list")); list = new FilteredList(360f, "tools/schema/palette/search") { SizeFlagsVertical = Control.SizeFlags.ExpandFill }; list.Chosen += OnChosen; left.AddChild(list); main.AddChild(CreateSidebar(left, 340)); var scroll = new ScrollContainer { HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled, SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }; main.AddChild(scroll); var column = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }; column.AddThemeConstantOverride("separation", UiTokens.Space2); scroll.AddChild(column); titleLabel = UiKit.Label(ToolUi.Tr("tools/schema/palette/pick"), UiTokens.FontBodyLarge, UiTokens.InkBright); column.AddChild(titleLabel); helpLabel = ToolUi.Hint(string.Empty); column.AddChild(helpLabel); form = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }; form.AddThemeConstantOverride("separation", UiTokens.Space2); column.AddChild(form); column.AddChild(ToolUi.Section("tools/schema/palette/section_line")); preview = new LineEdit { Editable = false, Name = "Preview", SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }; preview.AddThemeFontOverride("font", ToolUi.Mono()); column.AddChild(preview); issuesLabel = ToolUi.Hint(string.Empty); issuesLabel.Name = "Issues"; column.AddChild(issuesLabel); } private void Fill() { var catalog = SchemaCatalog.Current; var items = new List<(string Id, string Text, Texture2D Icon)>(); foreach (var a in catalog.Actions.Values.OrderBy(a => a.Category).ThenBy(a => a.Id, StringComparer.Ordinal)) items.Add((ActionPrefix + a.Id, Entry(a.Category, a.Name, "action " + a.Id), null)); foreach (var c in catalog.Commands.Values.OrderBy(c => c.Category).ThenBy(c => c.Id, StringComparer.Ordinal)) items.Add((CommandPrefix + c.Id, Entry(c.Category, c.Name, c.Id), null)); list.SetItems(items); SetStatus(string.Format(ToolUi.Tr("tools/schema/palette/count"), catalog.Actions.Count, catalog.Commands.Count)); } private static string Entry(string category, string nameKey, string syntax) { var name = string.IsNullOrEmpty(nameKey) ? syntax : ToolUi.Tr(nameKey); var group = string.IsNullOrEmpty(category) ? string.Empty : $"[{Category(category)}] "; return $"{group}{name} · {syntax}"; } private static string Category(string category) { var key = "tools/schema/category/" + category; var text = ToolUi.Tr(key); return text == key ? category : text; } private void OnChosen(string entryId) { action = null; command = null; var catalog = SchemaCatalog.Current; if (entryId?.StartsWith(ActionPrefix, StringComparison.Ordinal) == true) catalog.Actions.TryGetValue(entryId.Substring(ActionPrefix.Length), out action); else if (entryId?.StartsWith(CommandPrefix, StringComparison.Ordinal) == true) catalog.Commands.TryGetValue(entryId.Substring(CommandPrefix.Length), out command); BuildForm(); } private void BuildForm() { foreach (var child in form.GetChildren()) { form.RemoveChild(child); child.QueueFree(); } controls.Clear(); arrowEdit = null; var nameKey = action?.Name ?? command?.Name; titleLabel.Text = nameKey == null ? ToolUi.Tr("tools/schema/palette/pick") : ToolUi.Tr(nameKey); var help = action?.Description ?? command?.Description; var source = action?.SourceMod ?? command?.SourceMod; helpLabel.Text = (help == null ? string.Empty : ToolUi.Tr(help) + "\n") + (source == null ? string.Empty : string.Format(ToolUi.Tr("tools/schema/described_by"), source)); var fields = action != null ? action.Params : command != null ? (command.Positional ?? new()).Concat(command.Params ?? new()).ToList() : new List(); foreach (var field in fields) controls.Add(new SchemaFieldControl(form, field, Rebuild)); if (command?.Arrow != null) arrowEdit = ToolUi.Field(form, command.Arrow, ToolUi.Text(":label", OnArrowTyped), "tools/schema/palette/arrow_tip"); Rebuild(); } private void OnArrowTyped(string text) => Rebuild(); private void Rebuild() { var values = controls.ToDictionary(c => c.Field.Key, c => c.Value, StringComparer.OrdinalIgnoreCase); List issues; if (action != null) { preview.Text = SchemaLine.Action(action, values); issues = SchemaChecker.CheckNamed(action.Id, action.Params, values.Where(p => p.Value.Length > 0).ToDictionary(p => p.Key, p => p.Value), GameEntityIndex.Exists); } else if (command != null) { preview.Text = SchemaLine.Command(command, values, arrowEdit?.Text); var positional = (command.Positional ?? new()).Select(f => values.GetValueOrDefault(f.Key)).ToList(); issues = SchemaChecker.CheckPositional(command.Id, command.Positional, positional, GameEntityIndex.Exists); issues.AddRange(SchemaChecker.CheckNamed(command.Id, command.Params, (command.Params ?? new()).Where(f => values.GetValueOrDefault(f.Key)?.Length > 0).ToDictionary(f => f.Key, f => values[f.Key]), GameEntityIndex.Exists)); } else { preview.Text = string.Empty; issues = new List(); } issuesLabel.Text = issues.Count == 0 ? (preview.Text.Length > 0 ? ToolUi.Tr("tools/schema/palette/line_ok") : string.Empty) : string.Join("\n", issues.Select(i => (i.IsError ? "✖ " : "⚠ ") + i.Message)); issuesLabel.AddThemeColorOverride("font_color", issues.Any(i => i.IsError) ? UiTokens.DangerText : issues.Count > 0 ? UiTokens.Warning : UiTokens.SuccessText); } private void OnInsertPressed() => InsertIntoEditor(); private void OnCopyPressed() { if (string.IsNullOrEmpty(Line)) return; DisplayServer.ClipboardSet(Line); SetStatus(ToolUi.Tr("tools/schema/palette/copied"), ToolStatusKind.Ok); } } }