From a3379818ebbcf1c4e7f7857bef69fd85e86f13e8 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Thu, 20 Aug 2026 07:54:32 +0200 Subject: [PATCH] fix(popouts): the context menu never opened outside the main window Right-clicking a name or an item inside a pop-out did nothing at all. One payload handler is shared by the main window, every pop-out and the input preview, and it holds a single popup state. The main window is registered first, so it draws first, finds no open popup in its own scope, reads that as "closed" and clears the state -- before the window that actually opened the popup gets its turn. The popup now belongs to the surface that opened it. The others leave its state alone instead of dropping it. The rule itself sits in its own helper because the handler pulls in Dalamud and cannot be loaded from a test. --- HellionChat/PayloadHandler.cs | 11 ++++++++++- HellionChat/Ui/Components/MessageList.cs | 5 +++++ HellionChat/Ui/InputPreview.cs | 1 + HellionChat/Util/PopupOwnership.cs | 12 ++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 HellionChat/Util/PopupOwnership.cs diff --git a/HellionChat/PayloadHandler.cs b/HellionChat/PayloadHandler.cs index 2d3d95b..91aeb5e 100644 --- a/HellionChat/PayloadHandler.cs +++ b/HellionChat/PayloadHandler.cs @@ -49,6 +49,7 @@ internal sealed class PayloadHandler public uint LastHoverCounter; private (Chunk, Payload?)? _popup; + private object? _popupOwner; public PayloadHandler( ThemeRegistry themes, @@ -69,6 +70,10 @@ internal sealed class PayloadHandler _logger = logger; } + // Every chat surface shares this handler, so each one claims it before it + // renders. Without that the popup cannot tell whose click it belongs to. + internal object? ActiveSurface { get; set; } + internal void Draw() { DrawPopups(); @@ -84,7 +89,9 @@ internal sealed class PayloadHandler private void DrawPopups() { - if (_popup == null) + // A foreign surface finds no open popup here and would otherwise read + // that as "closed" and drop the state before the owner gets to draw. + if (_popup == null || !PopupOwnership.Owns(_popupOwner, ActiveSurface)) return; var (chunk, payload) = _popup.Value; @@ -93,6 +100,7 @@ internal sealed class PayloadHandler if (!popup.Success) { _popup = null; + _popupOwner = null; return; } @@ -934,6 +942,7 @@ internal sealed class PayloadHandler private void RightClickPayload(Chunk chunk, Payload? payload) { _popup = (chunk, payload); + _popupOwner = ActiveSurface; ImGui.OpenPopup(PopupId); } } diff --git a/HellionChat/Ui/Components/MessageList.cs b/HellionChat/Ui/Components/MessageList.cs index 4704632..89c0ffc 100644 --- a/HellionChat/Ui/Components/MessageList.cs +++ b/HellionChat/Ui/Components/MessageList.cs @@ -158,6 +158,11 @@ internal sealed class MessageList return; } + // Claim the shared handler, so a popup opened here stays with this + // window instead of being cleared by whichever surface draws first. + if (_handler is not null) + _handler.ActiveSurface = this; + // No own ImRaii.Child here — MainWindow already wraps the message // area in one. Nesting would give the window two stacked scrolls // and a runaway content-height computation. diff --git a/HellionChat/Ui/InputPreview.cs b/HellionChat/Ui/InputPreview.cs index 8de71c9..60b3c80 100644 --- a/HellionChat/Ui/InputPreview.cs +++ b/HellionChat/Ui/InputPreview.cs @@ -175,6 +175,7 @@ internal sealed class InputPreview : Window _handlerLender.ResetCounter(); var handler = _handlerLender.Borrow(); + handler.ActiveSurface = this; _chunkRenderer.DrawChunks( _previewMessage!.Content, wrap: true, diff --git a/HellionChat/Util/PopupOwnership.cs b/HellionChat/Util/PopupOwnership.cs new file mode 100644 index 0000000..cb6165d --- /dev/null +++ b/HellionChat/Util/PopupOwnership.cs @@ -0,0 +1,12 @@ +namespace HellionChat.Util; + +// Chat surfaces share one payload handler and all render it in the same frame. +// A popup belongs to the surface that opened it; the others must leave its +// state alone instead of reading "no popup here" as "popup was closed". +internal static class PopupOwnership +{ + // Reference identity on purpose -- two surfaces can compare equal without + // being the same window. + internal static bool Owns(object? owner, object? surface) => + owner is not null && ReferenceEquals(owner, surface); +}