diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index 5a4db83..fb80313 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -261,6 +261,10 @@ internal class HellionStrings internal static string Settings_Window_PopOutInputEnabled_Name => Get(nameof(Settings_Window_PopOutInputEnabled_Name)); internal static string Settings_Window_PopOutInputEnabled_Description => Get(nameof(Settings_Window_PopOutInputEnabled_Description)); + // Hellion Chat — Window position recovery (off-screen safety net) + internal static string Settings_Window_ResetPosition_Name => Get(nameof(Settings_Window_ResetPosition_Name)); + internal static string Settings_Window_ResetPosition_Description => Get(nameof(Settings_Window_ResetPosition_Description)); + // Hellion Chat — v0.6.0 one-time hint banner shown inside pop-outs internal static string Popout_v060_HintText => Get(nameof(Popout_v060_HintText)); internal static string Popout_v060_HintAck => Get(nameof(Popout_v060_HintAck)); diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 177116d..9f88ea0 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -591,6 +591,12 @@ Master-Switch: erlaubt direktes Tippen und Absenden in jedem Pop-Out-Fenster (inkl. Auto-Tell-Tabs). Channel-Wechsel im Pop-Out wirkt global wie im Hauptfenster; Text-Buffer und History-Cursor sind pro Pop-Out unabhängig. + + Fenster-Position zurücksetzen + + + Holt das Chat-Fenster und alle aktiven Pop-Outs zurück in die linke obere Ecke des Hauptmonitors. Hilfreich wenn ein Fenster nach einem Display-Layout-Wechsel außerhalb des sichtbaren Bereichs gelandet ist (Monitor abgezogen, Auflösung geändert). Das Plugin macht außerdem einmal pro Session einen automatischen Bounds-Check, dieser Button ist der manuelle Notausgang falls trotzdem etwas unerreichbar bleibt. + Neu in v0.6.0: Du kannst jetzt direkt im Pop-Out tippen. Master-Switch in den Fenster-Settings aktivieren. diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index c6c9703..aefa6c6 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -591,6 +591,12 @@ Master switch: lets you type and send messages directly inside every pop-out window (including auto-tell tabs). Channel changes inside a pop-out apply globally just like in the main window; the text buffer and history cursor stay independent per pop-out. + + Reset Window Position + + + Snaps the chat window and every active pop-out back to the primary monitor's top-left corner. Useful when a window has drifted off-screen after a display layout change (monitor disconnected, resolution changed). The plugin also runs an automatic bounds check once per session — this button is the manual backup if anything still ends up unreachable. + New in v0.6.0: you can type directly inside pop-out windows. Toggle the master switch in the window settings to enable it. diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index fe7e201..64d2db3 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -66,6 +66,14 @@ public sealed class ChatLogWindow : Window public Vector2 LastWindowPos { get; private set; } = Vector2.Zero; public Vector2 LastWindowSize { get; private set; } = Vector2.Zero; + // Window position recovery: guards against off-screen positions after a + // display layout change (monitor disconnected, resolution changed). On + // the first draw after plugin load we run a one-shot bounds check to see + // whether the stored position still overlaps any visible viewport area. + // The manual reset button in the settings forces the position regardless. + private bool DidOnLoadBoundsCheck; + internal bool RequestPositionReset { get; set; } + public unsafe ImGuiViewport* LastViewport; private bool WasDocked; @@ -542,6 +550,22 @@ public sealed class ChatLogWindow : Window LastWindowSize = currentSize; LastWindowPos = ImGui.GetWindowPos(); + // Window position recovery. Manual reset takes precedence and snaps + // the window to the safe default unconditionally; the one-shot + // on-load check only fires when the persisted position has no + // overlap with any visible viewport area. + if (RequestPositionReset) + { + RequestPositionReset = false; + DidOnLoadBoundsCheck = true; + ApplySafeDefaultPosition("manual-reset"); + } + else if (!DidOnLoadBoundsCheck) + { + DidOnLoadBoundsCheck = true; + EnsureWindowOnScreen("on-load"); + } + if (resized) LastResize.Restart(); @@ -2035,4 +2059,47 @@ public sealed class ChatLogWindow : Window var hashCode = $"{Salt}{playerName}{worldId}".GetHashCode(); return $"Player {hashCode:X8}"; } + + // Snap threshold in pixels: at least this much of the window must overlap + // a visible viewport so the user can still grab the first tab header. + // Below the threshold the window is considered off-screen. + private const int OnScreenMinOverlapX = 100; + private const int OnScreenMinOverlapY = 40; + + // Default snap position relative to the primary viewport (top-left with a + // safety margin from the game title bar). + private static readonly Vector2 SafeDefaultOffset = new(50, 50); + + private void EnsureWindowOnScreen(string source) + { + if (LastWindowSize.X < 1 || LastWindowSize.Y < 1) + return; + + var viewport = ImGui.GetMainViewport(); + var visibleMin = viewport.WorkPos; + var visibleMax = viewport.WorkPos + viewport.WorkSize; + + var overlapMin = Vector2.Max(LastWindowPos, visibleMin); + var overlapMax = Vector2.Min(LastWindowPos + LastWindowSize, visibleMax); + var overlap = overlapMax - overlapMin; + + if (overlap.X >= OnScreenMinOverlapX && overlap.Y >= OnScreenMinOverlapY) + return; + + ApplySafeDefaultPosition(source); + } + + private void ApplySafeDefaultPosition(string source) + { + var viewport = ImGui.GetMainViewport(); + var safePos = viewport.WorkPos + SafeDefaultOffset; + Position = safePos; + Plugin.Log.Info( + $"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}."); + + // Pop-outs are intentionally non-persistent (cleared on plugin reload), + // so an off-screen pop-out can never survive a session boundary. The + // main window above is the only persistence target that needs an + // explicit recovery path. + } } diff --git a/HellionChat/Ui/SettingsTabs/Window.cs b/HellionChat/Ui/SettingsTabs/Window.cs index 8d42587..5a7dfdc 100644 --- a/HellionChat/Ui/SettingsTabs/Window.cs +++ b/HellionChat/Ui/SettingsTabs/Window.cs @@ -142,6 +142,15 @@ internal sealed class Window : ISettingsTab ImGui.Checkbox(Language.Options_SidebarTabView_Name, ref Mutable.SidebarTabView); ImGuiUtil.HelpMarker(string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName)); + + ImGui.Spacing(); + + // Manual escape hatch for off-screen windows. The plugin already + // runs an automatic bounds check once per session, but a button + // is the user-friendly fallback after a display layout change. + if (ImGui.Button(HellionStrings.Settings_Window_ResetPosition_Name)) + Plugin.ChatLogWindow.RequestPositionReset = true; + ImGuiUtil.HelpMarker(HellionStrings.Settings_Window_ResetPosition_Description); } }