Files
HellionChat/HellionChat/Ui/InputPreview.cs
T
JonKazama-Hellion 16557213cd chore: comments, second pass -- the task codes the first pass missed
The first sweep matched a character class that swallowed the digit, so a bare
B1 slipped through while B1-2 was caught. Searching the whole A-Z space instead
of guessing prefixes turned up 130-odd more: B0 through B6, C2, C3, D1, H2, M6,
P7, P8, T2, W2, plus GP-04, KB-01, OD-1, PM-1, PM-3, SEC-01, TR-4, TR-7, UI-11,
UI-12, XC-8 and API-3.

Kept deliberately: 41 B4 01 is a byte signature, "N0" a format string,
#L119-L128 a source anchor, LS4/LS6 are linkshells, and A=FF B=0C G=41 R=C2
explains a colour-channel order. Those look like codes and are not.

Also translated the eight German comments left in the theme files and
ImGuiUtil. Seven of them described what a palette does to which channel, which
is worth reading -- just not in a second language in an otherwise English
codebase.
2026-08-19 22:03:12 +02:00

188 lines
6.1 KiB
C#

using System.Numerics;
using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using HellionChat.Code;
using HellionChat.Resources;
using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ui;
internal sealed class InputPreview : Window
{
private readonly Components.ChunkRenderer _chunkRenderer;
private readonly Lender<PayloadHandler> _handlerLender;
private readonly Windows.MainWindow _mainWindow;
private readonly Components.InputBar _inputBar;
private readonly ILogger<InputPreview> _logger;
private bool _drawing;
private bool _hasEvaluation;
internal float PreviewHeight;
private int _lastLength;
private Message? _previewMessage;
internal InputPreview(
Components.ChunkRenderer chunkRenderer,
Lender<PayloadHandler> handlerLender,
Windows.MainWindow mainWindow,
Components.InputBar inputBar,
ILogger<InputPreview> logger
)
: base("##chat2-inputpreview")
{
_chunkRenderer = chunkRenderer;
_handlerLender = handlerLender;
_mainWindow = mainWindow;
_inputBar = inputBar;
_logger = logger;
Flags =
ImGuiWindowFlags.NoSavedSettings
| ImGuiWindowFlags.NoTitleBar
| ImGuiWindowFlags.NoMove
| ImGuiWindowFlags.NoResize
| ImGuiWindowFlags.NoFocusOnAppearing
| ImGuiWindowFlags.NoScrollbar;
RespectCloseHotkey = false;
DisableWindowSounds = true;
IsOpen = true;
// TODO: remove discard once logging call sites exist
_ = _logger;
}
public void Dispose() { }
private bool ValidDraw =>
!string.IsNullOrEmpty(_inputBar.PendingMessage)
&& _inputBar.PendingMessage.Length >= Plugin.Config.PreviewMinimum;
// IsDrawable gates DrawConditions; it is also consumed externally by
// any component that needs to know whether the preview popup is visible.
internal bool IsDrawable => ValidDraw && _hasEvaluation;
private static bool IsWindowMode =>
Plugin.Config.PreviewPosition is PreviewPosition.Top or PreviewPosition.Bottom;
// PreOpenCheck owns state: it runs once per frame before the visibility
// gate so Drawing/PreviewMessage/HasEvaluation stay fresh even when the
// window is not ultimately drawn. PreDraw owns position/size to avoid
// wasted computation on frames where DrawConditions returns false
// (position math only matters when the window is about to render).
// This matches the v1.5.6 UpdateConditionCheck/PreDraw split — the
// Framework.Update subscribe is removed; PreOpenCheck runs at the same
// cadence via Dalamud's WindowSystem.
public override void PreOpenCheck()
{
_drawing = ValidDraw;
if (!_drawing)
{
_lastLength = 0;
PreviewHeight = 0;
_previewMessage = null;
_hasEvaluation = false;
return;
}
if (_previewMessage == null || _lastLength != _inputBar.PendingMessage.Length)
{
_lastLength = _inputBar.PendingMessage.Length;
var bytes = Encoding.UTF8.GetBytes(_inputBar.PendingMessage.Trim());
AutoTranslate.ReplaceWithPayload(ref bytes);
var chunks = ChunkUtil
.ToChunks(SeString.Parse(bytes), ChunkSource.Content, ChatType.Say)
.ToList();
_previewMessage = Message.FakeMessage(chunks, new ChatCode(XivChatType.Say, 0, 0));
_previewMessage.DecodeTextParam();
}
_hasEvaluation = !Plugin.Config.OnlyPreviewIf || _previewMessage.Content.Count > 1;
}
public override bool DrawConditions()
{
return IsWindowMode && IsDrawable;
}
public override void PreDraw()
{
var pos = _mainWindow.LastWindowPos;
var size = _mainWindow.LastWindowSize;
Size = size with { Y = PreviewHeight };
var y = Plugin.Config.PreviewPosition switch
{
PreviewPosition.Top => pos.Y - PreviewHeight,
PreviewPosition.Bottom => pos.Y + size.Y,
_ => throw new ArgumentOutOfRangeException(
nameof(Plugin.Config.PreviewPosition),
Plugin.Config.PreviewPosition,
null
),
};
Position = pos with { Y = y };
PositionCondition = ImGuiCond.Always;
}
public override void Draw()
{
CalculatePreviewHeight();
DrawPreview();
}
internal void CalculatePreviewHeight()
{
// Pre-draw offscreen once to measure actual rendered height; value is
// consumed next frame by PreDraw() for window sizing.
PreviewHeight = 0;
var pos = ImGui.GetCursorPos();
ImGui.SetCursorPos(new Vector2(-500, -500));
var before = ImGui.GetCursorPosY();
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
{
ImGui.TextUnformatted(Language.Options_Preview_Header);
_chunkRenderer.DrawChunks(_previewMessage!.Content, wrap: true, lineWidth: 0f);
}
var after = ImGui.GetCursorPosY();
ImGui.SetCursorPos(pos);
PreviewHeight = after - before;
PreviewHeight += IsWindowMode ? ImGui.GetStyle().WindowPadding.Y * 2 : 0;
}
internal void DrawPreview()
{
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
{
ImGui.TextUnformatted(Language.Options_Preview_Header);
// Primary path resets the Lender counter in MainWindow.Draw();
// this fallback covers the edge-case where MainWindow is closed but
// InputPreview is still open, preventing handler pool growth.
if (!_mainWindow.IsOpen)
_handlerLender.ResetCounter();
var handler = _handlerLender.Borrow();
_chunkRenderer.DrawChunks(
_previewMessage!.Content,
wrap: true,
handler: handler,
lineWidth: 0f
);
handler.Draw();
}
}
}