Skip to content

Commit 85a623e

Browse files
committed
feat(Profile::Hotkey): Profile can now share the same hotkey. Doing so let you switch between profile. A quick menu is also displayed.
Fixes #409
1 parent 0c72939 commit 85a623e

File tree

3 files changed

+118
-66
lines changed

3 files changed

+118
-66
lines changed

SoundSwitch/Framework/Profile/ProfileManager.cs

+68-63
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
using SoundSwitch.Audio.Manager.Interop.Enum;
1414
using SoundSwitch.Common.Framework.Audio.Device;
1515
using SoundSwitch.Framework.Configuration;
16+
using SoundSwitch.Framework.Profile.Hotkey;
1617
using SoundSwitch.Framework.Profile.Trigger;
1718
using SoundSwitch.Framework.WinApi;
18-
using SoundSwitch.Framework.WinApi.Keyboard;
1919
using SoundSwitch.Localization;
2020
using SoundSwitch.Model;
2121
using SoundSwitch.Util;
@@ -26,75 +26,94 @@ public class ProfileManager
2626
{
2727
public delegate void ShowError(string errorMessage, string errorTitle);
2828

29-
private readonly WindowMonitor _windowMonitor;
30-
private readonly AudioSwitcher _audioSwitcher;
31-
private readonly IAudioDeviceLister _activeDeviceLister;
32-
private readonly ShowError _showError;
33-
private readonly TriggerFactory _triggerFactory;
29+
private readonly WindowMonitor _windowMonitor;
30+
private readonly AudioSwitcher _audioSwitcher;
31+
private readonly IAudioDeviceLister _activeDeviceLister;
32+
private readonly ShowError _showError;
33+
private readonly TriggerFactory _triggerFactory;
3434
private readonly NotificationManager.NotificationManager _notificationManager;
3535

3636
private Profile? _steamProfile;
3737

3838
private readonly Dictionary<User32.NativeMethods.HWND, Profile> _activeWindowsTrigger = new();
3939

40-
private readonly Dictionary<HotKey, Profile> _profilesByHotkey = new();
4140
private readonly Dictionary<string, (Profile Profile, Trigger.Trigger Trigger)> _profileByApplication = new();
4241
private readonly Dictionary<string, (Profile Profile, Trigger.Trigger Trigger)> _profilesByWindowName = new();
43-
private readonly Dictionary<string, (Profile Profile, Trigger.Trigger Trigger)> _profilesByUwpApp = new();
42+
private readonly Dictionary<string, (Profile Profile, Trigger.Trigger Trigger)> _profilesByUwpApp = new();
43+
44+
private readonly ProfileHotkeyManager _profileHotkeyManager;
4445

4546

4647
public IReadOnlyCollection<Profile> Profiles => AppConfigs.Configuration.Profiles;
4748

48-
public ProfileManager(WindowMonitor windowMonitor,
49-
AudioSwitcher audioSwitcher,
50-
IAudioDeviceLister activeDeviceLister,
51-
ShowError showError,
52-
TriggerFactory triggerFactory,
49+
public ProfileManager(WindowMonitor windowMonitor,
50+
AudioSwitcher audioSwitcher,
51+
IAudioDeviceLister activeDeviceLister,
52+
ShowError showError,
53+
TriggerFactory triggerFactory,
5354
NotificationManager.NotificationManager notificationManager)
5455
{
55-
_windowMonitor = windowMonitor;
56-
_audioSwitcher = audioSwitcher;
57-
_activeDeviceLister = activeDeviceLister;
58-
_showError = showError;
59-
_triggerFactory = triggerFactory;
56+
_windowMonitor = windowMonitor;
57+
_audioSwitcher = audioSwitcher;
58+
_activeDeviceLister = activeDeviceLister;
59+
_showError = showError;
60+
_triggerFactory = triggerFactory;
6061
_notificationManager = notificationManager;
62+
_profileHotkeyManager = new(this);
6163
}
6264

63-
private void RegisterTriggers(Profile profile, bool onInit = false)
65+
private bool RegisterTriggers(Profile profile, bool onInit = false)
6466
{
67+
var success = true;
6568
foreach (var trigger in profile.Triggers)
6669
{
67-
trigger.Type.Switch(() => { _profilesByHotkey.Add(trigger.HotKey, profile); },
68-
() => { _profilesByWindowName.Add(trigger.WindowName.ToLower(), (profile, trigger)); },
69-
() => { _profileByApplication.Add(trigger.ApplicationPath.ToLower(), (profile, trigger)); },
70-
() => { _steamProfile = profile; },
70+
success &= trigger.Type.Match(() => _profileHotkeyManager.Add(trigger.HotKey, profile),
71+
() =>
72+
{
73+
_profilesByWindowName.Add(trigger.WindowName.ToLower(), (profile, trigger));
74+
return true;
75+
},
76+
() =>
77+
{
78+
_profileByApplication.Add(trigger.ApplicationPath.ToLower(), (profile, trigger));
79+
return true;
80+
},
81+
() =>
82+
{
83+
_steamProfile = profile;
84+
return true;
85+
},
7186
() =>
7287
{
7388
if (!onInit)
7489
{
75-
return;
90+
return true;
7691
}
7792

7893
SwitchAudio(profile);
79-
}, () => { _profilesByUwpApp.Add(trigger.WindowName.ToLower(), (profile, trigger)); },
80-
() => {});
94+
return true;
95+
}, () =>
96+
{
97+
_profilesByUwpApp.Add(trigger.WindowName.ToLower(), (profile, trigger));
98+
return true;
99+
},
100+
() => true);
101+
81102
}
103+
104+
return success;
82105
}
83106

84107
private void UnRegisterTriggers(Profile profile)
85108
{
86109
foreach (var trigger in profile.Triggers)
87110
{
88-
trigger.Type.Switch(() =>
89-
{
90-
WindowsAPIAdapter.UnRegisterHotKey(trigger.HotKey);
91-
_profilesByHotkey.Remove(trigger.HotKey);
92-
},
111+
trigger.Type.Switch(() => { _profileHotkeyManager.Remove(trigger.HotKey, profile); },
93112
() => { _profilesByWindowName.Remove(trigger.WindowName.ToLower()); },
94113
() => { _profileByApplication.Remove(trigger.ApplicationPath.ToLower()); },
95114
() => { _steamProfile = null; }, () => { },
96115
() => { _profilesByUwpApp.Remove(trigger.WindowName.ToLower()); },
97-
() => {});
116+
() => { });
98117
}
99118
}
100119

@@ -104,18 +123,10 @@ private void UnRegisterTriggers(Profile profile)
104123
/// <returns></returns>
105124
public Result<Profile[], VoidSuccess> Init()
106125
{
107-
foreach (var profile in AppConfigs.Configuration.Profiles)
108-
{
109-
RegisterTriggers(profile, true);
110-
}
126+
var errors = AppConfigs.Configuration.Profiles.Where(profile => !RegisterTriggers(profile, true)).ToArray();
111127

112128
RegisterEvents();
113129

114-
var errors = _profilesByHotkey
115-
.Where(pair => !WindowsAPIAdapter.RegisterHotKey(pair.Key))
116-
.Select(pair => pair.Value)
117-
.ToArray();
118-
119130
InitializeProfileExistingProcess();
120131

121132
if (errors.Length > 0)
@@ -139,12 +150,6 @@ private void RegisterEvents()
139150
if (HandleWindowName(@event)) return;
140151
};
141152

142-
WindowsAPIAdapter.HotKeyPressed += (sender, args) =>
143-
{
144-
if (!_profilesByHotkey.TryGetValue(args.HotKey, out var profile))
145-
return;
146-
SwitchAudio(profile);
147-
};
148153
WindowsAPIAdapter.WindowDestroyed += (sender, @event) => { RestoreState(@event.Hwnd); };
149154
}
150155

