The three sidebar/coupling steps add, insert and remove tabs straight from the framework thread while the message worker mutates the same list under TabsListLock. CurrentTabCouplingStep's Insert(0, ...) is the worst of them: it shifts every index, so DropOldestTempTab can remove the wrong tab between its index lookup and RemoveAt. Locks sit around the individual mutations, never around a Draw call, so no step holds the lock across rendering.
127 lines
4.7 KiB
C#
127 lines
4.7 KiB
C#
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Plugin.SelfTest;
|
|
using HellionChat.Code;
|
|
using HellionChat.GameFunctions.Types;
|
|
|
|
namespace HellionChat.SelfTests;
|
|
|
|
// B3-4: section headers render once per non-empty temp-tab pool, and compact
|
|
// mode suppresses the header text (separators stay). Drives the REAL
|
|
// Sidebar.Draw inside the /xlperf window frame (same render precedent as
|
|
// SidebarGreetedGlyphStep) and reads the render observability counter.
|
|
// Injects a mixed tab set (persistent + unpinned temp + pinned temp) and
|
|
// restores config in finally.
|
|
internal sealed class SidebarSectionHeaderStep : ISelfTestStep
|
|
{
|
|
private readonly Plugin plugin;
|
|
|
|
public SidebarSectionHeaderStep(Plugin plugin) => this.plugin = plugin;
|
|
|
|
public string Name => "Hellion Chat - Sidebar section header";
|
|
|
|
public SelfTestStepResult RunStep()
|
|
{
|
|
var sidebar = plugin.MainWindow.GetSidebarForSelfTest();
|
|
if (sidebar is null)
|
|
{
|
|
ImGui.Text("Sidebar null");
|
|
return SelfTestStepResult.Fail;
|
|
}
|
|
|
|
var savedCompact = Plugin.Config.AutoTellTabsCompactDisplay;
|
|
var savedSidebarWidth = Plugin.Config.SidebarWidth;
|
|
|
|
// Both headers need a populated pool behind them. Persistent tabs
|
|
// normally already exist — inject a probe only when the live config
|
|
// has none, so the section order has a real first section.
|
|
var injected = new List<Tab>();
|
|
if (Plugin.Config.Tabs.All(t => t.IsTempTab))
|
|
{
|
|
injected.Add(
|
|
new Tab
|
|
{
|
|
Name = "Persistent Probe@SelfTest",
|
|
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
|
|
{
|
|
[ChatType.Say] = (ChatSourceExt.All, ChatSourceExt.All),
|
|
},
|
|
}
|
|
);
|
|
}
|
|
injected.Add(BuildTempProbe("Tell Probe@SelfTest", pinned: false));
|
|
injected.Add(BuildTempProbe("Pinned Probe@SelfTest", pinned: true));
|
|
// Config.Tabs is mutated by the message worker under TabsListLock; a
|
|
// framework-thread writer must take the same lock.
|
|
lock (plugin.TabsListLock)
|
|
{
|
|
foreach (var tab in injected)
|
|
Plugin.Config.Tabs.Add(tab);
|
|
}
|
|
|
|
Tab? active = null;
|
|
var width = (float)Plugin.Config.SidebarAutoSwitchThresholdPx + 100f; // expanded
|
|
try
|
|
{
|
|
// Headers are not width-gated, but the pinned width keeps the step
|
|
// uniform with SidebarGreetedGlyphStep (expanded rows, no min-drag
|
|
// row drops while the probes render).
|
|
Plugin.Config.SidebarWidth = 220;
|
|
|
|
Plugin.Config.AutoTellTabsCompactDisplay = false;
|
|
sidebar.Draw(width, Plugin.Config.Tabs, ref active);
|
|
if (sidebar.LastDrawnSectionHeaderCount != 2)
|
|
{
|
|
ImGui.Text(
|
|
$"Expected 2 section headers with compact OFF, got {sidebar.LastDrawnSectionHeaderCount}"
|
|
);
|
|
return SelfTestStepResult.Fail;
|
|
}
|
|
|
|
Plugin.Config.AutoTellTabsCompactDisplay = true;
|
|
sidebar.Draw(width, Plugin.Config.Tabs, ref active);
|
|
if (sidebar.LastDrawnSectionHeaderCount != 0)
|
|
{
|
|
ImGui.Text(
|
|
$"Compact ON must suppress header text, got {sidebar.LastDrawnSectionHeaderCount}"
|
|
);
|
|
return SelfTestStepResult.Fail;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lock (plugin.TabsListLock)
|
|
{
|
|
foreach (var tab in injected)
|
|
Plugin.Config.Tabs.Remove(tab);
|
|
}
|
|
Plugin.Config.AutoTellTabsCompactDisplay = savedCompact;
|
|
Plugin.Config.SidebarWidth = savedSidebarWidth;
|
|
}
|
|
|
|
return SelfTestStepResult.Pass;
|
|
}
|
|
|
|
public void CleanUp() { }
|
|
|
|
// Mirror of AutoTellTabsService.BuildTempTab (the real builder is
|
|
// private); only the sheet-based tab name is replaced with a literal.
|
|
private static Tab BuildTempProbe(string name, bool pinned) =>
|
|
new()
|
|
{
|
|
Name = name,
|
|
IsTempTab = true,
|
|
IsPinned = pinned,
|
|
AllSenderMessages = true,
|
|
TellTarget = new TellTarget(name, 0, 0, TellReason.Direct),
|
|
Channel = InputChannel.Tell,
|
|
DisplayTimestamp = true,
|
|
UnreadMode = UnreadMode.Unseen,
|
|
HideWhenInactive = false,
|
|
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
|
|
{
|
|
[ChatType.TellIncoming] = (ChatSourceExt.All, ChatSourceExt.All),
|
|
[ChatType.TellOutgoing] = (ChatSourceExt.All, ChatSourceExt.All),
|
|
},
|
|
};
|
|
}
|