Files
HellionChat/HellionChat/Ui/Windows/MainWindow.cs
T
JonKazama-Hellion 29fb4b92eb fix(input-preview): wire Inside-mode + Tooltip-mode render paths
InputPreview was only rendered for PreviewPosition.Top/Bottom (the
DrawConditions IsWindowMode gate). Inside-mode (the default) and
Tooltip-mode had no caller at all because v1.5.6's inline-render path
lived on the deleted ChatLogWindow and was not migrated to the v1.7.0
Components-Layer.

Wire Inside-mode by calling CalculatePreviewHeight + DrawPreview
inline from MainWindow.DrawMainArea between the message-list child
and the input bar, with the message-list height reserved for the
preview block. Wire Tooltip-mode by sampling IsItemHovered() on the
input text widget inside InputBar.DrawInputField (analog to the
existing _isFocused = ImGui.IsItemFocused() idiom on the same line)
and exposing it as WasInputTextHovered; MainWindow opens the tooltip
after _input.Draw when both the hover-flag and PreviewPosition.Tooltip
are active.

Plan-drift acknowledged: the plan stated Plugin.InputPreview is
statically reachable, but the property was declared as an instance
member on Plugin.cs:101. Hoisted to internal static to match the
plan's intention (analog to Plugin.Config); updated the single
external instance-access site in PluginLifecycle.RegisterWindows
to the type-qualified form.

Verified in-game: Inside-mode preview block appears between message
list and input bar on first keystroke; tooltip-mode shows preview on
text-field hover only; Top/Bottom-mode unchanged; empty buffer hides
the preview in all modes. dotnet build clean, dotnet csharpier check
clean.
2026-05-28 13:21:59 +02:00

190 lines
6.4 KiB
C#

using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using HellionChat.Util;
namespace HellionChat.Ui.Windows;
// Top-level chat window assembled from the components layer. Layout from
// top to bottom: honorific header, horizontal body with sidebar + main
// area (messages + input bar), and the status strip pinned to the
// bottom. The window-level theme push stays on the global plugin draw
// path for now — this window only composes content.
//
// Components are fully qualified through the Ui.Components prefix so the
// old Ui.StatusBar type (still alive until the cleanup block removes it)
// cannot shadow the new layer through parent-namespace resolution.
internal sealed class MainWindow : Window
{
private const float DefaultWidth = 620f;
private const float DefaultHeight = 340f;
private const float MinWidth = 480f;
private const float MinHeight = 260f;
private readonly Components.HonorificHeader _honorific;
private readonly Components.Sidebar _sidebar;
private readonly Components.MessageList _messages;
private readonly Components.InputBar _input;
private readonly Components.StatusBar _status;
private readonly Lender<PayloadHandler> _handlerLender;
private Tab? _activeTab;
public Vector2 LastWindowPos { get; private set; } = Vector2.Zero;
public Vector2 LastWindowSize { get; private set; } = Vector2.Zero;
internal unsafe ImGuiViewport* LastViewport;
public MainWindow(
Components.HonorificHeader honorific,
Components.Sidebar sidebar,
Components.MessageList messages,
Components.InputBar input,
Components.StatusBar status,
Lender<PayloadHandler> handlerLender
)
: base($"{Plugin.PluginName}###hellion-main")
{
_honorific = honorific;
_sidebar = sidebar;
_messages = messages;
_input = input;
_status = status;
_handlerLender = handlerLender;
Size = new Vector2(DefaultWidth, DefaultHeight);
SizeCondition = ImGuiCond.FirstUseEver;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(MinWidth, MinHeight),
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
};
// The message list owns its own scroll inside the body child;
// the outer window must not show a second scrollbar.
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
IsOpen = Plugin.Config.MainWindowOpen;
RespectCloseHotkey = false;
}
public Tab? ActiveTab => _activeTab;
// Internal accessors for self-tests so the probes can reach the live
// component without exposing them as public surface.
internal Components.Sidebar GetSidebarForSelfTest() => _sidebar;
internal Components.HonorificHeader GetHonorificHeaderForSelfTest() => _honorific;
// new-shadow on Window.Toggle so the open path also writes Config —
// OnClose already covers the close path through the base behaviour.
public new void Toggle()
{
IsOpen = !IsOpen;
Plugin.Config.MainWindowOpen = IsOpen;
}
public override void OnClose()
{
Plugin.Config.MainWindowOpen = false;
}
public override void Draw()
{
LastWindowPos = ImGui.GetWindowPos();
LastWindowSize = ImGui.GetWindowSize();
unsafe
{
LastViewport = ImGui.GetWindowViewport().Handle;
}
// Primary pool-reset path; InputPreview has a defensive fallback for the MainWindow-closed edge case.
_handlerLender.ResetCounter();
// First-frame seed: the active tab defaults to the first persisted
// tab so the message list isn't empty on a clean session.
if (_activeTab is null && Plugin.Config.Tabs.Count > 0)
_activeTab = Plugin.Config.Tabs[0];
var statusHeight = Components.StatusBar.Height;
using (var body = ImRaii.Child("##hellion-body", new Vector2(-1f, -statusHeight)))
{
if (body.Success)
DrawBody();
}
_messages.DrawHandlerPopups();
_status.Draw(_activeTab);
}
private void DrawBody()
{
var bodyWidth = ImGui.GetContentRegionAvail().X;
_honorific.Draw(bodyWidth);
using (ImRaii.Group())
{
_sidebar.Draw(bodyWidth, Plugin.Config.Tabs, ref _activeTab);
}
ImGui.SameLine();
using (ImRaii.Group())
{
DrawMainArea();
}
}
private void DrawMainArea()
{
var inputHeight = Components.InputBar.Height;
// Shrink the message child when Inside-mode preview is active so the
// inline preview block does not overlap the message list. PreviewHeight
// lags one frame behind on the very first keystroke (same as v1.5.6).
var previewHeight =
Plugin.Config.PreviewPosition is PreviewPosition.Inside
&& Plugin.InputPreview.IsDrawable
? Plugin.InputPreview.PreviewHeight
: 0f;
using (
var messages = ImRaii.Child(
"##hellion-main-area",
new Vector2(-1f, -(inputHeight + previewHeight))
)
)
{
if (messages.Success)
_messages.Draw(_activeTab!);
}
// Inside-mode inline render: measure first so PreviewHeight is fresh
// for the next frame's reservation, then draw between messages and input.
if (
Plugin.Config.PreviewPosition is PreviewPosition.Inside
&& Plugin.InputPreview.IsDrawable
)
{
Plugin.InputPreview.CalculatePreviewHeight();
Plugin.InputPreview.DrawPreview();
}
_input.Draw(_activeTab);
// Tooltip-mode: sampled hover-state from InputBar reflects the actual
// InputText widget (after-Draw IsItemHovered would target a QuickButton).
// ImRaii.Tooltip has no Success guard — BeginTooltip always runs in ctor.
if (
Plugin.Config.PreviewPosition is PreviewPosition.Tooltip
&& Plugin.InputPreview.IsDrawable
&& _input.WasInputTextHovered
)
{
ImGui.SetNextWindowSize(new Vector2(500 * ImGuiHelpers.GlobalScale, -1));
using var tooltip = ImRaii.Tooltip();
Plugin.InputPreview.DrawPreview();
}
}
}