diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 4168ce6..b4f4bfd 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -400,6 +400,7 @@ public sealed class Plugin : IAsyncDalamudPlugin new SelfTests.DisclosureArmStep(this), new SelfTests.TellRoutingBuildStep(this), new SelfTests.TellPillTransparencyStep(this), + new SelfTests.TabRenamePersistStep(this), ]); // Re-surface the wizard for existing users when a major UX diff --git a/HellionChat/SelfTests/TabRenamePersistStep.cs b/HellionChat/SelfTests/TabRenamePersistStep.cs new file mode 100644 index 0000000..376bf82 --- /dev/null +++ b/HellionChat/SelfTests/TabRenamePersistStep.cs @@ -0,0 +1,59 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Plugin.SelfTest; +using HellionChat.Ui.Components; + +namespace HellionChat.SelfTests; + +// B3-1: rename must persist. Drives the real ApplyTabRename (the InputText +// callback path), then SaveConfig + reload from disk and asserts the new name +// survived — a fresh-from-config tab, not the same reference (a reference check +// would pass on a dead roundtrip). Uses a persistent (non-temp) tab: unpinned +// temp tabs are stripped on save (ShouldStripOnSave) and would not survive. +internal sealed class TabRenamePersistStep : ISelfTestStep +{ + private readonly Plugin plugin; + + public TabRenamePersistStep(Plugin plugin) => this.plugin = plugin; + + public string Name => "Hellion Chat - Tab rename persists"; + + public SelfTestStepResult RunStep() + { + var tab = Plugin.Config.Tabs.FirstOrDefault(t => !t.IsTempTab); + if (tab is null) + { + ImGui.Text("No persistent tab to rename"); + return SelfTestStepResult.Fail; + } + + var original = tab.Name; + var probe = original + "##selftest"; + try + { + if (!TabContextMenu.ApplyTabRename(tab, probe)) + { + ImGui.Text("ApplyTabRename reported no change"); + return SelfTestStepResult.Fail; + } + plugin.SaveConfig(); + + // Reload from disk into a throwaway config; assert the new name landed. + var reloaded = Plugin.Interface.GetPluginConfig() as Configuration; + var match = reloaded?.Tabs.Any(t => t.Name == probe) ?? false; + if (!match) + { + ImGui.Text("Renamed tab not found after reload"); + return SelfTestStepResult.Fail; + } + } + finally + { + tab.Name = original; + plugin.SaveConfig(); + } + + return SelfTestStepResult.Pass; + } + + public void CleanUp() { } +} diff --git a/HellionChat/Ui/Components/Sidebar.cs b/HellionChat/Ui/Components/Sidebar.cs index 536306e..9de2109 100644 --- a/HellionChat/Ui/Components/Sidebar.cs +++ b/HellionChat/Ui/Components/Sidebar.cs @@ -159,12 +159,7 @@ internal sealed class Sidebar if (expanded) dl.AddText(origin + new Vector2(32f, 8f), textAbgr, tab.Name); - if (ImGui.BeginPopupContextItem("ctx")) - { - if (ImGui.MenuItem("Pop Out")) - _pool.TryOpen(tab); - ImGui.EndPopup(); - } + TabContextMenu.Draw(tab, "ctx", _pool); var popHovered = false; if (hasPopOut) diff --git a/HellionChat/Ui/Components/TabContextMenu.cs b/HellionChat/Ui/Components/TabContextMenu.cs new file mode 100644 index 0000000..70939c3 --- /dev/null +++ b/HellionChat/Ui/Components/TabContextMenu.cs @@ -0,0 +1,44 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Utility; + +namespace HellionChat.Ui.Components; + +// Shared right-click menu for both tab layouts (Sidebar rows + TopTabBar). One +// source of truth instead of two divergent inline blocks. Static: it has no own +// state and reaches the live Config/Plugin through Plugin.Instance/Plugin.Config. +internal static class TabContextMenu +{ + // MUST be called immediately after the row-carrying ImGui item (Sidebar + // "row" InvisibleButton / TopTabBar Selectable). popupId only names the + // popup; the open trigger is a right-click on the LAST submitted item + // (g.LastItemData via IsItemHovered) — any interactive item in between + // would steal the trigger. Only DrawList ops may sit between. + public static void Draw(Tab tab, string popupId, Windows.ChannelPopoutPool pool) + { + if (!ImGui.BeginPopupContextItem(popupId)) + return; + + // Rename: focus the field the first frame the popup appears. + if (ImGui.IsWindowAppearing()) + ImGui.SetKeyboardFocusHere(); + ImGui.SetNextItemWidth(250f * ImGuiHelpers.GlobalScale); + var name = tab.Name; + if (ImGui.InputText("##tab-name", ref name, 512) && ApplyTabRename(tab, name)) + Plugin.Instance.SaveConfig(); + + if (ImGui.MenuItem("Pop Out")) + pool.TryOpen(tab); + + ImGui.EndPopup(); + } + + // Factored out so the SelfTest drives the real rename path, not a field poke. + // Returns true when the name actually changed (gates the SaveConfig write). + internal static bool ApplyTabRename(Tab tab, string newName) + { + if (string.IsNullOrEmpty(newName) || newName == tab.Name) + return false; + tab.Name = newName; + return true; + } +} diff --git a/HellionChat/Ui/Components/TopTabBar.cs b/HellionChat/Ui/Components/TopTabBar.cs index 71291e7..d41da8b 100644 --- a/HellionChat/Ui/Components/TopTabBar.cs +++ b/HellionChat/Ui/Components/TopTabBar.cs @@ -38,12 +38,7 @@ internal sealed class TopTabBar TabLifecycleHelpers.EnsureCurrentChannel(tab); } - if (ImGui.BeginPopupContextItem($"toptab_ctx_{i}")) - { - if (ImGui.MenuItem("Pop Out")) - _pool.TryOpen(tab); - ImGui.EndPopup(); - } + TabContextMenu.Draw(tab, $"toptab_ctx_{i}", _pool); } ImGui.Separator();