diff --git a/Silkstring/Configuration.cs b/Silkstring/Configuration.cs index 01bdebb..c6e3a61 100644 --- a/Silkstring/Configuration.cs +++ b/Silkstring/Configuration.cs @@ -8,14 +8,40 @@ namespace Silkstring; [Serializable] public class Configuration : IPluginConfiguration { + [NonSerialized] + private bool _isDirty = false; + [NonSerialized] + private DateTime _lastDirty = DateTime.MinValue; + + private int _commandDelay = 100; + public int CommandDelay + { + get => _commandDelay; + set => _commandDelay = Math.Clamp(value, 0, 1000); + } + public int Version { get; set; } = 1; public List Folders = new(); public List Aliases = new(); - public int CommandDelay { get; set; } = 100; - public bool MultilineCommands { get; set; } = false; + public bool MultilineCommands { get; set; } public void Save() { Plugin.PluginInterface.SavePluginConfig(this); } + + public void MarkDirty() + { + _isDirty = true; + _lastDirty = DateTime.UtcNow; + } + + public void TrySave(TimeSpan debounce) + { + if (_isDirty && DateTime.UtcNow - _lastDirty > debounce) + { + Save(); + _isDirty = false; + } + } } diff --git a/Silkstring/Models/AliasEntry.cs b/Silkstring/Models/AliasEntry.cs index a4abae8..c324689 100644 --- a/Silkstring/Models/AliasEntry.cs +++ b/Silkstring/Models/AliasEntry.cs @@ -1,12 +1,21 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; namespace Silkstring.Models; public class AliasEntry { - public static readonly string[] Blacklist = ["silkstring", "xlplugins", "xlsettings", "xldclose", "xldev"]; + private static int _nextId = 0; + public static readonly JsonSerializerOptions SerializerOptions = new() { IncludeFields = true }; + + public static readonly HashSet Blacklist = new(StringComparer.OrdinalIgnoreCase) + { + "silkstring", "xlplugins", "xlsettings", "xldclose", "xldev" + }; public string DisplayName = string.Empty; public bool Enabled = true; @@ -17,21 +26,26 @@ public class AliasEntry public bool Delete; [NonSerialized] + [JsonIgnore] public int UniqueId; + public AliasEntry() + { + UniqueId = Interlocked.Increment(ref _nextId); + } + public bool IsValid() { - var names = Name.Split('|', StringSplitOptions.TrimEntries); + var names = Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (names.Length == 0) return false; foreach (var name in names) { - if (string.IsNullOrWhiteSpace(name)) return false; if (Blacklist.Contains(name)) return false; if (name.Contains(' ')) return false; if (name.Contains('/')) return false; } - if (Output.Count == 0) return false; + if (Output.Count == 0) return false; return !Output.Any(command => string.IsNullOrWhiteSpace(command.Command)); } diff --git a/Silkstring/Models/AliasFolder.cs b/Silkstring/Models/AliasFolder.cs index d7dd9be..6f3a12a 100644 --- a/Silkstring/Models/AliasFolder.cs +++ b/Silkstring/Models/AliasFolder.cs @@ -1,9 +1,22 @@ +using System; using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading; namespace Silkstring.Models; public class AliasFolder { + private static int _nextId = 0; public string Name { get; set; } = string.Empty; public List Aliases { get; set; } = new(); + + [NonSerialized] + [JsonIgnore] + public int UniqueId; + + public AliasFolder() + { + UniqueId = Interlocked.Increment(ref _nextId); + } } diff --git a/Silkstring/Models/CommandEntry.cs b/Silkstring/Models/CommandEntry.cs index e29b3be..8025bea 100644 --- a/Silkstring/Models/CommandEntry.cs +++ b/Silkstring/Models/CommandEntry.cs @@ -1,17 +1,27 @@ using System; +using System.Text.Json.Serialization; +using System.Threading; namespace Silkstring.Models; public class CommandEntry { + private static int _nextId = 0; + public string Command = string.Empty; [NonSerialized] public bool Delete; [NonSerialized] + [JsonIgnore] public int UniqueId; + public CommandEntry() + { + UniqueId = Interlocked.Increment(ref _nextId); + } + public CommandEntry Clone() { return new CommandEntry() diff --git a/Silkstring/Plugin.cs b/Silkstring/Plugin.cs index 3c5c315..6d038fc 100644 --- a/Silkstring/Plugin.cs +++ b/Silkstring/Plugin.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading; using Dalamud.Game.Command; using Dalamud.Hooking; using Dalamud.IoC; @@ -31,17 +32,21 @@ public sealed unsafe class Plugin : IDalamudPlugin [PluginService] internal static IFramework Framework { get; private set; } = null!; + [PluginService] + internal static INotificationManager NotificationManager { get; private set; } = null!; + private const string CommandName = "/silkstring"; public Configuration Configuration { get; init; } public readonly WindowSystem WindowSystem = new("Silkstring"); - private EditWindow EditWindow { get; init; } private ConfigWindow ConfigWindow { get; init; } private MainWindow MainWindow { get; init; } private Hook processChatInputHook; + private readonly CancellationTokenSource _cts = new(); + public Plugin() { Configuration = PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); @@ -53,13 +58,11 @@ public Plugin() ProcessChatInputDetour); processChatInputHook.Enable(); - EditWindow = new EditWindow(this); ConfigWindow = new ConfigWindow(this); MainWindow = new MainWindow(this, ToggleConfigUi); WindowSystem.AddWindow(MainWindow); WindowSystem.AddWindow(ConfigWindow); - WindowSystem.AddWindow(EditWindow); CommandManager.AddHandler(CommandName, new CommandInfo(OnCommand) { @@ -69,15 +72,20 @@ public Plugin() PluginInterface.UiBuilder.Draw += WindowSystem.Draw; PluginInterface.UiBuilder.OpenConfigUi += ToggleConfigUi; PluginInterface.UiBuilder.OpenMainUi += ToggleMainUi; + Framework.Update += OnFrameworkUpdate; } public void Dispose() { ECommonsMain.Dispose(); + _cts.Cancel(); + _cts.Dispose(); + PluginInterface.UiBuilder.Draw -= WindowSystem.Draw; PluginInterface.UiBuilder.OpenConfigUi -= ToggleConfigUi; PluginInterface.UiBuilder.OpenMainUi -= ToggleMainUi; + Framework.Update -= OnFrameworkUpdate; processChatInputHook?.Disable(); processChatInputHook?.Dispose(); @@ -86,7 +94,6 @@ public void Dispose() MainWindow.Dispose(); ConfigWindow.Dispose(); - EditWindow.Dispose(); CommandManager.RemoveHandler(CommandName); } @@ -96,6 +103,11 @@ private void OnCommand(string command, string args) MainWindow.Toggle(); } + private void OnFrameworkUpdate(IFramework framework) + { + Configuration.TrySave(TimeSpan.FromMilliseconds(500)); + } + public void ToggleConfigUi() => ConfigWindow.Toggle(); public void ToggleMainUi() => MainWindow.Toggle(); @@ -107,29 +119,29 @@ private void ProcessChatInputDetour(ShellCommandModule* shellCommandModule, Utf8 if (inputString.StartsWith('/')) { var splitString = inputString.Split(' '); - if (splitString.Length > 0) + var commandName = splitString[0][1..]; + var alias = Configuration.Aliases.Concat( + Configuration.Folders.SelectMany(g => g.Aliases)).FirstOrDefault(a => + a.Enabled && + a.IsValid() && + a.Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase))); + if (alias != null) { - var commandName = splitString[0][1..]; - var alias = Configuration.Aliases.Concat( - Configuration.Folders.SelectMany(g => g.Aliases)).FirstOrDefault(a => - a.Enabled && - a.IsValid() && - a.Name.Split('|', StringSplitOptions.TrimEntries).Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase))); - if (alias != null) - { - var commands = alias.Output - .Where(c => !string.IsNullOrWhiteSpace(c.Command)) - .Select(c => "/" + c.Command.TrimStart('/')) - .ToList(); - _ = CommandHandler.ExecuteAsync(commands, Configuration.CommandDelay); - return; - } + var commands = alias.Output + .Where(c => !string.IsNullOrWhiteSpace(c.Command)) + .Select(c => "/" + c.Strip()) + .ToList(); + + + CommandHandler.ExecuteAsync(commands, Configuration.CommandDelay, _cts.Token) + .ContinueWith(t => Log.Error(t.Exception, "Command execution failed"), TaskContinuationOptions.OnlyOnFaulted); + return; } } } catch (Exception ex) { - Log.Error(ex.Message); + Log.Error(ex, "An error occurred while processing command"); } processChatInputHook.Original(shellCommandModule, message, uiModule); } diff --git a/Silkstring/Services/CommandHandler.cs b/Silkstring/Services/CommandHandler.cs index 21b4e49..d9d9618 100644 --- a/Silkstring/Services/CommandHandler.cs +++ b/Silkstring/Services/CommandHandler.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using ECommons.Automation; @@ -6,15 +7,13 @@ namespace Silkstring.Services; public static class CommandHandler { - public static async Task ExecuteAsync(List commands, int delayMs = 100) + public static async Task ExecuteAsync(IReadOnlyList commands, int delayMs = 100, CancellationToken cancellationToken = default) { - foreach (var command in commands) + for (var i = 0; i < commands.Count; i++) { - await Plugin.Framework.RunOnFrameworkThread(() => - { - Chat.SendMessage(command); - }); - await Task.Delay(delayMs); + var cmd = commands[i]; + await Plugin.Framework.RunOnFrameworkThread(() => Chat.SendMessage(cmd)); + if (i < commands.Count - 1) await Task.Delay(delayMs, cancellationToken); } } } diff --git a/Silkstring/UI/AliasEditPanel.cs b/Silkstring/UI/AliasEditPanel.cs index 4965dd1..6cb2b61 100644 --- a/Silkstring/UI/AliasEditPanel.cs +++ b/Silkstring/UI/AliasEditPanel.cs @@ -4,7 +4,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Components; -using ECommons.ImGuiMethods; using Silkstring.Models; using Silkstring.Windows; @@ -13,20 +12,21 @@ namespace Silkstring.Ui; public class AliasEditPanel { private readonly Configuration _configuration; - private readonly MainWindow _mainWindow; + + private AliasEntry? _selectedAlias; private string _multilineBuffer = string.Empty; private int _multilineAliasId = -1; - public AliasEditPanel(Configuration configuration, MainWindow mainWindow) + public AliasEditPanel(Configuration configuration, MainWindow mainWindow) { + mainWindow.SelectionChanged += (alias, _) => _selectedAlias = alias; _configuration = configuration; - _mainWindow = mainWindow; } public void Draw() { - var alias = _mainWindow.SelectedAlias; + var alias = _selectedAlias; if (alias == null) { @@ -51,18 +51,20 @@ private void DrawEmptyState() private void DrawAliasHeader(AliasEntry alias) { + var tooltipText = alias.Enabled ? "Disable this alias" : "Enable this alias"; if (ImGui.Checkbox($"###enabled{alias.UniqueId}", ref alias.Enabled)) _configuration.Save(); + if (ImGui.IsItemHovered()) ImGui.SetTooltip(tooltipText); ImGui.SameLine(); ImGui.SetNextItemWidth(-1); - if(ImGui.InputTextWithHint($"###aliasName{alias.UniqueId}", "activation command", ref alias.Name, 100)) _configuration.Save(); - if(ImGui.IsItemHovered()) ImGui.SetTooltip("Seperate multiple aliases with | e.g. mew|meow|mreow"); + if (ImGui.InputTextWithHint($"###aliasName{alias.UniqueId}", "activation command", ref alias.Name, 100)) _configuration.MarkDirty(); + if (ImGui.IsItemHovered()) ImGui.SetTooltip("Separate multiple aliases with | e.g. mew|meow|mreow"); } private void DrawCommandList(AliasEntry alias) { if (_configuration.MultilineCommands) { - DrawMultlineView(alias); + DrawMultilineView(alias); } else { @@ -70,7 +72,7 @@ private void DrawCommandList(AliasEntry alias) } } - private void DrawMultlineView(AliasEntry alias) + private void DrawMultilineView(AliasEntry alias) { if (_multilineAliasId != alias.UniqueId) { @@ -80,33 +82,30 @@ private void DrawMultlineView(AliasEntry alias) if (ImGui.InputTextMultiline($"###multiline{alias.UniqueId}", ref _multilineBuffer, 5000, new Vector2(-1, ImGui.GetContentRegionAvail().Y))) { - var lines = _multilineBuffer.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(c => c.Trim()).ToList(); + var lines = _multilineBuffer.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); for (var i = 0; i < lines.Count; i++) { if (i < alias.Output.Count) alias.Output[i].Command = lines[i]; - else alias.Output.Add(new CommandEntry { Command = lines[i] }); + else alias.Output.Add(new CommandEntry { Command = lines[i] }); } if (alias.Output.Count > lines.Count) alias.Output.RemoveRange(lines.Count, alias.Output.Count - lines.Count); - _configuration.Save(); + _configuration.MarkDirty(); } } private void DrawListView(AliasEntry alias) { ImGui.Text("Commands:"); - if (ImGui.BeginChild("###commandList")) + ImGui.BeginChild("###commandList"); + foreach (var command in alias.Output) { - foreach (var command in alias.Output) - { - if (command.UniqueId == 0) command.UniqueId = alias.Output.Count == 0 ? 1 : alias.Output.Max(c => c.UniqueId) + 1; - DrawCommandRow(command); - } - - alias.Output.RemoveAll(c => c.Delete); + DrawCommandRow(command); } + alias.Output.RemoveAll(c => c.Delete); + if (ImGuiComponents.IconButton((int)FontAwesomeIcon.Plus, FontAwesomeIcon.Plus)) { alias.Output.Add(new CommandEntry()); @@ -119,7 +118,7 @@ private void DrawListView(AliasEntry alias) private void DrawCommandRow(CommandEntry command) { ImGui.SetNextItemWidth(-60); - if (ImGui.InputText($"###cmd{command.UniqueId}", ref command.Command, 200)) _configuration.Save(); + if (ImGui.InputText($"###cmd{command.UniqueId}", ref command.Command, 200)) _configuration.MarkDirty(); ImGui.SameLine(); var canDelete = ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl; diff --git a/Silkstring/UI/AliasSelectPanel.cs b/Silkstring/UI/SelectPanel/AliasSelectPanel.cs similarity index 55% rename from Silkstring/UI/AliasSelectPanel.cs rename to Silkstring/UI/SelectPanel/AliasSelectPanel.cs index b9e594a..737fac9 100644 --- a/Silkstring/UI/AliasSelectPanel.cs +++ b/Silkstring/UI/SelectPanel/AliasSelectPanel.cs @@ -3,8 +3,6 @@ using System.Numerics; using System.Text.Json; using Dalamud.Bindings.ImGui; -using Dalamud.Interface; -using Dalamud.Interface.Components; using Silkstring.Models; using Silkstring.Windows; @@ -14,10 +12,9 @@ public class AliasSelectPanel { private readonly Configuration _configuration; private readonly MainWindow _mainWindow; - private readonly Action _openSettings; + private readonly SelectPanelFooter _footer; private string _filter = string.Empty; - private bool MatchesFilter(AliasEntry alias) => string.IsNullOrWhiteSpace(_filter) || alias.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase); private AliasEntry? _draggedAlias; private AliasFolder? _draggedFromFolder; @@ -35,11 +32,11 @@ public class AliasSelectPanel private static readonly Vector4 FolderColor = new(0.7f, 0.5f, 1.0f, 1.0f); - public AliasSelectPanel(Configuration configuration, MainWindow mainWindow, Action openSettings) + public AliasSelectPanel(Configuration configuration, MainWindow mainWindow) { _configuration = configuration; _mainWindow = mainWindow; - _openSettings = openSettings; + _footer = new SelectPanelFooter(configuration, mainWindow, BeginRenameAlias, BeginRenameFolder); } public void Draw() @@ -50,17 +47,15 @@ public void Draw() var frameHeight = ImGui.GetFrameHeight(); var listHeight = ImGui.GetContentRegionAvail().Y - frameHeight - ImGui.GetStyle().ItemSpacing.Y; - if (ImGui.BeginChild("###aliasList", new Vector2(0, listHeight))) - { - DrawFolders(); - DrawUnsorted(); - } + ImGui.BeginChild("###aliasList", new Vector2(0, listHeight)); + DrawFolders(); + DrawUnsorted(); ImGui.EndChild(); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); - - if (ImGui.BeginChild("###footer", new Vector2(-1, frameHeight))) DrawFooter(); + ImGui.BeginChild("###footer", new Vector2(-1, frameHeight)); + _footer.Draw(); ImGui.EndChild(); ImGui.PopStyleVar(2); } @@ -69,7 +64,7 @@ private void DrawFolders() { foreach (var folder in _configuration.Folders.ToList()) { - var filtered = folder.Aliases.Where(a => MatchesFilter(a)) + var filtered = folder.Aliases.Where(MatchesFilter) .OrderBy(a => string.IsNullOrWhiteSpace(a.DisplayName) ? a.Name : a.DisplayName) .ToList(); @@ -87,7 +82,7 @@ private void DrawFolders() } ImGui.SetNextItemWidth(-1); - ImGui.InputText($"###rename{folder.GetHashCode()}", ref _renameBuffer, 100); + ImGui.InputText($"###rename{folder.UniqueId}", ref _renameBuffer, 100); if (ImGui.IsItemDeactivated()) { @@ -107,20 +102,14 @@ private void DrawFolders() else { ImGui.PushStyleColor(ImGuiCol.Text, FolderColor); - open = ImGui.TreeNodeEx($"{folder.Name}###{folder.GetHashCode()}folder", + open = ImGui.TreeNodeEx($"{folder.Name}###{folder.UniqueId}folder", ImGuiTreeNodeFlags.SpanAvailWidth); ImGui.PopStyleColor(); needsTreePop = open; - if (ImGui.BeginPopupContextItem($"###folderContext{folder.GetHashCode()}")) + if (ImGui.BeginPopupContextItem($"###folderContext{folder.UniqueId}")) { - if (ImGui.MenuItem("Rename")) - { - _renamingFolder = folder; - _renameBuffer = folder.Name; - _preRenameName = folder.Name; - _focusRename = true; - } + if (ImGui.MenuItem("Rename")) BeginRenameFolder(folder); if (ImGui.MenuItem("Delete")) { @@ -159,7 +148,7 @@ private void DrawFolders() private void DrawUnsorted() { - var filtered = _configuration.Aliases.Where(a => MatchesFilter(a)).OrderBy(a => string.IsNullOrWhiteSpace(a.DisplayName) ? a.Name : a.DisplayName).ToList(); + var filtered = _configuration.Aliases.Where(MatchesFilter).OrderBy(a => string.IsNullOrWhiteSpace(a.DisplayName) ? a.Name : a.DisplayName).ToList(); foreach (var alias in filtered) { @@ -181,12 +170,6 @@ private void DrawUnsorted() private void DrawAliasRow(AliasEntry alias, AliasFolder? owningFolder) { - if (alias.UniqueId == 0) - { - var allAliases = _configuration.Aliases.Concat(_configuration.Folders.SelectMany(f => f.Aliases)); - alias.UniqueId = allAliases.Any() ? allAliases.Max(a => a.UniqueId) + 1 : 1; - } - if (_renamingAlias == alias) { if (_focusRenameAlias) @@ -213,17 +196,14 @@ private void DrawAliasRow(AliasEntry alias, AliasFolder? owningFolder) var isSelected = _mainWindow.SelectedAlias == alias; if (ImGui.Selectable(label, isSelected)) { - _mainWindow.SelectedAlias = alias; - _mainWindow.SelectedFolder = owningFolder; + _mainWindow.SetSelection(alias, owningFolder); } if (ImGui.BeginPopupContextItem($"###aliasContext{alias.UniqueId}")) { if (ImGui.MenuItem("Rename")) { - _renamingAlias = alias; - _renameAliasBuffer = alias.DisplayName; - _focusRenameAlias = true; + BeginRenameAlias(alias); } if (!string.IsNullOrWhiteSpace(alias.DisplayName) && ImGui.MenuItem("Clear Display Name")) @@ -234,7 +214,7 @@ private void DrawAliasRow(AliasEntry alias, AliasFolder? owningFolder) if (ImGui.MenuItem("Export to Clipboard")) { - var json = JsonSerializer.Serialize(alias, new JsonSerializerOptions { IncludeFields = true }); + var json = JsonSerializer.Serialize(alias, AliasEntry.SerializerOptions); ImGui.SetClipboardText(json); } @@ -266,107 +246,25 @@ private void MoveAlias(AliasEntry alias, AliasFolder? from, AliasFolder? toFolde _draggedFromFolder = null; } - private void DrawFooter() + private void BeginRenameAlias(AliasEntry alias) { - var available = ImGui.GetContentRegionAvail(); - var buttonCount = 5; - var buttonSize = new Vector2(MathF.Floor(available.X / buttonCount), available.Y); - var canDelete = ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl; - - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - - if (DrawIconButton(FontAwesomeIcon.Plus, buttonSize, "New Alias")) - { - var newAlias = new AliasEntry(); - _configuration.Aliases.Add(newAlias); - _configuration.Save(); - _mainWindow.SelectedAlias = newAlias; - _mainWindow.SelectedFolder = null; - _renamingAlias = newAlias; - _renameAliasBuffer = string.Empty; - _focusRenameAlias = true; - } - - ImGui.SameLine(); - if (DrawIconButton(FontAwesomeIcon.FileImport, buttonSize, "Import from Clipboard")) - { - try - { - var json = ImGui.GetClipboardText(); - var imported = JsonSerializer.Deserialize(json, new JsonSerializerOptions { IncludeFields = true }); - if (imported != null) - { - imported.UniqueId = 0; - _configuration.Aliases.Add(imported); - _configuration.Save(); - _mainWindow.SelectedAlias = imported; - _renamingAlias = imported; - _renameAliasBuffer = imported.DisplayName; - _focusRenameAlias = true; - } - } - catch {} - } - - ImGui.SameLine(); - if(DrawIconButton(FontAwesomeIcon.Clone, buttonSize, "Clone Alias", disabled: _mainWindow.SelectedAlias == null)) - { - var source = _mainWindow.SelectedAlias!; - var cloned = source.Clone(); - cloned.UniqueId = 0; - - if (_mainWindow.SelectedFolder != null) _mainWindow.SelectedFolder.Aliases.Add(cloned); - else _configuration.Aliases.Add(cloned); - - _configuration.Save(); - _mainWindow.SelectedAlias = cloned; - _renamingAlias = cloned; - _renameAliasBuffer = cloned.DisplayName; - _focusRenameAlias = true; - } - - ImGui.SameLine(); - if (DrawIconButton(FontAwesomeIcon.FolderPlus, buttonSize, "New Folder")) - { - var newFolder = new AliasFolder { Name = "New Folder" }; - _configuration.Folders.Add(newFolder); - _configuration.Save(); - _renamingFolder = newFolder; - _renameBuffer = string.Empty; - _preRenameName = string.Empty; - _focusRename = true; - } - - ImGui.SameLine(); - if (DrawIconButton(FontAwesomeIcon.Trash, buttonSize, - canDelete ? "Delete Selected" : "Hold Shift + Ctrl to delete", - disabled: !canDelete || _mainWindow.SelectedAlias == null)) - { - if (_mainWindow.SelectedFolder != null) _mainWindow.SelectedFolder.Aliases.Remove(_mainWindow.SelectedAlias!); - else _configuration.Aliases.Remove(_mainWindow.SelectedAlias!); - - _mainWindow.SelectedAlias = null; - _mainWindow.SelectedFolder = null; - _configuration.Save(); - } - - ImGui.PopStyleVar(); + _renamingAlias = alias; + _renameAliasBuffer = alias.DisplayName; + _focusRenameAlias = true; } - private bool DrawIconButton(FontAwesomeIcon icon, Vector2 size, string tooltip, bool disabled = false) + private void BeginRenameFolder(AliasFolder folder, bool isNew = false) { - var framePadding = ImGui.GetStyle().FramePadding; - var pX = Math.Max(0, (size.X - ImGui.GetFrameHeight()) / 2f + framePadding.X); - - if (disabled) ImGui.BeginDisabled(); - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(pX, framePadding.Y)); - ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0f); - var clicked = ImGuiComponents.IconButton((int)icon, icon); - ImGui.PopStyleVar(2); - if (disabled) ImGui.EndDisabled(); - - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip(tooltip); - return clicked; + _renamingFolder = folder; + _renameBuffer = isNew ? string.Empty : folder.Name; + _preRenameName = isNew ? string.Empty : folder.Name; + _focusRename = true; + } + private bool MatchesFilter(AliasEntry alias) + { + if (string.IsNullOrWhiteSpace(_filter)) return true; + return alias.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase) || + alias.DisplayName.Contains(_filter, StringComparison.OrdinalIgnoreCase); } } diff --git a/Silkstring/UI/SelectPanel/SelectPanelFooter.cs b/Silkstring/UI/SelectPanel/SelectPanelFooter.cs new file mode 100644 index 0000000..7f89975 --- /dev/null +++ b/Silkstring/UI/SelectPanel/SelectPanelFooter.cs @@ -0,0 +1,131 @@ +using System; +using System.Numerics; +using System.Text.Json; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification; +using Serilog; +using Silkstring.Models; +using Silkstring.Windows; + +namespace Silkstring.Ui; + +public class SelectPanelFooter +{ + private const int FooterButtonCount = 5; + + private readonly Configuration _configuration; + private readonly MainWindow _mainWindow; + private readonly Action _beginRenameAlias; + private readonly Action _beginRenameFolder; + + public SelectPanelFooter( + Configuration configuration, MainWindow mainWindow, Action beginRenameAlias, + Action beginRenameFolder) + { + _configuration = configuration; + _mainWindow = mainWindow; + _beginRenameAlias = beginRenameAlias; + _beginRenameFolder = beginRenameFolder; + } + public void Draw() + { + var available = ImGui.GetContentRegionAvail(); + var buttonSize = new Vector2(MathF.Floor(available.X / FooterButtonCount), available.Y); + var canDelete = ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl; + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + + if (DrawIconButton(FontAwesomeIcon.Plus, buttonSize, "New Alias")) + { + var newAlias = new AliasEntry(); + _configuration.Aliases.Add(newAlias); + _configuration.Save(); + _mainWindow.SetSelection(newAlias, null); + _beginRenameAlias(newAlias); + } + + ImGui.SameLine(); + if (DrawIconButton(FontAwesomeIcon.FileImport, buttonSize, "Import from Clipboard")) + { + try + { + var json = ImGui.GetClipboardText(); + var imported = + JsonSerializer.Deserialize(json, AliasEntry.SerializerOptions); + if (imported != null) + { + _configuration.Aliases.Add(imported); + _configuration.Save(); + _mainWindow.SetSelection(imported, null); + _beginRenameAlias(imported); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to import alias from clipboard"); + Plugin.NotificationManager.AddNotification(new Notification + { + Title = "Import Failure", + Content = "Could not import alias: Clipboard contents are not valid.", + Type = NotificationType.Error + }); + + } + } + + ImGui.SameLine(); + if(DrawIconButton(FontAwesomeIcon.Clone, buttonSize, "Clone Alias", disabled: _mainWindow.SelectedAlias == null)) + { + var source = _mainWindow.SelectedAlias!; + var cloned = source.Clone(); + + if (_mainWindow.SelectedFolder != null) _mainWindow.SelectedFolder.Aliases.Add(cloned); + else _configuration.Aliases.Add(cloned); + + _configuration.Save(); + _mainWindow.SetSelection(cloned, _mainWindow.SelectedFolder); + _beginRenameAlias(cloned); + } + + ImGui.SameLine(); + if (DrawIconButton(FontAwesomeIcon.FolderPlus, buttonSize, "New Folder")) + { + var newFolder = new AliasFolder { Name = "New Folder" }; + _configuration.Folders.Add(newFolder); + _configuration.Save(); + _beginRenameFolder(newFolder, true); + } + + ImGui.SameLine(); + if (DrawIconButton(FontAwesomeIcon.Trash, buttonSize, + canDelete ? "Delete Selected" : "Hold Shift + Ctrl to delete", + disabled: !canDelete || _mainWindow.SelectedAlias == null)) + { + if (_mainWindow.SelectedFolder != null) _mainWindow.SelectedFolder.Aliases.Remove(_mainWindow.SelectedAlias!); + else _configuration.Aliases.Remove(_mainWindow.SelectedAlias!); + + _mainWindow.SetSelection(null, null); + _configuration.Save(); + } + + ImGui.PopStyleVar(); + } + + private bool DrawIconButton(FontAwesomeIcon icon, Vector2 size, string tooltip, bool disabled = false) + { + var framePadding = ImGui.GetStyle().FramePadding; + var pX = Math.Max(0, (size.X - ImGui.GetFrameHeight()) / 2f + framePadding.X); + + if (disabled) ImGui.BeginDisabled(); + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(pX, framePadding.Y)); + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0f); + var clicked = ImGuiComponents.IconButton((int)icon, icon); + ImGui.PopStyleVar(2); + if (disabled) ImGui.EndDisabled(); + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip(tooltip); + return clicked; + } +} diff --git a/Silkstring/Windows/ConfigWindow.cs b/Silkstring/Windows/ConfigWindow.cs index 84ace2a..427c628 100644 --- a/Silkstring/Windows/ConfigWindow.cs +++ b/Silkstring/Windows/ConfigWindow.cs @@ -32,16 +32,15 @@ public override void Draw() ImGui.SetNextItemWidth(150); if (ImGui.InputInt("###delayInput", ref delay, 10, 100)) { - delay = Math.Clamp(delay, 0, 1000); configuration.CommandDelay = delay; - configuration.Save(); + configuration.MarkDirty(); } ImGui.SameLine(); ImGui.SetNextItemWidth(200); if (ImGui.SliderInt("###delaySlider", ref delay, 0, 1000)) { configuration.CommandDelay = delay; - configuration.Save(); + configuration.MarkDirty(); } var multiline = configuration.MultilineCommands; @@ -66,9 +65,4 @@ public override void Draw() IsOpen = false; } } - - public void Open() - { - IsOpen = true; - } } diff --git a/Silkstring/Windows/EditWindow.cs b/Silkstring/Windows/EditWindow.cs deleted file mode 100644 index cc2dd5f..0000000 --- a/Silkstring/Windows/EditWindow.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Windowing; -using Silkstring.Models; - -namespace Silkstring.Windows; - -public class EditWindow : Window, IDisposable -{ - private readonly Configuration configuration; - private AliasEntry? aliasCache; - private AliasEntry? originalAlias; - private string multilineBuffer = string.Empty; - - public EditWindow(Plugin plugin) : base("Edit Alias###EditWindow") - { - configuration = plugin.Configuration; - } - public void Dispose() { } - - public override void PreDraw() - { - SizeConstraints = new WindowSizeConstraints - { - MinimumSize = new Vector2(400, 200), - MaximumSize = new Vector2(float.MaxValue, float.MaxValue) - }; - } - - public override void Draw() - { - if (aliasCache == null || originalAlias == null) return; - ImGui.InputText("Alias", ref aliasCache.Name); - - if (configuration.MultilineCommands) - { - DrawMultilineView(); - } - else - { - ImGui.Columns(2); - - ImGui.Text("Command"); - ImGui.NextColumn(); - ImGui.NextColumn(); - - foreach (var command in aliasCache.Output) - { - if (command.UniqueId == 0) - command.UniqueId = aliasCache.Output.Count == 0 ? 1 : aliasCache.Output.Max(c => c.UniqueId) + 1; - DrawListView(command); - } - aliasCache.Output.RemoveAll(c => c.Delete); - } - - ImGui.Columns(1); - - if (!configuration.MultilineCommands) - { - ImGui.Separator(); - if (ImGui.Button("Add Command")) - { - aliasCache.Output.Add(new CommandEntry()); - } - } - - 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); - ImGui.Separator(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 25); - if (ImGui.Button("Save")) - { - if (configuration.MultilineCommands) - { - aliasCache.Output = multilineBuffer - .Split('\n', StringSplitOptions.RemoveEmptyEntries) - .Select(c => new CommandEntry { Command = c.Trim() }) - .ToList(); - } - - originalAlias.Name = aliasCache.Name; - originalAlias.Enabled = aliasCache.Enabled; - originalAlias.Output = aliasCache.Output; - configuration.Save(); - IsOpen = false; - } - - var closeButtonWidth = ImGui.CalcTextSize("Close").X + ImGui.GetStyle().FramePadding.X * 2; - ImGui.SameLine(ImGui.GetWindowWidth() - closeButtonWidth - ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().WindowPadding.X - 16); - if (ImGui.Button("Cancel")) - { - IsOpen = false; - } - } - - public void Open(AliasEntry alias) - { - aliasCache = alias.Clone(); - originalAlias = alias; - multilineBuffer = string.Join("\n", aliasCache.Output.Select(c => c.Command)); - IsOpen = true; - } - - private void DrawListView(CommandEntry command) - { - ImGui.SetNextItemWidth(-1); - ImGui.InputText("###command" + command.UniqueId, ref command.Command, 100); - ImGui.NextColumn(); - - var canDelete = ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl; - ImGui.BeginDisabled(!canDelete); - if (ImGui.Button("Delete###delete" + command.UniqueId)) command.Delete = true; - ImGui.EndDisabled(); - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip("Hold Shift + Ctrl to delete"); - ImGui.NextColumn(); - } - - private void DrawMultilineView() - { - var footerHeight = ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y + ImGui.GetStyle().WindowPadding.Y * 2 + 4; - ImGui.InputTextMultiline("###multilineCommands", ref multilineBuffer, 5000, new Vector2(-1, ImGui.GetContentRegionAvail().Y - footerHeight)); - } -} diff --git a/Silkstring/Windows/MainWindow.cs b/Silkstring/Windows/MainWindow.cs index bbbea95..26b629c 100644 --- a/Silkstring/Windows/MainWindow.cs +++ b/Silkstring/Windows/MainWindow.cs @@ -10,17 +10,21 @@ namespace Silkstring.Windows; public class MainWindow : Window, IDisposable { + public event Action? SelectionChanged; + private readonly AliasSelectPanel _selectPanel; private readonly AliasEditPanel _editPanel; - private readonly Action _openSettings; - internal AliasEntry? SelectedAlias { get; set; } - internal AliasFolder? SelectedFolder { get; set; } + private AliasEntry? _selectedAlias; + private AliasFolder? _selectedFolder; + + internal AliasEntry? SelectedAlias => _selectedAlias; + internal AliasFolder? SelectedFolder => _selectedFolder; + public MainWindow(Plugin plugin, Action openSettings) : base("Silkstring###Main") { - _openSettings = openSettings; - _selectPanel = new AliasSelectPanel(plugin.Configuration, this, openSettings); + _selectPanel = new AliasSelectPanel(plugin.Configuration, this); _editPanel = new AliasEditPanel(plugin.Configuration, this); TitleBarButtons.Add(new TitleBarButton @@ -44,15 +48,25 @@ public override void PreDraw() public override void Draw() { - var scale= ImGui.GetIO().FontGlobalScale; + var scale = ImGui.GetIO().FontGlobalScale; var leftWidth = new Vector2(250 * scale, 0); - if (ImGui.BeginChild("###selector", leftWidth, true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) _selectPanel.Draw(); + ImGui.BeginChild("###selector", leftWidth, true, + ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse); + _selectPanel.Draw(); ImGui.EndChild(); ImGui.SameLine(); - if (ImGui.BeginChild("###editor", new Vector2(0, 0), true)) _editPanel.Draw(); + ImGui.BeginChild("###editor", new Vector2(0, 0), true); + _editPanel.Draw(); ImGui.EndChild(); } + + public void SetSelection(AliasEntry? alias, AliasFolder? folder) + { + _selectedAlias = alias; + _selectedFolder = folder; + SelectionChanged?.Invoke(alias, folder); + } }