From 8431fbcf802bd46eb0efa21ff28c64d0e60c0cd1 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 27 May 2026 20:35:25 +0200 Subject: [PATCH] feat(input-preview): full R1 migration (PreOpenCheck/PreDraw split + Lender) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I resurrects InputPreview as a fully ctor-injected window: - Class header public → internal sealed (Components-Layer style); Plugin.cs property visibility corrected to internal to match - Ctor takes 5 DI deps (ChunkRenderer, Lender, MainWindow, InputBar, ILogger) via Factory-Lambda DI-reg - Window-hook split: PreOpenCheck() owns the state (Drawing/PreviewMessage/ HasEvaluation/PreviewHeight/LastLength), PreDraw() owns position/size computation. Matches v1.5.6's split — avoids wasted position-math when Window isn't drawn (DrawConditions gates on IsDrawable getter). - Framework.Update subscribe/unsubscribe removed (PreOpenCheck runs per draw-frame, same cadence as Framework.Update for our needs) - Draw() borrows fresh PayloadHandler per-frame from Lender for popup isolation (preview hover doesn't bleed into log) - Defensive ResetCounter fallback when MainWindow closed + InputPreview open — primary path is A2's MainWindow.Draw() ResetCounter R2/R3 (J/K) next. A2 closes out the Lender DI-cycle for MainWindow. --- HellionChat/Plugin.cs | 2 +- HellionChat/PluginHostFactory.cs | 8 +- HellionChat/Ui/InputPreview.cs | 180 +++++++++++++++++++++++++++++-- 3 files changed, 177 insertions(+), 13 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 9983214..904c820 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -98,7 +98,7 @@ public sealed class Plugin : IAsyncDalamudPlugin internal Ui.Windows.MainWindow MainWindow { get; private set; } = null!; internal Ui.Windows.SettingsWindow SettingsWindow { get; private set; } = null!; public DbViewer DbViewer { get; private set; } = null!; - public InputPreview InputPreview { get; private set; } = null!; + internal InputPreview InputPreview { get; private set; } = null!; public CommandHelpWindow CommandHelpWindow { get; private set; } = null!; public SeStringDebugger SeStringDebugger { get; private set; } = null!; public FirstRunWizard FirstRunWizard { get; private set; } = null!; diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index c74bc5d..33b7db7 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -278,7 +278,13 @@ internal static class PluginHostFactory sp.GetRequiredService(), sp.GetRequiredService>() )); - services.AddSingleton(sp => new InputPreview(sp.GetRequiredService())); + services.AddSingleton(sp => new InputPreview( + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>() + )); services.AddSingleton(sp => new CommandHelpWindow(sp.GetRequiredService())); services.AddSingleton(sp => new SeStringDebugger(sp.GetRequiredService())); services.AddSingleton(sp => new DebuggerWindow(sp.GetRequiredService())); diff --git a/HellionChat/Ui/InputPreview.cs b/HellionChat/Ui/InputPreview.cs index f1b1dcd..0dd3e72 100644 --- a/HellionChat/Ui/InputPreview.cs +++ b/HellionChat/Ui/InputPreview.cs @@ -1,20 +1,50 @@ +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.Interface.Utility.Raii; using Dalamud.Interface.Windowing; +using HellionChat.Code; +using HellionChat.Resources; +using HellionChat.Util; +using Microsoft.Extensions.Logging; namespace HellionChat.Ui; -// 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 +internal sealed partial class InputPreview : Window { - private readonly Plugin _plugin; + private readonly Components.ChunkRenderer _chunkRenderer; + private readonly Lender _handlerLender; + private readonly Windows.MainWindow _mainWindow; + private readonly Components.InputBar _inputBar; + private readonly ILogger _logger; - internal InputPreview(Plugin plugin) + private bool _drawing; + private bool _hasEvaluation; + internal float PreviewHeight; + + private int _lastLength; + private Message? _previewMessage; + + internal int SelectedCursorPos = -1; + + public InputPreview( + Components.ChunkRenderer chunkRenderer, + Lender handlerLender, + Windows.MainWindow mainWindow, + Components.InputBar inputBar, + ILogger logger + ) : base("##chat2-inputpreview") { - _plugin = plugin; + _chunkRenderer = chunkRenderer; + _handlerLender = handlerLender; + _mainWindow = mainWindow; + _inputBar = inputBar; + _logger = logger; + Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoTitleBar @@ -22,14 +52,142 @@ public class InputPreview : Window | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoScrollbar; + RespectCloseHotkey = false; DisableWindowSounds = true; - IsOpen = false; + IsOpen = true; + + // Logger injected for future diagnostic hooks (no call-sites yet in R1). + _ = _logger; } public void Dispose() { } - public override bool DrawConditions() => false; + private bool ValidDraw => + !string.IsNullOrEmpty(_inputBar.PendingMessage) + && _inputBar.PendingMessage.Length >= Plugin.Config.PreviewMinimum; - public override void Draw() { } + // 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(); + } + + private 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; + } + + private void DrawPreview() + { + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImGui.TextUnformatted(Language.Options_Preview_Header); + + // Primary path (A2) 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(); + } + } + + [GeneratedRegex(@"(\s)")] + private static partial Regex WhitespaceRegex(); }