Merge branch 'feature/v1.8.7' into feature/v1.8.0
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||
<PropertyGroup>
|
||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||
<Version>1.8.6</Version>
|
||||
<Version>1.8.7</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Use lock file to pin exact versions -->
|
||||
|
||||
@@ -195,4 +195,23 @@ internal sealed class HonorificService : IDisposable
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Test seam: the three status fields are private-set and IPC-driven, which a
|
||||
// headless /xlperf run can't reach (Honorific is usually absent in tests).
|
||||
// Callers MUST snapshot the prior values and restore them in CleanUp, and
|
||||
// MUST drive Set -> Draw -> Assert within ONE synchronous RunStep (never
|
||||
// Waiting between Set and Assert) — a between-frame OnReady/OnTitleChanged
|
||||
// would otherwise clobber this state and a CleanUp restore can't un-corrupt a
|
||||
// mid-flight assertion. (A FontsReady precondition gate returning Waiting
|
||||
// BEFORE the snapshot/Set is fine — nothing is mutated yet.)
|
||||
internal void TestOnly_SetState(
|
||||
bool isAvailable,
|
||||
(uint Major, uint Minor)? detectedApiVersion,
|
||||
HonorificTitleData? title
|
||||
)
|
||||
{
|
||||
IsAvailable = isAvailable;
|
||||
DetectedApiVersion = detectedApiVersion;
|
||||
CurrentTitle = title;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace HellionChat.Integrations;
|
||||
|
||||
internal enum HonorificStatusKind
|
||||
{
|
||||
NotInstalled,
|
||||
Incompatible,
|
||||
Detected,
|
||||
}
|
||||
|
||||
internal static class HonorificStatus
|
||||
{
|
||||
// Mirrors the 1.5.6 three-state discriminator (1d3b429:About.cs:171/183/196):
|
||||
// it keys on IsAvailable + the *nullability* of DetectedApiVersion, never a
|
||||
// recomputed major check. IsAvailable already encodes the compatibility
|
||||
// result HonorificService set during the initial pull. Null-safe: an
|
||||
// (isAvailable=true, detectedApiVersion=null) state a test seam can produce
|
||||
// resolves to NotInstalled rather than dereferencing null.
|
||||
internal static HonorificStatusKind Resolve(
|
||||
bool isAvailable,
|
||||
(uint Major, uint Minor)? detectedApiVersion
|
||||
)
|
||||
{
|
||||
if (isAvailable && detectedApiVersion is not null)
|
||||
return HonorificStatusKind.Detected;
|
||||
if (detectedApiVersion is not null)
|
||||
return HonorificStatusKind.Incompatible;
|
||||
return HonorificStatusKind.NotInstalled;
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,10 @@ namespace HellionChat.Integrations;
|
||||
// Local DTO mirroring Honorific's TitleData — no hard reference to Honorific.dll
|
||||
// so HellionChat loads cleanly when Honorific is absent.
|
||||
//
|
||||
// Only Glow is rendered. Color3, GradientColourSet and GradientAnimationStyle
|
||||
// are parsed but unused — the animated gradient lives entirely inside Honorific
|
||||
// and is not exposed over IPC, so reproducing it here would mean shipping our
|
||||
// own copy of Honorific's colour palette. The fields stay in the DTO so the
|
||||
// JSON roundtrip remains lossless.
|
||||
// Color is rendered in the header title slot (HonorificHeader). Glow, Color3,
|
||||
// GradientColourSet and GradientAnimationStyle are parsed but not rendered —
|
||||
// the animated gradient lives inside Honorific and is not exposed over IPC.
|
||||
// The fields stay in the DTO so the JSON roundtrip remains lossless.
|
||||
internal sealed record HonorificTitleData(
|
||||
string? Title,
|
||||
bool IsPrefix,
|
||||
|
||||
@@ -388,6 +388,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
new SelfTests.ConfigMigrationV23Step(this),
|
||||
new SelfTests.HoverSheenAllocStep(this),
|
||||
new SelfTests.HonorificHeaderRenderStep(this),
|
||||
new SelfTests.AboutIntegrationsStatusStep(this),
|
||||
new SelfTests.PerformanceBaselineStep(this),
|
||||
new SelfTests.MainWindowFocusOpacityStep(this),
|
||||
new SelfTests.MainWindowFlagsStep(this),
|
||||
|
||||
@@ -166,7 +166,8 @@ internal static class PluginHostFactory
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.LivePreviewPanel(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
|
||||
sp.GetRequiredService<FontManager>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.ThemeImportExportRow(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
@@ -195,7 +196,10 @@ internal static class PluginHostFactory
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.AboutTab(
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<ILogger<Ui.Components.Settings.Tabs.AboutTab>>()
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<Integrations.HonorificService>(),
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<IPlatformUtil>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.StatusBar(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin.SelfTest;
|
||||
using HellionChat.Integrations;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// Verifies the About-tab integrations status. The pure HonorificStatus.Resolve
|
||||
// covers the three-state mapping (false-green-free); driving the real AboutTab
|
||||
// render once proves the render path actually calls the resolver (sets
|
||||
// LastHonorificStatusKey). Set -> Draw -> Assert happen in ONE synchronous
|
||||
// RunStep so a between-frame Honorific IPC callback can't clobber the seam
|
||||
// state; the prior service state is restored in CleanUp.
|
||||
internal sealed class AboutIntegrationsStatusStep : ISelfTestStep
|
||||
{
|
||||
private readonly Plugin plugin;
|
||||
|
||||
private HonorificService? _svc;
|
||||
private bool _prevAvailable;
|
||||
private (uint Major, uint Minor)? _prevVersion;
|
||||
private HonorificTitleData? _prevTitle;
|
||||
private bool _snapshotted;
|
||||
|
||||
public AboutIntegrationsStatusStep(Plugin plugin)
|
||||
{
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public string Name => "Hellion Chat - About integrations status";
|
||||
|
||||
public SelfTestStepResult RunStep()
|
||||
{
|
||||
// AboutTab.Draw renders DrawBrand/coming-soon under _fonts.FontAwesome.Push;
|
||||
// wait until the atlas is built so the render can't misbehave. Returned
|
||||
// BEFORE any snapshot/Set, so no seam state leaks (same guard as the header
|
||||
// step; precedent FoxBannerTextureSmokeStep).
|
||||
if (!plugin.FontManager.FontsReady)
|
||||
{
|
||||
return SelfTestStepResult.Waiting;
|
||||
}
|
||||
|
||||
// Pure mapping (incl. the isAvailable=true + null boundary -> NotInstalled).
|
||||
if (
|
||||
HonorificStatus.Resolve(true, (3, 1)) != HonorificStatusKind.Detected
|
||||
|| HonorificStatus.Resolve(false, (2, 5)) != HonorificStatusKind.Incompatible
|
||||
|| HonorificStatus.Resolve(false, null) != HonorificStatusKind.NotInstalled
|
||||
|| HonorificStatus.Resolve(true, null) != HonorificStatusKind.NotInstalled
|
||||
)
|
||||
{
|
||||
ImGui.Text("HonorificStatus.Resolve mapping is wrong");
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
|
||||
var about = plugin.SettingsWindow.GetAboutTabForSelfTest();
|
||||
if (about is null)
|
||||
{
|
||||
ImGui.Text("SettingsWindow.AboutTab reference is null");
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
|
||||
_svc = plugin.MainWindow.GetHonorificHeaderForSelfTest()?.GetServiceForSelfTest();
|
||||
if (_svc is null)
|
||||
{
|
||||
ImGui.Text("HonorificService reference is null");
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
|
||||
_prevAvailable = _svc.IsAvailable;
|
||||
_prevVersion = _svc.DetectedApiVersion;
|
||||
_prevTitle = _svc.CurrentTitle;
|
||||
_snapshotted = true;
|
||||
|
||||
try
|
||||
{
|
||||
// Drive the real render once and confirm the resolver is wired in.
|
||||
_svc.TestOnly_SetState(true, (3, 1), null);
|
||||
about.Draw();
|
||||
if (about.LastHonorificStatusKey != HonorificStatusKind.Detected.ToString())
|
||||
{
|
||||
ImGui.Text(
|
||||
$"About render did not resolve Detected (got {about.LastHonorificStatusKey})"
|
||||
);
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ImGui.Text($"AboutTab.Draw threw: {ex.GetType().Name}: {ex.Message}");
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
|
||||
return SelfTestStepResult.Pass;
|
||||
}
|
||||
|
||||
public void CleanUp()
|
||||
{
|
||||
if (!_snapshotted || _svc is null)
|
||||
return;
|
||||
_svc.TestOnly_SetState(_prevAvailable, _prevVersion, _prevTitle);
|
||||
_snapshotted = false;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin.SelfTest;
|
||||
using HellionChat.Integrations;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
@@ -20,8 +21,26 @@ internal sealed class HonorificHeaderRenderStep : ISelfTestStep
|
||||
|
||||
public string Name => "Hellion Chat - HonorificHeader render";
|
||||
|
||||
private HonorificService? _svc;
|
||||
private bool _prevAvailable;
|
||||
private (uint Major, uint Minor)? _prevVersion;
|
||||
private HonorificTitleData? _prevTitle;
|
||||
private bool _prevToggle;
|
||||
private bool _snapshotted;
|
||||
|
||||
public SelfTestStepResult RunStep()
|
||||
{
|
||||
// HonorificHeader.Draw early-returns on !FontsReady (HonorificHeader.cs:40-44)
|
||||
// and never reaches the gated title branch, which would make assert (a) a
|
||||
// false FAIL during a font-atlas rebuild. Return Waiting BEFORE any
|
||||
// snapshot/mutation so the runner re-polls cleanly and no seam state leaks
|
||||
// (precedent: FoxBannerTextureSmokeStep). This is a pre-Set precondition
|
||||
// gate, not a mid-test Waiting — the Set->Draw->Assert window stays synchronous.
|
||||
if (!plugin.FontManager.FontsReady)
|
||||
{
|
||||
return SelfTestStepResult.Waiting;
|
||||
}
|
||||
|
||||
var header = plugin.MainWindow.GetHonorificHeaderForSelfTest();
|
||||
if (header is null)
|
||||
{
|
||||
@@ -29,9 +48,48 @@ internal sealed class HonorificHeaderRenderStep : ISelfTestStep
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
|
||||
_svc = header.GetServiceForSelfTest();
|
||||
_prevAvailable = _svc.IsAvailable;
|
||||
_prevVersion = _svc.DetectedApiVersion;
|
||||
_prevTitle = _svc.CurrentTitle;
|
||||
_prevToggle = Plugin.Config.ShowHonorificTitleInHeader;
|
||||
_snapshotted = true;
|
||||
|
||||
var valid = new HonorificTitleData("Champion", false, false, null, null, null, null, null);
|
||||
var original = new HonorificTitleData("Champion", false, true, null, null, null, null, null);
|
||||
|
||||
// Draw at a deliberately wide 420px so the title never hits the truncation
|
||||
// clamp — LastTitleRendered then reflects the GATE outcome, not the width.
|
||||
try
|
||||
{
|
||||
// (a) available + valid title + toggle on -> title renders
|
||||
Plugin.Config.ShowHonorificTitleInHeader = true;
|
||||
_svc.TestOnly_SetState(true, (3, 1), valid);
|
||||
header.Draw(420f);
|
||||
if (!header.LastTitleRendered)
|
||||
{
|
||||
ImGui.Text("Gate failed: valid title did not render");
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
|
||||
// (b) toggle off -> title suppressed (crown stays, untestable headless)
|
||||
Plugin.Config.ShowHonorificTitleInHeader = false;
|
||||
header.Draw(420f);
|
||||
if (header.LastTitleRendered)
|
||||
{
|
||||
ImGui.Text("Gate failed: title rendered with toggle off");
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
|
||||
// (c) IsOriginal title -> suppressed even with toggle on
|
||||
Plugin.Config.ShowHonorificTitleInHeader = true;
|
||||
_svc.TestOnly_SetState(true, (3, 1), original);
|
||||
header.Draw(420f);
|
||||
if (header.LastTitleRendered)
|
||||
{
|
||||
ImGui.Text("Gate failed: original title rendered");
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -42,5 +100,12 @@ internal sealed class HonorificHeaderRenderStep : ISelfTestStep
|
||||
return SelfTestStepResult.Pass;
|
||||
}
|
||||
|
||||
public void CleanUp() { }
|
||||
public void CleanUp()
|
||||
{
|
||||
if (!_snapshotted || _svc is null)
|
||||
return;
|
||||
Plugin.Config.ShowHonorificTitleInHeader = _prevToggle;
|
||||
_svc.TestOnly_SetState(_prevAvailable, _prevVersion, _prevTitle);
|
||||
_snapshotted = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,12 @@ step explicitly as smoke-only instead of faking a headless pass.
|
||||
|
||||
## Anti-pattern of record
|
||||
|
||||
`HonorificService.ShouldRenderSlot` had zero production callers and was green
|
||||
only because the test called it directly — a test passing on a path the game
|
||||
never runs. That is the failure this standard prevents.
|
||||
`HonorificService.ShouldRenderSlot` once had zero production callers and was
|
||||
green only because the test called it directly — a test passing on a path the
|
||||
game never runs. v1.8.7 retired it: the gate is now wired into the real
|
||||
`HonorificHeader.Draw` and asserted through it via
|
||||
`HonorificHeader.LastTitleRendered` (see `HonorificHeaderRenderStep`). Kept here
|
||||
as the canonical example of the failure this standard prevents.
|
||||
|
||||
## Step classification
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@ internal sealed class HonorificHeader
|
||||
{
|
||||
public const float Height = 30f;
|
||||
|
||||
// SelfTest observables — set on the real Draw path so a headless step can
|
||||
// assert the gate/colour/truncation outcome instead of re-implementing it.
|
||||
internal bool LastTitleRendered { get; private set; }
|
||||
internal uint LastTitleColorAbgr { get; private set; }
|
||||
internal string? LastRenderedTitle { get; private set; }
|
||||
|
||||
private readonly HonorificService _honorific;
|
||||
private readonly FontManager _fonts;
|
||||
private readonly ThemeRegistry _themes;
|
||||
@@ -33,8 +39,15 @@ internal sealed class HonorificHeader
|
||||
_resolver = resolver;
|
||||
}
|
||||
|
||||
// Same singleton the AboutTab integrations section uses; lets a SelfTest
|
||||
// drive the gate branches via HonorificService.TestOnly_SetState.
|
||||
internal HonorificService GetServiceForSelfTest() => _honorific;
|
||||
|
||||
public void Draw(float maxWidth)
|
||||
{
|
||||
LastTitleRendered = false;
|
||||
LastRenderedTitle = null;
|
||||
|
||||
// First-frame guard: components must not lay out before the atlas
|
||||
// is finished or text metrics collapse into placeholder widths.
|
||||
if (!_fonts.FontsReady)
|
||||
@@ -58,11 +71,35 @@ internal sealed class HonorificHeader
|
||||
dl.AddText(origin + new Vector2(0f, 8f), crownColor, crownGlyph);
|
||||
}
|
||||
|
||||
var title = _honorific.IsAvailable ? _honorific.CurrentTitle?.Title : null;
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
// Gate the bracketed title through the 1.5.6 contract (toggle, IPC
|
||||
// availability, IsOriginal, empty-title) — the crown above stays
|
||||
// unconditional as the permanent brand anchor. NOTE divergence from
|
||||
// 1.5.6: there a failed gate hid the whole slot incl. crown; here the
|
||||
// crown persists by design.
|
||||
if (
|
||||
HonorificService.ShouldRenderSlot(
|
||||
Plugin.Config.ShowHonorificTitleInHeader,
|
||||
_honorific.IsAvailable,
|
||||
_honorific.CurrentTitle
|
||||
)
|
||||
)
|
||||
{
|
||||
var titleColor = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
|
||||
dl.AddText(origin + new Vector2(crownWidth + 6f, 8f), titleColor, $"«{title}»");
|
||||
var current = _honorific.CurrentTitle!;
|
||||
var titleColor = HonorificTitleColor.ResolveTitleAbgr(current.Color, theme);
|
||||
LastTitleColorAbgr = titleColor;
|
||||
|
||||
// Budget the title against the row width. CalcTextSize inside
|
||||
// TruncateToFitWidth measures the *Regular* font, so this must run
|
||||
// OUTSIDE the FontAwesome.Push block above (crownWidth was measured
|
||||
// inside it, which is correct).
|
||||
var maxTitleWidth = maxWidth - crownWidth - 6f - 8f;
|
||||
if (maxTitleWidth > 0f)
|
||||
{
|
||||
var rendered = StringUtil.TruncateToFitWidth($"«{current.Title}»", maxTitleWidth);
|
||||
LastRenderedTitle = rendered;
|
||||
dl.AddText(origin + new Vector2(crownWidth + 6f, 8f), titleColor, rendered);
|
||||
LastTitleRendered = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve the row height even when no title rendered so the layout
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Numerics;
|
||||
using HellionChat.Themes;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Ui.Components;
|
||||
|
||||
// Resolves the bracketed-title colour for the Honorific header, shared by the
|
||||
// real header (HonorificHeader) and the settings theme preview (LivePreviewPanel)
|
||||
// so the fallback never drifts between them. A title colour supplied by Honorific
|
||||
// (0..1 normalised RGB over IPC) renders as-is; absent colour falls back to the
|
||||
// theme's primary text. The Vector4ToRgba path clamps each component to [0,1] so
|
||||
// an out-of-range value from the JSON IPC payload cannot wrap the byte cast.
|
||||
internal static class HonorificTitleColor
|
||||
{
|
||||
internal static uint ResolveTitleAbgr(Vector3? color, Theme theme)
|
||||
{
|
||||
return color is { } c
|
||||
? ColourUtil.RgbaToAbgr(ColourUtil.Vector4ToRgba(new Vector4(c, 1f)))
|
||||
: ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using HellionChat.Themes;
|
||||
using HellionChat.Ui.StyleEngine;
|
||||
@@ -21,21 +22,21 @@ internal sealed class LivePreviewPanel : IDisposable
|
||||
private const string MockTell = "Tell → Player: Hey, want to party?";
|
||||
private const string MockFc = "FC: Welcome aboard.";
|
||||
|
||||
// FontAwesome is intentionally not pulled in — crown/cog render as Unicode
|
||||
// glyphs in the default font so this panel stays DI-light (Step 2 scope).
|
||||
private const string CrownGlyph = "♛";
|
||||
private const string CogGlyph = "⚙";
|
||||
// Crown/cog render via the FontAwesome font (FontManager) so the preview
|
||||
// matches the real header glyphs; the bundled text font has no crown glyph.
|
||||
|
||||
private const float MiddleBandHeight = 220f;
|
||||
private const float SidebarWidth = 70f;
|
||||
|
||||
private readonly ThemeRegistry _themes;
|
||||
private readonly TokenResolver _resolver;
|
||||
private readonly FontManager _fonts;
|
||||
|
||||
public LivePreviewPanel(ThemeRegistry themes, TokenResolver resolver)
|
||||
public LivePreviewPanel(ThemeRegistry themes, TokenResolver resolver, FontManager fonts)
|
||||
{
|
||||
_themes = themes;
|
||||
_resolver = resolver;
|
||||
_fonts = fonts;
|
||||
_themes.OnEditingBufferChanged += OnBufferChanged;
|
||||
Interlocked.Increment(ref InstanceCount);
|
||||
}
|
||||
@@ -124,7 +125,7 @@ internal sealed class LivePreviewPanel : IDisposable
|
||||
ImGui.Dummy(new Vector2(width, height));
|
||||
}
|
||||
|
||||
private static void DrawHonorificHeader(Theme theme)
|
||||
private void DrawHonorificHeader(Theme theme)
|
||||
{
|
||||
const float height = 32f;
|
||||
var draw = ImGui.GetWindowDrawList();
|
||||
@@ -139,15 +140,30 @@ internal sealed class LivePreviewPanel : IDisposable
|
||||
);
|
||||
|
||||
var crownAbgr = ColourUtil.RgbaToAbgr(theme.Colors.Identity);
|
||||
var textAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
|
||||
// Shared fallback path with the real header (Weiche 3). The mock has no
|
||||
// Honorific colour, so this resolves to TextPrimary today — visually
|
||||
// unchanged — but both paths now share one resolver. No truncation here:
|
||||
// the preview draws a fixed, centred "«Champion» Preview" string.
|
||||
var textAbgr = HonorificTitleColor.ResolveTitleAbgr(null, theme);
|
||||
var title = "«Champion» Preview";
|
||||
var crownSize = ImGui.CalcTextSize(CrownGlyph);
|
||||
var crownGlyph = FontAwesomeIcon.Crown.ToIconString();
|
||||
|
||||
// Crown is a FontAwesome glyph (matches the real header); measure + draw
|
||||
// it inside the FontAwesome push, the title stays in the default font.
|
||||
float crownWidth;
|
||||
using (_fonts.FontAwesome.Push())
|
||||
{
|
||||
crownWidth = ImGui.CalcTextSize(crownGlyph).X;
|
||||
}
|
||||
var titleSize = ImGui.CalcTextSize(title);
|
||||
var totalWidth = crownSize.X + 4f + titleSize.X;
|
||||
var totalWidth = crownWidth + 4f + titleSize.X;
|
||||
var startX = origin.X + (width - totalWidth) * 0.5f;
|
||||
var y = origin.Y + (height - titleSize.Y) * 0.5f;
|
||||
draw.AddText(new Vector2(startX, y), crownAbgr, CrownGlyph);
|
||||
draw.AddText(new Vector2(startX + crownSize.X + 4f, y), textAbgr, title);
|
||||
using (_fonts.FontAwesome.Push())
|
||||
{
|
||||
draw.AddText(new Vector2(startX, y), crownAbgr, crownGlyph);
|
||||
}
|
||||
draw.AddText(new Vector2(startX + crownWidth + 4f, y), textAbgr, title);
|
||||
|
||||
ImGui.Dummy(new Vector2(width, height));
|
||||
}
|
||||
@@ -240,7 +256,7 @@ internal sealed class LivePreviewPanel : IDisposable
|
||||
ImGui.Dummy(new Vector2(totalWidth, MiddleBandHeight));
|
||||
}
|
||||
|
||||
private static void DrawInputBar(Theme theme)
|
||||
private void DrawInputBar(Theme theme)
|
||||
{
|
||||
const float height = 24f;
|
||||
const float pillWidth = 50f;
|
||||
@@ -268,9 +284,16 @@ internal sealed class LivePreviewPanel : IDisposable
|
||||
var phPos = new Vector2(pillMax.X + 6f, origin.Y + (height - phSize.Y) * 0.5f);
|
||||
draw.AddText(phPos, ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary), placeholder);
|
||||
|
||||
var cogSize = ImGui.CalcTextSize(CogGlyph);
|
||||
var cogPos = new Vector2(max.X - cogSize.X - 6f, origin.Y + (height - cogSize.Y) * 0.5f);
|
||||
draw.AddText(cogPos, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted), CogGlyph);
|
||||
var cogGlyph = FontAwesomeIcon.Cog.ToIconString();
|
||||
using (_fonts.FontAwesome.Push())
|
||||
{
|
||||
var cogSize = ImGui.CalcTextSize(cogGlyph);
|
||||
var cogPos = new Vector2(
|
||||
max.X - cogSize.X - 6f,
|
||||
origin.Y + (height - cogSize.Y) * 0.5f
|
||||
);
|
||||
draw.AddText(cogPos, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted), cogGlyph);
|
||||
}
|
||||
|
||||
ImGui.Dummy(new Vector2(width, height));
|
||||
}
|
||||
|
||||
@@ -1,30 +1,52 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using HellionChat.Branding;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using HellionChat.Integrations;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Themes;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Ui.Components.Settings.Tabs;
|
||||
|
||||
internal sealed class AboutTab
|
||||
{
|
||||
private readonly FontManager _fonts;
|
||||
private readonly ILogger<AboutTab> _logger;
|
||||
private readonly Plugin _plugin;
|
||||
private readonly HonorificService _honorific;
|
||||
private readonly ThemeRegistry _themes;
|
||||
private readonly IPlatformUtil _platformUtil;
|
||||
|
||||
public AboutTab(FontManager fonts, ILogger<AboutTab> logger)
|
||||
// SelfTest observable — the status key the real render path resolved.
|
||||
internal string? LastHonorificStatusKey { get; private set; }
|
||||
|
||||
public AboutTab(
|
||||
FontManager fonts,
|
||||
Plugin plugin,
|
||||
HonorificService honorific,
|
||||
ThemeRegistry themes,
|
||||
IPlatformUtil platformUtil
|
||||
)
|
||||
{
|
||||
_fonts = fonts;
|
||||
_logger = logger;
|
||||
_plugin = plugin;
|
||||
_honorific = honorific;
|
||||
_themes = themes;
|
||||
_platformUtil = platformUtil;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
// Reset the SelfTest observable each frame so a stale value from a prior
|
||||
// real render can never let the integrations-status SelfTest pass falsely.
|
||||
LastHonorificStatusKey = null;
|
||||
DrawPluginInfo();
|
||||
DrawSectionHeader("Brand");
|
||||
DrawBrand();
|
||||
DrawSectionHeader("Links");
|
||||
DrawLinks();
|
||||
DrawSectionHeader("Integrations");
|
||||
DrawIntegrations();
|
||||
DrawSectionHeader("Credits");
|
||||
DrawCredits();
|
||||
DrawSectionHeader("License");
|
||||
@@ -74,24 +96,152 @@ internal sealed class AboutTab
|
||||
DrawLinkButton("Custom repo manifest", BrandingLinks.HellionChatCustomRepoManifest);
|
||||
}
|
||||
|
||||
// URLs in v1.7.0 are exclusively hardcoded BrandingLinks.* constants —
|
||||
// Process.Start with UseShellExecute=true is safe under that constraint.
|
||||
// If a future cycle ever feeds user-supplied URLs here, add an https/http
|
||||
// allow-list filter via Uri.TryCreate before Process.Start; without it
|
||||
// UseShellExecute would happily launch file:// or shell-protocol handlers.
|
||||
private void DrawIntegrations()
|
||||
{
|
||||
ImGui.TextWrapped(HellionStrings.Settings_Integrations_Intro);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.TextUnformatted(HellionStrings.Settings_Integrations_Honorific_SectionHeader);
|
||||
DrawHonorificStatus();
|
||||
DrawToggle(
|
||||
HellionStrings.Settings_Integrations_Honorific_Toggle,
|
||||
() => Plugin.Config.ShowHonorificTitleInHeader,
|
||||
v => Plugin.Config.ShowHonorificTitleInHeader = v
|
||||
);
|
||||
ImGui.TextDisabled(HellionStrings.Settings_Integrations_Honorific_ToggleHint);
|
||||
DrawLinkButton(
|
||||
HellionStrings.Settings_Integrations_Honorific_LinkRepo,
|
||||
IntegrationLinks.HonorificRepo
|
||||
);
|
||||
DrawLinkButton(
|
||||
HellionStrings.Settings_Integrations_Honorific_LinkAuthor,
|
||||
IntegrationLinks.HonorificAuthor
|
||||
);
|
||||
|
||||
DrawComingSoon();
|
||||
DrawGotAnIdea();
|
||||
}
|
||||
|
||||
private void DrawHonorificStatus()
|
||||
{
|
||||
var kind = HonorificStatus.Resolve(_honorific.IsAvailable, _honorific.DetectedApiVersion);
|
||||
LastHonorificStatusKey = kind.ToString();
|
||||
var colors = _themes.Active.Colors;
|
||||
|
||||
// Null-safety via the `is { } v` pattern, never `.Value` raw (spec SEC-2):
|
||||
// the version is bound only on the arms that have it; the impossible
|
||||
// Detected/Incompatible-without-version state falls through to default.
|
||||
switch (kind)
|
||||
{
|
||||
case HonorificStatusKind.Detected when _honorific.DetectedApiVersion is { } v:
|
||||
DrawStatusGlyph('●', colors.StatusSuccess);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(
|
||||
string.Format(
|
||||
HellionStrings.Settings_Integrations_Honorific_Status_Detected,
|
||||
v.Major,
|
||||
v.Minor
|
||||
)
|
||||
);
|
||||
break;
|
||||
case HonorificStatusKind.Incompatible when _honorific.DetectedApiVersion is { } iv:
|
||||
DrawStatusGlyph('⚠', colors.StatusWarning);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(
|
||||
string.Format(
|
||||
HellionStrings.Settings_Integrations_Honorific_Status_Incompatible,
|
||||
HonorificService.ExpectedApiMajor,
|
||||
iv.Major,
|
||||
iv.Minor
|
||||
)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
DrawStatusGlyph('○', colors.TextMuted);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(
|
||||
HellionStrings.Settings_Integrations_Honorific_Status_NotInstalled
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawStatusGlyph(char glyph, uint rgba)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(rgba));
|
||||
ImGui.TextUnformatted(glyph.ToString());
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
private void DrawComingSoon()
|
||||
{
|
||||
ImGui.Spacing();
|
||||
ImGui.TextUnformatted(HellionStrings.Settings_Integrations_ComingSoon_SectionHeader);
|
||||
ImGui.TextDisabled(HellionStrings.Settings_Integrations_ComingSoon_Intro);
|
||||
DrawComingSoonItem(
|
||||
HellionStrings.Settings_Integrations_ComingSoon_ContextMenu_Title,
|
||||
HellionStrings.Settings_Integrations_ComingSoon_ContextMenu_Description
|
||||
);
|
||||
DrawComingSoonItem(
|
||||
HellionStrings.Settings_Integrations_ComingSoon_Notifications_Title,
|
||||
HellionStrings.Settings_Integrations_ComingSoon_Notifications_Description
|
||||
);
|
||||
DrawComingSoonItem(
|
||||
HellionStrings.Settings_Integrations_ComingSoon_RPStatus_Title,
|
||||
HellionStrings.Settings_Integrations_ComingSoon_RPStatus_Description
|
||||
);
|
||||
DrawComingSoonItem(
|
||||
HellionStrings.Settings_Integrations_ComingSoon_ExtraChat_Title,
|
||||
HellionStrings.Settings_Integrations_ComingSoon_ExtraChat_Description
|
||||
);
|
||||
DrawComingSoonItem(
|
||||
HellionStrings.Settings_Integrations_ComingSoon_QuickDM_Title,
|
||||
HellionStrings.Settings_Integrations_ComingSoon_QuickDM_Description
|
||||
);
|
||||
}
|
||||
|
||||
private void DrawComingSoonItem(string title, string description)
|
||||
{
|
||||
using (_fonts.FontAwesome.Push())
|
||||
{
|
||||
ImGui.TextDisabled(FontAwesomeIcon.Hourglass.ToIconString());
|
||||
}
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(title);
|
||||
ImGui.TextDisabled(description);
|
||||
}
|
||||
|
||||
private void DrawGotAnIdea()
|
||||
{
|
||||
ImGui.Spacing();
|
||||
ImGui.TextUnformatted(HellionStrings.Settings_Integrations_GotAnIdea_SectionHeader);
|
||||
ImGui.TextWrapped(HellionStrings.Settings_Integrations_GotAnIdea_Body);
|
||||
if (ImGui.Button(HellionStrings.Settings_Integrations_GotAnIdea_LinkLabel))
|
||||
{
|
||||
_platformUtil.OpenLink(BrandingLinks.HellionForgeDiscordInvite);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawToggle(string label, Func<bool> get, Action<bool> set)
|
||||
{
|
||||
var current = get();
|
||||
if (ImGui.Checkbox(label, ref current))
|
||||
{
|
||||
set(current);
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
// URLs are exclusively hardcoded BrandingLinks/IntegrationLinks constants,
|
||||
// validated to http/https at module-init. OpenLink centralises the browser
|
||||
// open on an off-draw thread (it internally uses the same ShellExecute, so
|
||||
// this is a consistency cleanup, not a security change). The standalone Copy
|
||||
// button stays as the clipboard path.
|
||||
private void DrawLinkButton(string label, string url)
|
||||
{
|
||||
if (ImGui.Button(label))
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not open {Url}, copying to clipboard instead", url);
|
||||
ImGui.SetClipboardText(url);
|
||||
}
|
||||
_platformUtil.OpenLink(url);
|
||||
}
|
||||
ImGui.SameLine();
|
||||
if (ImGui.SmallButton($"Copy##{url}"))
|
||||
|
||||
@@ -112,4 +112,8 @@ internal sealed class SettingsWindow : Window
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// AboutTab is owned here (not MainWindow) and rendered only via the private
|
||||
// RenderActiveTab; this exposes it for the integrations-status SelfTest.
|
||||
internal AboutTab GetAboutTabForSelfTest() => _about;
|
||||
}
|
||||
|
||||
@@ -34,8 +34,7 @@ internal static class StringUtil
|
||||
|
||||
// Returns the text unchanged when it already fits the width budget,
|
||||
// otherwise the longest prefix plus a horizontal-ellipsis character that
|
||||
// still fits. Used by the chat header Honorific title slot and reused by
|
||||
// the chat-line truncation path in later cycles.
|
||||
// still fits. Used by the HonorificHeader title slot (HonorificHeader.Draw).
|
||||
public static string TruncateToFitWidth(string text, float maxWidth)
|
||||
{
|
||||
if (ImGui.CalcTextSize(text).X <= maxWidth)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"Author": "Jon Kazama (Hellion Forge)",
|
||||
"Name": "Hellion Chat",
|
||||
"InternalName": "HellionChat",
|
||||
"AssemblyVersion": "1.8.6.0",
|
||||
"AssemblyVersion": "1.8.7.0",
|
||||
"Description": "A Hellion Forge plugin — privacy-focused chat replacement for FINAL FANTASY XIV, built for EU, US and JP data rules.\n\nBy default only your own conversations are stored. Public chat, NPC dialogue, system messages and battle logs are discarded at the storage layer unless you opt in. Retention windows are configurable per channel, history can be wiped retroactively, and everything can be exported on demand.\n\nFeatures:\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with preview and Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with four profiles: Privacy-First, Casual, Roleplay, Full History\n- Multi-language UI (24 locales) with live language switching\n- Own config and database — no shared state with other plugins\n\nBased on Chat 2 by Infi and Anna (EUPL-1.2).\nSupport: https://discord.gg/X9V7Kcv5gR",
|
||||
"ApplicableVersion": "any",
|
||||
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat",
|
||||
@@ -25,7 +25,7 @@
|
||||
"DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.6/latest.zip",
|
||||
"DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.6/latest.zip",
|
||||
"DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.6/latest.zip",
|
||||
"TestingAssemblyVersion": "1.8.6.0",
|
||||
"TestingAssemblyVersion": "1.8.7.0",
|
||||
"IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png",
|
||||
"ImageUrls": [
|
||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png",
|
||||
|
||||
Reference in New Issue
Block a user