using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; namespace EpisodeRenamer { /// /// Класс для обработки горячих клавиш с использованием Task /// public class HotkeyListener : IDisposable { #region Windows API Imports [DllImport("user32.dll")] private static extern short GetAsyncKeyState(int vKey); [DllImport("kernel32.dll")] private static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); #endregion #region Key Codes public static class VirtualKeyCodes { public const int VK_CONTROL = 0x11; public const int VK_ALT = 0x12; public const int VK_SHIFT = 0x10; public const int VK_R = 0x52; public const int VK_ESCAPE = 0x1B; // Добавьте другие коды клавиш по необходимости } #endregion #region Events and Delegates /// /// Делегат для события нажатия комбинации клавиш /// public delegate void HotkeyEventHandler(string combinationName); /// /// Событие, возникающее при нажатии зарегистрированной комбинации клавиш /// public event HotkeyEventHandler OnHotkeyPressed; /// /// Событие, возникающее при ошибке в работе слушателя /// public event Action OnError; #endregion #region Private Fields private readonly List _keyCombinations; private CancellationTokenSource _cancellationTokenSource; private Task _listenerTask; private bool _isDisposed; private readonly object _lockObject = new object(); private bool _isRunning; private IntPtr consoleHandle = Process.GetCurrentProcess().MainWindowHandle; #endregion #region Key Combination Class /// /// Класс для описания комбинации клавиш /// private class KeyCombination { public string Name { get; set; } public int[] KeyCodes { get; set; } public int CooldownMs { get; set; } = 300; public DateTime LastTriggered { get; set; } = DateTime.MinValue; } #endregion #region Constructor /// /// Создает новый экземпляр слушателя горячих клавиш /// public HotkeyListener() { _keyCombinations = new List(); _cancellationTokenSource = new CancellationTokenSource(); } #endregion #region Public Methods /// /// Регистрирует новую комбинацию клавиш для отслеживания /// /// Имя комбинации /// Коды клавиш (все должны быть нажаты одновременно) /// Время задержки между срабатываниями (мс) public void RegisterCombination(string name, int[] keyCodes, int cooldownMs = 300) { if (string.IsNullOrEmpty(name)) throw new ArgumentException("Имя комбинации не может быть пустым", nameof(name)); if (keyCodes == null || keyCodes.Length == 0) throw new ArgumentException("Массив кодов клавиш не может быть пустым", nameof(keyCodes)); lock (_lockObject) { var existing = _keyCombinations.Find(k => k.Name == name); if (existing != null) { _keyCombinations.Remove(existing); } _keyCombinations.Add(new KeyCombination { Name = name, KeyCodes = keyCodes, CooldownMs = cooldownMs }); } } /// /// Удаляет комбинацию клавиш из отслеживания /// /// Имя комбинации public void UnregisterCombination(string name) { lock (_lockObject) { var combination = _keyCombinations.Find(k => k.Name == name); if (combination != null) { _keyCombinations.Remove(combination); } } } /// /// Запускает прослушивание горячих клавиш /// public async Task StartListeningAsync() { if (_isRunning) return; lock (_lockObject) { if (_isRunning) return; _cancellationTokenSource = new CancellationTokenSource(); _isRunning = true; } try { _listenerTask = Task.Run(() => ListenForHotkeys(_cancellationTokenSource.Token), _cancellationTokenSource.Token); await Task.CompletedTask; } catch (Exception ex) { OnError?.Invoke(ex); throw; } } /// /// Останавливает прослушивание горячих клавиш /// public async Task StopListeningAsync() { if (!_isRunning) return; lock (_lockObject) { if (!_isRunning) return; _isRunning = false; } try { _cancellationTokenSource?.Cancel(); if (_listenerTask != null && !_listenerTask.IsCompleted) { await _listenerTask.ContinueWith(t => { // Игнорируем исключения отмены задачи if (t.IsFaulted) OnError?.Invoke(t.Exception); }); } } catch (OperationCanceledException) { // Ожидаемое исключение при отмене } catch (Exception ex) { OnError?.Invoke(ex); throw; } } /// /// Получает статус работы слушателя /// public bool IsListening => _isRunning; /// /// Получает список зарегистрированных комбинаций /// public IReadOnlyList RegisteredCombinations { get { lock (_lockObject) { return _keyCombinations.ConvertAll(k => k.Name).AsReadOnly(); } } } #endregion #region Private Methods private async Task ListenForHotkeys(CancellationToken cancellationToken) { try { while (!cancellationToken.IsCancellationRequested) { await Task.Delay(10, cancellationToken); // Небольшая задержка для снижения нагрузки lock (_lockObject) { foreach (var combination in _keyCombinations) { if (IsCombinationPressed(combination.KeyCodes)) { // Проверяем cooldown if ((DateTime.Now - combination.LastTriggered).TotalMilliseconds >= combination.CooldownMs) { combination.LastTriggered = DateTime.Now; TriggerHotkeyEvent(combination.Name); } } } } } } catch (OperationCanceledException) { // Ожидаемое исключение при отмене } catch (Exception ex) { OnError?.Invoke(ex); } } private bool IsCombinationPressed(int[] keyCodes) { foreach (var keyCode in keyCodes) { if (!IsKeyPressed(keyCode)) return false; } return true; } private bool IsKeyPressed(int keyCode) { return (GetAsyncKeyState(keyCode) & 0x8000) != 0; } private void TriggerHotkeyEvent(string combinationName) { if (GetForegroundWindow() != consoleHandle) return; try { OnHotkeyPressed?.Invoke(combinationName); } catch (Exception ex) { OnError?.Invoke(new InvalidOperationException( $"Ошибка при обработке события для комбинации {combinationName}", ex)); } } #endregion #region IDisposable Implementation public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_isDisposed) { if (disposing) { StopListeningAsync().GetAwaiter().GetResult(); _cancellationTokenSource?.Dispose(); } _isDisposed = true; } } ~HotkeyListener() { Dispose(false); } #endregion } }