diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 80eae80..7172db8 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -261,6 +261,9 @@ public class Configuration : IPluginConfiguration // v20 fields: window visibility state, channel popout pool size and // sidebar auto-switch threshold. All initializers double as the // migration defaults for configs loaded at v19 or earlier. + // Still written on open/close, but no longer read for the start state: the + // window always shows on login (1.5.6 parity, MainWindow ctor). Kept for the + // migration round-trip and a possible future "remember session state" opt-in. public bool MainWindowOpen = true; public bool SettingsWindowOpen; public int MaxParallelPopouts = 8; diff --git a/HellionChat/GameFunctions/KeybindManager.cs b/HellionChat/GameFunctions/KeybindManager.cs index 3861623..ec7c7ea 100644 --- a/HellionChat/GameFunctions/KeybindManager.cs +++ b/HellionChat/GameFunctions/KeybindManager.cs @@ -501,12 +501,13 @@ internal unsafe class KeybindManager : IDisposable return; Plugin.KeyState[currentBest.Item1] = false; - if (!KeybindsToIntercept.TryGetValue(currentBest.Item2, out var info)) + if (!KeybindsToIntercept.ContainsKey(currentBest.Item2)) return; - // Chat-window Activated integration is offline until the new chat - // layer surfaces an Activated entry point. - _ = info; + // Re-surface the chat-activation entry point retired in v1.6.0: a chat-open + // keybind shows + focuses the window, restoring it from a user-hide or a + // closed state. Channel/prefill routing from the bind stays out of scope. + Plugin.Instance.MainWindow?.ActivateChat(); } // Tab-cycle dispatch is offline until the new chat layer surfaces a diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index c38232e..7f4e746 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -383,6 +383,7 @@ public sealed class Plugin : IAsyncDalamudPlugin new SelfTests.ColorEditorBufferStep(this), new SelfTests.ThemePickerCategoryStep(this), new SelfTests.QuickPickerSelfTestStep(this), + new SelfTests.HideRestoreSelfTestStep(this), new SelfTests.SettingsWindowOpenStep(this), new SelfTests.OnOpenMainUiRoutesMainWindowStep(this), new SelfTests.TypingIpcStateStep(this), diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index 3c6d0d4..1fb745f 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -156,7 +156,8 @@ internal static class PluginHostFactory sp.GetRequiredService>(), () => sp.GetRequiredService().SettingsWindow.Toggle(), sp.GetRequiredService(), - sp.GetRequiredService() + sp.GetRequiredService(), + () => sp.GetRequiredService().MainWindow.UserHide() )); services.AddSingleton(sp => new Ui.Components.Settings.TabSidebar( sp.GetRequiredService() diff --git a/HellionChat/SelfTests/HideRestoreSelfTestStep.cs b/HellionChat/SelfTests/HideRestoreSelfTestStep.cs new file mode 100644 index 0000000..cb5ebf9 --- /dev/null +++ b/HellionChat/SelfTests/HideRestoreSelfTestStep.cs @@ -0,0 +1,69 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Plugin.SelfTest; +using HellionChat.Ui.Windows; + +namespace HellionChat.SelfTests; + +// P8 wiring: UserHide() suppresses DrawConditions; both ActivateChat() (Enter) and +// Toggle() (/hellion) restore it. Pure window-state — the focus side is left to smoke. +internal sealed class HideRestoreSelfTestStep : ISelfTestStep +{ + private readonly Plugin _plugin; + + public HideRestoreSelfTestStep(Plugin plugin) + { + _plugin = plugin; + } + + public string Name => "Hellion Chat - Hide + activate restore"; + + public SelfTestStepResult RunStep() + { + var window = _plugin.MainWindow; + if (window is null) + { + ImGui.Text("Plugin.MainWindow is null"); + return SelfTestStepResult.Fail; + } + + var savedOpen = window.IsOpen; + var result = Evaluate(window); + + // Never leave the window stuck hidden, even if an assertion failed. + window.ActivateChat(); + window.IsOpen = savedOpen; + return result; + } + + private static SelfTestStepResult Evaluate(MainWindow window) + { + window.UserHide(); + if (window.DrawConditions()) + { + ImGui.Text("UserHide did not suppress DrawConditions"); + return SelfTestStepResult.Fail; + } + + window.ActivateChat(); + if (!window.DrawConditions() || !window.IsOpen) + { + ImGui.Text( + $"ActivateChat failed: DrawConditions={window.DrawConditions()}, IsOpen={window.IsOpen}" + ); + return SelfTestStepResult.Fail; + } + + // /hellion (Toggle) must also clear a user-hide, not just flip IsOpen. + window.UserHide(); + window.Toggle(); + if (!window.DrawConditions()) + { + ImGui.Text("Toggle did not restore the window from a user-hide"); + return SelfTestStepResult.Fail; + } + + return SelfTestStepResult.Pass; + } + + public void CleanUp() { } +} diff --git a/HellionChat/Ui/Components/InputBar.cs b/HellionChat/Ui/Components/InputBar.cs index f3c13e4..55bb2d0 100644 --- a/HellionChat/Ui/Components/InputBar.cs +++ b/HellionChat/Ui/Components/InputBar.cs @@ -45,6 +45,9 @@ internal sealed class InputBar // pop-out would be confusing). The main window's InputBar gets the instance. private readonly ThemeQuickPicker? _themeQuickPicker; + // Null in pop-outs (those have their own close button). Hides the main window. + private readonly Action? _onHideWindow; + private string _pendingMessage = string.Empty; private bool _isFocused; private bool _wasInputTextHovered; @@ -81,7 +84,8 @@ internal sealed class InputBar ILogger logger, Action onOpenSettings, CommandHelpWindow commandHelpWindow, - ThemeQuickPicker? themeQuickPicker = null + ThemeQuickPicker? themeQuickPicker = null, + Action? onHideWindow = null ) { _symbolPicker = symbolPicker; @@ -92,6 +96,7 @@ internal sealed class InputBar _onOpenSettings = onOpenSettings; _commandHelpWindow = commandHelpWindow; _themeQuickPicker = themeQuickPicker; + _onHideWindow = onHideWindow; } public string PendingMessage => _pendingMessage; @@ -540,20 +545,17 @@ internal sealed class InputBar ImGui.SetTooltip("Settings"); } - // Hide button gated on ShowHideButton (1.5.6 parity). It is the last - // button in the row, so skipping it (with its leading SameLine) leaves - // no dangling SameLine. Shared by main + pop-out InputBars. - if (Plugin.Config.ShowHideButton) + // Hides the window (1.5.6 UserHide). One-way — Enter brings it back. + // Main window only (pop-outs have their own close); last in the row. + if (Plugin.Config.ShowHideButton && _onHideWindow is not null) { ImGui.SameLine(); - var hidden = Plugin.Config.HideChat; - var visIcon = hidden ? FontAwesomeIcon.EyeSlash : FontAwesomeIcon.Eye; - if (ImGui.Button(visIcon.ToIconString())) - Plugin.Config.HideChat = !hidden; + if (ImGui.Button(FontAwesomeIcon.EyeSlash.ToIconString())) + _onHideWindow(); if (ImGui.IsItemHovered()) { using (ImRaii.DefaultFont()) - ImGui.SetTooltip(hidden ? "Unhide chat" : "Hide chat"); + ImGui.SetTooltip("Hide chat (Enter to bring back)"); } } } diff --git a/HellionChat/Ui/Windows/MainWindow.cs b/HellionChat/Ui/Windows/MainWindow.cs index 310c8e0..646a9e9 100644 --- a/HellionChat/Ui/Windows/MainWindow.cs +++ b/HellionChat/Ui/Windows/MainWindow.cs @@ -33,6 +33,10 @@ internal sealed class MainWindow : Window private Tab? _activeTab; + // Runtime-only hide: window stays IsOpen but DrawConditions skips it, so the + // chat-activation key can restore it (1.5.6 HideState.User parity). + private bool _userHidden; + public Vector2 LastWindowPos { get; private set; } = Vector2.Zero; public Vector2 LastWindowSize { get; private set; } = Vector2.Zero; internal unsafe ImGuiViewport* LastViewport; @@ -66,7 +70,9 @@ internal sealed class MainWindow : Window MinimumSize = new Vector2(MinWidth, MinHeight), MaximumSize = new Vector2(float.MaxValue, float.MaxValue), }; - IsOpen = Plugin.Config.MainWindowOpen; + // 1.5.6 parity: the chat always shows on login. The window stays closeable + // and hideable within a session, but that state is not carried across starts. + IsOpen = true; RespectCloseHotkey = false; } @@ -162,11 +168,33 @@ internal sealed class MainWindow : Window internal Components.MessageList GetMessageListForSelfTest() => _messages; - // new-shadow on Window.Toggle so the open path also writes Config — - // OnClose already covers the close path through the base behaviour. + public override bool DrawConditions() => !_userHidden; + + internal void UserHide() => _userHidden = true; + + // Chat-activation keybind (Enter) entry point. Field writes only, so it is safe + // from the framework thread; the draw path applies focus next frame. + internal void ActivateChat() + { + _userHidden = false; + if (!IsOpen) + { + IsOpen = true; + Plugin.Config.MainWindowOpen = true; + } + BringToFront(); + _input.Activate = true; + } + + // new-shadow on Window.Toggle so the open path also writes Config. A user-hide + // counts as "not visible", so /hellion is a reliable one-press recovery even when + // the Enter keybind can't fire (DirectChat / a focused game text field). public new void Toggle() { - IsOpen = !IsOpen; + var visible = IsOpen && !_userHidden; + IsOpen = !visible; + if (IsOpen) + _userHidden = false; Plugin.Config.MainWindowOpen = IsOpen; }