From 36ea8ddcfc95c2ad13d64ca0edfa689f609575a0 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Thu, 21 May 2026 10:39:09 +0200 Subject: [PATCH] feat(ui): add per-tab notification sound for inactive tabs --- HellionChat/Configuration.cs | 6 ++++++ HellionChat/MessageManager.cs | 27 ++++++++++++++++++++++++ HellionChat/Ui/SettingsTabs/Tabs.cs | 21 ++++++++++++++++++ HellionChat/_Helpers/TabSoundDecision.cs | 14 ++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 HellionChat/_Helpers/TabSoundDecision.cs diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 8594e46..ae2c356 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -446,6 +446,10 @@ public class Tab public bool AllSenderMessages; public TellTarget TellTarget = TellTarget.Empty(); + // UI-3: per-tab notification sound for messages arriving in an inactive tab. + public bool EnableNotificationSound; + public uint NotificationSoundId = 1; + [NonSerialized] public uint Unread; @@ -564,6 +568,8 @@ public class Tab IsPinned = IsPinned, AllSenderMessages = AllSenderMessages, TellTarget = TellTarget.Clone(), + EnableNotificationSound = EnableNotificationSound, + NotificationSoundId = NotificationSoundId, IsGreeted = IsGreeted, }; } diff --git a/HellionChat/MessageManager.cs b/HellionChat/MessageManager.cs index fc11a2f..3a0758f 100644 --- a/HellionChat/MessageManager.cs +++ b/HellionChat/MessageManager.cs @@ -7,7 +7,9 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using HellionChat._Helpers; using HellionChat.Code; using HellionChat.Resources; using HellionChat.Util; @@ -330,6 +332,7 @@ internal class MessageManager : IAsyncDisposable Store.UpsertMessage(message); var currentMatches = Plugin.CurrentTab.Matches(message); + uint? notificationSound = null; foreach (var tab in Plugin.Config.Tabs) { var unread = !( @@ -337,7 +340,31 @@ internal class MessageManager : IAsyncDisposable ); if (tab.Matches(message)) + { tab.AddMessage(message, unread); + + // UI-3: per-tab notification sound. Fire once for the first + // inactive tab that wants it — keeps a message matching several + // background tabs from stacking sounds. + // TEST-MIRROR: ../_Helpers/TabSoundDecision.cs + if (notificationSound is null + && TabSoundDecision.ShouldPlay( + Plugin.CurrentTab == tab, + tab.EnableNotificationSound, + Plugin.Config.PlaySounds)) + { + notificationSound = tab.NotificationSoundId; + } + } + } + + if (notificationSound is { } soundId) + { + // ProcessMessage runs on the PendingMessageThread worker; the native + // UIGlobals.PlaySoundEffect must be marshalled onto the framework + // thread (reference_dalamud_framework_thread). + Plugin.Framework.RunOnFrameworkThread( + () => { unsafe { UIGlobals.PlaySoundEffect(soundId); } }); } MessageProcessed?.Invoke(message); diff --git a/HellionChat/Ui/SettingsTabs/Tabs.cs b/HellionChat/Ui/SettingsTabs/Tabs.cs index 3f0948b..7f51c35 100755 --- a/HellionChat/Ui/SettingsTabs/Tabs.cs +++ b/HellionChat/Ui/SettingsTabs/Tabs.cs @@ -165,6 +165,27 @@ 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) + { + using var indent = ImRaii.PushIndent(10.0f); + var soundPreview = $"{HellionStrings.Tabs_NotificationSound_Option} {tab.NotificationSoundId}"; + using var combo = ImRaii.Combo($"##notif-sound-{i}", soundPreview); + 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.Checkbox(Language.Options_Tabs_PopOut, ref tab.PopOut); if (tab.PopOut) { diff --git a/HellionChat/_Helpers/TabSoundDecision.cs b/HellionChat/_Helpers/TabSoundDecision.cs new file mode 100644 index 0000000..ac46cb6 --- /dev/null +++ b/HellionChat/_Helpers/TabSoundDecision.cs @@ -0,0 +1,14 @@ +namespace HellionChat._Helpers; + +// UI-3 pure decision helper: should an incoming message play a per-tab +// notification sound? Kept Dalamud-free so the Build Suite can test the +// "inactive + enabled + global-allowed" rule in isolation. +// TEST-MIRROR: ../../../Hellion Build test/Ui/TabSoundDecisionTests.cs +public static class TabSoundDecision +{ + // True only when the message landed in a tab the user is not looking at, + // that tab has its own sound switched on, and the global sound master is + // not muted. + public static bool ShouldPlay(bool isActiveTab, bool tabSoundEnabled, bool globalSoundsEnabled) + => !isActiveTab && tabSoundEnabled && globalSoundsEnabled; +}