diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 154176b..cf10c47 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Extract version from tag id: version shell: bash @@ -58,6 +60,7 @@ jobs: "$output/Silkstring.dll", "$output/Silkstring.json", "$output/ECommons.dll", + "$output/DalamudTextEdit.dll", "$output/Silkstring.deps.json", "$output/images", "$output/Silkstring" diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..07cc53d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "extern/DalamudTextEdit"] + path = extern/DalamudTextEdit + url = https://github.com/devoreofox/DalamudTextEdit.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 62b3f94..08e62e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v1.5.0.0 - 2026-07-02 +### Added +- Syntax highlighting in the multiline editor: commands, keywords (`:if`, `:set`, `:wait`), variables, parameters, and quoted text are each given their own color as you type, and anything malformed is shown in red (a bad `:wait` time, an unfinished `:if`, a `:set` for a variable you have not created, a mistyped keyword, or an unclosed `{`) +- Line numbers in the multiline editor, with a setting to turn them off +- A Colors section in the settings to recolor the editor and interface, updating live as you change them + ## v1.4.1.0 - 2026-07-01 ### Fixed - Removed TBD and dated changelog (oops) diff --git a/README.md b/README.md index 412281f..1beabd9 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ A Dalamud plugin for FFXIV that lets you define custom command aliases. Each ali - Filter aliases by name, and import, export, or clone them - Enable or disable an alias without deleting it - Built-in help window with a live command tester +- Syntax highlighting in the multiline editor, with customizable colors - Cycle detection warns you if aliases would trigger each other in a loop ## Installation @@ -174,11 +175,11 @@ Create a folder with the **New Folder** button, then drag aliases into or out of ### Multiline entry -By default each line of an alias is its own row. You can switch to a single multiline text box in the settings, with one line per row, which is handy for pasting or editing longer aliases. +By default each line of an alias is its own row. You can switch to a single multiline editor in the settings, which is handy for pasting or editing longer aliases. The editor highlights your syntax as you type: commands, keywords, variables, parameters, and quoted text each get their own color, and anything malformed (a bad `:wait`, an unfinished `:if`, an unknown `:set` name, a mistyped keyword, or an unclosed `{`) is shown in red. It also shows line numbers, which you can turn off in the settings. ### Settings -Open settings with the cog icon in the Silkstring title bar, or the cog next to Silkstring in `/xlplugins`. You can set the delay between lines in milliseconds (0 to 1000, default 100), and toggle multiline command entry. +Open settings with the cog icon in the Silkstring title bar, or the cog next to Silkstring in `/xlplugins`. You can set the delay between lines in milliseconds (0 to 1000, default 100), toggle multiline command entry, toggle line numbers in the editor, and open the Colors section to recolor the editor and interface to your taste. ## Notes diff --git a/Silkstring.sln b/Silkstring.sln index abca2b9..efa4443 100644 --- a/Silkstring.sln +++ b/Silkstring.sln @@ -7,28 +7,63 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silkstring", "Silkstring\Si EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{BC8C19B7-F609-4B27-A05B-03D50E7E46BB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "extern", "extern", "{8C8C186C-83DC-ECBC-707A-D920EEBF6757}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DalamudTextEdit", "extern\DalamudTextEdit\DalamudTextEdit.csproj", "{B36EF61A-B235-4249-925B-FC6C0DB568EA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 + Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 Release|x64 = Release|x64 + Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x86.ActiveCfg = Debug|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x86.Build.0 = Debug|Any CPU {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 - {4FEC9558-EB25-419F-B86E-51B8CFDA32B7}.Debug|x64.ActiveCfg = Debug|x64 - {4FEC9558-EB25-419F-B86E-51B8CFDA32B7}.Debug|x64.Build.0 = Debug|x64 - {4FEC9558-EB25-419F-B86E-51B8CFDA32B7}.Release|x64.ActiveCfg = Release|x64 - {4FEC9558-EB25-419F-B86E-51B8CFDA32B7}.Release|x64.Build.0 = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x86.ActiveCfg = Release|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x86.Build.0 = Release|Any CPU {BC8C19B7-F609-4B27-A05B-03D50E7E46BB}.Debug|x64.ActiveCfg = Debug|Any CPU {BC8C19B7-F609-4B27-A05B-03D50E7E46BB}.Debug|x64.Build.0 = Debug|Any CPU + {BC8C19B7-F609-4B27-A05B-03D50E7E46BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC8C19B7-F609-4B27-A05B-03D50E7E46BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC8C19B7-F609-4B27-A05B-03D50E7E46BB}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC8C19B7-F609-4B27-A05B-03D50E7E46BB}.Debug|x86.Build.0 = Debug|Any CPU {BC8C19B7-F609-4B27-A05B-03D50E7E46BB}.Release|x64.ActiveCfg = Release|Any CPU {BC8C19B7-F609-4B27-A05B-03D50E7E46BB}.Release|x64.Build.0 = Release|Any CPU + {BC8C19B7-F609-4B27-A05B-03D50E7E46BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC8C19B7-F609-4B27-A05B-03D50E7E46BB}.Release|Any CPU.Build.0 = Release|Any CPU + {BC8C19B7-F609-4B27-A05B-03D50E7E46BB}.Release|x86.ActiveCfg = Release|Any CPU + {BC8C19B7-F609-4B27-A05B-03D50E7E46BB}.Release|x86.Build.0 = Release|Any CPU + {B36EF61A-B235-4249-925B-FC6C0DB568EA}.Debug|x64.ActiveCfg = Debug|Any CPU + {B36EF61A-B235-4249-925B-FC6C0DB568EA}.Debug|x64.Build.0 = Debug|Any CPU + {B36EF61A-B235-4249-925B-FC6C0DB568EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B36EF61A-B235-4249-925B-FC6C0DB568EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B36EF61A-B235-4249-925B-FC6C0DB568EA}.Debug|x86.ActiveCfg = Debug|Any CPU + {B36EF61A-B235-4249-925B-FC6C0DB568EA}.Debug|x86.Build.0 = Debug|Any CPU + {B36EF61A-B235-4249-925B-FC6C0DB568EA}.Release|x64.ActiveCfg = Release|Any CPU + {B36EF61A-B235-4249-925B-FC6C0DB568EA}.Release|x64.Build.0 = Release|Any CPU + {B36EF61A-B235-4249-925B-FC6C0DB568EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B36EF61A-B235-4249-925B-FC6C0DB568EA}.Release|Any CPU.Build.0 = Release|Any CPU + {B36EF61A-B235-4249-925B-FC6C0DB568EA}.Release|x86.ActiveCfg = Release|Any CPU + {B36EF61A-B235-4249-925B-FC6C0DB568EA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B36EF61A-B235-4249-925B-FC6C0DB568EA} = {8C8C186C-83DC-ECBC-707A-D920EEBF6757} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} EndGlobalSection diff --git a/Silkstring/Configuration.cs b/Silkstring/Configuration.cs index 0fccfe8..cdc8332 100644 --- a/Silkstring/Configuration.cs +++ b/Silkstring/Configuration.cs @@ -24,9 +24,13 @@ public int CommandDelay public const int CurrentVersion = 2; public string? LastSeenVersion { get; set; } public int Version { get; set; } = CurrentVersion; + public List Folders = new(); public List Aliases = new(); public List UserVariables = new(); + + public ThemeColors Theme = new(); + public bool ShowLineNumbers = true; public bool MultilineCommands { get; set; } public void Save() diff --git a/Silkstring/Models/ThemeColors.cs b/Silkstring/Models/ThemeColors.cs new file mode 100644 index 0000000..82ec42d --- /dev/null +++ b/Silkstring/Models/ThemeColors.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +namespace Silkstring.Models; + +public class ThemeColors +{ + public Vector4 Heading = new(0.7f, 0.5f, 1.0f, 1.0f); + public Vector4 Folder = new(0.7f, 0.5f, 1.0f, 1.0f); + public Vector4 LineNumber = new(0.5f, 0.5f, 0.5f, 1.0f); + public Vector4 Error = new(1.0f, 0.4f, 0.4f, 1.0f); + public Vector4 Success = new(0.4f, 1.0f, 0.4f, 1.0f); + public Vector4 Variable = new(0.9f, 0.8f, 0.4f, 1.0f); + public Vector4 Parameter = new(1.0f, 0.6f, 0.3f, 1.0f); + public Vector4 Keyword = new(0.4f, 0.8f, 1.0f, 1.0f); + public Vector4 String = new(0.6f, 0.8f, 0.5f, 1.0f); + public Vector4 Command = new(0.7f, 0.5f, 1.0f, 1.0f); + public Vector4 Text = new(0.9f, 0.9f, 0.9f, 1.0f); +} diff --git a/Silkstring/Plugin.cs b/Silkstring/Plugin.cs index 7abf7ac..350d445 100644 --- a/Silkstring/Plugin.cs +++ b/Silkstring/Plugin.cs @@ -11,6 +11,7 @@ using ECommons; using Silkstring.Services; using Silkstring.Services.Variables; +using Silkstring.UI; using Silkstring.Windows; namespace Silkstring; @@ -45,6 +46,7 @@ public sealed class Plugin : IDalamudPlugin public Plugin() { Configuration = PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); + Palette.Apply(Configuration.Theme); var migration = ConfigMigrator.Migrate(Configuration, PluginInterface); if (migration != null) @@ -140,6 +142,7 @@ private void OnCommand(string command, string args) case "help": ToggleHelpUi(); break; case "changelog": ToggleChangelogUi(); break; case "variables": ToggleVariablesUi(); break; + case "edit": ToggleConfigUi(); break; default: MainWindow.Toggle(); break; } } diff --git a/Silkstring/Silkstring.csproj b/Silkstring/Silkstring.csproj index 8bc1ab1..f06fbab 100644 --- a/Silkstring/Silkstring.csproj +++ b/Silkstring/Silkstring.csproj @@ -22,6 +22,10 @@ + + + + diff --git a/Silkstring/UI/Palette.cs b/Silkstring/UI/Palette.cs index 8879bb4..a1e90de 100644 --- a/Silkstring/UI/Palette.cs +++ b/Silkstring/UI/Palette.cs @@ -1,8 +1,34 @@ using System.Numerics; +using Silkstring.Models; namespace Silkstring.UI; public static class Palette { - public static readonly Vector4 VariableToken = new(0.4f, 0.8f, 1.0f, 1.0f); + public static Vector4 Variable; + public static Vector4 Parameter; + public static Vector4 Keyword; + public static Vector4 String; + public static Vector4 Command; + public static Vector4 Text; + public static Vector4 Error; + public static Vector4 Heading; + public static Vector4 Folder; + public static Vector4 Success; + public static Vector4 LineNumber; + + public static void Apply(ThemeColors t) + { + Variable = t.Variable; + Parameter = t.Parameter; + Keyword = t.Keyword; + String = t.String; + Command = t.Command; + Text = t.Text; + Error = t.Error; + Heading = t.Heading; + Folder = t.Folder; + Success = t.Success; + LineNumber = t.LineNumber; + } } diff --git a/Silkstring/UI/Panels/AliasEditPanel.cs b/Silkstring/UI/Panels/AliasEditPanel.cs index c8cbbc5..e8de8cc 100644 --- a/Silkstring/UI/Panels/AliasEditPanel.cs +++ b/Silkstring/UI/Panels/AliasEditPanel.cs @@ -5,6 +5,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Components; +using ImGuiColorTextEditNet; using Silkstring.Models; using Silkstring.Services; using Silkstring.Windows; @@ -19,18 +20,23 @@ public class AliasEditPanel private List? _detectedCycle; private string? _blockError; - private string _multilineBuffer = string.Empty; - private int _multilineAliasId = -1; + private readonly TextEditor _editor; + private int _editorAliasId = -1; public AliasEditPanel(Configuration configuration, MainWindow mainWindow) { + _configuration = configuration; + _editor = new TextEditor + { + SyntaxHighlighter = new SilkstringHighlighter(() => new HashSet(_configuration.UserVariables.Select(v => v.Name), StringComparer.OrdinalIgnoreCase)) + }; + _editor.Renderer.ColorizeEveryFrame = true; mainWindow.SelectionChanged += (alias, _) => { _selectedAlias = alias; if (alias != null) RefreshCycleCheck(); else { _detectedCycle = null; _blockError = null; } }; - _configuration = configuration; } public void Draw() @@ -47,7 +53,7 @@ public void Draw() ImGui.Separator(); if (_blockError != null) { - ImGui.TextColored(new Vector4(1f, 0.4f, 0.4f, 1f), _blockError); + ImGui.TextColored(Palette.Error, _blockError); ImGui.Spacing(); } DrawCommandList(alias); @@ -95,21 +101,20 @@ private void DrawCommandList(AliasEntry alias) private void DrawMultilineView(AliasEntry alias) { - SyncMultilineBuffer(alias); + if (_editorAliasId != alias.UniqueId) + { + _editor.AllText = string.Join("\n", alias.Output.Select(c => c.Command)); + _editorAliasId = alias.UniqueId; + } - if (ImGui.InputTextMultiline($"###multiline{alias.UniqueId}", ref _multilineBuffer, 5000, new Vector2(-1, ImGui.GetContentRegionAvail().Y))) + _editor.Renderer.ShowLineNumbers = _configuration.ShowLineNumbers; + SilkstringHighlighter.ApplyPalette(_editor); + + if (_editor.Render("###aliasEditor", new Vector2(-1, ImGui.GetContentRegionAvail().Y))) { - if (ImGui.IsKeyPressed(ImGuiKey.Escape)) - { - _multilineAliasId = -1; - } - - else - { - ApplyMultiline(alias, _multilineBuffer); - _configuration.MarkDirty(); - RefreshCycleCheck(); - } + ApplyMultiline(alias, _editor.AllText); + _configuration.MarkDirty(); + RefreshCycleCheck(); } } @@ -158,13 +163,6 @@ private void RefreshCycleCheck() _blockError = AliasValidator.ValidateBlocks(_selectedAlias) ?? AliasValidator.ValidateSets(_selectedAlias, defined) ?? AliasValidator.ValidateWaits(_selectedAlias); } - private void SyncMultilineBuffer(AliasEntry alias) - { - if (_multilineAliasId == alias.UniqueId) return; - _multilineBuffer = string.Join("\n", alias.Output.Select(c => c.Command)); - _multilineAliasId = alias.UniqueId; - } - private static void ApplyMultiline(AliasEntry alias, string text) { var lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); diff --git a/Silkstring/UI/Panels/AliasSelectPanel.cs b/Silkstring/UI/Panels/AliasSelectPanel.cs index 639e61d..4d40221 100644 --- a/Silkstring/UI/Panels/AliasSelectPanel.cs +++ b/Silkstring/UI/Panels/AliasSelectPanel.cs @@ -24,8 +24,6 @@ public class AliasSelectPanel private const string DragDropType = "ALIAS"; - private static readonly Vector4 FolderColor = new(0.7f, 0.5f, 1.0f, 1.0f); - public AliasSelectPanel(Configuration configuration, MainWindow mainWindow) { _configuration = configuration; @@ -86,7 +84,7 @@ private void DrawFolders() } else { - ImGui.PushStyleColor(ImGuiCol.Text, FolderColor); + ImGui.PushStyleColor(ImGuiCol.Text, Palette.Folder); open = ImGui.TreeNodeEx($"{folder.Name}###{folder.UniqueId}folder", ImGuiTreeNodeFlags.SpanAvailWidth); ImGui.PopStyleColor(); diff --git a/Silkstring/UI/SilkstringHighlighter.cs b/Silkstring/UI/SilkstringHighlighter.cs new file mode 100644 index 0000000..f4bdde4 --- /dev/null +++ b/Silkstring/UI/SilkstringHighlighter.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text.RegularExpressions; +using Dalamud.Bindings.ImGui; +using ImGuiColorTextEditNet; +using ImGuiColorTextEditNet.Syntax; +using Silkstring.Services.Conditions; + +namespace Silkstring.UI; + +public sealed class SilkstringHighlighter : ISyntaxHighlighter +{ + private const PaletteIndex Error = PaletteIndex.CharLiteral; + + private static readonly object Empty = new(); + private static readonly Regex TokenRegex = new(@"\{(\d+\.\.\d+|\.\.\d+|\d+\.\.|\w+|\*)\}", RegexOptions.Compiled); + private static readonly Regex StringRegex = new("\"[^\"]*\"?", RegexOptions.Compiled); + + private readonly Func> _definedVariables; + + public SilkstringHighlighter(Func> definedVariables) => _definedVariables = definedVariables; + + public bool AutoIndentation => false; + public int MaxLinesPerFrame => 1000; + public string? GetTooltip(string id) => null; + + public object Colorize(Span line, object? state) + { + var chars = new char[line.Length]; + for (var i = 0; i < line.Length; i++) + { + chars[i] = line[i].Char; + line[i] = new Glyph(line[i].Char, PaletteIndex.Default); + } + var text = new string(chars); + + var (kind, expression) = BlockInterpreter.Classify(text); + var exprStart = text.Length - expression.Length; + + switch (kind) + { + case BlockKind.If: + Paint(line, 0, exprStart, PaletteIndex.Keyword); + if (TryParseCondition(expression)) PaintContent(line, text, exprStart); + else Paint(line, exprStart, text.Length, Error); + break; + + case BlockKind.Else: + case BlockKind.EndIf: + Paint(line, 0, exprStart, PaletteIndex.Keyword); + break; + + case BlockKind.Set: + Paint(line, 0, exprStart, PaletteIndex.Keyword); + var (name, _) = BlockInterpreter.ParseSet(expression); + if (name.Length == 0 || !_definedVariables().Contains(name)) + Paint(line, exprStart, exprStart + Math.Max(name.Length, 1), Error); + PaintContent(line, text, exprStart + name.Length); + break; + + case BlockKind.Wait: + Paint(line, 0, exprStart, PaletteIndex.Keyword); + if (!expression.Contains('{') && !BlockInterpreter.TryParseDuration(expression.Trim(), out _)) + Paint(line, exprStart, text.Length, Error); + else PaintContent(line, text, exprStart); + break; + + default: + if (text.Length > 1 && text[0] == ':' && char.IsLetter(text[1])) + { + var kw = text.IndexOf(' '); + if (kw < 0) kw = text.Length; + Paint(line, 0, kw, Error); + PaintContent(line, text, kw); + } + else + { + if (text.StartsWith('/')) + { + var end = text.IndexOf(' '); + Paint(line, 0, end < 0 ? text.Length : end, PaletteIndex.KnownIdentifier); + } + PaintContent(line, text, 0); + } + break; + } + + return Empty; + } + + private static bool TryParseCondition(string expression) + { + try { new Parser(Tokenizer.Tokenize(expression)).Parse(); return true; } + catch (ConditionException) { return false; } + } + + private static void PaintContent(Span line, string text, int from) + { + if (from >= text.Length) return; + foreach (Match m in StringRegex.Matches(text, from)) + Paint(line, m.Index, m.Index + m.Length, PaletteIndex.String); + PaintTokens(line, text, from); + PaintMalformedBraces(line, from); + } + + private static void PaintMalformedBraces(Span line, int from) + { + for (var i = from; i < line.Length; i++) + { + if (line[i].Char != '{' || line[i].ColorIndex != PaletteIndex.Default) continue; + var end = i + 1; + while (end < line.Length && line[end].Char != '}') end++; + if (end < line.Length) end++; + Paint(line, i, end, Error); + i = end - 1; + } + } + + private static void PaintTokens(Span line, string text, int from) + { + if (from >= text.Length) return; + foreach (Match m in TokenRegex.Matches(text, from)) + { + var color = IsParameter(m.Groups[1].Value) ? PaletteIndex.Number : PaletteIndex.Identifier; + Paint(line, m.Index, m.Index + m.Length, color); + } + } + + private static void Paint(Span line, int start, int end, PaletteIndex color) + { + for (var i = start; i < end && i < line.Length; i++) + line[i] = new Glyph(line[i].Char, color); + } + + private static bool IsParameter(string token) => token == "*" || token.Contains("..") || token.All(char.IsDigit); + + public static void ApplyPalette(TextEditor editor) + { + var r = editor.Renderer; + r.SetColor(PaletteIndex.Default, U32(Palette.Text)); + r.SetColor(PaletteIndex.Keyword, U32(Palette.Keyword)); + r.SetColor(PaletteIndex.KnownIdentifier, U32(Palette.Command)); + r.SetColor(PaletteIndex.Identifier, U32(Palette.Variable)); + r.SetColor(PaletteIndex.Number, U32(Palette.Parameter)); + r.SetColor(PaletteIndex.String, U32(Palette.String)); + r.SetColor(PaletteIndex.CharLiteral, U32(Palette.Error)); + r.SetColor(PaletteIndex.LineNumber, U32(Palette.LineNumber)); + } + + private static uint U32(Vector4 c) => ImGui.ColorConvertFloat4ToU32(c); +} diff --git a/Silkstring/Windows/ChangelogWindow.cs b/Silkstring/Windows/ChangelogWindow.cs index 5a8daf0..613f48d 100644 --- a/Silkstring/Windows/ChangelogWindow.cs +++ b/Silkstring/Windows/ChangelogWindow.cs @@ -5,13 +5,12 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface.Windowing; using Silkstring.Services; +using Silkstring.UI; namespace Silkstring.Windows; public class ChangelogWindow : Window, IDisposable { - private static readonly Vector4 HeadingColor = new(0.7f, 0.5f, 1.0f, 1.0f); - private readonly IReadOnlyList _sections; private readonly string[] _versions; private int _selected; @@ -63,7 +62,7 @@ private static void DrawBody(string body) else if (line.StartsWith("### ")) { ImGui.Spacing(); - ImGui.TextColored(HeadingColor, line[4..]); + ImGui.TextColored(Palette.Heading, line[4..]); ImGui.Separator(); } else if (line.StartsWith(" - ")) diff --git a/Silkstring/Windows/ConfigWindow.cs b/Silkstring/Windows/ConfigWindow.cs index 427c628..8eb9578 100644 --- a/Silkstring/Windows/ConfigWindow.cs +++ b/Silkstring/Windows/ConfigWindow.cs @@ -1,13 +1,23 @@ using System; using Dalamud.Interface.Windowing; using System.Numerics; +using System.Reflection; +using System.Text.RegularExpressions; using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Silkstring.Models; +using Silkstring.UI; namespace Silkstring.Windows; + public class ConfigWindow : Window, IDisposable { private readonly Configuration configuration; + private static readonly FieldInfo[] ThemeFields = typeof(ThemeColors).GetFields(); + private static readonly ThemeColors Defaults = new(); + private static string Prettify(string name) => Regex.Replace(name, "(\\B[A-Z])", " $1"); public ConfigWindow(Plugin plugin) : base("Silkstring Settings###Config") { @@ -50,6 +60,45 @@ public override void Draw() configuration.Save(); } + var showLineNumbers = configuration.ShowLineNumbers; + if (ImGui.Checkbox("Line numbers in the editor", ref showLineNumbers)) + { + configuration.ShowLineNumbers = showLineNumbers; + configuration.Save(); + } + + if (ImGui.CollapsingHeader("Colors")) + { + void Sync() { Palette.Apply(configuration.Theme); configuration.MarkDirty(); } + + foreach (var field in ThemeFields) + { + if (ImGuiComponents.IconButton($"reset{field.Name}", FontAwesomeIcon.Undo)) + { + field.SetValue(configuration.Theme, field.GetValue(Defaults)); + Sync(); + } + ImGuiUtil.Tooltip("Reset to default"); + ImGui.SameLine(); + + var value = (Vector4)field.GetValue(configuration.Theme)!; + if (ImGui.ColorEdit4($"{Prettify(field.Name)}##{field.Name}", ref value, ImGuiColorEditFlags.NoInputs)) + { + field.SetValue(configuration.Theme, value); + Sync(); + } + } + + if (ImGui.Button("Reset colors")) + { + configuration.Theme = new ThemeColors(); + Palette.Apply(configuration.Theme); + configuration.Save(); + + } + + } + var availableHeight = ImGui.GetContentRegionAvail().Y; var buttonHeight = ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y + ImGui.GetStyle().WindowPadding.Y * 2 + 4; ImGui.SetCursorPosY(ImGui.GetCursorPosY() + availableHeight - buttonHeight); diff --git a/Silkstring/Windows/HelpWindow.cs b/Silkstring/Windows/HelpWindow.cs index d30fb0b..d4f2085 100644 --- a/Silkstring/Windows/HelpWindow.cs +++ b/Silkstring/Windows/HelpWindow.cs @@ -21,8 +21,7 @@ private enum Tab } private Tab _selectedTab = Tab.Commands; - - private static readonly Vector4 HeadingColor = new(0.7f, 0.5f, 1.0f, 1.0f); + private string _testerInput = string.Empty; private readonly CommandResolver _resolver; @@ -78,7 +77,7 @@ public override void Draw() private void DrawCommandTester() { - ImGui.TextColored(HeadingColor, "Command Tester"); + ImGui.TextColored(Palette.Heading, "Command Tester"); ImGui.Spacing(); ImGui.SetNextItemWidth(-1); ImGui.InputTextWithHint("###testerInput", "Enter a command e.g. /say Hello {character}!", ref _testerInput, 200); @@ -94,19 +93,19 @@ private void DrawCommandTester() else if (resolved == _testerInput) ImGui.TextDisabled(resolved); else - ImGui.TextColored(new Vector4(0.4f, 1f, 0.4f, 1f), resolved); + ImGui.TextColored(Palette.Success, resolved); } private void DrawCommandsHelp() { - ImGui.TextColored(HeadingColor, "What is an Alias?"); + ImGui.TextColored(Palette.Heading, "What is an Alias?"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("An alias lets you create a custom chat command that fires one or more FFXIV commands in sequence. " + "Type your trigger in the chat box just like any other command and Silkstring will intercept it and execute your defined command list."); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Triggers"); + ImGui.TextColored(Palette.Heading, "Triggers"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("A trigger is the command you type in chat to activate the alias. Triggers must start with a forward slash when used in chat, " + @@ -115,7 +114,7 @@ private void DrawCommandsHelp() ImGui.BulletText("Triggers cannot contain spaces."); ImGui.BulletText("Triggers should not conflict with built-in FFXIV or Dalamud commands."); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Multiple Triggers"); + ImGui.TextColored(Palette.Heading, "Multiple Triggers"); ImGui.Spacing(); ImGui.TextWrapped("You can assign multiple triggers to a single alias by separating them with a pipe character:"); ImGui.Spacing(); @@ -126,7 +125,7 @@ private void DrawCommandsHelp() ImGui.TextWrapped("Any of these typed in chat will fire the same alias."); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Commands"); + ImGui.TextColored(Palette.Heading, "Commands"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("Commands are the actions Silkstring runs when your alias is triggered. Each command must start with a slash, exactly as you would type it into the chat box."); @@ -142,7 +141,7 @@ private void DrawCommandsHelp() ImGui.Unindent(); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Chat Messages"); + ImGui.TextColored(Palette.Heading, "Chat Messages"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("Any line that does not start with a slash is sent as a chat message to whatever channel you currently have active."); @@ -156,7 +155,7 @@ private void DrawCommandsHelp() ImGui.Unindent(); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Variables"); + ImGui.TextColored(Palette.Heading, "Variables"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("Variables let you insert dynamic values into your commands at execution time using curly brace syntax:"); @@ -169,19 +168,19 @@ private void DrawCommandsHelp() "See the Variables tab for a full list of supported variables."); ImGui.TextWrapped("You can also define your own variables, see the Variables tab."); - ImGui.TextColored(HeadingColor, "Parameters"); + ImGui.TextColored(Palette.Heading, "Parameters"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("Aliases can take arguments typed after the trigger, inserted with numbered braces like {0}. See the Parameters tab for the full syntax."); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Conditionals"); + ImGui.TextColored(Palette.Heading, "Conditionals"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("Run commands only when a condition is true with :if / :else / :endif blocks, and pause with :wait. See the Conditionals tab."); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Macros"); + ImGui.TextColored(Palette.Heading, "Macros"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("Silkstring aliases are intentionally disabled when called from within FFXIV macros. " + @@ -189,7 +188,7 @@ private void DrawCommandsHelp() "Aliases must be typed directly into the chat box to function."); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Cycles & Recursion"); + ImGui.TextColored(Palette.Heading, "Cycles & Recursion"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("A cycle occurs when an alias triggers itself either directly or through a chain of other aliases:"); @@ -207,7 +206,7 @@ private void DrawCommandsHelp() private void DrawVariablesHelp() { - ImGui.TextColored(HeadingColor, "What are Variables?"); + ImGui.TextColored(Palette.Heading, "What are Variables?"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("Variables let you insert dynamic values into your commands at execution time. " + @@ -223,7 +222,7 @@ private void DrawVariablesHelp() "it is left as-is in the command string rather than being replaced with an empty value."); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Your Own Variables"); + ImGui.TextColored(Palette.Heading, "Your Own Variables"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("Besides the built-in variables below, you can define your own. Open the Variables window with /silkstring variables, give it a name and a value, then use it anywhere with {name}."); @@ -238,7 +237,7 @@ private void DrawVariablesHelp() ImGui.TextWrapped("A :set only works on a variable you have already created. The alias editor warns you if it names one that does not exist."); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Available Variables"); + ImGui.TextColored(Palette.Heading, "Available Variables"); ImGui.Separator(); ImGui.Spacing(); @@ -250,7 +249,7 @@ private void DrawVariablesHelp() private void DrawVariableCategory(string category, IEnumerable variables) { - ImGui.TextColored(HeadingColor, category); + ImGui.TextColored(Palette.Heading, category); if (ImGui.BeginTable($"###varTable_{category}", 3, ImGuiTableFlags.BordersInnerH | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter)) { ImGui.TableSetupColumn("Variable", ImGuiTableColumnFlags.WidthFixed, 120); @@ -267,7 +266,7 @@ private void DrawVariableRow(string variable, string description, string current { ImGui.TableNextRow(); ImGui.TableNextColumn(); - ImGui.TextColored(Palette.VariableToken, variable); + ImGui.TextColored(Palette.Variable, variable); ImGui.TableNextColumn(); ImGui.TextWrapped(description); @@ -277,7 +276,7 @@ private void DrawVariableRow(string variable, string description, string current private void DrawParametersHelp() { - ImGui.TextColored(HeadingColor, "What are Parameters?"); + ImGui.TextColored(Palette.Heading, "What are Parameters?"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("Parameters let an alias take arguments. Anything you type after the trigger becomes a numbered argument, starting at zero, that you insert into command lines with curly braces."); @@ -293,7 +292,7 @@ private void DrawParametersHelp() ImGui.TextWrapped("If you reference an argument that was not supplied, it is left as written (for example {3} stays {3})."); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Parameter Tokens"); + ImGui.TextColored(Palette.Heading, "Parameter Tokens"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("Ranges work like C# ranges: the end is exclusive."); @@ -326,7 +325,7 @@ private void DrawParameterRow(string token, string meaning) private void DrawConditionalsHelp() { - ImGui.TextColored(HeadingColor, "What are Conditionals?"); + ImGui.TextColored(Palette.Heading, "What are Conditionals?"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("Conditionals let an alias run some commands only when a condition is true. They use block syntax: everything between :if and :endif runs only when the condition holds."); @@ -345,7 +344,7 @@ private void DrawConditionalsHelp() ImGui.BulletText("The :if, :else, and :endif lines are never sent to chat."); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Conditions"); + ImGui.TextColored(Palette.Heading, "Conditions"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("A condition either compares two values, or checks a true/false variable on its own. Comparisons can be combined with and (&&) and or (||)."); @@ -365,7 +364,7 @@ private void DrawConditionalsHelp() ImGui.Unindent(); ImGui.Spacing(); - ImGui.TextColored(HeadingColor, "Waits"); + ImGui.TextColored(Palette.Heading, "Waits"); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("Pause between lines with :wait, followed by a number of seconds. Decimals work, and the duration can come from a variable or parameter. Waits are capped at 60 seconds, and a :wait inside a condition only pauses when that branch runs. Like the other control lines, it is never sent to chat."); diff --git a/Silkstring/Windows/VariablesWindow.cs b/Silkstring/Windows/VariablesWindow.cs index b8c18e3..a59da56 100644 --- a/Silkstring/Windows/VariablesWindow.cs +++ b/Silkstring/Windows/VariablesWindow.cs @@ -58,7 +58,7 @@ private void DrawAddRow() } } ImGuiUtil.Tooltip("Add Variable"); - if (_addError != null) ImGui.TextColored(new Vector4(1f, 0.4f, 0.4f, 1f), _addError); + if (_addError != null) ImGui.TextColored(Palette.Error, _addError); } private void DrawList() diff --git a/Silkstring/packages.lock.json b/Silkstring/packages.lock.json index b4c31dc..5c4cf0d 100644 --- a/Silkstring/packages.lock.json +++ b/Silkstring/packages.lock.json @@ -19,6 +19,9 @@ "requested": "[3.2.0.18, )", "resolved": "3.2.0.18", "contentHash": "esPIjfuveuw05c4PI7gqpRcifaUaUZ9sf7JnN90CBM1Ah2emxTBrKQl6A+ghHobwQOOe+yflN03TJnv32NEkVw==" + }, + "dalamudtextedit": { + "type": "Project" } } } diff --git a/extern/DalamudTextEdit b/extern/DalamudTextEdit new file mode 160000 index 0000000..f341119 --- /dev/null +++ b/extern/DalamudTextEdit @@ -0,0 +1 @@ +Subproject commit f341119fc9c0c4fe2e42e34b96424c236559da02