diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ActionProcessor.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ActionProcessor.cs index e9b24325c..139114bee 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ActionProcessor.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ActionProcessor.cs @@ -1,14 +1,11 @@ using System; -using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using EarTrumpet.Actions.DataModel.Enum; using EarTrumpet.Actions.DataModel.Serialization; -using EarTrumpet.DataModel.AppInformation; using EarTrumpet.DataModel.Audio; using EarTrumpet.DataModel.WindowsAudio; using EarTrumpet.Extensions; -using Windows.Win32; namespace EarTrumpet.Actions.DataModel.Processing; @@ -41,7 +38,7 @@ public static void Invoke(BaseAction a) { if (setAppVolumeAction.App.Id == AppRef.ForegroundAppId) { - var app = FindForegroundApp(device.Groups); + var app = ForegroundAppResolver.FindForegroundApp(device.Groups); if (app != null) { DoAudioAction(setAppVolumeAction.Option, app, setAppVolumeAction); @@ -66,7 +63,7 @@ public static void Invoke(BaseAction a) { if (setAppMuteAction.App.Id == AppRef.ForegroundAppId) { - var app = FindForegroundApp(device.Groups); + var app = ForegroundAppResolver.FindForegroundApp(device.Groups); if (app != null) { DoAudioAction(setAppMuteAction.Option, app); @@ -109,64 +106,6 @@ public static void Invoke(BaseAction a) } } - private static IAudioDeviceSession FindForegroundApp(ObservableCollection groups) - { - var hWnd = PInvoke.GetForegroundWindow(); - if (hWnd == (HWND)null) - { - Trace.WriteLine($"ActionProcessor FindForegroundApp: No Window (1)"); - return null; - } - - var className = string.Empty; - unsafe - { - Span classNameBuffer = stackalloc char[(int)PInvoke.MAX_CLASS_NAME_LEN]; - fixed (char* pClassNameBuffer = classNameBuffer) - { - _ = PInvoke.GetClassName(hWnd, pClassNameBuffer, classNameBuffer.Length); - className = new PWSTR(pClassNameBuffer).ToString(); - } - } - - // ApplicationFrameWindow.exe, find the real hosted process in the child CoreWindow. - if (className.ToString() == "ApplicationFrameWindow") - { - hWnd = PInvoke.FindWindowEx(hWnd, (HWND)null, "Windows.UI.Core.CoreWindow", null); - } - if (hWnd == (HWND)null) - { - Trace.WriteLine($"ActionProcessor FindForegroundApp: No Window (2)"); - return null; - } - - var processId = 0U; - unsafe - { - _ = PInvoke.GetWindowThreadProcessId(hWnd, &processId); - } - - try - { - var appInfo = AppInformationFactory.CreateForProcess(processId); - - foreach(var group in groups) - { - if (group.AppId == appInfo.PackageInstallPath || group.AppId == appInfo.AppId) - { - Trace.WriteLine($"ActionProcessor FindForegroundApp: {group.DisplayName}"); - return group; - } - } - } - catch(Exception ex) - { - Trace.WriteLine(ex); - } - Trace.WriteLine("ActionProcessor FindForegroundApp Didn't locate foreground app"); - return null; - } - private static void DoAudioAction(MuteKind action, IStreamWithVolumeControl stream) { switch (action) diff --git a/EarTrumpet/App.xaml.cs b/EarTrumpet/App.xaml.cs index ee4a2952b..0812abfe7 100644 --- a/EarTrumpet/App.xaml.cs +++ b/EarTrumpet/App.xaml.cs @@ -1,3 +1,4 @@ +using EarTrumpet.DataModel.Audio; using System; using System.Collections.Generic; using System.Diagnostics; @@ -57,6 +58,7 @@ public DeviceCollectionViewModel CollectionViewModel private static readonly Stopwatch s_appTimer = Stopwatch.StartNew(); private FlyoutViewModel _flyoutViewModel; + private const int c_focusedAppVolumeStep = 2; private ShellNotifyIcon _trayIcon; private WindowHolder _mixerWindow; private WindowHolder _settingsWindow; @@ -150,6 +152,9 @@ private void CompleteStartup() Settings.SettingsHotkeyTyped += () => _settingsWindow.OpenOrBringToFront(); Settings.AbsoluteVolumeUpHotkeyTyped += AbsoluteVolumeIncrement; Settings.AbsoluteVolumeDownHotkeyTyped += AbsoluteVolumeDecrement; + Settings.FocusedAppVolumeUpHotkeyTyped += FocusedAppVolumeIncrement; + Settings.FocusedAppVolumeDownHotkeyTyped += FocusedAppVolumeDecrement; + Settings.FocusedAppToggleMuteHotkeyTyped += FocusedAppToggleMute; Settings.RegisterHotkeys(); Settings.UseLogarithmicVolumeChanged += (_, __) => UpdateTrayTooltip(); @@ -428,4 +433,51 @@ private void AbsoluteVolumeDecrement() } } } + + private void FocusedAppVolumeIncrement() + { + ChangeFocusedAppVolume(c_focusedAppVolumeStep); + } + + private void FocusedAppVolumeDecrement() + { + ChangeFocusedAppVolume(-c_focusedAppVolumeStep); + } + + private void ChangeFocusedAppVolume(int delta) + { + foreach (var app in GetFocusedApps()) + { + app.Volume = Math.Max(0, Math.Min(100, app.Volume + delta)); + } + } + + private void FocusedAppToggleMute() + { + var apps = GetFocusedApps().ToArray(); + if (!apps.Any()) + { + return; + } + + var shouldMute = apps.Any(app => !app.IsMuted); + foreach (var app in apps) + { + app.IsMuted = shouldMute; + } + } + + private IEnumerable GetFocusedApps() + { + var foregroundAppIds = ForegroundAppResolver.TryGetForegroundAppIds(); + if (foregroundAppIds.Count == 0) + { + return Enumerable.Empty(); + } + + return CollectionViewModel.AllDevices + .SelectMany(device => device.Apps) + .Where(app => foregroundAppIds.Contains(app.AppId)) + .ToArray(); + } } diff --git a/EarTrumpet/AppSettings.cs b/EarTrumpet/AppSettings.cs index 310cad2d5..ebc050dff 100644 --- a/EarTrumpet/AppSettings.cs +++ b/EarTrumpet/AppSettings.cs @@ -16,6 +16,9 @@ public class AppSettings public event Action SettingsHotkeyTyped; public event Action AbsoluteVolumeUpHotkeyTyped; public event Action AbsoluteVolumeDownHotkeyTyped; + public event Action FocusedAppVolumeUpHotkeyTyped; + public event Action FocusedAppVolumeDownHotkeyTyped; + public event Action FocusedAppToggleMuteHotkeyTyped; private readonly ISettingsBag _settings = StorageFactory.GetSettings(); @@ -26,6 +29,9 @@ public void RegisterHotkeys() HotkeyManager.Current.Register(SettingsHotkey); HotkeyManager.Current.Register(AbsoluteVolumeUpHotkey); HotkeyManager.Current.Register(AbsoluteVolumeDownHotkey); + HotkeyManager.Current.Register(FocusedAppVolumeUpHotkey); + HotkeyManager.Current.Register(FocusedAppVolumeDownHotkey); + HotkeyManager.Current.Register(FocusedAppToggleMuteHotkey); HotkeyManager.Current.KeyPressed += (hotkey) => { @@ -54,6 +60,21 @@ public void RegisterHotkeys() Trace.WriteLine("AppSettings AbsoluteVolumeDownHotkeyTyped"); AbsoluteVolumeDownHotkeyTyped?.Invoke(); } + else if (hotkey.Equals(FocusedAppVolumeUpHotkey)) + { + Trace.WriteLine("AppSettings FocusedAppVolumeUpHotkeyTyped"); + FocusedAppVolumeUpHotkeyTyped?.Invoke(); + } + else if (hotkey.Equals(FocusedAppVolumeDownHotkey)) + { + Trace.WriteLine("AppSettings FocusedAppVolumeDownHotkeyTyped"); + FocusedAppVolumeDownHotkeyTyped?.Invoke(); + } + else if (hotkey.Equals(FocusedAppToggleMuteHotkey)) + { + Trace.WriteLine("AppSettings FocusedAppToggleMuteHotkeyTyped"); + FocusedAppToggleMuteHotkeyTyped?.Invoke(); + } }; } @@ -112,6 +133,39 @@ public HotkeyData AbsoluteVolumeDownHotkey } } + public HotkeyData FocusedAppVolumeUpHotkey + { + get => _settings.Get("FocusedAppVolumeUpHotkey", new HotkeyData { }); + set + { + HotkeyManager.Current.Unregister(FocusedAppVolumeUpHotkey); + _settings.Set("FocusedAppVolumeUpHotkey", value); + HotkeyManager.Current.Register(FocusedAppVolumeUpHotkey); + } + } + + public HotkeyData FocusedAppVolumeDownHotkey + { + get => _settings.Get("FocusedAppVolumeDownHotkey", new HotkeyData { }); + set + { + HotkeyManager.Current.Unregister(FocusedAppVolumeDownHotkey); + _settings.Set("FocusedAppVolumeDownHotkey", value); + HotkeyManager.Current.Register(FocusedAppVolumeDownHotkey); + } + } + + public HotkeyData FocusedAppToggleMuteHotkey + { + get => _settings.Get("FocusedAppToggleMuteHotkey", new HotkeyData { }); + set + { + HotkeyManager.Current.Unregister(FocusedAppToggleMuteHotkey); + _settings.Set("FocusedAppToggleMuteHotkey", value); + HotkeyManager.Current.Register(FocusedAppToggleMuteHotkey); + } + } + public bool UseLegacyIcon { get diff --git a/EarTrumpet/DataModel/Audio/ForegroundAppResolver.cs b/EarTrumpet/DataModel/Audio/ForegroundAppResolver.cs new file mode 100644 index 000000000..80fbfdcfd --- /dev/null +++ b/EarTrumpet/DataModel/Audio/ForegroundAppResolver.cs @@ -0,0 +1,97 @@ +using EarTrumpet.DataModel.AppInformation; +using EarTrumpet.Interop; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace EarTrumpet.DataModel.Audio +{ + public static class ForegroundAppResolver + { + public static IReadOnlyList TryGetForegroundAppIds() + { + var hWnd = User32.GetForegroundWindow(); + var foregroundClassName = new StringBuilder(User32.MAX_CLASSNAME_LENGTH); + User32.GetClassName(hWnd, foregroundClassName, foregroundClassName.Capacity); + + if (hWnd == IntPtr.Zero) + { + Trace.WriteLine("ForegroundAppResolver: No Window (1)"); + return Array.Empty(); + } + + if (foregroundClassName.ToString() == "ApplicationFrameWindow") + { + hWnd = User32.FindWindowEx(hWnd, IntPtr.Zero, "Windows.UI.Core.CoreWindow", IntPtr.Zero); + } + + if (hWnd == IntPtr.Zero) + { + Trace.WriteLine("ForegroundAppResolver: No Window (2)"); + return Array.Empty(); + } + + User32.GetWindowThreadProcessId(hWnd, out uint processId); + + try + { + var appInfo = AppInformationFactory.CreateForProcess((int)processId); + return new[] { appInfo.PackageInstallPath, appInfo.AppId } + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct() + .ToArray(); + } + catch (Exception ex) + { + Trace.WriteLine(ex); + return Array.Empty(); + } + } + + public static IAudioDeviceSession FindForegroundApp(ObservableCollection groups) + { + var foregroundAppIds = TryGetForegroundAppIds(); + if (foregroundAppIds.Count == 0) + { + return null; + } + + var group = groups.FirstOrDefault(candidate => foregroundAppIds.Contains(candidate.AppId)); + if (group != null) + { + Trace.WriteLine($"ForegroundAppResolver: {group.DisplayName}"); + } + else + { + Trace.WriteLine("ForegroundAppResolver: Didn't locate foreground app"); + } + + return group; + } + + public static IAudioDeviceSession FindForegroundApp(IEnumerable devices) + { + var foregroundAppIds = TryGetForegroundAppIds(); + if (foregroundAppIds.Count == 0) + { + return null; + } + + foreach (var device in devices) + { + var group = device.Groups.FirstOrDefault(candidate => foregroundAppIds.Contains(candidate.AppId)); + if (group != null) + { + Trace.WriteLine($"ForegroundAppResolver: {group.DisplayName}"); + return group; + } + } + + Trace.WriteLine("ForegroundAppResolver: Didn't locate foreground app"); + return null; + } + } +} \ No newline at end of file diff --git a/EarTrumpet/Properties/Resources.Designer.cs b/EarTrumpet/Properties/Resources.Designer.cs index e3aef0abd..d271487d5 100644 --- a/EarTrumpet/Properties/Resources.Designer.cs +++ b/EarTrumpet/Properties/Resources.Designer.cs @@ -1386,6 +1386,33 @@ public static string SettingsAbsoluteVolumeUpText { return ResourceManager.GetString("SettingsAbsoluteVolumeUpText", resourceCulture); } } + + /// + /// Looks up a localized string similar to Decrease volume for the focused app. + /// + public static string SettingsFocusedAppVolumeDownText { + get { + return ResourceManager.GetString("SettingsFocusedAppVolumeDownText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Increase volume for the focused app. + /// + public static string SettingsFocusedAppVolumeUpText { + get { + return ResourceManager.GetString("SettingsFocusedAppVolumeUpText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Toggle mute for the focused app. + /// + public static string SettingsFocusedAppToggleMuteText { + get { + return ResourceManager.GetString("SettingsFocusedAppToggleMuteText", resourceCulture); + } + } /// /// Looks up a localized string similar to General. diff --git a/EarTrumpet/Properties/Resources.resx b/EarTrumpet/Properties/Resources.resx index 97f1fb8d9..743ee924d 100644 --- a/EarTrumpet/Properties/Resources.resx +++ b/EarTrumpet/Properties/Resources.resx @@ -652,6 +652,15 @@ Open [https://eartrumpet.app/jmp/fixstartup] to resolve this? Increase volume for all devices + + Decrease volume for the focused app + + + Increase volume for the focused app + + + Toggle mute for the focused app + App volume and device preferences diff --git a/EarTrumpet/UI/ViewModels/EarTrumpetShortcutsPageViewModel.cs b/EarTrumpet/UI/ViewModels/EarTrumpetShortcutsPageViewModel.cs index 3e484dafc..a5ccfe8e1 100644 --- a/EarTrumpet/UI/ViewModels/EarTrumpetShortcutsPageViewModel.cs +++ b/EarTrumpet/UI/ViewModels/EarTrumpetShortcutsPageViewModel.cs @@ -21,6 +21,15 @@ internal class EarTrumpetShortcutsPageViewModel : SettingsPageViewModel public HotkeyViewModel AbsoluteVolumeDownHotkey { get; } public static string DefaultAbsoluteVolumeDownHotkey => s_hotkeyNoneText; + public HotkeyViewModel FocusedAppVolumeUpHotkey { get; } + public static string DefaultFocusedAppVolumeUpHotkey => s_hotkeyNoneText; + + public HotkeyViewModel FocusedAppVolumeDownHotkey { get; } + public static string DefaultFocusedAppVolumeDownHotkey => s_hotkeyNoneText; + + public HotkeyViewModel FocusedAppToggleMuteHotkey { get; } + public static string DefaultFocusedAppToggleMuteHotkey => s_hotkeyNoneText; + public EarTrumpetShortcutsPageViewModel(AppSettings settings) : base(null) { Title = Properties.Resources.ShortcutsPageText; @@ -31,5 +40,8 @@ public EarTrumpetShortcutsPageViewModel(AppSettings settings) : base(null) OpenSettingsHotkey = new HotkeyViewModel(settings.SettingsHotkey, (newHotkey) => settings.SettingsHotkey = newHotkey); AbsoluteVolumeUpHotkey = new HotkeyViewModel(settings.AbsoluteVolumeUpHotkey, (newHotkey) => settings.AbsoluteVolumeUpHotkey = newHotkey); AbsoluteVolumeDownHotkey = new HotkeyViewModel(settings.AbsoluteVolumeDownHotkey, (newHotkey) => settings.AbsoluteVolumeDownHotkey = newHotkey); + FocusedAppVolumeUpHotkey = new HotkeyViewModel(settings.FocusedAppVolumeUpHotkey, (newHotkey) => settings.FocusedAppVolumeUpHotkey = newHotkey); + FocusedAppVolumeDownHotkey = new HotkeyViewModel(settings.FocusedAppVolumeDownHotkey, (newHotkey) => settings.FocusedAppVolumeDownHotkey = newHotkey); + FocusedAppToggleMuteHotkey = new HotkeyViewModel(settings.FocusedAppToggleMuteHotkey, (newHotkey) => settings.FocusedAppToggleMuteHotkey = newHotkey); } } \ No newline at end of file diff --git a/EarTrumpet/UI/Views/SettingsWindow.xaml b/EarTrumpet/UI/Views/SettingsWindow.xaml index b72d72c62..dd8596b9a 100644 --- a/EarTrumpet/UI/Views/SettingsWindow.xaml +++ b/EarTrumpet/UI/Views/SettingsWindow.xaml @@ -162,6 +162,84 @@ Content="{Binding AbsoluteVolumeDownHotkey}" IsTabStop="False" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +