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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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() { }
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user