Skip to content

Commit

Permalink
project: use hotkey model instead of keycodes to allow for key combos.
Browse files Browse the repository at this point in the history
  • Loading branch information
Ganom committed Oct 14, 2024
1 parent 5e7569a commit 7c0ad35
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 28 deletions.
2 changes: 1 addition & 1 deletion SimpleTwitchEmoteSounds/Models/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace SimpleTwitchEmoteSounds.Models;
public partial class AppSettings : ObservableObject
{
[ObservableProperty] private ObservableCollection<SoundCommand> _soundCommands = [];
[ObservableProperty] private KeyCode _enableKey = KeyCode.VcF20;
[ObservableProperty] private Hotkey _enableKey = new([KeyCode.VcF20]);

public void RefreshSubscriptions()
{
Expand Down
26 changes: 26 additions & 0 deletions SimpleTwitchEmoteSounds/Models/Hotkey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Linq;
using SharpHook.Native;

namespace SimpleTwitchEmoteSounds.Models;

public class Hotkey(HashSet<KeyCode> keys)
{
private HashSet<KeyCode> Keys { get; } = [..keys];

public override bool Equals(object? obj)
{
return obj is Hotkey combo &&
Keys.SetEquals(combo.Keys);
}

public override int GetHashCode()
{
return Keys.Count;
}

public override string ToString()
{
return string.Join("+", Keys.Select(k => k.ToString().Replace("Vc", "")));
}
}
60 changes: 55 additions & 5 deletions SimpleTwitchEmoteSounds/Services/ConfigService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,72 @@ private static void SubscribeToChanges()
{
Directory.CreateDirectory(settingsFolder);
var defaultConfig = new T();
var defaultConfigJson =
JsonSerializer.Serialize(defaultConfig, Options);
File.WriteAllText(configFilePath, defaultConfigJson);
SaveConfig(name, defaultConfig);
return defaultConfig;
}

var configJson = File.ReadAllText(configFilePath);
return JsonSerializer.Deserialize<T>(configJson) ?? new T();
var config = new T();
var configChanged = false;

try
{
using var document = JsonDocument.Parse(configJson);
var root = document.RootElement;
foreach (var prop in typeof(T).GetProperties())
{
if (root.TryGetProperty(prop.Name, out var element))
{
try
{
var value =
JsonSerializer.Deserialize(element.GetRawText(), prop.PropertyType, Options);
prop.SetValue(config, value);
}
catch (JsonException)
{
Log.Warning($"Failed to deserialize property {prop.Name}. Using default value.");
configChanged = true;
}
}
else
{
Log.Warning($"Property {prop.Name} not found in config. Using default value.");
configChanged = true;
}
}
}
catch (JsonException ex)
{
Log.Error(ex, $"Error parsing {name} config. Using default values.");
config = new T();
configChanged = true;
}

if (configChanged)
{
SaveConfig(name, config, true);
}

return config;
}

private static void SaveConfig<T>(string name, T config) where T : class
private static void SaveConfig<T>(string name, T config, bool createBackup = false) where T : class
{
var appLocation = AppDomain.CurrentDomain.BaseDirectory;
var settingsFolder = Path.Combine(appLocation, "Settings");
var configFilePath = Path.Combine(settingsFolder, $"{name}.json");

if (createBackup && File.Exists(configFilePath))
{
var timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
var backupFilePath = Path.Combine(settingsFolder, $"{name}_backup_{timestamp}.json");
File.Copy(configFilePath, backupFilePath);
Log.Information($"Created backup of {name} config due to invalid properties: {backupFilePath}");
}

var configJson = JsonSerializer.Serialize(config, Options);
File.WriteAllText(configFilePath, configJson);
Log.Information($"Saved updated {name} config");
}
}
64 changes: 49 additions & 15 deletions SimpleTwitchEmoteSounds/Services/HotkeyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,91 @@
using SharpHook;
using SharpHook.Native;
using SharpHook.Reactive;
using SimpleTwitchEmoteSounds.Models;

namespace SimpleTwitchEmoteSounds.Services;

