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)