339 lines
11 KiB
C#
339 lines
11 KiB
C#
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
|
||
}
|
||
} |