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:
@@ -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),
|
||||
|
||||
@@ -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() { }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user