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.
This commit is contained in:
2026-06-15 19:53:31 +02:00
parent 0352e6c199
commit 6813b80d58
6 changed files with 231 additions and 2 deletions
+1
View File
@@ -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),
+6 -1
View File
@@ -144,6 +144,10 @@ internal static class PluginHostFactory
sp.GetRequiredService<Ui.Components.ChunkRenderer>()
));
services.AddSingleton(_ => new Ui.Components.SymbolPicker());
services.AddSingleton(sp => new Ui.Components.ThemeQuickPicker(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Plugin>()
));
services.AddSingleton(sp => new Ui.Components.InputBar(
sp.GetRequiredService<Ui.Components.SymbolPicker>(),
sp.GetRequiredService<FontManager>(),
@@ -151,7 +155,8 @@ internal static class PluginHostFactory
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
sp.GetRequiredService<ILogger<Ui.Components.InputBar>>(),
() => sp.GetRequiredService<Plugin>().SettingsWindow.Toggle(),
sp.GetRequiredService<Ui.CommandHelpWindow>()
sp.GetRequiredService<Ui.CommandHelpWindow>(),
sp.GetRequiredService<Ui.Components.ThemeQuickPicker>()
));
services.AddSingleton(sp => new Ui.Components.Settings.TabSidebar(
sp.GetRequiredService<FontManager>()
@@ -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() { }
}
+23 -1
View File
@@ -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<InputBar> 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()))
{
@@ -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<Theme> AllThemes()
{
var all = new List<Theme>();
foreach (var slug in ThemePicker.CategoryMapSlugs)
if (_themes.TryGet(slug, out var theme))
all.Add(theme);
all.AddRange(_themes.AllCustom());
return all;
}
}
+13
View File
@@ -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;