The header was the only place that knew a tab name can be a person. The sidebar, the tab strip and a pop-out's window title drew the same "Player@World" string untouched, so a screenshot of the default view still named the partner while the messages underneath were anonymised. Guarding one surface out of four guards nobody. The rule sits in one place now and all four read it. Names are replaced rather than blanked: a nameless tab in a strip of tabs is worse to use than a placeholder, and the sidebar has no room to explain itself. The salt is drawn fresh on every plugin load, the same reasoning the message path uses -- a stable label would let two screenshots taken weeks apart be tied together. The tab strip resolves once and both measures and draws that value. Measuring one string and drawing another would have sized every tab wrong the moment the mode came on, which is the kind of thing that looks like a layout bug and gets fixed in the wrong place.
184 lines
6.8 KiB
C#
184 lines
6.8 KiB
C#
using System.Numerics;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Interface;
|
|
using Dalamud.Interface.Utility.Raii;
|
|
using Dalamud.Interface.Windowing;
|
|
using HellionChat.Themes;
|
|
using HellionChat.Ui.Components;
|
|
using HellionChat.Ui.StyleEngine.Widgets;
|
|
using HellionChat.Util;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace HellionChat.Ui.Windows;
|
|
|
|
// One pre-allocated pop-out window bound to a single Tab. Pure DI-sink: the
|
|
// PayloadHandler arrives via AttachPayloadHandler (post-build setter), NEVER
|
|
// via ctor — see plan §B.2. The ###id carries the slot index so all N
|
|
// instances are unique for WindowSystem.AddWindow and ImGui state is stable
|
|
// per slot (not per bound tab).
|
|
internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow
|
|
{
|
|
private readonly int _slotIndex;
|
|
private readonly MessageList _messages;
|
|
private readonly InputBar _input;
|
|
private readonly ILogger<ChannelPopoutWindow> _logger;
|
|
private readonly FontManager _fonts;
|
|
private readonly Ui.StyleEngine.SurfaceBackdrop _backdrop;
|
|
private readonly ThemeRegistry _themes;
|
|
private readonly Ui.StyleEngine.TokenResolver _resolver;
|
|
|
|
public ChannelPopoutWindow(
|
|
int slotIndex,
|
|
MessageList messages,
|
|
InputBar input,
|
|
ILogger<ChannelPopoutWindow> logger,
|
|
FontManager fonts,
|
|
Ui.StyleEngine.SurfaceBackdrop backdrop,
|
|
ThemeRegistry themes,
|
|
Ui.StyleEngine.TokenResolver resolver
|
|
)
|
|
: base($"{Plugin.PluginName}###hellion_popout_{slotIndex}")
|
|
{
|
|
_slotIndex = slotIndex;
|
|
_messages = messages;
|
|
_input = input;
|
|
_logger = logger;
|
|
_fonts = fonts;
|
|
_backdrop = backdrop;
|
|
_themes = themes;
|
|
_resolver = resolver;
|
|
// The pop-in button lives in the input row. Wired here rather than
|
|
// through the constructor because this window is what it has to call.
|
|
_input.OnPopIn = () =>
|
|
{
|
|
if (Bound is { } tab)
|
|
CloseRequested?.Invoke(tab.Identifier);
|
|
};
|
|
|
|
IsOpen = false;
|
|
RespectCloseHotkey = false;
|
|
ShowCloseButton = false;
|
|
}
|
|
|
|
public int SlotIndex => _slotIndex;
|
|
|
|
public Tab? Bound { get; private set; }
|
|
|
|
// Wired post-build by ChannelPopoutPool so closing routes through the pool
|
|
// (which owns the slot map). The window can't reach the pool by ctor without
|
|
// a DI cycle, so the pool sets this after construction. See plan §B.2.
|
|
public Action<Guid>? CloseRequested { get; set; }
|
|
|
|
// Post-build setter — see plan §B.2. Wired by ChannelPopoutInitHostedService.
|
|
public void AttachPayloadHandler(PayloadHandler handler) =>
|
|
_messages.AttachPayloadHandler(handler);
|
|
|
|
public void Bind(Tab tab)
|
|
{
|
|
Bound = tab;
|
|
|
|
var isTell = tab is { IsTempTab: true, TellTarget: { } target } && target.IsSet();
|
|
// Master §4.3 default sizes: Tell is the more compact conversation window.
|
|
Size = isTell ? new Vector2(380f, 320f) : new Vector2(420f, 320f);
|
|
SizeCondition = ImGuiCond.FirstUseEver;
|
|
|
|
// Visible label tracks the bound tab; the ###id stays slot-stable so
|
|
// ImGui keeps this slot's position/size across binds.
|
|
// The title bar is a surface too, and the header deliberately stays
|
|
// silent in this mode because the title already carries the name.
|
|
var label = Util.TabDisplayName.Resolve(
|
|
tab.Name,
|
|
tab.NameCameFromPartner,
|
|
Plugin.Config.ScreenshotMode
|
|
);
|
|
WindowName = $"{label}###hellion_popout_{_slotIndex}";
|
|
|
|
IsOpen = true;
|
|
}
|
|
|
|
public void Unbind()
|
|
{
|
|
Bound = null;
|
|
IsOpen = false;
|
|
}
|
|
|
|
// IFocusableChatWindow — this pop-out's own InputBar carries the focus state
|
|
// the keybind tail checks when deciding whether to route at this surface (C3).
|
|
public bool HasFocusedInput => _input.IsFocused;
|
|
|
|
// Arm-and-hold the one-frame Activate flag; the pop-out's Draw applies the
|
|
// ImGui focus next frame. Framework-thread safe (field write only).
|
|
public void RequestInputFocus()
|
|
{
|
|
BringToFront();
|
|
_input.Activate = true;
|
|
}
|
|
|
|
public override void PreDraw()
|
|
{
|
|
// Gate the native title bar on the user toggle (1.5.6 parity). DrawHeader
|
|
// carries the close button in-body regardless, so hiding the title bar
|
|
// never strands the pop-out. Reset from a fresh base each frame so
|
|
// toggling the bar back on clears NoTitleBar.
|
|
Flags = Plugin.Config.ShowPopOutTitleBar
|
|
? ImGuiWindowFlags.None
|
|
: ImGuiWindowFlags.NoTitleBar;
|
|
}
|
|
|
|
public override void Draw()
|
|
{
|
|
if (Bound is null)
|
|
return;
|
|
|
|
// v1.13.0: the plain title row became the channel header. The warning it
|
|
// left behind still holds and is now the rule the header follows -- with
|
|
// the title bar on, the window title already carries the tab name, so the
|
|
// header drops the name and shows only the world and the clock.
|
|
//
|
|
// Drawn before the body child on purpose, and the child's height is left
|
|
// alone: it is a negative value, which ImGui resolves against the space
|
|
// still available from the current cursor, and the header has already
|
|
// taken its share.
|
|
StyleEngine.Widgets.ChannelHeader.Draw(
|
|
Bound,
|
|
Plugin.Config.ShowPopOutTitleBar
|
|
? StyleEngine.Widgets.ChannelHeaderMode.DetailOnly
|
|
: StyleEngine.Widgets.ChannelHeaderMode.Full,
|
|
Plugin.Instance.FontManager,
|
|
StyleEngine.Widgets.ChannelHeader.CurrentDetail(),
|
|
InputBar.Height
|
|
);
|
|
|
|
// Anything drawn above can unbind us mid-frame (CloseRequested ->
|
|
// pool.TryClose -> Unbind nulls Bound). Re-check before the body so we
|
|
// never hand a null tab to MessageList/InputBar in this same Draw call.
|
|
if (Bound is null)
|
|
return;
|
|
|
|
// POP-1f: the bound tab is live-visible in this pop-out, so it carries no
|
|
// unread badge — mirror MainWindow's per-frame zero for the active tab.
|
|
// View-state reset only (tab.Messages store is untouched).
|
|
Bound.Unread = 0;
|
|
|
|
var inputHeight = InputBar.Height;
|
|
using (
|
|
var body = ImRaii.Child(
|
|
$"##hellion-popout-body-{_slotIndex}",
|
|
new Vector2(-1f, -inputHeight)
|
|
)
|
|
)
|
|
{
|
|
if (body.Success)
|
|
{
|
|
// Same floor as the main window's log, and the same reasoning:
|
|
// no accent wash and barely any motes, because a chat log is read
|
|
// line by line.
|
|
_backdrop.Draw(accentWashHeight: 0f, moteIntensity: 0.10f, strength: 0.45f);
|
|
_messages.Draw(Bound);
|
|
}
|
|
}
|
|
|
|
_input.Draw(Bound);
|
|
}
|
|
}
|