diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 5f3dc27..a460488 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -175,8 +175,6 @@ public sealed class Plugin : IAsyncDalamudPlugin // PerformanceBaselineStep so the hot path stays allocation-free. internal double LastDrawMs; - internal int DeferredSaveFrames = -1; - // Cancels the v1.4.8 FTS5 bulk-insert worker on plugin teardown. The // worker runs off the framework thread on its own SqliteConnection, so a // Dispose mid-rebuild must signal cancellation before MessageManager @@ -277,8 +275,6 @@ public sealed class Plugin : IAsyncDalamudPlugin ImGuiUtil.Initialize(this); - DeferredSaveFrames = -1; - // Custom themes dir + seed run before the container builds so the // ThemeRegistry factory lambda finds the directory ready. var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes"); @@ -641,19 +637,6 @@ public sealed class Plugin : IAsyncDalamudPlugin } ); - // Flush a pending DeferredSave — FrameworkUpdate won't fire it anymore. - failure = CaptureFailure( - failure, - () => - { - if (DeferredSaveFrames >= 0) - { - SaveConfig(); - DeferredSaveFrames = -1; - } - } - ); - // Framework-thread cleanup the container does not reach. try { @@ -1134,9 +1117,6 @@ public sealed class Plugin : IAsyncDalamudPlugin private void FrameworkUpdate(IFramework framework) { - if (DeferredSaveFrames >= 0 && DeferredSaveFrames-- == 0) - SaveConfig(); - if (!Config.HideChat) return; diff --git a/HellionChat/Ui/Components/Settings/Tabs/ChannelsTab.cs b/HellionChat/Ui/Components/Settings/Tabs/ChannelsTab.cs index b23658d..4e1e338 100644 --- a/HellionChat/Ui/Components/Settings/Tabs/ChannelsTab.cs +++ b/HellionChat/Ui/Components/Settings/Tabs/ChannelsTab.cs @@ -116,10 +116,11 @@ internal sealed class ChannelsTab { var current = get(); ImGui.SetNextItemWidth(200); + // Sliders report a change every frame while dragging; deferring the write + // to release turns ~30 full-config disk writes per second into one. if (ImGui.SliderInt(label, ref current, min, max, "%d")) - { set(current); + if (ImGui.IsItemDeactivatedAfterEdit()) _plugin.SaveConfig(); - } } } diff --git a/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs b/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs index 043aa31..307adef 100644 --- a/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs +++ b/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs @@ -108,10 +108,11 @@ internal sealed class DataPrivacyTab { var current = get(); ImGui.SetNextItemWidth(200); + // Sliders report a change every frame while dragging; deferring the write + // to release turns ~30 full-config disk writes per second into one. if (ImGui.SliderInt(label, ref current, min, max, "%d")) - { set(current); + if (ImGui.IsItemDeactivatedAfterEdit()) _plugin.SaveConfig(); - } } } diff --git a/HellionChat/Ui/Components/Settings/Tabs/GeneralTab.cs b/HellionChat/Ui/Components/Settings/Tabs/GeneralTab.cs index b82948f..e82f597 100644 --- a/HellionChat/Ui/Components/Settings/Tabs/GeneralTab.cs +++ b/HellionChat/Ui/Components/Settings/Tabs/GeneralTab.cs @@ -80,11 +80,12 @@ internal sealed class GeneralTab { var current = get(); ImGui.SetNextItemWidth(200); + // Sliders report a change every frame while dragging; deferring the write + // to release turns ~30 full-config disk writes per second into one. if (ImGui.SliderFloat(label, ref current, min, max, "%.2f")) - { set(current); + if (ImGui.IsItemDeactivatedAfterEdit()) _plugin.SaveConfig(); - } } // Wires the already-present ImGuiUtil.KeybindInput capture widget (dead/unwired diff --git a/HellionChat/Ui/Components/Settings/Tabs/WindowTab.cs b/HellionChat/Ui/Components/Settings/Tabs/WindowTab.cs index e108486..19806b2 100644 --- a/HellionChat/Ui/Components/Settings/Tabs/WindowTab.cs +++ b/HellionChat/Ui/Components/Settings/Tabs/WindowTab.cs @@ -134,21 +134,23 @@ internal sealed class WindowTab { var current = get(); ImGui.SetNextItemWidth(200); + // Sliders report a change every frame while dragging; deferring the write + // to release turns ~30 full-config disk writes per second into one. if (ImGui.SliderFloat(label, ref current, min, max, "%.2f")) - { set(current); + if (ImGui.IsItemDeactivatedAfterEdit()) _plugin.SaveConfig(); - } } private void DrawSliderInt(string label, Func get, Action set, int min, int max) { var current = get(); ImGui.SetNextItemWidth(200); + // Sliders report a change every frame while dragging; deferring the write + // to release turns ~30 full-config disk writes per second into one. if (ImGui.SliderInt(label, ref current, min, max, "%d")) - { set(current); + if (ImGui.IsItemDeactivatedAfterEdit()) _plugin.SaveConfig(); - } } } diff --git a/HellionChat/Ui/Components/TabContextMenu.cs b/HellionChat/Ui/Components/TabContextMenu.cs index 3acb9e8..738a6e7 100644 --- a/HellionChat/Ui/Components/TabContextMenu.cs +++ b/HellionChat/Ui/Components/TabContextMenu.cs @@ -12,6 +12,12 @@ namespace HellionChat.Ui.Components; // state and reaches the live Config/Plugin through Plugin.Instance/Plugin.Config. internal static class TabContextMenu { + // Pending rename, scoped to one tab. ImGui never re-submits the input when the + // popup is dismissed by clicking outside, so IsItemDeactivatedAfterEdit never + // fires there — without this the rename would be lost. + private static Guid _renamingTab; + private static bool _renameDirty; + // 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 @@ -20,7 +26,18 @@ internal static class TabContextMenu public static void Draw(Tab tab, string popupId, Windows.ChannelPopoutPool pool) { if (!ImGui.BeginPopupContextItem(popupId)) + { + // Popup gone: flush a pending rename. Scoped to the OWNING tab — every + // other tab's Draw lands here too and would flush foreign state. + if (_renamingTab == tab.Identifier) + { + if (_renameDirty) + Plugin.Instance.SaveConfig(); + ClearPendingRename(); + } + return; + } // Rename: focus the field the first frame the popup appears. if (ImGui.IsWindowAppearing()) @@ -28,7 +45,19 @@ internal static class TabContextMenu ImGui.SetNextItemWidth(250f * ImGuiHelpers.GlobalScale); var name = tab.Name; if (ImGui.InputText("##tab-name", ref name, 512) && ApplyTabRename(tab, name)) - Plugin.Instance.SaveConfig(); + { + _renamingTab = tab.Identifier; + _renameDirty = true; + } + + // Covers leaving the field while the popup stays open; the dismissed-popup + // case is handled above. + if (ImGui.IsItemDeactivatedAfterEdit() && _renamingTab == tab.Identifier) + { + if (_renameDirty) + Plugin.Instance.SaveConfig(); + ClearPendingRename(); + } // Per-tab notification sound (B3-3). The checkbox gates the picker so // tabs that never want a sound keep the popup short. @@ -49,6 +78,15 @@ internal static class TabContextMenu ImGui.EndPopup(); } + // The flush depends on Draw running once more for this tab. If it never does — + // LRU eviction, logout, window closed or collapsed, plugin unload, game exit — + // the name only lives in memory until some other SaveConfig happens to run. + private static void ClearPendingRename() + { + _renamingTab = Guid.Empty; + _renameDirty = false; + } + // Sound picker: 16 numbered game sounds, a separator, then the 3 bundled // Hellion clips stored as ids 17-19 (1.5.6 parity order). The collapsed // preview reuses the entry label scheme so the current pick reads the same