1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00
Robert 2a0a0f3cfd Core - Split up RGBService into RenderService and DeviceService
Core - Removed rendering from CoreService
Core - Store and restore original device LEDs when applying/clearing layouts
2023-10-05 20:38:07 +02:00

439 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using RGB.NET.Core;
using Serilog;
namespace Artemis.Core.Services;
internal class InputService : IInputService
{
private readonly ILogger _logger;
private readonly IDeviceService _deviceService;
private ArtemisDevice? _firstKeyboard;
private ArtemisDevice? _firstMouse;
private int _keyboardCount;
private int _mouseCount;
public InputService(ILogger logger, IDeviceService deviceService)
{
_logger = logger;
_deviceService = deviceService;
_deviceService.DeviceAdded += DeviceServiceOnDevicesModified;
_deviceService.DeviceRemoved += DeviceServiceOnDevicesModified;
BustIdentifierCache();
}
protected virtual void OnKeyboardKeyUpDown(ArtemisKeyboardKeyUpDownEventArgs e)
{
KeyboardKeyUpDown?.Invoke(this, e);
}
protected virtual void OnKeyboardKeyDown(ArtemisKeyboardKeyEventArgs e)
{
KeyboardKeyDown?.Invoke(this, e);
}
protected virtual void OnKeyboardKeyUp(ArtemisKeyboardKeyEventArgs e)
{
KeyboardKeyUp?.Invoke(this, e);
}
protected virtual void OnKeyboardToggleStatusChanged(ArtemisKeyboardToggleStatusArgs e)
{
KeyboardToggleStatusChanged?.Invoke(this, e);
}
protected virtual void OnMouseButtonUpDown(ArtemisMouseButtonUpDownEventArgs e)
{
MouseButtonUpDown?.Invoke(this, e);
}
protected virtual void OnMouseButtonDown(ArtemisMouseButtonEventArgs e)
{
MouseButtonDown?.Invoke(this, e);
}
protected virtual void OnMouseButtonUp(ArtemisMouseButtonEventArgs e)
{
MouseButtonUp?.Invoke(this, e);
}
protected virtual void OnMouseScroll(ArtemisMouseScrollEventArgs e)
{
MouseScroll?.Invoke(this, e);
}
protected virtual void OnMouseMove(ArtemisMouseMoveEventArgs e)
{
MouseMove?.Invoke(this, e);
}
protected virtual void OnDeviceIdentified()
{
DeviceIdentified?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
public void Dispose()
{
while (_inputProviders.Any())
{
InputProvider provider = _inputProviders.First();
RemoveInputProvider(provider);
provider.Dispose();
}
}
public event EventHandler<ArtemisKeyboardKeyUpDownEventArgs>? KeyboardKeyUpDown;
public event EventHandler<ArtemisKeyboardKeyEventArgs>? KeyboardKeyDown;
public event EventHandler<ArtemisKeyboardKeyEventArgs>? KeyboardKeyUp;
public event EventHandler<ArtemisKeyboardToggleStatusArgs>? KeyboardToggleStatusChanged;
public event EventHandler<ArtemisMouseButtonUpDownEventArgs>? MouseButtonUpDown;
public event EventHandler<ArtemisMouseButtonEventArgs>? MouseButtonDown;
public event EventHandler<ArtemisMouseButtonEventArgs>? MouseButtonUp;
public event EventHandler<ArtemisMouseScrollEventArgs>? MouseScroll;
public event EventHandler<ArtemisMouseMoveEventArgs>? MouseMove;
public event EventHandler? DeviceIdentified;
#region Providers
private readonly List<InputProvider> _inputProviders = new();
public KeyboardToggleStatus KeyboardToggleStatus { get; private set; } = new(false, false, false);
public void AddInputProvider(InputProvider inputProvider)
{
inputProvider.IdentifierReceived += InputProviderOnIdentifierReceived;
inputProvider.KeyboardDataReceived += InputProviderOnKeyboardDataReceived;
inputProvider.KeyboardToggleStatusReceived += InputProviderOnKeyboardToggleStatusReceived;
inputProvider.MouseButtonDataReceived += InputProviderOnMouseButtonDataReceived;
inputProvider.MouseScrollDataReceived += InputProviderOnMouseScrollDataReceived;
inputProvider.MouseMoveDataReceived += InputProviderOnMouseMoveDataReceived;
_inputProviders.Add(inputProvider);
inputProvider.OnKeyboardToggleStatusRequested();
}
public void RemoveInputProvider(InputProvider inputProvider)
{
if (!_inputProviders.Contains(inputProvider))
return;
_inputProviders.Remove(inputProvider);
inputProvider.IdentifierReceived -= InputProviderOnIdentifierReceived;
inputProvider.KeyboardDataReceived -= InputProviderOnKeyboardDataReceived;
inputProvider.KeyboardToggleStatusReceived -= InputProviderOnKeyboardToggleStatusReceived;
inputProvider.MouseButtonDataReceived -= InputProviderOnMouseButtonDataReceived;
inputProvider.MouseScrollDataReceived -= InputProviderOnMouseScrollDataReceived;
inputProvider.MouseMoveDataReceived -= InputProviderOnMouseMoveDataReceived;
}
#endregion
#region Identification
private readonly Dictionary<Tuple<InputProvider, object>, ArtemisDevice> _deviceCache = new();
private List<ArtemisDevice> _devices = new();
private ArtemisDevice? _identifyingDevice;
public void IdentifyDevice(ArtemisDevice device)
{
if (device.DeviceType != RGBDeviceType.Keyboard && device.DeviceType != RGBDeviceType.Mouse)
throw new ArtemisCoreException($"Cannot initialize input-identification for a device of type {device.DeviceType}. \r\n" +
"Only keyboard and mouse is supported.");
_identifyingDevice = device;
_logger.Debug("Start identifying device {device}", device);
}
public void StopIdentify()
{
if (_identifyingDevice == null)
return;
_logger.Debug("Stop identifying device {device}", _identifyingDevice);
_identifyingDevice = null;
_deviceService.SaveDevices();
BustIdentifierCache();
}
// TODO: Move the OnIdentifierReceived logic into here and get rid of OnIdentifierReceived, this and OnIdentifierReceived are always called in combination with each other
public ArtemisDevice? GetDeviceByIdentifier(InputProvider provider, object identifier, InputDeviceType type)
{
if (provider == null) throw new ArgumentNullException(nameof(provider));
if (identifier == null) throw new ArgumentNullException(nameof(identifier));
// We will almost always only have zero or one of each
if (type == InputDeviceType.Keyboard)
{
if (_keyboardCount == 0)
return null;
if (_keyboardCount == 1)
return _firstKeyboard;
}
if (type == InputDeviceType.Mouse)
{
if (_mouseCount == 0)
return null;
if (_mouseCount == 1)
return _firstMouse;
}
// Try cache first
ArtemisDevice? cacheMatch = GetDeviceFromCache(provider, identifier);
if (cacheMatch != null)
return cacheMatch;
ArtemisDevice? match = _devices.FirstOrDefault(m => m.InputIdentifiers.Any(i => Equals(i.InputProvider, provider.ProviderName) && Equals(i.Identifier, identifier)));
// If a match was found cache it to speed up the next event and return the match
if (match != null)
{
AddDeviceToCache(match, provider, identifier);
return match;
}
if (type == InputDeviceType.Keyboard)
return _firstKeyboard;
if (type == InputDeviceType.Mouse)
return _firstMouse;
return null;
}
public void BustIdentifierCache()
{
_deviceCache.Clear();
_devices = _deviceService.EnabledDevices.Where(d => d.InputIdentifiers.Any()).ToList();
}
private void AddDeviceToCache(ArtemisDevice match, InputProvider provider, object identifier)
{
_deviceCache.TryAdd(new Tuple<InputProvider, object>(provider, identifier), match);
}
private ArtemisDevice? GetDeviceFromCache(InputProvider provider, object identifier)
{
_deviceCache.TryGetValue(new Tuple<InputProvider, object>(provider, identifier), out ArtemisDevice? device);
return device;
}
private void InputProviderOnIdentifierReceived(object? sender, InputProviderIdentifierEventArgs e)
{
// Don't match if there is no device or if the device type differs from the event device type
if (_identifyingDevice == null ||
(_identifyingDevice.DeviceType == RGBDeviceType.Keyboard && e.DeviceType == InputDeviceType.Mouse) ||
(_identifyingDevice.DeviceType == RGBDeviceType.Mouse && e.DeviceType == InputDeviceType.Keyboard))
return;
if (!(sender is InputProvider inputProvider))
return;
// Remove existing identification
_identifyingDevice.InputIdentifiers.RemoveAll(i => i.InputProvider == inputProvider.ProviderName);
_identifyingDevice.InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(inputProvider.ProviderName, e.Identifier));
StopIdentify();
OnDeviceIdentified();
}
private void DeviceServiceOnDevicesModified(object? sender, DeviceEventArgs args)
{
_firstKeyboard = _deviceService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard);
_firstMouse = _deviceService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse);
_keyboardCount = _deviceService.Devices.Count(d => d.DeviceType == RGBDeviceType.Keyboard);
_mouseCount = _deviceService.Devices.Count(d => d.DeviceType == RGBDeviceType.Mouse);
BustIdentifierCache();
}
#endregion
#region Keyboard
private readonly Dictionary<ArtemisDevice, HashSet<KeyboardKey>> _pressedKeys = new();
private readonly Dictionary<ArtemisDevice, KeyboardModifierKey> _keyboardModifier = new();
private KeyboardModifierKey _globalModifiers;
private void InputProviderOnKeyboardDataReceived(object? sender, InputProviderKeyboardEventArgs e)
{
KeyboardModifierKey keyboardModifierKey = UpdateModifierKeys(e.Device, e.Key, e.IsDown);
// if UpdatePressedKeys is true, the key is already pressed, then we skip this event
if (UpdatePressedKeys(e.Device, e.Key, e.IsDown))
return;
// Get the LED
bool foundLedId = InputKeyUtilities.TryGetLedIdFromKeyboardKey(e.Key, out LedId ledId);
// If we find a backslash but the keyboard is ISO, we need to use the non-US backslash.
// The scancode is the same, but the LED ID is different
if (ledId == LedId.Keyboard_Backslash && e.Device?.PhysicalLayout == KeyboardLayoutType.ISO)
{
ledId = LedId.Keyboard_NonUsTilde;
}
ArtemisLed? led = null;
if (foundLedId && e.Device != null)
led = e.Device.GetLed(ledId, true);
// Create the UpDown event args because it can be used for every event
ArtemisKeyboardKeyUpDownEventArgs eventArgs = new(e.Device, led, e.Key, keyboardModifierKey, e.IsDown);
OnKeyboardKeyUpDown(eventArgs);
if (e.IsDown)
OnKeyboardKeyDown(eventArgs);
else
OnKeyboardKeyUp(eventArgs);
// _logger.Verbose("Keyboard data: LED ID: {ledId}, key: {key}, is down: {isDown}, modifiers: {modifiers}, device: {device} ", ledId, e.Key, e.IsDown, keyboardModifierKey, e.Device);
}
private void InputProviderOnKeyboardToggleStatusReceived(object? sender, InputProviderKeyboardToggleEventArgs e)
{
KeyboardToggleStatus old = KeyboardToggleStatus;
if (KeyboardToggleStatus.CapsLock == e.KeyboardToggleStatus.CapsLock &&
KeyboardToggleStatus.NumLock == e.KeyboardToggleStatus.NumLock &&
KeyboardToggleStatus.ScrollLock == e.KeyboardToggleStatus.ScrollLock)
return;
KeyboardToggleStatus = e.KeyboardToggleStatus;
OnKeyboardToggleStatusChanged(new ArtemisKeyboardToggleStatusArgs(old, KeyboardToggleStatus));
}
private bool UpdatePressedKeys(ArtemisDevice? device, KeyboardKey key, bool isDown)
{
if (device != null)
{
// Ensure the device is in the dictionary
_pressedKeys.TryAdd(device, new HashSet<KeyboardKey>());
// Get the hash set of the device
HashSet<KeyboardKey> pressedDeviceKeys = _pressedKeys[device];
// See if the key is already pressed
bool alreadyPressed = pressedDeviceKeys.Contains(key);
// Prevent triggering already down keys again. When a key is held, we don't want to spam events like Windows does
if (isDown && alreadyPressed)
return true;
// Either add or remove the key depending on its status
if (isDown && !alreadyPressed)
pressedDeviceKeys.Add(key);
else if (!isDown && alreadyPressed)
pressedDeviceKeys.Remove(key);
}
return false;
}
private KeyboardModifierKey UpdateModifierKeys(ArtemisDevice? device, KeyboardKey key, in bool isDown)
{
KeyboardModifierKey modifiers = _globalModifiers;
if (device != null)
_keyboardModifier.TryGetValue(device, out modifiers);
if (key == KeyboardKey.LeftAlt || key == KeyboardKey.RightAlt)
{
if (isDown)
modifiers |= KeyboardModifierKey.Alt;
else
modifiers &= ~KeyboardModifierKey.Alt;
}
else if (key == KeyboardKey.LeftCtrl || key == KeyboardKey.RightCtrl)
{
if (isDown)
modifiers |= KeyboardModifierKey.Control;
else
modifiers &= ~KeyboardModifierKey.Control;
}
else if (key == KeyboardKey.LeftShift || key == KeyboardKey.RightShift)
{
if (isDown)
modifiers |= KeyboardModifierKey.Shift;
else
modifiers &= ~KeyboardModifierKey.Shift;
}
else if (key == KeyboardKey.LeftWin || key == KeyboardKey.RightWin)
{
if (isDown)
modifiers |= KeyboardModifierKey.Windows;
else
modifiers &= ~KeyboardModifierKey.Windows;
}
if (device != null)
_keyboardModifier[device] = modifiers;
else
_globalModifiers = modifiers;
return modifiers;
}
public void ReleaseAll()
{
foreach ((ArtemisDevice? device, HashSet<KeyboardKey>? keys) in _pressedKeys.ToList())
{
foreach (KeyboardKey keyboardKey in keys)
InputProviderOnKeyboardDataReceived(this, new InputProviderKeyboardEventArgs(device, keyboardKey, false));
}
}
public bool IsKeyDown(KeyboardKey key)
{
return _pressedKeys.Any(p => p.Value.Contains(key));
}
#endregion
#region Mouse
private readonly HashSet<MouseButton> _pressedButtons = new();
private void InputProviderOnMouseButtonDataReceived(object? sender, InputProviderMouseButtonEventArgs e)
{
bool foundLedId = InputKeyUtilities.TryGetLedIdFromMouseButton(e.Button, out LedId ledId);
ArtemisLed? led = null;
if (foundLedId && e.Device != null)
led = e.Device.Leds.FirstOrDefault(l => l.RgbLed.Id == ledId);
// Create the UpDown event args because it can be used for every event
ArtemisMouseButtonUpDownEventArgs eventArgs = new(e.Device, led, e.Button, e.IsDown);
OnMouseButtonUpDown(eventArgs);
if (e.IsDown)
{
if (!_pressedButtons.Contains(e.Button))
_pressedButtons.Add(e.Button);
OnMouseButtonDown(eventArgs);
}
else
{
if (_pressedButtons.Contains(e.Button))
_pressedButtons.Remove(e.Button);
OnMouseButtonUp(eventArgs);
}
// _logger.Verbose("Mouse button data: LED ID: {ledId}, button: {button}, is down: {isDown}, device: {device} ", ledId, e.Button, e.IsDown, e.Device);
}
private void InputProviderOnMouseScrollDataReceived(object? sender, InputProviderMouseScrollEventArgs e)
{
OnMouseScroll(new ArtemisMouseScrollEventArgs(e.Device, e.Direction, e.Delta));
// _logger.Verbose("Mouse scroll data: Direction: {direction}, delta: {delta}, device: {device} ", e.Direction, e.Delta, e.Device);
}
private void InputProviderOnMouseMoveDataReceived(object? sender, InputProviderMouseMoveEventArgs e)
{
OnMouseMove(new ArtemisMouseMoveEventArgs(e.Device, e.CursorX, e.CursorY, e.DeltaX, e.DeltaY));
// _logger.Verbose("Mouse move data: XY: {X},{Y} - delta XY: {deltaX},{deltaY} - device: {device} ", e.CursorX, e.CursorY, e.DeltaX, e.DeltaY, e.Device);
}
public bool IsButtonDown(MouseButton button)
{
return _pressedButtons.Contains(button);
}
#endregion
}