feat(payload-handler): wire DrawPopups + Integrations/ContextFooter/StringifyMessage

E2 fills the popup-dispatch layer of PayloadHandler:
- DrawPopups: switch-dispatch over payload types, with TODO(E3)/(E4)
  markers at the deferred Draw{Player,Item,Status,Uri}Popup call sites
- Integrations: invokes registered IPC integrations (LogWindow.Plugin.Ipc
  -> _ipc substitution per §4.2)
- ContextFooter: ScreenshotMode + HideChat checkboxes (Plugin.Config
  static-bridge substitutions per §4.2)
- StringifyMessage: pure helper, 1:1 from v1.5.6

Adds PopupSfx const (E1 polish — needed by E5's Click for
UIGlobals.PlaySoundEffect). Removes #pragma CS0169 for _popup since
DrawPopups now writes the field; the warning no longer triggers.

E3 will fill DrawPlayerPopup + FindCharacterForPayload; E4 the
Item/Status/Uri popups; E5 the Hover/Click bodies; E6 MoveTooltip.
This commit is contained in:
2026-05-27 12:19:50 +02:00
parent e46b6a7520
commit 63f5a28834
+160 -3
View File
@@ -1,8 +1,15 @@
using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility.Raii;
using HellionChat.Code;
using HellionChat.Resources;
using HellionChat.Themes;
using HellionChat.Ui.Components;
using HellionChat.Ui.Windows;
using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat;
@@ -10,6 +17,7 @@ namespace HellionChat;
internal sealed class PayloadHandler
{
private const string PopupId = "hellionchat-context-popup";
private const uint PopupSfx = 1;
private readonly ThemeRegistry _themes;
private readonly IpcManager _ipc;
@@ -24,9 +32,7 @@ internal sealed class PayloadHandler
public uint HoverCounter;
public uint LastHoverCounter;
#pragma warning disable CS0169 // used in DrawPopups (E2)
private (Chunk, Payload?)? _popup;
#pragma warning restore CS0169
public PayloadHandler(
ThemeRegistry themes,
@@ -49,7 +55,7 @@ internal sealed class PayloadHandler
internal void Draw()
{
// TODO(E2): DrawPopups();
DrawPopups();
if (HandleTooltips && ++HoverCounter - LastHoverCounter > 1)
{
@@ -60,6 +66,157 @@ internal sealed class PayloadHandler
}
}
private void DrawPopups()
{
if (_popup == null)
return;
var (chunk, payload) = _popup.Value;
using var popup = ImRaii.Popup(PopupId);
if (!popup.Success)
{
_popup = null;
return;
}
using var id = ImRaii.PushId(PopupId);
var drawn = false;
switch (payload)
{
case PlayerPayload player:
// TODO(E3): DrawPlayerPopup(chunk, player);
drawn = true;
break;
case ItemPayload item:
// TODO(E4): DrawItemPopup(item);
drawn = true;
break;
case UriPayload uri:
// TODO(E4): DrawUriPopup(uri);
drawn = true;
break;
case StatusPayload status:
// TODO(E4): DrawStatusPopup(status);
drawn = true;
break;
}
ContextFooter(drawn, chunk);
Integrations(chunk, payload);
}
private void Integrations(Chunk chunk, Payload? payload)
{
var registered = _ipc.Registered;
if (registered.Count == 0)
return;
ImGui.Separator();
var contentId = chunk.Message?.ContentId ?? 0;
var sender =
chunk.Message?.Sender.Select(c => c.Link).FirstOrDefault(p => p is PlayerPayload)
as PlayerPayload;
using var menu = ImRaii.Menu(Language.Context_Integrations);
if (!menu.Success)
return;
var cursor = ImGui.GetCursorPos();
foreach (var integrationId in registered)
{
try
{
_ipc.Invoke(
integrationId,
sender,
contentId,
payload,
chunk.Message?.SenderSource,
chunk.Message?.ContentSource
);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing integration");
}
}
if (cursor == ImGui.GetCursorPos())
{
using var pushedColor = ImRaii.PushColor(
ImGuiCol.Text,
ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]
);
ImGui.Text("No integrations available");
}
}
private void ContextFooter(bool didCustomContext, Chunk chunk)
{
ImRaii.MenuDisposable menu = default;
if (didCustomContext)
{
ImGui.Separator();
// Only place these menu items in a submenu if we've already drawn
// custom context menu items based on the payload.
//
// It makes it much more convenient in the majority of cases to
// copy the message content without having to open a submenu.
menu = ImRaii.Menu(Plugin.PluginName);
if (!menu.Success)
return;
}
ImGui.Checkbox(Language.Context_ScreenshotMode, ref Plugin.Config.ScreenshotMode);
if (ImGui.Selectable(Language.Context_HideChat))
Plugin.Config.HideChat = true;
if (chunk.Message is { } message)
{
if (ImGui.Selectable(Language.Context_Copy))
{
ImGui.SetClipboardText(StringifyMessage(message, true));
WrapperUtil.AddNotification(Language.Context_CopySuccess, NotificationType.Info);
}
// Only show a separate "Copy content" option if the message has
// Sender chunks, so it doesn't show for system messages.
if (message.Sender.Count > 0 && ImGui.Selectable(Language.Context_CopyContent))
{
ImGui.SetClipboardText(StringifyMessage(message));
WrapperUtil.AddNotification(
Language.Context_CopyContentSuccess,
NotificationType.Info
);
}
using var pushedColor = ImRaii.PushColor(
ImGuiCol.Text,
ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]
);
ImGui.TextUnformatted(message.Code.Type.Name());
}
menu.Dispose();
}
private static string StringifyMessage(Message? message, bool withSender = false)
{
if (message == null)
return string.Empty;
var chunks = withSender ? message.Sender.Concat(message.Content) : message.Content;
return chunks
.Where(chunk => chunk is TextChunk)
.Cast<TextChunk>()
.Select(text => text.Content)
.Aggregate(string.Concat);
}
internal void Hover(Payload payload) { }
internal unsafe void Click(Chunk chunk, Payload? payload, ImGuiMouseButton button) { }