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:
@@ -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