@@ -209,18 +214,18 @@ private bool SaveCurrentState(User32.NativeMethods.HWND windowHandle, Profile pr
209214
return false;
210215
}
211216

212-
var communication = _audioSwitcher.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eCommunications);
213-
var playback = _audioSwitcher.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia);
214-
var recording = _audioSwitcher.GetDefaultAudioEndpoint(EDataFlow.eCapture, ERole.eMultimedia);
217+
var communication = _audioSwitcher.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eCommunications);
218+
var playback = _audioSwitcher.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia);
219+
var recording = _audioSwitcher.GetDefaultAudioEndpoint(EDataFlow.eCapture, ERole.eMultimedia);
215220

216221
var currentState = new Profile
217222
{
218223
AlsoSwitchDefaultDevice = true,
219-
Name = SettingsStrings.profile_trigger_restoreDevices_title,
220-
Communication = communication,
221-
Playback = playback,
222-
Recording = recording,
223-
NotifyOnActivation = profile.NotifyOnActivation
224+
Name = SettingsStrings.profile_trigger_restoreDevices_title,
225+
Communication = communication,
226+
Playback = playback,
227+
Recording = recording,
228+
NotifyOnActivation = profile.NotifyOnActivation
224229
};
225230
_activeWindowsTrigger.Add(windowHandle, currentState);
226231
return true;
@@ -283,7 +288,7 @@ private void SwitchAudio(Profile profile, uint processId)
283288
_audioSwitcher.SwitchProcessTo(
284289
deviceToUse.Id,
285290
device.Role,
286-
(EDataFlow) deviceToUse.Type,
291+
(EDataFlow)deviceToUse.Type,
287292
processId);
288293

