refactor(ui): retire ChatLogWindow and the v1.5.6 chat-window layer

The legacy ChatLogWindow.cs and its tightly coupled neighbours are gone:
PayloadHandler, Popout, ChatInputBar, AutoCompleteInfo, AutoTellTabTint,
the three tab-icon helpers, the old Ui/StatusBar and Ui/SymbolPicker
behind the components-layer replacements, HellionStyle + helpers, the
CompactInputSubmitter test mirror and the QuickPickerSelfTestStep. The
new component layer (MainWindow + the five components + GlobalStyleScope)
now drives the whole chat surface.

InputPreview, CommandHelpWindow and Debugger lose their ChatLogWindow
backref. The first two are skeleton windows for now — DrawConditions
always returns false until the new chat layer exposes equivalent state.
Debugger keeps the current-tab and vanilla-chat blocks; the payload
counters are explicitly marked offline. DbViewer renders Sender/Content
columns as plain TextValue strings instead of the removed DrawChunks.

GameFunctions.Chat and GameFunctions.KeybindManager keep the hook
plumbing intact but mark every ChatLogWindow.Activated /
ChangeTabDelta / TellSpecial site as offline so the FFXIV-side
integration still compiles and runs without an Activated entry point.
TypingIpc.BuildState reports the IPC state as not-typing / not-focused
until the new chat layer surfaces real focus and buffer state again.

Plugin.cs Draw uses StyleEngine.GlobalStyleScope.Push for the per-frame
theme push and stops calling BeginFrame / FinalizeFrame / HideStateCheck
/ DefaultText through the dead window. ImGuiUtil drops PostPayload +
WrapText + the surrounding word-wrap pipeline. PluginHostFactory and
PluginLifecycle drop the legacy DI singletons and AddWindow entries.

