feat(window): restore hide-chat-window + Enter-to-restore (1.5.6)

The eye/hide button now hides the HellionChat window (runtime-only, via a new
DrawConditions gate) instead of toggling native-chat suppression, matching 1.5.6.
The chat-activation keybind (Enter / "/"), whose dispatch was a dead stub in the
KeybindManager since the v1.6.0 rewrite, is re-wired to MainWindow.ActivateChat:
it un-hides, opens if closed, brings the window to front and focuses the input --
so the chat reacts to Enter again from any state. /hellion is a reliable one-press
recovery (Toggle now clears the hide), and the window always shows on login
(start state no longer read from the persisted flag). Adds HideRestoreSelfTestStep.
This commit is contained in:
2026-06-15 20:49:14 +02:00
parent b397591ba4
commit a73f4d0d0c
7 changed files with 124 additions and 19 deletions
+3
View File
@@ -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;
+5 -4
View File
@@ -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
+1
View File
@@ -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),
+2 -1
View File
@@ -156,7 +156,8 @@ internal static class PluginHostFactory
sp.GetRequiredService<ILogger<Ui.Components.InputBar>>(),
() => sp.GetRequiredService<Plugin>().SettingsWindow.Toggle(),
sp.GetRequiredService<Ui.CommandHelpWindow>(),
sp.GetRequiredService<Ui.Components.ThemeQuickPicker>()
sp.GetRequiredService<Ui.Components.ThemeQuickPicker>(),
() => sp.GetRequiredService<Plugin>().MainWindow.UserHide()
));
services.AddSingleton(sp => new Ui.Components.Settings.TabSidebar(
sp.GetRequiredService<FontManager>()
@@ -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() { }
}
+12 -10
View File
@@ -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<InputBar> 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)");
}
}
}
+32 -4
View File
@@ -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;
}