289294
if (profile.AlsoSwitchDefaultDevice)
@@ -317,7 +322,7 @@ public void SwitchAudio(Profile profile)
317322
/// <returns></returns>
318323
public IEnumerable<ITriggerDefinition> AvailableTriggers()
319324
{
320-
var triggers = Profiles.SelectMany(profile => profile.Triggers).GroupBy(trigger => trigger.Type).ToDictionary(grouping => grouping.Key, grouping => grouping.Count());
325+
var triggers = Profiles.SelectMany(profile => profile.Triggers).GroupBy(trigger => trigger.Type).ToDictionary(grouping => grouping.Key, grouping => grouping.Count());
321326
var triggerFactory = new TriggerFactory();
322327
return triggerFactory.AllImplementations
323328
.Where(pair =>
@@ -353,7 +358,7 @@ public Result<string, VoidSuccess> AddProfile(Profile profile)
353358
/// </summary>
354359
public Result<string, VoidSuccess> UpdateProfile(Profile oldProfile, Profile newProfile)
355360
{
356-
DeleteProfiles(new[] {oldProfile});
361+
DeleteProfiles(new[] { oldProfile });
357362
return ValidateProfile(newProfile)
358363
.Map(success =>
359364
{
@@ -394,7 +399,7 @@ private Result<string, VoidSuccess> ValidateProfile(Profile profile)
394399
{
395400
var error = trigger.Type.Match(() =>
396401
{
397-
if (trigger.HotKey == null || _profilesByHotkey.ContainsKey(trigger.HotKey) || !WindowsAPIAdapter.RegisterHotKey(trigger.HotKey))
402+
if (trigger.HotKey == null || !_profileHotkeyManager.IsValidHotkey(trigger.HotKey))
398403
{
399404
return string.Format(SettingsStrings.profile_error_hotkey, trigger.HotKey);
400405
}
@@ -454,7 +459,7 @@ private Result<string, VoidSuccess> ValidateAddProfile(ProfileSetting profile)
454459
return SettingsStrings.profile_error_needPlaybackOrRecording;
455460
}
456461

457-
if (profile.HotKey != null && _profilesByHotkey.ContainsKey(profile.HotKey))
462+
if (profile.HotKey != null && !_profileHotkeyManager.IsValidHotkey(profile.HotKey))
458463
{
459464
return string.Format(SettingsStrings.profile_error_hotkey, profile.HotKey);
460465
}
@@ -484,8 +489,8 @@ private Result<string, VoidSuccess> ValidateAddProfile(ProfileSetting profile)
484489
/// </summary>
485490
public Result<Profile[], VoidSuccess> DeleteProfiles(IEnumerable<Profile> profilesToDelete)
486491
{
487-
var errors = new List<Profile>();
488-
var profiles = profilesToDelete.ToArray();
492+
var errors = new List<Profile>();
493+
var profiles = profilesToDelete.ToArray();
489494
var resetProcessAudio = profiles.Any(profile => profile.Triggers.Any(trigger => trigger.Type == TriggerFactory.Enum.Process || trigger.Type == TriggerFactory.Enum.Window));
490495
foreach (var profile in profiles)
491496
{
@@ -540,7 +545,7 @@ private void InitializeProfileExistingProcess()
540545
{
541546
var handle = User32.NativeMethods.HWND.Cast(process.Handle);
542547
SaveCurrentState(handle, profile.Profile, profile.Trigger);
543-
SwitchAudio(profile.Profile, (uint) process.Id);
548+
SwitchAudio(profile.Profile, (uint)process.Id);
544549
}
545550
}
546551
catch (Win32Exception)

SoundSwitch/UI/Component/HotKeyTextBox.cs

+49-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
using System.Drawing;
44
using System.Linq;
55
using System.Windows.Forms;
6-
using HotKey = SoundSwitch.Framework.WinApi.Keyboard.HotKey;
7-
using KeyboardWindowsAPI = SoundSwitch.Framework.WinApi.Keyboard.KeyboardWindowsAPI;
6+
using SoundSwitch.Framework.WinApi;
7+
using SoundSwitch.Framework.WinApi.Keyboard;
88

99
namespace SoundSwitch.UI.Component
1010
{
@@ -27,13 +27,59 @@ public HotKey HotKey
2727
}
2828

2929
private HotKey _hotKey;
30-
[Browsable(true)] public event EventHandler<Event> HotKeyChanged;
30+
private bool _listenToHotkey;
31+
32+
[Browsable(true)]
33+
public event EventHandler<Event> HotKeyChanged;
34+
35+
[Browsable(true)]
36+
public bool ListenToHotkey
37+
{
38+
get => _listenToHotkey;
39+
set
40+
{
41+
_listenToHotkey = value;
42+
if (value)
43+
{
44+
WindowsAPIAdapter.HotKeyPressed += WindowsAPIAdapterOnHotKeyPressed;
45+
}
46+
else
47+
{
48+
WindowsAPIAdapter.HotKeyPressed -= WindowsAPIAdapterOnHotKeyPressed;
49+
}
50+
}
51+
}
3152

3253
public void CleanHotKeyChangedHandler()
3354
{
3455
HotKeyChanged = null;
3556
}
3657

58+
59+
protected override void Dispose(bool disposing)
60+
{
61+
if (disposing && ListenToHotkey)
62+
{
63+
WindowsAPIAdapter.HotKeyPressed -= WindowsAPIAdapterOnHotKeyPressed;
64+
}
65+
base.Dispose(disposing);
66+
}
67+
68+
private void WindowsAPIAdapterOnHotKeyPressed(object? sender, WindowsAPIAdapter.KeyPressedEventArgs e)
69+
{
70+
Invoke(new Action(() =>
71+
{
72+
if (!Visible)
73+
{
74+
return;
75+
}
76+
77+
HotKey = e.HotKey;
78+
ForeColor = Color.Green;
79+
HotKeyChanged?.Invoke(this, new Event());
80+
}), null);
81+
}
82+
3783
protected override void OnKeyDown(KeyEventArgs e)
3884
{
3985
HotKey.ModifierKeys modifierKeys = 0;

SoundSwitch/UI/Forms/UpsertProfileExtended.cs

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public UpsertProfileExtended(Profile profile, IEnumerable<DeviceFullInfo> playba
3838
HideTriggerComponents();
3939

4040
hotKeyControl.Location = textInput.Location;
41+
hotKeyControl.ListenToHotkey = true;
4142

4243
LocalizeForm();
4344
using var iconBitmap = new Bitmap(Resources.profile_menu_icon);

0 commit comments

Comments
 (0)