diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index 0de13eb..9c02edc 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -104,6 +104,11 @@ internal static class PluginHostFactory // Transient: each surface owns its motes, so two backdrops on screen do // not drift in lockstep. + services.AddSingleton(sp => new Ui.Components.Settings.SectionRenderer( + sp.GetRequiredService(), + sp.GetRequiredService() + )); + services.AddTransient(sp => new Ui.StyleEngine.SurfaceBackdrop( sp.GetRequiredService(), sp.GetRequiredService() @@ -176,10 +181,12 @@ internal static class PluginHostFactory )); services.AddSingleton(sp => new Ui.Components.Settings.ThemePicker( sp.GetRequiredService(), - sp.GetRequiredService() + sp.GetRequiredService(), + sp.GetRequiredService() )); services.AddSingleton(sp => new Ui.Components.Settings.ColorPicker( - sp.GetRequiredService() + sp.GetRequiredService(), + sp.GetRequiredService() )); services.AddSingleton(sp => new Ui.Components.Settings.LivePreviewPanel( sp.GetRequiredService(), @@ -192,11 +199,13 @@ internal static class PluginHostFactory )); services.AddSingleton(sp => new Ui.Components.Settings.FontsSection( sp.GetRequiredService(), - sp.GetRequiredService() + sp.GetRequiredService(), + sp.GetRequiredService() )); services.AddSingleton(sp => new Ui.Components.Settings.ChatColourPicker( sp.GetRequiredService(), - sp.GetRequiredService() + sp.GetRequiredService(), + sp.GetRequiredService() )); services.AddSingleton(sp => new Ui.Components.Settings.Tabs.AppearanceTab( sp.GetRequiredService(), diff --git a/HellionChat/Ui/Components/Settings/ChatColourPicker.cs b/HellionChat/Ui/Components/Settings/ChatColourPicker.cs index 733305f..efade1a 100644 --- a/HellionChat/Ui/Components/Settings/ChatColourPicker.cs +++ b/HellionChat/Ui/Components/Settings/ChatColourPicker.cs @@ -17,11 +17,13 @@ internal sealed class ChatColourPicker { private readonly Plugin _plugin; private readonly ThemeRegistry _themes; + private readonly SectionRenderer _sections; private string? _applyDismissedFor; private string? _lastSeenSlug; - public ChatColourPicker(Plugin plugin, ThemeRegistry themes) + public ChatColourPicker(Plugin plugin, ThemeRegistry themes, SectionRenderer sections) { + _sections = sections; _plugin = plugin; _themes = themes; } @@ -32,7 +34,13 @@ internal sealed class ChatColourPicker public void Draw() { - if (!ImGui.CollapsingHeader(HellionStrings.Settings_Section_Colours)) + if ( + !_sections.Draw( + ImGui.GetID("appearance.chatcolours"u8), + HellionStrings.Settings_Section_Colours, + open: false + ) + ) return; DrawPresetButtons(); diff --git a/HellionChat/Ui/Components/Settings/ColorPicker.cs b/HellionChat/Ui/Components/Settings/ColorPicker.cs index b991910..bda8c08 100644 --- a/HellionChat/Ui/Components/Settings/ColorPicker.cs +++ b/HellionChat/Ui/Components/Settings/ColorPicker.cs @@ -9,9 +9,11 @@ namespace HellionChat.Ui.Components.Settings; internal sealed class ColorPicker { private readonly ThemeRegistry _themes; + private readonly SectionRenderer _sections; - public ColorPicker(ThemeRegistry themes) + public ColorPicker(ThemeRegistry themes, SectionRenderer sections) { + _sections = sections; _themes = themes; } @@ -89,6 +91,7 @@ internal sealed class ColorPicker ImGui.Separator(); DrawSection( + ImGui.GetID("colors.surfaces"u8), "Surfaces", buffer, c => @@ -112,6 +115,7 @@ internal sealed class ColorPicker ); DrawSection( + ImGui.GetID("colors.borders"u8), "Borders", buffer, c => new[] { ("Border", c.Border) }, @@ -119,6 +123,7 @@ internal sealed class ColorPicker ); DrawSection( + ImGui.GetID("colors.text"u8), "Text", buffer, c => @@ -138,6 +143,7 @@ internal sealed class ColorPicker ); DrawSection( + ImGui.GetID("colors.brand.primary"u8), "Brand — Primary", buffer, c => @@ -159,6 +165,7 @@ internal sealed class ColorPicker ); DrawSection( + ImGui.GetID("colors.brand.accent"u8), "Brand — Accent", buffer, c => @@ -178,6 +185,7 @@ internal sealed class ColorPicker ); DrawSection( + ImGui.GetID("colors.identity"u8), "Identity", buffer, c => new[] { ("Identity", c.Identity) }, @@ -185,6 +193,7 @@ internal sealed class ColorPicker ); DrawSection( + ImGui.GetID("colors.status"u8), "Status", buffer, c => @@ -210,13 +219,17 @@ internal sealed class ColorPicker } private void DrawSection( + uint key, string title, Theme buffer, Func slots, Func writeBack ) { - if (!ImGui.CollapsingHeader(title, ImGuiTreeNodeFlags.DefaultOpen)) + // Key as a parameter: this runs seven times with seven titles, and + // ImGui would key each header off its label, so a translation could + // collapse two sections onto one shared state. + if (!_sections.Draw(key, title)) { return; } diff --git a/HellionChat/Ui/Components/Settings/FontsSection.cs b/HellionChat/Ui/Components/Settings/FontsSection.cs index ce6c604..279af05 100644 --- a/HellionChat/Ui/Components/Settings/FontsSection.cs +++ b/HellionChat/Ui/Components/Settings/FontsSection.cs @@ -15,9 +15,11 @@ internal sealed class FontsSection { private readonly Plugin _plugin; private readonly FontManager _fontManager; + private readonly SectionRenderer _sections; - public FontsSection(Plugin plugin, FontManager fontManager) + public FontsSection(Plugin plugin, FontManager fontManager, SectionRenderer sections) { + _sections = sections; _plugin = plugin; _fontManager = fontManager; } @@ -30,7 +32,13 @@ internal sealed class FontsSection public void Draw() { - if (!ImGui.CollapsingHeader(HellionStrings.Settings_Section_Fonts)) + if ( + !_sections.Draw( + ImGui.GetID("appearance.fonts"u8), + HellionStrings.Settings_Section_Fonts, + open: false + ) + ) return; // Readout so the user can see which font is actually active. @@ -126,7 +134,13 @@ internal sealed class FontsSection // ExtraGlyphRanges stays reachable regardless of the font source so the // user can verify/override the per-language auto-activation (v1.5.3 note). ImGui.Spacing(); - if (ImGui.CollapsingHeader(Language.Options_ExtraGlyphs_Name)) + if ( + _sections.Draw( + ImGui.GetID("appearance.fonts.glyphs"u8), + Language.Options_ExtraGlyphs_Name, + open: false + ) + ) { ImGuiUtil.HelpMarker( string.Format(Language.Options_ExtraGlyphs_Description, Plugin.PluginName) diff --git a/HellionChat/Ui/Components/Settings/SectionRenderer.cs b/HellionChat/Ui/Components/Settings/SectionRenderer.cs new file mode 100644 index 0000000..b8624cd --- /dev/null +++ b/HellionChat/Ui/Components/Settings/SectionRenderer.cs @@ -0,0 +1,39 @@ +using Dalamud.Bindings.ImGui; +using HellionChat.Themes; +using HellionChat.Ui.StyleEngine; +using HellionChat.Ui.StyleEngine.Widgets; + +namespace HellionChat.Ui.Components.Settings; + +// Section headings for the components that are not built out of setting rows -- +// the theme picker, the colour editor, the font panel. They need the heading and +// nothing else from SettingsWidgets, and pulling that in would hand each of them +// a Plugin reference they have no use for. +internal sealed class SectionRenderer +{ + private readonly ThemeRegistry _themes; + private readonly SettingsPalette _palette; + + private int _frame = -1; + private SectionHeaderColors _colors; + + internal SectionRenderer(ThemeRegistry themes, TokenResolver resolver) + { + _themes = themes; + _palette = new SettingsPalette(resolver); + } + + // Key, not title. ImGui's own storage keys a collapsing header off its + // label, so translated titles would reset every open section on a language + // switch, and two categories translating alike would share one state. + internal bool Draw(uint key, string title, bool open = true, bool disabled = false) + { + if (_frame != ImGui.GetFrameCount()) + { + _frame = ImGui.GetFrameCount(); + _colors = _palette.Section(_themes.Active.Colors); + } + + return SectionHeader.Draw(key, title, null, _colors, defaultOpen: open, disabled: disabled); + } +} diff --git a/HellionChat/Ui/Components/Settings/ThemePicker.cs b/HellionChat/Ui/Components/Settings/ThemePicker.cs index eab7a23..e9a392c 100644 --- a/HellionChat/Ui/Components/Settings/ThemePicker.cs +++ b/HellionChat/Ui/Components/Settings/ThemePicker.cs @@ -32,10 +32,12 @@ internal sealed class ThemePicker private const float CardHeight = 132f; private readonly ThemeRegistry _themes; + private readonly SectionRenderer _sections; private readonly Plugin _plugin; - public ThemePicker(ThemeRegistry themes, Plugin plugin) + public ThemePicker(ThemeRegistry themes, Plugin plugin, SectionRenderer sections) { + _sections = sections; _themes = themes; _plugin = plugin; } @@ -46,12 +48,20 @@ internal sealed class ThemePicker using (ImRaii.Disabled(locked)) { - foreach (var (category, slugs, defaultExpanded) in CategoryMap) + // Keyed by position, not by name. CategoryMap is a fixed literal + // list, so the index is stable, and it survives the category names + // being translated later -- which the titles themselves would not. + for (var i = 0; i < CategoryMap.Length; i++) { - var flags = defaultExpanded - ? ImGuiTreeNodeFlags.DefaultOpen - : ImGuiTreeNodeFlags.None; - if (ImGui.CollapsingHeader(category, flags)) + var (category, slugs, defaultExpanded) = CategoryMap[i]; + if ( + _sections.Draw( + ImGui.GetID($"theme.category.{i}"), + category, + open: defaultExpanded, + disabled: locked + ) + ) { DrawThemeGrid(Resolve(slugs)); } @@ -62,10 +72,14 @@ internal sealed class ThemePicker var customs = _themes.AllCustom().ToList(); if (customs.Count > 0) { + // Fixed key although the label carries a count: keying off the + // text would reset the section every time a theme is imported + // or deleted. if ( - ImGui.CollapsingHeader( + _sections.Draw( + ImGui.GetID("theme.category.custom"u8), $"Custom ({customs.Count})", - ImGuiTreeNodeFlags.DefaultOpen + disabled: locked ) ) {