From 6813b80d5834071cfbbbe1acd5d01b218295d56c Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 15 Jun 2026 19:53:31 +0200 Subject: [PATCH] feat(themes): restore the header theme/tab quick-picker Brings back the 1.5.4 quick-picker lost in the v1.6.0 rewrite: a palette button in the input-bar button row (left of the cog) opens a popup that switches the theme (built-in + custom, active row checked) and jumps between chat tabs without opening settings. Theme switch mirrors the settings ThemePicker; the tab jump routes through a new MainWindow.ActivateTab that replays the click path (previous -> set -> OnTabActivated) so tell/unread handling is unchanged. Main window only -- pop-out InputBars get a null picker. Adds QuickPickerSelfTestStep. --- HellionChat/Plugin.cs | 1 + HellionChat/PluginHostFactory.cs | 7 +- .../SelfTests/QuickPickerSelfTestStep.cs | 45 ++++++ HellionChat/Ui/Components/InputBar.cs | 24 ++- HellionChat/Ui/Components/ThemeQuickPicker.cs | 143 ++++++++++++++++++ HellionChat/Ui/Windows/MainWindow.cs | 13 ++ 6 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 HellionChat/SelfTests/QuickPickerSelfTestStep.cs create mode 100644 HellionChat/Ui/Components/ThemeQuickPicker.cs diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 96d278d..c38232e 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -382,6 +382,7 @@ public sealed class Plugin : IAsyncDalamudPlugin new SelfTests.SidebarModeAutoSwitchStep(this), new SelfTests.ColorEditorBufferStep(this), new SelfTests.ThemePickerCategoryStep(this), + new SelfTests.QuickPickerSelfTestStep(this), new SelfTests.SettingsWindowOpenStep(this), new SelfTests.OnOpenMainUiRoutesMainWindowStep(this), new SelfTests.TypingIpcStateStep(this), diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index e26d9aa..3c6d0d4 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -144,6 +144,10 @@ internal static class PluginHostFactory sp.GetRequiredService() )); services.AddSingleton(_ => new Ui.Components.SymbolPicker()); + services.AddSingleton(sp => new Ui.Components.ThemeQuickPicker( + sp.GetRequiredService(), + sp.GetRequiredService() + )); services.AddSingleton(sp => new Ui.Components.InputBar( sp.GetRequiredService(), sp.GetRequiredService(), @@ -151,7 +155,8 @@ internal static class PluginHostFactory sp.GetRequiredService(), sp.GetRequiredService>(), () => sp.GetRequiredService().SettingsWindow.Toggle(), - sp.GetRequiredService() + sp.GetRequiredService(), + sp.GetRequiredService() )); services.AddSingleton(sp => new Ui.Components.Settings.TabSidebar( sp.GetRequiredService() diff --git a/HellionChat/SelfTests/QuickPickerSelfTestStep.cs b/HellionChat/SelfTests/QuickPickerSelfTestStep.cs new file mode 100644 index 0000000..2564f26 --- /dev/null +++ b/HellionChat/SelfTests/QuickPickerSelfTestStep.cs @@ -0,0 +1,45 @@ +using System.Linq; +using Dalamud.Bindings.ImGui; +using Dalamud.Plugin.SelfTest; +using HellionChat.Resources; + +namespace HellionChat.SelfTests; + +// Guards the header quick-picker's data contract: its three section/tooltip +// strings must resolve and there must be at least one theme to switch to. The +// render path itself (FontAwesome push) can't run headless, so this checks the +// data the popup depends on, not the draw. +internal sealed class QuickPickerSelfTestStep : ISelfTestStep +{ + private readonly Plugin _plugin; + + public QuickPickerSelfTestStep(Plugin plugin) + { + _plugin = plugin; + } + + public string Name => "Hellion Chat - Theme quick-picker contract"; + + public SelfTestStepResult RunStep() + { + if ( + string.IsNullOrEmpty(HellionStrings.Settings_QuickPicker_Tooltip) + || string.IsNullOrEmpty(HellionStrings.Settings_QuickPicker_Themes_Header) + || string.IsNullOrEmpty(HellionStrings.Settings_QuickPicker_Tabs_Header) + ) + { + ImGui.Text("Quick-picker strings did not resolve."); + return SelfTestStepResult.Fail; + } + + if (!_plugin.ThemeRegistry.BuiltinSlugs.Any()) + { + ImGui.Text("No built-in themes available for the quick-picker."); + return SelfTestStepResult.Fail; + } + + return SelfTestStepResult.Pass; + } + + public void CleanUp() { } +} diff --git a/HellionChat/Ui/Components/InputBar.cs b/HellionChat/Ui/Components/InputBar.cs index 71b248d..1552b66 100644 --- a/HellionChat/Ui/Components/InputBar.cs +++ b/HellionChat/Ui/Components/InputBar.cs @@ -40,6 +40,11 @@ internal sealed class InputBar private readonly Action _onOpenSettings; private readonly CommandHelpWindow _commandHelpWindow; + // Null in pop-out windows: the theme/tab quick-picker only belongs in the + // main window (1.5.4 had no pop-outs, and a tab jump from a channel-bound + // pop-out would be confusing). The main window's InputBar gets the instance. + private readonly ThemeQuickPicker? _themeQuickPicker; + private string _pendingMessage = string.Empty; private bool _isFocused; private bool _wasInputTextHovered; @@ -75,7 +80,8 @@ internal sealed class InputBar TokenResolver resolver, ILogger logger, Action onOpenSettings, - CommandHelpWindow commandHelpWindow + CommandHelpWindow commandHelpWindow, + ThemeQuickPicker? themeQuickPicker = null ) { _symbolPicker = symbolPicker; @@ -85,6 +91,7 @@ internal sealed class InputBar _logger = logger; _onOpenSettings = onOpenSettings; _commandHelpWindow = commandHelpWindow; + _themeQuickPicker = themeQuickPicker; } public string PendingMessage => _pendingMessage; @@ -188,6 +195,9 @@ internal sealed class InputBar if (inserted is not null && _pendingMessage.Length + inserted.Length <= BufferCapacity) _pendingMessage += inserted; + // Theme/tab quick-picker popup (main window only; null in pop-outs). + _themeQuickPicker?.Draw(); + // Auto-translate popup runs after all other popups so the OpenPopup // anchor lands on the InputText item we just drew. DrawAutoCompletePopup(); @@ -507,6 +517,18 @@ internal sealed class InputBar ImGui.SetTooltip("Insert symbol"); } + if (_themeQuickPicker is not null) + { + ImGui.SameLine(); + if (ImGui.Button(FontAwesomeIcon.Palette.ToIconString())) + _themeQuickPicker.OpenPopup(); + if (ImGui.IsItemHovered()) + { + using (ImRaii.DefaultFont()) + ImGui.SetTooltip(HellionStrings.Settings_QuickPicker_Tooltip); + } + } + ImGui.SameLine(); if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString())) { diff --git a/HellionChat/Ui/Components/ThemeQuickPicker.cs b/HellionChat/Ui/Components/ThemeQuickPicker.cs new file mode 100644 index 0000000..3cb763d --- /dev/null +++ b/HellionChat/Ui/Components/ThemeQuickPicker.cs @@ -0,0 +1,143 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using HellionChat.Resources; +using HellionChat.Themes; +using HellionChat.Ui.Components.Settings; + +namespace HellionChat.Ui.Components; + +// Restores the 1.5.4 header quick-picker (a46d89c:ChatLogWindow.cs:481-558): a +// palette button in the input-bar button row opening a popup that switches the +// theme (built-in + custom) and jumps between chat tabs without opening settings. +// Switch path mirrors the settings ThemePicker exactly; the tab jump routes +// through MainWindow.ActivateTab so tell/unread handling matches a real tab click. +internal sealed class ThemeQuickPicker +{ + private const string PopupId = "##hellion-quick-picker"; + private const float SectionWidth = 220f; + private const float RowHeight = 22f; + private const float MaxSectionHeight = 200f; + + private readonly ThemeRegistry _themes; + private readonly Plugin _plugin; + + public ThemeQuickPicker(ThemeRegistry themes, Plugin plugin) + { + _themes = themes; + _plugin = plugin; + } + + public void OpenPopup() => ImGui.OpenPopup(PopupId); + + public void Draw() + { + using var popup = ImRaii.Popup(PopupId); + if (!popup) + return; + + DrawThemeSection(); + ImGui.Spacing(); + DrawTabSection(); + } + + private void DrawThemeSection() + { + ImGui.TextDisabled(HellionStrings.Settings_QuickPicker_Themes_Header); + ImGui.Separator(); + + var themes = AllThemes(); + var height = MathF.Min(themes.Count * RowHeight, MaxSectionHeight); + using var child = ImRaii.Child( + "##hellion-quick-picker-themes", + new Vector2(SectionWidth, height) + ); + if (!child) + return; + + var activeSlug = _themes.Active.Slug; + foreach (var theme in themes) + { + var isActive = string.Equals( + theme.Slug, + activeSlug, + StringComparison.OrdinalIgnoreCase + ); + DrawGlyph(isActive); + if ( + ImGui.Selectable( + $"{theme.Name}##quick-theme-{theme.Slug}", + isActive, + ImGuiSelectableFlags.DontClosePopups + ) && !isActive + ) + { + _themes.Switch(theme.Slug); + Plugin.Config.Theme = theme.Slug; + _plugin.SaveConfig(); + } + } + } + + private void DrawTabSection() + { + ImGui.TextDisabled(HellionStrings.Settings_QuickPicker_Tabs_Header); + ImGui.Separator(); + + // Snapshot so a worker-thread temp-tab strip can't shift the list mid-loop. + var tabs = Plugin.Config.Tabs.ToList(); + var height = MathF.Min(tabs.Count * RowHeight, MaxSectionHeight); + using var child = ImRaii.Child( + "##hellion-quick-picker-tabs", + new Vector2(SectionWidth, height) + ); + if (!child) + return; + + var window = _plugin.MainWindow; + var active = window?.ActiveTab; + for (var i = 0; i < tabs.Count; i++) + { + var tab = tabs[i]; + var isActive = ReferenceEquals(tab, active); + DrawGlyph(isActive); + if ( + ImGui.Selectable( + $"{tab.Name}##quick-tab-{i}", + isActive, + ImGuiSelectableFlags.DontClosePopups + ) && !isActive + ) + { + window?.ActivateTab(tab); + } + } + } + + // Leading check glyph for the active row; inactive rows reserve an equal-width + // blank so labels stay aligned. The FontAwesome font is pushed on its own line + // then SameLine() so it doesn't bleed into the body-font label (1.5.4 trick). + private void DrawGlyph(bool isActive) + { + var check = FontAwesomeIcon.Check.ToIconString(); + using (_plugin.FontManager.FontAwesome.Push()) + { + if (isActive) + ImGui.TextUnformatted(check); + else + ImGui.Dummy(new Vector2(ImGui.CalcTextSize(check).X, ImGui.GetTextLineHeight())); + } + ImGui.SameLine(); + } + + private List AllThemes() + { + var all = new List(); + foreach (var slug in ThemePicker.CategoryMapSlugs) + if (_themes.TryGet(slug, out var theme)) + all.Add(theme); + all.AddRange(_themes.AllCustom()); + return all; + } +} diff --git a/HellionChat/Ui/Windows/MainWindow.cs b/HellionChat/Ui/Windows/MainWindow.cs index 2dd3cbf..90b1032 100644 --- a/HellionChat/Ui/Windows/MainWindow.cs +++ b/HellionChat/Ui/Windows/MainWindow.cs @@ -135,6 +135,19 @@ internal sealed class MainWindow : Window TabLifecycleHelpers.OnTabActivated(next, removed); } + // Programmatic tab activation for the header quick-picker. Mirrors the click + // path in TopTabBar/Sidebar exactly (previous → set → OnTabActivated) so a + // header pick strips tell-state and resets unread the way a real click does. + internal void ActivateTab(Tab tab) + { + if (ReferenceEquals(_activeTab, tab)) + return; + + var previous = _activeTab; + _activeTab = tab; + TabLifecycleHelpers.OnTabActivated(tab, previous); + } + // Internal accessors for self-tests so the probes can reach the live // component without exposing them as public surface. internal Components.Sidebar GetSidebarForSelfTest() => _sidebar;