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.SidebarModeAutoSwitchStep(this),
|
||||||
new SelfTests.ColorEditorBufferStep(this),
|
new SelfTests.ColorEditorBufferStep(this),
|
||||||
new SelfTests.ThemePickerCategoryStep(this),
|
new SelfTests.ThemePickerCategoryStep(this),
|
||||||
|
new SelfTests.QuickPickerSelfTestStep(this),
|
||||||
new SelfTests.SettingsWindowOpenStep(this),
|
new SelfTests.SettingsWindowOpenStep(this),
|
||||||
new SelfTests.OnOpenMainUiRoutesMainWindowStep(this),
|
new SelfTests.OnOpenMainUiRoutesMainWindowStep(this),
|
||||||
new SelfTests.TypingIpcStateStep(this),
|
new SelfTests.TypingIpcStateStep(this),
|
||||||
|
|||||||
@@ -144,6 +144,10 @@ internal static class PluginHostFactory
|
|||||||
sp.GetRequiredService<Ui.Components.ChunkRenderer>()
|
sp.GetRequiredService<Ui.Components.ChunkRenderer>()
|
||||||
));
|
));
|
||||||
services.AddSingleton(_ => new Ui.Components.SymbolPicker());
|
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(
|
services.AddSingleton(sp => new Ui.Components.InputBar(
|
||||||
sp.GetRequiredService<Ui.Components.SymbolPicker>(),
|
sp.GetRequiredService<Ui.Components.SymbolPicker>(),
|
||||||
sp.GetRequiredService<FontManager>(),
|
sp.GetRequiredService<FontManager>(),
|
||||||
@@ -151,7 +155,8 @@ internal static class PluginHostFactory
|
|||||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
|
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
|
||||||
sp.GetRequiredService<ILogger<Ui.Components.InputBar>>(),
|
sp.GetRequiredService<ILogger<Ui.Components.InputBar>>(),
|
||||||
() => sp.GetRequiredService<Plugin>().SettingsWindow.Toggle(),
|
() => 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(
|
services.AddSingleton(sp => new Ui.Components.Settings.TabSidebar(
|
||||||
sp.GetRequiredService<FontManager>()
|
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 Action _onOpenSettings;
|
||||||
private readonly CommandHelpWindow _commandHelpWindow;
|
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 string _pendingMessage = string.Empty;
|
||||||
private bool _isFocused;
|
private bool _isFocused;
|
||||||
private bool _wasInputTextHovered;
|
private bool _wasInputTextHovered;
|
||||||
@@ -75,7 +80,8 @@ internal sealed class InputBar
|
|||||||
TokenResolver resolver,
|
TokenResolver resolver,
|
||||||
ILogger<InputBar> logger,
|
ILogger<InputBar> logger,
|
||||||
Action onOpenSettings,
|
Action onOpenSettings,
|
||||||
CommandHelpWindow commandHelpWindow
|
CommandHelpWindow commandHelpWindow,
|
||||||
|
ThemeQuickPicker? themeQuickPicker = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_symbolPicker = symbolPicker;
|
_symbolPicker = symbolPicker;
|
||||||
@@ -85,6 +91,7 @@ internal sealed class InputBar
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_onOpenSettings = onOpenSettings;
|
_onOpenSettings = onOpenSettings;
|
||||||
_commandHelpWindow = commandHelpWindow;
|
_commandHelpWindow = commandHelpWindow;
|
||||||
|
_themeQuickPicker = themeQuickPicker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string PendingMessage => _pendingMessage;
|
public string PendingMessage => _pendingMessage;
|
||||||
@@ -188,6 +195,9 @@ internal sealed class InputBar
|
|||||||
if (inserted is not null && _pendingMessage.Length + inserted.Length <= BufferCapacity)
|
if (inserted is not null && _pendingMessage.Length + inserted.Length <= BufferCapacity)
|
||||||
_pendingMessage += inserted;
|
_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
|
// Auto-translate popup runs after all other popups so the OpenPopup
|
||||||
// anchor lands on the InputText item we just drew.
|
// anchor lands on the InputText item we just drew.
|
||||||
DrawAutoCompletePopup();
|
DrawAutoCompletePopup();
|
||||||
@@ -507,6 +517,18 @@ internal sealed class InputBar
|
|||||||
ImGui.SetTooltip("Insert symbol");
|
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();
|
ImGui.SameLine();
|
||||||
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString()))
|
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);
|
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
|
// Internal accessors for self-tests so the probes can reach the live
|
||||||
// component without exposing them as public surface.
|
// component without exposing them as public surface.
|
||||||
internal Components.Sidebar GetSidebarForSelfTest() => _sidebar;
|
internal Components.Sidebar GetSidebarForSelfTest() => _sidebar;
|
||||||
|
|||||||
Reference in New Issue
Block a user