From ee39fd0eecee737ba1f477973e17d9ea6d52a746 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Sat, 23 May 2026 02:11:04 +0200 Subject: [PATCH] refactor(settings): rebuild the per-tab panel into sub-sections --- .../Resources/HellionStrings.Designer.cs | 8 + HellionChat/Resources/HellionStrings.resx | 20 + HellionChat/Ui/SettingsTabs/Tabs.cs | 602 ++++++++++-------- 3 files changed, 379 insertions(+), 251 deletions(-) diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index adff707..f538536 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -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)); } diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index 83f049a..f8c30ce 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -1158,4 +1158,24 @@ Frame + + + + Channels + + + Display + + + Notification + + + Input + + + Pop-out window + + + This volume applies to all tabs. + diff --git a/HellionChat/Ui/SettingsTabs/Tabs.cs b/HellionChat/Ui/SettingsTabs/Tabs.cs index 5f571a3..4bd008d 100755 --- a/HellionChat/Ui/SettingsTabs/Tabs.cs +++ b/HellionChat/Ui/SettingsTabs/Tabs.cs @@ -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()) - { - 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()) - 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()) { - 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()) + 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)