refactor(settings): rebuild the per-tab panel into sub-sections

This commit is contained in:
2026-05-23 02:11:04 +02:00
parent 78efd654e6
commit ee39fd0eec
3 changed files with 379 additions and 251 deletions
+8
View File
@@ -497,4 +497,12 @@ internal class HellionStrings
internal static string Settings_Section_Hide => Get(nameof(Settings_Section_Hide));
internal static string Settings_Section_InactivityHide => Get(nameof(Settings_Section_InactivityHide));
internal static string Settings_Section_Frame => Get(nameof(Settings_Section_Frame));
// v1.5.6: Tabs tab per-tab-item sub-section titles (R6)
internal static string Settings_Section_Tab_Channels => Get(nameof(Settings_Section_Tab_Channels));
internal static string Settings_Section_Tab_Display => Get(nameof(Settings_Section_Tab_Display));
internal static string Settings_Section_Tab_Notification => Get(nameof(Settings_Section_Tab_Notification));
internal static string Settings_Section_Tab_Input => Get(nameof(Settings_Section_Tab_Input));
internal static string Settings_Section_Tab_PopOut => Get(nameof(Settings_Section_Tab_PopOut));
internal static string Settings_Section_Tab_Volume_AllTabsHint => Get(nameof(Settings_Section_Tab_Volume_AllTabsHint));
}
+20
View File
@@ -1158,4 +1158,24 @@
<data name="Settings_Section_Frame" xml:space="preserve">
<value>Frame</value>
</data>
<!-- v1.5.6: Tabs tab per-tab-item sub-section titles (R6) -->
<data name="Settings_Section_Tab_Channels" xml:space="preserve">
<value>Channels</value>
</data>
<data name="Settings_Section_Tab_Display" xml:space="preserve">
<value>Display</value>
</data>
<data name="Settings_Section_Tab_Notification" xml:space="preserve">
<value>Notification</value>
</data>
<data name="Settings_Section_Tab_Input" xml:space="preserve">
<value>Input</value>
</data>
<data name="Settings_Section_Tab_PopOut" xml:space="preserve">
<value>Pop-out window</value>
</data>
<data name="Settings_Section_Tab_Volume_AllTabsHint" xml:space="preserve">
<value>This volume applies to all tabs.</value>
</data>
</root>
+351 -251
View File
@@ -72,6 +72,13 @@ internal sealed class Tabs : ISettingsTab
{
var tab = Mutable.Tabs[i];
// Sub-sections (Channels/Display/Notification/Input/Pop-out) are inlined into
// this loop body rather than extracted to helpers, because each one closes over
// the per-iteration `i` and `tab` state. Extraction would mean passing both
// into every helper without meaningful encapsulation gain.
// ToOpen controls which tab-item TreeNode is open (e.g. after add/move).
// This is the outer level — not touched by sectionJustEntered.
if (doOpens)
ImGui.SetNextItemOpen(i == ToOpen);
@@ -117,6 +124,7 @@ internal sealed class Tabs : ISettingsTab
ToOpen = i + 1;
}
// Name and Icon are always visible — no sub-section collapse for these.
ImGui.InputText(
Language.Options_Tabs_Name,
ref tab.Name,
@@ -165,284 +173,376 @@ internal sealed class Tabs : ISettingsTab
}
}
ImGui.Checkbox(Language.Options_Tabs_ShowTimestamps, ref tab.DisplayTimestamp);
ImGui.Checkbox(
HellionStrings.Tabs_NotificationSound_Enable_Name,
ref tab.EnableNotificationSound
);
ImGuiUtil.HelpMarker(HellionStrings.Tabs_NotificationSound_Description);
if (tab.EnableNotificationSound)
ImGui.Spacing();
// ── Sub-section: Channels ─────────────────────────────────────────
// First because it answers "what does this tab collect?" — most important.
if (sectionJustEntered) ImGui.SetNextItemOpen(false);
using (var secChannels = ImRaii.TreeNode(HellionStrings.Settings_Section_Tab_Channels + $"##sec-channels-{i}"))
{
using var indent = ImRaii.PushIndent(10.0f);
// Build a readable preview label for the currently selected sound.
var soundPreview =
tab.NotificationSoundId <= 16
? $"{HellionStrings.Tabs_NotificationSound_Option} {tab.NotificationSoundId}"
: $"{HellionStrings.Tabs_NotificationSound_CustomOption} {tab.NotificationSoundId - 16}";
using (var combo = ImRaii.Combo($"##notif-sound-{i}", soundPreview))
if (secChannels.Success)
{
if (combo.Success)
{
for (uint s = 1; s <= 16; s++)
{
if (
ImGui.Selectable(
$"{HellionStrings.Tabs_NotificationSound_Option} {s}",
tab.NotificationSoundId == s
)
)
tab.NotificationSoundId = s;
}
ImGui.Separator();
// Bundled custom sounds (ids 17-19).
for (uint n = 1; n <= 3; n++)
{
var customId = 16 + n;
if (
ImGui.Selectable(
$"{HellionStrings.Tabs_NotificationSound_CustomOption} {n}",
tab.NotificationSoundId == customId
)
)
tab.NotificationSoundId = customId;
}
}
}
// Let the user hear the currently selected sound without waiting
// for a real message to arrive in this tab.
ImGui.SameLine();
if (
ImGuiUtil.IconButton(
FontAwesomeIcon.Play,
tooltip: HellionStrings.Tabs_NotificationSound_Preview
)
)
{
var previewId = tab.NotificationSoundId;
if (previewId <= 16)
{
Plugin.Framework.RunOnFrameworkThread(() =>
{
unsafe
{
UIGlobals.PlaySoundEffect(previewId);
}
});
}
else
{
Plugin.CustomAudioPlayer.Play((int)previewId - 16, Mutable.CustomSoundVolume);
}
}
}
ImGui.Checkbox(Language.Options_Tabs_PopOut, ref tab.PopOut);
if (tab.PopOut)
{
using var _ = ImRaii.PushIndent(10.0f);
ImGui.Checkbox(
Language.Options_Tabs_IndependentOpacity,
ref tab.IndependentOpacity
);
if (tab.IndependentOpacity)
ImGuiUtil.DragFloatVertical(
Language.Options_Tabs_Opacity,
ref tab.Opacity,
0.25f,
0f,
100f,
$"{tab.Opacity:N2}%%",
ImGuiSliderFlags.AlwaysClamp
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
ImGuiUtil.ChannelSelector(Language.Options_Tabs_Channels, tab.SelectedChannels);
ImGuiUtil.ExtraChatSelector(
Language.Options_Tabs_ExtraChatChannels,
ref tab.ExtraChatAll,
tab.ExtraChatChannels
);
ImGui.Checkbox(Language.Options_Tabs_IndependentHide, ref tab.IndependentHide);
if (tab.IndependentHide)
{
using var __ = ImRaii.PushIndent(10.0f);
ImGuiUtil.OptionCheckbox(
ref tab.HideDuringCutscenes,
Language.Options_HideDuringCutscenes_Name
);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(
ref tab.HideWhenNotLoggedIn,
Language.Options_HideWhenNotLoggedIn_Name
);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(
ref tab.HideWhenUiHidden,
Language.Options_HideWhenUiHidden_Name
);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(
ref tab.HideInLoadingScreens,
Language.Options_HideInLoadingScreens_Name
);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(
ref tab.HideInBattle,
Language.Options_HideInBattle_Name
);
ImGui.Spacing();
}
ImGuiUtil.OptionCheckbox(ref tab.CanMove, Language.Popout_CanMove_Name);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref tab.CanResize, Language.Popout_CanResize_Name);
ImGui.Spacing();
}
using (
var combo = ImGuiUtil.BeginComboVertical(
Language.Options_Tabs_UnreadMode,
tab.UnreadMode.Name()
)
)
{
if (combo.Success)
{
foreach (var mode in Enum.GetValues<UnreadMode>())
{
if (ImGui.Selectable(mode.Name(), tab.UnreadMode == mode))
tab.UnreadMode = mode;
if (mode.Tooltip() is { } tooltip && ImGui.IsItemHovered())
ImGuiUtil.Tooltip(tooltip);
}
}
}
if (Mutable.HideWhenInactive)
ImGui.Checkbox(Language.Options_Tabs_InactivityBehaviour, ref tab.UnhideOnActivity);
ImGui.Spacing();
ImGui.Checkbox(Language.Options_Tabs_NoInput, ref tab.InputDisabled);
if (!tab.InputDisabled)
// ── Sub-section: Display ──────────────────────────────────────────
if (sectionJustEntered) ImGui.SetNextItemOpen(false);
using (var secDisplay = ImRaii.TreeNode(HellionStrings.Settings_Section_Tab_Display + $"##sec-display-{i}"))
{
var input =
tab.Channel?.ToChatType().Name() ?? Language.Options_Tabs_NoInputChannel;
using (
var combo = ImGuiUtil.BeginComboVertical(
Language.Options_Tabs_InputChannel,
input
)
)
if (secDisplay.Success)
{
if (combo.Success)
{
if (
ImGui.Selectable(
Language.Options_Tabs_NoInputChannel,
tab.Channel == null
)
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
ImGui.Checkbox(Language.Options_Tabs_ShowTimestamps, ref tab.DisplayTimestamp);
using (
var combo = ImGuiUtil.BeginComboVertical(
Language.Options_Tabs_UnreadMode,
tab.UnreadMode.Name()
)
tab.Channel = null;
foreach (var channel in Enum.GetValues<InputChannel>())
if (
ImGui.Selectable(
channel.ToChatType().Name(),
tab.Channel == channel
)
)
tab.Channel = channel;
}
}
var player = Plugin.ObjectTable.LocalPlayer;
if (tab.Channel == InputChannel.Tell && player != null)
{
ImGui.Checkbox(Language.Options_Tabs_SenderMessages, ref tab.AllSenderMessages);
ImGuiUtil.HelpText(Language.Options_Help_SenderMessages);
var worlds = Sheets
.WorldsOnDatacenter(player)
.OrderByDescending(world => world.DataCenter.RowId)
.ThenBy(world => world.Name.ToString())
.ToList();
using (ImRaii.ItemWidth(ImGui.GetWindowWidth() / 3f))
)
{
ImGui.Text(Language.Options_Header_Target);
ImGui.SameLine();
var name = tab.TellTarget.Name;
if (ImGui.InputText("##targetInput", ref name, 21))
tab.TellTarget.Name = name;
ImGui.SameLine();
// Guard against an empty worlds list (character switch or sheet not yet populated)
// to avoid an out-of-bounds crash on worlds[selectedWorld].
if (worlds.Count == 0)
if (combo.Success)
{
ImGui.TextDisabled("(no worlds available)");
}
else
{
var selectedWorld = worlds.FindIndex(world =>
world.RowId == tab.TellTarget.World
);
if (selectedWorld == -1)
selectedWorld = 0;
using (
var combo = ImRaii.Combo(
"###player-world",
worlds[selectedWorld].Name.ToString()
)
)
foreach (var mode in Enum.GetValues<UnreadMode>())
{
if (combo.Success)
if (ImGui.Selectable(mode.Name(), tab.UnreadMode == mode))
tab.UnreadMode = mode;
if (mode.Tooltip() is { } tooltip && ImGui.IsItemHovered())
ImGuiUtil.Tooltip(tooltip);
}
}
}
// Only relevant when the global hide-when-inactive is on.
if (Mutable.HideWhenInactive)
ImGui.Checkbox(Language.Options_Tabs_InactivityBehaviour, ref tab.UnhideOnActivity);
}
}
ImGui.Spacing();
// ── Sub-section: Notification ─────────────────────────────────────
if (sectionJustEntered) ImGui.SetNextItemOpen(false);
using (var secNotif = ImRaii.TreeNode(HellionStrings.Settings_Section_Tab_Notification + $"##sec-notif-{i}"))
{
if (secNotif.Success)
{
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
ImGui.Checkbox(
HellionStrings.Tabs_NotificationSound_Enable_Name,
ref tab.EnableNotificationSound
);
ImGuiUtil.HelpMarker(HellionStrings.Tabs_NotificationSound_Description);
if (tab.EnableNotificationSound)
{
using var notifIndent = ImRaii.PushIndent(10.0f);
// Build a readable preview label for the currently selected sound.
var soundPreview =
tab.NotificationSoundId <= 16
? $"{HellionStrings.Tabs_NotificationSound_Option} {tab.NotificationSoundId}"
: $"{HellionStrings.Tabs_NotificationSound_CustomOption} {tab.NotificationSoundId - 16}";
using (var combo = ImRaii.Combo($"##notif-sound-{i}", soundPreview))
{
if (combo.Success)
{
for (uint s = 1; s <= 16; s++)
{
var lastDc = worlds.First().DataCenter.RowId;
foreach (var (idx, world) in worlds.Index())
{
if (
ImGui.Selectable(
world.Name.ToString(),
selectedWorld == idx
)
if (
ImGui.Selectable(
$"{HellionStrings.Tabs_NotificationSound_Option} {s}",
tab.NotificationSoundId == s
)
{
selectedWorld = idx;
tab.TellTarget.World = worlds[selectedWorld].RowId;
}
)
tab.NotificationSoundId = s;
}
if (lastDc == world.DataCenter.RowId)
continue;
ImGui.Separator();
lastDc = world.DataCenter.RowId;
ImGui.Separator();
}
// Bundled custom sounds (ids 17-19).
for (uint n = 1; n <= 3; n++)
{
var customId = 16 + n;
if (
ImGui.Selectable(
$"{HellionStrings.Tabs_NotificationSound_CustomOption} {n}",
tab.NotificationSoundId == customId
)
)
tab.NotificationSoundId = customId;
}
}
}
// Let the user hear the currently selected sound without waiting
// for a real message to arrive in this tab.
ImGui.SameLine();
if (
ImGuiUtil.IconButton(
FontAwesomeIcon.Play,
tooltip: HellionStrings.Tabs_NotificationSound_Preview
)
)
{
var previewId = tab.NotificationSoundId;
if (previewId <= 16)
{
Plugin.Framework.RunOnFrameworkThread(() =>
{
unsafe
{
UIGlobals.PlaySoundEffect(previewId);
}
});
}
else
{
Plugin.CustomAudioPlayer.Play((int)previewId - 16, Mutable.CustomSoundVolume);
}
}
}
var target =
(Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target)
as IPlayerCharacter;
using (ImRaii.Disabled(target == null))
// Volume is stored as a 0-1 float but shown as 0-100%.
// Same field as General → Sound; shown here for convenience.
// DragFloatVertical derives its widget ID from the label text and exposes no
// override. We inline the equivalent (text label + SetNextItemWidth + DragFloat)
// to keep an explicit ##tab-volume-{i} ID, which reads more clearly than relying
// on the surrounding PushId("tab-{i}") scope to disambiguate identical labels.
// Volume is global (Mutable.CustomSoundVolume) and applies to every tab's
// notification sound, so it is shown unconditionally — not gated by the
// per-tab EnableNotificationSound toggle.
ImGui.TextUnformatted(HellionStrings.Settings_General_CustomSoundVolume_Name);
ImGui.SetNextItemWidth(-1);
var customSoundVolumePercent = Mutable.CustomSoundVolume * 100f;
if (
ImGui.DragFloat(
$"##tab-volume-{i}",
ref customSoundVolumePercent,
1f,
0f,
100f,
$"{customSoundVolumePercent:N0}%%",
ImGuiSliderFlags.AlwaysClamp
)
)
{
if (ImGui.Button("Set to target") && target != null)
tab.TellTarget.FromTarget(target);
Mutable.CustomSoundVolume = customSoundVolumePercent / 100f;
}
// Applies globally — same value as in General → Sound.
ImGuiUtil.HelpMarker(HellionStrings.Settings_General_CustomSoundVolume_Description + "\n\n" + HellionStrings.Settings_Section_Tab_Volume_AllTabsHint);
}
}
ImGui.Spacing();
// ── Sub-section: Input ────────────────────────────────────────────
if (sectionJustEntered) ImGui.SetNextItemOpen(false);
using (var secInput = ImRaii.TreeNode(HellionStrings.Settings_Section_Tab_Input + $"##sec-input-{i}"))
{
if (secInput.Success)
{
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
ImGui.Checkbox(Language.Options_Tabs_NoInput, ref tab.InputDisabled);
if (!tab.InputDisabled)
{
var input =
tab.Channel?.ToChatType().Name() ?? Language.Options_Tabs_NoInputChannel;
using (
var combo = ImGuiUtil.BeginComboVertical(
Language.Options_Tabs_InputChannel,
input
)
)
{
if (combo.Success)
{
if (
ImGui.Selectable(
Language.Options_Tabs_NoInputChannel,
tab.Channel == null
)
)
tab.Channel = null;
foreach (var channel in Enum.GetValues<InputChannel>())
if (
ImGui.Selectable(
channel.ToChatType().Name(),
tab.Channel == channel
)
)
tab.Channel = channel;
}
}
var player = Plugin.ObjectTable.LocalPlayer;
if (tab.Channel == InputChannel.Tell && player != null)
{
ImGui.Checkbox(Language.Options_Tabs_SenderMessages, ref tab.AllSenderMessages);
ImGuiUtil.HelpText(Language.Options_Help_SenderMessages);
var worlds = Sheets
.WorldsOnDatacenter(player)
.OrderByDescending(world => world.DataCenter.RowId)
.ThenBy(world => world.Name.ToString())
.ToList();
using (ImRaii.ItemWidth(ImGui.GetWindowWidth() / 3f))
{
ImGui.Text(Language.Options_Header_Target);
ImGui.SameLine();
var name = tab.TellTarget.Name;
if (ImGui.InputText("##targetInput", ref name, 21))
tab.TellTarget.Name = name;
ImGui.SameLine();
// Guard against an empty worlds list (character switch or sheet not yet populated)
// to avoid an out-of-bounds crash on worlds[selectedWorld].
if (worlds.Count == 0)
{
ImGui.TextDisabled("(no worlds available)");
}
else
{
var selectedWorld = worlds.FindIndex(world =>
world.RowId == tab.TellTarget.World
);
if (selectedWorld == -1)
selectedWorld = 0;
using (
var combo = ImRaii.Combo(
"###player-world",
worlds[selectedWorld].Name.ToString()
)
)
{
if (combo.Success)
{
var lastDc = worlds.First().DataCenter.RowId;
foreach (var (idx, world) in worlds.Index())
{
if (
ImGui.Selectable(
world.Name.ToString(),
selectedWorld == idx
)
)
{
selectedWorld = idx;
tab.TellTarget.World = worlds[selectedWorld].RowId;
}
if (lastDc == world.DataCenter.RowId)
continue;
lastDc = world.DataCenter.RowId;
ImGui.Separator();
}
}
}
}
}
var target =
(Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target)
as IPlayerCharacter;
using (ImRaii.Disabled(target == null))
{
if (ImGui.Button("Set to target") && target != null)
tab.TellTarget.FromTarget(target);
}
}
}
}
}
ImGuiUtil.ChannelSelector(Language.Options_Tabs_Channels, tab.SelectedChannels);
ImGuiUtil.ExtraChatSelector(
Language.Options_Tabs_ExtraChatChannels,
ref tab.ExtraChatAll,
tab.ExtraChatChannels
);
ImGui.Spacing();
// ── Sub-section: Pop-out window ───────────────────────────────────
if (sectionJustEntered) ImGui.SetNextItemOpen(false);
using (var secPopOut = ImRaii.TreeNode(HellionStrings.Settings_Section_Tab_PopOut + $"##sec-popout-{i}"))
{
if (secPopOut.Success)
{
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
ImGui.Checkbox(Language.Options_Tabs_PopOut, ref tab.PopOut);
if (tab.PopOut)
{
using var _ = ImRaii.PushIndent(10.0f);
ImGui.Checkbox(
Language.Options_Tabs_IndependentOpacity,
ref tab.IndependentOpacity
);
if (tab.IndependentOpacity)
ImGuiUtil.DragFloatVertical(
Language.Options_Tabs_Opacity,
ref tab.Opacity,
0.25f,
0f,
100f,
$"{tab.Opacity:N2}%%",
ImGuiSliderFlags.AlwaysClamp
);
ImGui.Checkbox(Language.Options_Tabs_IndependentHide, ref tab.IndependentHide);
if (tab.IndependentHide)
{
using var __ = ImRaii.PushIndent(10.0f);
ImGuiUtil.OptionCheckbox(
ref tab.HideDuringCutscenes,
Language.Options_HideDuringCutscenes_Name
);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(
ref tab.HideWhenNotLoggedIn,
Language.Options_HideWhenNotLoggedIn_Name
);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(
ref tab.HideWhenUiHidden,
Language.Options_HideWhenUiHidden_Name
);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(
ref tab.HideInLoadingScreens,
Language.Options_HideInLoadingScreens_Name
);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(
ref tab.HideInBattle,
Language.Options_HideInBattle_Name
);
ImGui.Spacing();
}
ImGuiUtil.OptionCheckbox(ref tab.CanMove, Language.Popout_CanMove_Name);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref tab.CanResize, Language.Popout_CanResize_Name);
ImGui.Spacing();
}
}
}
}
if (toRemove > -1)