public class HotkeyService : IHotkeyService
{
private readonly SimpleReactiveGlobalHook _globalHook = new();
private readonly Dictionary<KeyCode, Action> _hotkeys = new();
private Action<KeyCode>? _nextKeyCallback;
private readonly Dictionary<Hotkey, Action> _hotkeys = new();
private readonly HashSet<KeyCode> _currentlyPressedKeys = [];
private Action<Hotkey>? _nextKeyCallback;
private bool _isListeningForHotkey;

private static readonly HashSet<KeyCode> ModifierKeys =
[
KeyCode.VcLeftControl,
KeyCode.VcRightControl,
KeyCode.VcLeftShift,
KeyCode.VcRightShift,
KeyCode.VcLeftAlt,
KeyCode.VcRightAlt,
KeyCode.VcLeftMeta,
KeyCode.VcRightMeta
];

public HotkeyService()
{
_globalHook.KeyPressed.Subscribe(OnKeyPressed);
_globalHook.KeyReleased.Subscribe(OnKeyReleased);
_globalHook.RunAsync();
}

public void RegisterHotkey(KeyCode key, Action action)
public void RegisterHotkey(Hotkey combo, Action action)
{
_hotkeys[key] = action;
_hotkeys[combo] = action;
}

public void UnregisterHotkey(KeyCode key)
public void UnregisterHotkey(Hotkey combo)
{
_hotkeys.Remove(key);
_hotkeys.Remove(combo);
}

public void StartListeningForNextKey(Action<KeyCode> onKeyPressed)
public void StartListeningForNextKey(Action<Hotkey> onKeyPressed)
{
_nextKeyCallback = onKeyPressed;
_isListeningForHotkey = true;
_currentlyPressedKeys.Clear();
}

public void StopListeningForNextKey()
{
_nextKeyCallback = null;
_isListeningForHotkey = false;
_currentlyPressedKeys.Clear();
}

private void OnKeyPressed(KeyboardHookEventArgs e)
{
_currentlyPressedKeys.Add(e.Data.KeyCode);

Dispatcher.UIThread.InvokeAsync(() =>
{
if (_nextKeyCallback != null)
if (_isListeningForHotkey)
{
_nextKeyCallback(e.Data.KeyCode);
_nextKeyCallback = null;
if (ModifierKeys.Contains(e.Data.KeyCode)) return;

var combo = new Hotkey(_currentlyPressedKeys);
_nextKeyCallback?.Invoke(combo);
StopListeningForNextKey();
}
else if (_hotkeys.TryGetValue(e.Data.KeyCode, out var action))
else
{
action();
var combo = new Hotkey(_currentlyPressedKeys);
if (_hotkeys.TryGetValue(combo, out var action))
{
action();
}
}
});
}

private void OnKeyReleased(KeyboardHookEventArgs e)
{
_currentlyPressedKeys.Remove(e.Data.KeyCode);
}

public void Dispose()
{
_globalHook.Dispose();
Expand All @@ -63,8 +97,8 @@ public void Dispose()

public interface IHotkeyService : IDisposable
{
void RegisterHotkey(KeyCode key, Action action);
void UnregisterHotkey(KeyCode key);
void StartListeningForNextKey(Action<KeyCode> onKeyPressed);
void RegisterHotkey(Hotkey combo, Action action);
void UnregisterHotkey(Hotkey combo);
void StartListeningForNextKey(Action<Hotkey> onKeyPressed);
void StopListeningForNextKey();
}
11 changes: 4 additions & 7 deletions SimpleTwitchEmoteSounds/ViewModels/DashboardViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
using Serilog;
using SharpHook.Native;
using SimpleTwitchEmoteSounds.Extensions;
using SimpleTwitchEmoteSounds.Models;
using SimpleTwitchEmoteSounds.Services;
Expand All @@ -34,7 +33,7 @@ public partial class DashboardViewModel : ViewModelBase
[ObservableProperty] private string _searchText = string.Empty;
[ObservableProperty] private string _toggleButtonText = "Register Hotkey";
[ObservableProperty] private bool _isListening;
private static KeyCode ToggleHotkey => ConfigService.Settings.EnableKey;
private static Hotkey ToggleHotkey => ConfigService.Settings.EnableKey;
private static ObservableCollection<SoundCommand> SoundCommands => ConfigService.Settings.SoundCommands;
public FilteredObservableCollection<SoundCommand> FilteredSoundCommands { get; }

Expand Down Expand Up @@ -333,20 +332,18 @@ private void ToggleListening()
}

[RelayCommand]
private void RegisterHotkey(KeyCode key)
private void RegisterHotkey(Hotkey combo)
{
_hotkeyService.UnregisterHotkey(ToggleHotkey);

ConfigService.Settings.EnableKey = key;

ConfigService.Settings.EnableKey = combo;
_hotkeyService.RegisterHotkey(ToggleHotkey, ToggleEnabled);
ResetState();
}

private void ResetState()
{
IsListening = false;
ToggleButtonText = $"{ToggleHotkey.ToString().ToUpperInvariant().Replace("VC", "")}";
ToggleButtonText = ToggleHotkey.ToString();
_hotkeyService.StopListeningForNextKey();
}

Expand Down

0 comments on commit 7c0ad35

Please sign in to comment.