Files
EpisodeRenamer/KeyListener.cs
2025-09-23 09:54:08 +07:00

339 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace EpisodeRenamer
{
/// <summary>
/// Класс для обработки горячих клавиш с использованием Task
/// </summary>
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);
#endregion
#region Key Codes
private 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
/// <summary>
/// Делегат для события нажатия комбинации клавиш
/// </summary>
public delegate void HotkeyEventHandler(string combinationName);
/// <summary>
/// Событие, возникающее при нажатии зарегистрированной комбинации клавиш
/// </summary>
public event HotkeyEventHandler OnHotkeyPressed;
/// <summary>
/// Событие, возникающее при ошибке в работе слушателя
/// </summary>
public event Action<Exception> OnError;
#endregion
#region Private Fields
private readonly List<KeyCombination> _keyCombinations;
private CancellationTokenSource _cancellationTokenSource;
private Task _listenerTask;
private bool _isDisposed;
private readonly object _lockObject = new object();
private bool _isRunning;
#endregion
#region Key Combination Class
/// <summary>
/// Класс для описания комбинации клавиш
/// </summary>
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
/// <summary>
/// Создает новый экземпляр слушателя горячих клавиш
/// </summary>
public HotkeyListener()
{
_keyCombinations = new List<KeyCombination>();
_cancellationTokenSource = new CancellationTokenSource();
// Добавляем комбинацию по умолчанию
RegisterCombination("ControlAltR",
new[] { VirtualKeyCodes.VK_CONTROL, VirtualKeyCodes.VK_ALT, VirtualKeyCodes.VK_R });
}
#endregion
#region Public Methods
/// <summary>
/// Регистрирует новую комбинацию клавиш для отслеживания
/// </summary>
/// <param name="name">Имя комбинации</param>
/// <param name="keyCodes">Коды клавиш (все должны быть нажаты одновременно)</param>
/// <param name="cooldownMs">Время задержки между срабатываниями (мс)</param>
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
});
}
}
/// <summary>
/// Удаляет комбинацию клавиш из отслеживания
/// </summary>
/// <param name="name">Имя комбинации</param>
public void UnregisterCombination(string name)
{
lock (_lockObject)
{
var combination = _keyCombinations.Find(k => k.Name == name);
if (combination != null)
{
_keyCombinations.Remove(combination);
}
}
}
/// <summary>
/// Запускает прослушивание горячих клавиш
/// </summary>
public async Task StartListeningAsync()
{
if (_isRunning)
return;
lock (_lockObject)
{
if (_isRunning)
return;
_cancellationTokenSource = new CancellationTokenSource();
_isRunning = true;
}
try
{
// Переводим консоль на передний план для лучшего захвата клавиш
BringConsoleToFront();
_listenerTask = Task.Run(() => ListenForHotkeys(_cancellationTokenSource.Token),
_cancellationTokenSource.Token);
await Task.CompletedTask;
}
catch (Exception ex)
{
OnError?.Invoke(ex);
throw;
}
}
/// <summary>
/// Останавливает прослушивание горячих клавиш
/// </summary>
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;
}
}
/// <summary>
/// Получает статус работы слушателя
/// </summary>
public bool IsListening => _isRunning;
/// <summary>
/// Получает список зарегистрированных комбинаций
/// </summary>
public IReadOnlyList<string> 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)
{
try
{
OnHotkeyPressed?.Invoke(combinationName);
}
catch (Exception ex)
{
OnError?.Invoke(new InvalidOperationException(
$"Ошибка при обработке события для комбинации {combinationName}", ex));
}
}
private void BringConsoleToFront()
{
try
{
var consoleHandle = GetConsoleWindow();
SetForegroundWindow(consoleHandle);
}
catch (Exception ex)
{
// Не критичная ошибка, просто логируем
OnError?.Invoke(new InvalidOperationException("Не удалось перевести консоль на передний план", 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
}
}