feat(settings): convert the remaining four standard tabs

Chat, Channels, General and Data & Privacy now draw the same way Window does. A
half-converted window is worse than an unconverted one: before, all seven tabs
were consistently dated; after the pilot, one looked current and six looked
abandoned, and switching between them made the seam obvious.

Descriptions move out of the help markers and onto the rows. Those strings were
written to be read, and a (?) the user has to hover is where an explanation goes
to be ignored. Several were translated into 25 languages and had never appeared
on screen at all.

Two settings that could not use the standard helpers -- the language picker,
which rebuilds the font atlas, and the keybind mode, whose description depends
on the selected value -- go through a plain Row that hands the caller the control
column and leaves the save logic alone.

Privacy filter labels now come from the resources that already existed for them,
same defect as the channels tab last round: strings present, translated, and
reachable from no line of code.

Four more constructors take a TokenResolver, so this wants the DI smoke pass.
Only the appearance tab still draws stock collapsing headers; its four
components are their own block.
This commit is contained in:
2026-08-18 18:23:34 +02:00
parent 7df18bd552
commit 57780351d4
6 changed files with 159 additions and 106 deletions
+8 -4
View File
@@ -208,20 +208,24 @@ internal static class PluginHostFactory
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.GeneralTab(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<FontManager>()
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.ChatTab(
sp.GetRequiredService<Plugin>()
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.WindowTab(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.ChannelsTab(
sp.GetRequiredService<Plugin>()
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.DataPrivacyTab(
sp.GetRequiredService<Plugin>()
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.AboutTab(
sp.GetRequiredService<FontManager>(),
@@ -71,6 +71,20 @@ internal sealed class SettingsWidgets
// The whole row toggles, label included. The switch itself gets its own
// invisible button because SettingRow's hit area stops at the label column,
// and clicking the control is what a user tries first.
// Escape hatch for controls the helpers do not cover -- a keybind capture,
// a picker with side effects. The caller draws whatever it likes into the
// control column and keeps its own save logic.
internal void Row(
uint id,
string label,
string? description,
Action<SettingRowContext> drawControl
)
{
EnsureFrame();
SettingRow.Draw(id, label, description, _row, drawControl);
}
internal void ToggleRow(
uint id,
string label,
@@ -1,111 +1,116 @@
using Dalamud.Bindings.ImGui;
using HellionChat.Resources;
using HellionChat.Util;
using HellionChat.Ui.StyleEngine;
namespace HellionChat.Ui.Components.Settings.Tabs;
internal sealed class ChannelsTab
{
private readonly Plugin _plugin;
private readonly SettingsWidgets _w;
public ChannelsTab(Plugin plugin)
public ChannelsTab(Plugin plugin, TokenResolver resolver)
{
_plugin = plugin;
_w = new SettingsWidgets(plugin);
_w = new SettingsWidgets(plugin, new SettingsPalette(resolver));
}
public void Draw()
{
if (
ImGui.CollapsingHeader(
HellionStrings.Settings_Section_AutoTellTabs,
ImGuiTreeNodeFlags.DefaultOpen
_w.Section(
ImGui.GetID("channels.autotell"u8),
HellionStrings.Settings_Section_AutoTellTabs
)
)
{
_w.Toggle(
_w.ToggleRow(
ImGui.GetID("channels.autotell.enable"u8),
HellionStrings.ChatLog_AutoTellTabs_Enable_Name,
HellionStrings.ChatLog_AutoTellTabs_Enable_Description,
() => Plugin.Config.EnableAutoTellTabs,
v => Plugin.Config.EnableAutoTellTabs = v
);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Enable_Description);
_w.SliderInt(
_w.SliderIntRow(
ImGui.GetID("channels.autotell.limit"u8),
HellionStrings.ChatLog_AutoTellTabs_Limit_Name,
HellionStrings.ChatLog_AutoTellTabs_Limit_Description,
() => Plugin.Config.AutoTellTabsLimit,
v => Plugin.Config.AutoTellTabsLimit = v,
1,
50
);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Limit_Description);
_w.Toggle(
_w.ToggleRow(
ImGui.GetID("channels.autotell.compact"u8),
HellionStrings.ChatLog_AutoTellTabs_Compact_Name,
HellionStrings.ChatLog_AutoTellTabs_Compact_Description,
() => Plugin.Config.AutoTellTabsCompactDisplay,
v => Plugin.Config.AutoTellTabsCompactDisplay = v
);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Compact_Description);
_w.SliderInt(
_w.SliderIntRow(
ImGui.GetID("channels.autotell.preload"u8),
HellionStrings.Privacy_AutoTellTabs_Preload_Name,
HellionStrings.Privacy_AutoTellTabs_Preload_Description,
() => Plugin.Config.AutoTellTabsHistoryPreload,
v => Plugin.Config.AutoTellTabsHistoryPreload = v,
0,
200
);
ImGuiUtil.HelpMarker(HellionStrings.Privacy_AutoTellTabs_Preload_Description);
_w.Toggle(
_w.ToggleRow(
ImGui.GetID("channels.autotell.greeted"u8),
HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Name,
HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Description,
() => Plugin.Config.AutoTellTabsShowGreetedToggle,
v => Plugin.Config.AutoTellTabsShowGreetedToggle = v
);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Description);
_w.Toggle(
_w.ToggleRow(
ImGui.GetID("channels.autotell.popout"u8),
HellionStrings.ChatLog_AutoTellTabs_OpenAsPopout_Name,
HellionStrings.ChatLog_AutoTellTabs_OpenAsPopout_Description,
() => Plugin.Config.AutoTellTabsOpenAsPopout,
v => Plugin.Config.AutoTellTabsOpenAsPopout = v
);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_OpenAsPopout_Description);
// Written for this screen and never shown until now. It names the
// one setting in a third-party plugin that silently stops auto-tell
// tabs from ever opening, which is not something a user guesses.
// Written for this screen and never shown until this cycle. It names
// the one setting in a third-party plugin that silently stops
// auto-tell tabs from ever opening, which is not something a user
// works out alone.
ImGui.Spacing();
ImGui.TextWrapped(HellionStrings.ChatLog_AutoTellTabs_ConflictHint);
}
if (ImGui.CollapsingHeader("Tell auto-open mode", ImGuiTreeNodeFlags.DefaultOpen))
if (_w.Section(ImGui.GetID("channels.autoopen"u8), "Tell auto-open"))
{
_w.EnumCombo(
_w.EnumComboRow(
ImGui.GetID("channels.autoopen.mode"u8),
"Tell auto-open mode",
"Where a tell opens when it arrives.",
() => Plugin.Config.TellAutoOpenMode,
v => Plugin.Config.TellAutoOpenMode = v,
v => v.Name()
);
_w.Toggle(
_w.ToggleRow(
ImGui.GetID("channels.autoopen.switch"u8),
"Switch to the tab on every tell",
"Otherwise the tab opens in the background after the first one.",
() => Plugin.Config.TellAutoOpenSwitchAlways,
v => Plugin.Config.TellAutoOpenSwitchAlways = v
);
}
if (ImGui.CollapsingHeader("Sidebar"))
if (_w.Section(ImGui.GetID("channels.sidebar"u8), "Sidebar", open: false))
{
// Bounds come from the constants rather than repeating the numbers,
// so the slider cannot drift away from the clamp in Sidebar.GetWidth.
// The stored value is unscaled; display scaling is applied where the
// sidebar is drawn.
_w.SliderInt(
_w.SliderIntRow(
ImGui.GetID("channels.sidebar.width"u8),
HellionStrings.Settings_ThemeAndLayout_SidebarWidth_Name,
HellionStrings.Settings_ThemeAndLayout_SidebarWidth_Description,
() => Plugin.Config.SidebarWidth,
v => Plugin.Config.SidebarWidth = v,
(int)Sidebar.MinSidebarWidth,
(int)Sidebar.MaxSidebarWidth
);
ImGuiUtil.HelpMarker(HellionStrings.Settings_ThemeAndLayout_SidebarWidth_Description);
}
}
}
@@ -1,68 +1,79 @@
using Dalamud.Bindings.ImGui;
using HellionChat.Resources;
using HellionChat.Util;
using HellionChat.Ui.StyleEngine;
namespace HellionChat.Ui.Components.Settings.Tabs;
internal sealed class ChatTab
{
private readonly Plugin _plugin;
private readonly SettingsWidgets _w;
public ChatTab(Plugin plugin)
public ChatTab(Plugin plugin, TokenResolver resolver)
{
_plugin = plugin;
_w = new SettingsWidgets(plugin);
_w = new SettingsWidgets(plugin, new SettingsPalette(resolver));
}
public void Draw()
{
if (ImGui.CollapsingHeader("Display modes", ImGuiTreeNodeFlags.DefaultOpen))
if (_w.Section(ImGui.GetID("chat.display"u8), "Display modes"))
{
_w.Toggle(
"Compact density (card vs compact)",
_w.ToggleRow(
ImGui.GetID("chat.display.density"u8),
"Compact density",
"Trades the card layout for one line per message.",
() => Plugin.Config.UseCompactDensity,
v => Plugin.Config.UseCompactDensity = v
);
_w.Toggle(
_w.ToggleRow(
ImGui.GetID("chat.display.clock"u8),
"24-hour clock",
null,
() => Plugin.Config.Use24HourClock,
v => Plugin.Config.Use24HourClock = v
);
_w.EnumCombo(
// Descriptions move out of the help markers and onto the row. They
// were written to be read; a (?) the user has to hover is where an
// explanation goes to be ignored.
_w.EnumComboRow(
ImGui.GetID("chat.display.worldsuffix"u8),
HellionStrings.Settings_Chat_WorldSuffix_Name,
HellionStrings.Settings_Chat_WorldSuffix_Description,
() => Plugin.Config.WorldSuffixMode,
v => Plugin.Config.WorldSuffixMode = v,
v => v.Name()
);
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_WorldSuffix_Description);
_w.EnumCombo(
_w.EnumComboRow(
ImGui.GetID("chat.display.nameform"u8),
HellionStrings.Settings_Chat_NameForm_Name,
HellionStrings.Settings_Chat_NameForm_Description,
() => Plugin.Config.NameFormMode,
v => Plugin.Config.NameFormMode = v,
v => v.Name()
);
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_NameForm_Description);
}
if (ImGui.CollapsingHeader("Command help"))
if (_w.Section(ImGui.GetID("chat.commandhelp"u8), "Command help", open: false))
{
_w.EnumCombo(
_w.EnumComboRow(
ImGui.GetID("chat.commandhelp.side"u8),
"Command help side",
"Which side the command hint list appears on while typing.",
() => Plugin.Config.CommandHelpSide,
v => Plugin.Config.CommandHelpSide = v,
v => v.Name()
);
}
if (ImGui.CollapsingHeader("Plugin disclosure"))
if (_w.Section(ImGui.GetID("chat.disclosure"u8), "Plugin disclosure", open: false))
{
_w.Toggle(
_w.ToggleRow(
ImGui.GetID("chat.disclosure.notify"u8),
HellionStrings.Settings_Chat_NotifyPluginDisclosure_Name,
HellionStrings.Settings_Chat_NotifyPluginDisclosure_Description,
() => Plugin.Config.NotifyPluginDisclosure,
v => Plugin.Config.NotifyPluginDisclosure = v
);
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_NotifyPluginDisclosure_Description);
}
}
}
@@ -10,35 +10,37 @@ internal sealed class DataPrivacyTab
private readonly Plugin _plugin;
private readonly SettingsWidgets _w;
public DataPrivacyTab(Plugin plugin)
public DataPrivacyTab(Plugin plugin, Ui.StyleEngine.TokenResolver resolver)
{
_plugin = plugin;
_w = new SettingsWidgets(plugin);
_w = new SettingsWidgets(plugin, new SettingsPalette(resolver));
}
public void Draw()
{
if (ImGui.CollapsingHeader("Logging", ImGuiTreeNodeFlags.DefaultOpen))
if (_w.Section(ImGui.GetID("privacy.logging"u8), HellionStrings.Retention_Heading))
{
_w.Toggle(
_w.ToggleRow(
ImGui.GetID("privacy.logging.enabled"u8),
HellionStrings.Retention_Enabled_Name,
HellionStrings.Retention_Enabled_Description,
() => Plugin.Config.RetentionEnabled,
v => Plugin.Config.RetentionEnabled = v
);
ImGuiUtil.HelpMarker(HellionStrings.Retention_Enabled_Description);
// Down to 0, which DeleteByRetentionPolicy reads as "keep forever"
// for channels without an override. The slider started at 1, so the
// one value that means "never delete anything" was unreachable
// through the UI while the label promised it.
_w.SliderInt(
_w.SliderIntRow(
ImGui.GetID("privacy.logging.default"u8),
HellionStrings.Retention_Default_Label,
HellionStrings.Retention_Default_Help,
() => Plugin.Config.RetentionDefaultDays,
v => Plugin.Config.RetentionDefaultDays = v,
0,
365
);
ImGuiUtil.HelpMarker(HellionStrings.Retention_Default_Help);
// RetentionLastRunAt defaults to MinValue on a fresh install, which
// would render as "0001-01-01 00:00" and look like a bug; the "Never"
@@ -54,22 +56,31 @@ internal sealed class DataPrivacyTab
DrawRetentionOverrides();
}
if (ImGui.CollapsingHeader("Privacy filter", ImGuiTreeNodeFlags.DefaultOpen))
if (
_w.Section(
ImGui.GetID("privacy.filter"u8),
HellionStrings.Settings_Section_PrivacyFilter
)
)
{
_w.Toggle(
"Enable privacy filter",
_w.ToggleRow(
ImGui.GetID("privacy.filter.enabled"u8),
HellionStrings.Privacy_FilterEnabled_Name,
HellionStrings.Privacy_FilterEnabled_Description,
() => Plugin.Config.PrivacyFilterEnabled,
v => Plugin.Config.PrivacyFilterEnabled = v
);
DrawPrivacyPersistChannelsGrid();
_w.Toggle(
"Persist unknown channels",
_w.ToggleRow(
ImGui.GetID("privacy.filter.unknown"u8),
HellionStrings.Privacy_PersistUnknown_Name,
null,
() => Plugin.Config.PrivacyPersistUnknownChannels,
v => Plugin.Config.PrivacyPersistUnknownChannels = v
);
DrawPrivacyPersistChannelsGrid();
}
if (ImGui.CollapsingHeader("Telemetry"))
if (_w.Section(ImGui.GetID("privacy.telemetry"u8), "Telemetry", open: false))
{
// Read-only placeholder; no telemetry is wired in v1.7.0. Do not
// promote this to a toggle without an explicit Sub-Spec change.
@@ -1,5 +1,6 @@
using Dalamud.Bindings.ImGui;
using HellionChat.Resources;
using HellionChat.Ui.StyleEngine;
using HellionChat.Util;
namespace HellionChat.Ui.Components.Settings.Tabs;
@@ -16,10 +17,10 @@ internal sealed class GeneralTab
// localised label never affects the ordering.
private static readonly LanguageOverride[] LanguageOrder = BuildLanguageOrder();
public GeneralTab(Plugin plugin, FontManager fonts)
public GeneralTab(Plugin plugin, FontManager fonts, TokenResolver resolver)
{
_plugin = plugin;
_w = new SettingsWidgets(plugin);
_w = new SettingsWidgets(plugin, new SettingsPalette(resolver));
_fonts = fonts;
}
@@ -33,17 +34,19 @@ internal sealed class GeneralTab
public void Draw()
{
if (ImGui.CollapsingHeader("Behavior", ImGuiTreeNodeFlags.DefaultOpen))
if (_w.Section(ImGui.GetID("general.behaviour"u8), "Behaviour"))
{
_w.Toggle(
"Reduce motion (no theme crossfade)",
_w.ToggleRow(
ImGui.GetID("general.behaviour.reducemotion"u8),
"Reduce motion",
"Turns off theme crossfades, hover fades and the drifting motes.",
() => Plugin.Config.ReduceMotion,
v => Plugin.Config.ReduceMotion = v
);
DrawLanguagePicker();
}
if (ImGui.CollapsingHeader("Keybinds", ImGuiTreeNodeFlags.DefaultOpen))
if (_w.Section(ImGui.GetID("general.keybinds"u8), "Keybinds"))
{
ImGui.TextDisabled("Click a button, then press the key combination. Esc clears.");
DrawKeybind(
@@ -61,20 +64,19 @@ internal sealed class GeneralTab
DrawKeybindModePicker();
}
if (ImGui.CollapsingHeader("Notifications", ImGuiTreeNodeFlags.DefaultOpen))
if (_w.Section(ImGui.GetID("general.notifications"u8), "Notifications"))
{
_w.Toggle(
_w.ToggleRow(
ImGui.GetID("general.notifications.sounds"u8),
Language.Options_PlaySounds_Name,
Language.Options_PlaySounds_Description,
() => Plugin.Config.PlaySounds,
v => Plugin.Config.PlaySounds = v
);
ImGuiUtil.HelpMarker(Language.Options_PlaySounds_Description);
}
if (ImGui.CollapsingHeader("Volumes", ImGuiTreeNodeFlags.DefaultOpen))
{
_w.SliderFloat(
_w.SliderFloatRow(
ImGui.GetID("general.notifications.volume"u8),
"Custom sound volume",
null,
() => Plugin.Config.CustomSoundVolume,
v => Plugin.Config.CustomSoundVolume = v,
0f,
@@ -95,18 +97,19 @@ internal sealed class GeneralTab
for (var i = 0; i < LanguageOrder.Length; i++)
labels[i] = LanguageOrder[i].Name();
ImGui.SetNextItemWidth(200f);
if (ImGui.Combo(Language.Options_Language_Name, ref selected, labels, labels.Length))
{
if (selected >= 0 && selected < LanguageOrder.Length)
ApplyLanguage(LanguageOrder[selected]);
}
ImGuiUtil.HelpMarker(
string.Format(Language.Options_Language_Description, Plugin.PluginName)
+ "\n\nSwitching rebuilds the font atlas, so the chat window goes blank"
+ " for a moment. Scripts a language needs stay enabled afterwards;"
+ " clear them under Appearance -> Fonts -> Extra glyphs."
_w.Row(
ImGui.GetID("general.behaviour.language"u8),
Language.Options_Language_Name,
"Switching rebuilds the font atlas, so the chat goes blank for a moment.",
ctx =>
{
ImGui.SetNextItemWidth(ctx.ControlWidth);
if (ImGui.Combo("##hc-language", ref selected, labels, labels.Length))
{
if (selected >= 0 && selected < LanguageOrder.Length)
ApplyLanguage(LanguageOrder[selected]);
}
}
);
}
@@ -155,18 +158,23 @@ internal sealed class GeneralTab
for (var i = 0; i < values.Length; i++)
labels[i] = values[i].Name();
ImGui.SetNextItemWidth(200f);
if (ImGui.Combo(Language.Options_KeybindMode_Name, ref selected, labels, labels.Length))
{
if (selected >= 0 && selected < values.Length)
_w.Row(
ImGui.GetID("general.keybinds.mode"u8),
Language.Options_KeybindMode_Name,
Plugin.Config.KeybindMode.Tooltip(),
ctx =>
{
Plugin.Config.KeybindMode = values[selected];
_plugin.SaveConfig();
ImGui.SetNextItemWidth(ctx.ControlWidth);
if (ImGui.Combo("##hc-keybindmode", ref selected, labels, labels.Length))
{
if (selected >= 0 && selected < values.Length)
{
Plugin.Config.KeybindMode = values[selected];
_plugin.SaveConfig();
}
}
}
}
if (Plugin.Config.KeybindMode.Tooltip() is { } tip)
ImGuiUtil.HelpMarker(tip);
);
}
// Wires the already-present ImGuiUtil.KeybindInput capture widget (dead/unwired