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);
}
}