Build is clean and csharpier is clean across the trimmed 131-file tree.
This commit is contained in:
2026-05-23 20:31:12 +02:00
parent c9e746a8e3
commit cf4705e01f
27 changed files with 98 additions and 6342 deletions
+12 -275
View File
@@ -1,40 +1,20 @@
using System.Numerics;
using System.Text;
using System.Text.RegularExpressions;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services;
using HellionChat.Code;
using HellionChat.Resources;
using HellionChat.Util;
namespace HellionChat.Ui;
public partial class InputPreview : Window
// Pre-send chunk preview is offline while the chat input pipeline is
// rebuilt. The window stays in the system so the DI graph keeps a single
// shape across cycles, but DrawConditions always returns false until the
// new preview lands on top of the components layer.
public class InputPreview : Window
{
private ChatLogWindow LogWindow { get; }
private readonly Plugin _plugin;
private bool Drawing;
private bool HasEvaluation;
internal float PreviewHeight;
private int LastLength;
private Message? PreviewMessage;
private int CursorPosition;
private bool NextChunkIsAutoTranslate;
internal int SelectedCursorPos = -1;
internal InputPreview(ChatLogWindow logWindow)
internal InputPreview(Plugin plugin)
: base("##chat2-inputpreview")
{
LogWindow = logWindow;
_plugin = plugin;
Flags =
ImGuiWindowFlags.NoSavedSettings
| ImGuiWindowFlags.NoTitleBar
@@ -42,257 +22,14 @@ public partial class InputPreview : Window
| ImGuiWindowFlags.NoResize
| ImGuiWindowFlags.NoFocusOnAppearing
| ImGuiWindowFlags.NoScrollbar;
RespectCloseHotkey = false;
DisableWindowSounds = true;
IsOpen = true;
Plugin.Framework.Update += UpdateConditionCheck;
IsOpen = false;
}
public void Dispose()
{
Plugin.Framework.Update -= UpdateConditionCheck;
}
public void Dispose() { }
private bool ValidDraw =>
!string.IsNullOrEmpty(LogWindow.Chat)
&& LogWindow.Chat.Length >= Plugin.Config.PreviewMinimum;
public override bool DrawConditions() => false;
private void UpdateConditionCheck(IFramework framework)
{
Drawing = ValidDraw;
if (!Drawing)
{
LastLength = 0;
PreviewHeight = 0;
PreviewMessage = null;
HasEvaluation = false;
return;
}
if (PreviewMessage == null || LastLength != LogWindow.Chat.Length)
{
LastLength = LogWindow.Chat.Length;
var bytes = Encoding.UTF8.GetBytes(LogWindow.Chat.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;
}
internal bool IsDrawable => ValidDraw && HasEvaluation;
private static bool IsWindowMode =>
Plugin.Config.PreviewPosition is PreviewPosition.Top or PreviewPosition.Bottom;
public override bool DrawConditions()
{
return IsWindowMode && IsDrawable;
}
public override void PreDraw()
{
var pos = LogWindow.LastWindowPos;
var size = LogWindow.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()
{
CalculatePreview();
DrawPreview();
}
internal void CalculatePreview()
{
// We Pre-draw this once to get the actual height :HideThePain:
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);
DrawChunksPreview(PreviewMessage!.Content);
}
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);
var handler = LogWindow.HandlerLender.Borrow();
DrawChunksPreview(PreviewMessage!.Content, handler, unique: 10000);
handler.Draw();
}
}
private void DrawChunksPreview(
IReadOnlyList<Chunk> chunks,
PayloadHandler? handler = null,
float lineWidth = 0f,
int unique = 0
)
{
CursorPosition = 0;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
for (var i = 0; i < chunks.Count; i++)
{
if (chunks[i] is TextChunk text && string.IsNullOrEmpty(text.Content))
continue;
DrawChunkPreview(chunks[i], handler, lineWidth, unique);
if (i < chunks.Count - 1)
{
ImGui.SameLine();
}
else if (chunks[i].Link is EmotePayload && Plugin.Config.ShowEmotes)
{
// Emote payloads seem to not automatically put newlines, which
// is an issue when modern mode is disabled.
ImGui.SameLine();
// Use default ImGui behavior for newlines.
ImGui.TextUnformatted("");
}
}
}
private void DrawChunkPreview(
Chunk chunk,
PayloadHandler? handler = null,
float lineWidth = 0f,
int unique = 0
)
{
if (chunk is IconChunk icon)
{
LogWindow.DrawIcon(chunk, icon, handler);
if (icon.Icon != BitmapFontIcon.AutoTranslateBegin)
return;
NextChunkIsAutoTranslate = true;
// Malformed chunks could carry an AutoTranslateBegin icon without the matching
// payload; bail out instead of dereferencing a null Link.
if (chunk.Link is not AutoTranslatePayload payload)
return;
CursorPosition += $"<at:{payload.Group},{payload.Key}>".Length;
return;
}
if (chunk is not TextChunk text)
return;
if (chunk.Link is EmotePayload emotePayload && Plugin.Config.ShowEmotes)
{
var emoteSize = ImGui.CalcTextSize("W");
emoteSize = emoteSize with { Y = emoteSize.X } * 1.5f;
// TextWrap doesn't work for emotes, so we have to wrap them manually
if (ImGui.GetContentRegionAvail().X < emoteSize.X)
ImGui.NewLine();
// We only draw a dummy if it is still loading, in case it failed, we draw the actual name
var image = EmoteCache.GetEmote(emotePayload.Code);
if (image is { Failed: false })
{
if (image.IsLoaded)
image.Draw(emoteSize);
else
ImGui.Dummy(emoteSize);
if (ImGui.IsItemHovered())
ImGuiUtil.Tooltip(emotePayload.Code);
CursorPosition += emotePayload.Code.Length;
return;
}
}
if (NextChunkIsAutoTranslate)
{
NextChunkIsAutoTranslate = false;
ImGuiUtil.WrapText(text.Content, chunk, handler, LogWindow.DefaultText, lineWidth);
return;
}
if (text.Link != null)
{
if (text.Link is ItemPayload)
CursorPosition += "<item>".Length;
else if (text.Link is MapLinkPayload)
CursorPosition += "<flag>".Length;
else if (text.Link is EmotePayload emote)
CursorPosition += emote.Code.Length;
else if (text.Link is UriPayload)
CursorPosition += text.Content.Length;
ImGuiUtil.WrapText(text.Content, chunk, handler, LogWindow.DefaultText, lineWidth);
return;
}
foreach (var word in WhitespaceRegex().Split(text.Content).Where(s => s != string.Empty))
{
var wordSize = ImGui.CalcTextSize(word);
if (ImGui.GetContentRegionAvail().X < wordSize.X)
ImGui.NewLine();
foreach (var letter in word)
{
var letterSize = ImGui.CalcTextSize(letter.ToString());
CursorPosition++;
if (
ImGui.Selectable(
$"{letter}##{CursorPosition + unique}",
false,
ImGuiSelectableFlags.None,
letterSize
)
)
{
SelectedCursorPos = CursorPosition;
LogWindow.FocusedPreview = true;
}
ImGui.SameLine();
}
}
ImGui.NewLine();
}
[GeneratedRegex(@"(\s)")]
private static partial Regex WhitespaceRegex();
public override void Draw() { }
}