feat(input-preview): full R1 migration (PreOpenCheck/PreDraw split + Lender)
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<PayloadHandler>, 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.
This commit is contained in:
@@ -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!;
|
||||
|
||||
@@ -278,7 +278,13 @@ internal static class PluginHostFactory
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<ILogger<DbViewer>>()
|
||||
));
|
||||
services.AddSingleton(sp => new InputPreview(sp.GetRequiredService<Plugin>()));
|
||||
services.AddSingleton(sp => new InputPreview(
|
||||
sp.GetRequiredService<Ui.Components.ChunkRenderer>(),
|
||||
sp.GetRequiredService<Lender<PayloadHandler>>(),
|
||||
sp.GetRequiredService<Ui.Windows.MainWindow>(),
|
||||
sp.GetRequiredService<Ui.Components.InputBar>(),
|
||||
sp.GetRequiredService<ILogger<InputPreview>>()
|
||||
));
|
||||
services.AddSingleton(sp => new CommandHelpWindow(sp.GetRequiredService<Plugin>()));
|
||||
services.AddSingleton(sp => new SeStringDebugger(sp.GetRequiredService<Plugin>()));
|
||||
services.AddSingleton(sp => new DebuggerWindow(sp.GetRequiredService<Plugin>()));
|
||||
|
||||
+169
-11
@@ -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<PayloadHandler> _handlerLender;
|
||||
private readonly Windows.MainWindow _mainWindow;
|
||||
private readonly Components.InputBar _inputBar;
|
||||
private readonly ILogger<InputPreview> _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<PayloadHandler> handlerLender,
|
||||
Windows.MainWindow mainWindow,
|
||||
Components.InputBar inputBar,
|
||||
ILogger<InputPreview> 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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user