using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Timers;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Interop;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Utilities;
using Linearstar.Windows.RawInput;
using Linearstar.Windows.RawInput.Native;
using Serilog;
using MouseButton = Artemis.Core.Services.MouseButton;
namespace Artemis.UI.InputProviders
{
public class NativeWindowInputProvider : InputProvider
{
private const int WM_INPUT = 0x00FF;
private readonly IInputService _inputService;
private readonly ILogger _logger;
private DateTime _lastMouseUpdate;
private SpongeWindow _sponge;
private System.Timers.Timer _taskManagerTimer;
public NativeWindowInputProvider(ILogger logger, IInputService inputService)
{
_logger = logger;
_inputService = inputService;
_sponge = new SpongeWindow();
_sponge.WndProcCalled += SpongeOnWndProcCalled;
_taskManagerTimer = new System.Timers.Timer(500);
_taskManagerTimer.Elapsed += TaskManagerTimerOnElapsed;
_taskManagerTimer.Start();
RawInputDevice.RegisterDevice(HidUsageAndPage.Keyboard, RawInputDeviceFlags.InputSink, _sponge.Handle);
RawInputDevice.RegisterDevice(HidUsageAndPage.Mouse, RawInputDeviceFlags.InputSink, _sponge.Handle);
}
#region Overrides of InputProvider
///
public override void OnKeyboardToggleStatusRequested()
{
UpdateToggleStatus();
}
#endregion
#region IDisposable
///
protected override void Dispose(bool disposing)
{
if (disposing)
{
_sponge?.DestroyHandle();
_sponge = null;
_taskManagerTimer?.Dispose();
_taskManagerTimer = null;
}
base.Dispose(disposing);
}
#endregion
private void SpongeOnWndProcCalled(object sender, Message message)
{
if (message.Msg != WM_INPUT)
return;
RawInputData data = RawInputData.FromHandle(message.LParam);
switch (data)
{
case RawInputMouseData mouse:
HandleMouseData(data, mouse);
break;
case RawInputKeyboardData keyboard:
HandleKeyboardData(data, keyboard);
break;
}
}
private void TaskManagerTimerOnElapsed(object sender, ElapsedEventArgs e)
{
// If task manager has focus then we can't track keys properly, release everything to avoid them getting stuck
// Same goes for Idle which is what you get when you press Ctrl+Alt+Del
Process active = Process.GetProcessById(WindowUtilities.GetActiveProcessId());
if (active?.ProcessName == "Taskmgr" || active?.ProcessName == "Idle")
_inputService.ReleaseAll();
}
#region Keyboard
private void HandleKeyboardData(RawInputData data, RawInputKeyboardData keyboardData)
{
KeyboardKey key = (KeyboardKey) KeyInterop.KeyFromVirtualKey(keyboardData.Keyboard.VirutalKey);
// Debug.WriteLine($"VK: {key} ({keyboardData.Keyboard.VirutalKey}), Flags: {keyboardData.Keyboard.Flags}, Scan code: {keyboardData.Keyboard.ScanCode}");
// Sometimes we get double hits and they resolve to None, ignore those
if (key == KeyboardKey.None)
return;
// Right alt triggers LeftCtrl with a different scan code for some reason, ignore those
if (key == KeyboardKey.LeftCtrl && keyboardData.Keyboard.ScanCode == 56)
return;
string identifier = data.Device?.DevicePath;
// Let the core know there is an identifier so it can store new identifications if applicable
if (identifier != null)
OnIdentifierReceived(identifier, InputDeviceType.Keyboard);
ArtemisDevice device = null;
if (identifier != null)
try
{
device = _inputService.GetDeviceByIdentifier(this, identifier, InputDeviceType.Keyboard);
}
catch (Exception e)
{
_logger.Warning(e, "Failed to retrieve input device by its identifier");
}
// Duplicate keys with different positions can be identified by the LeftKey flag (even though its set of the key that's physically on the right)
if (keyboardData.Keyboard.Flags == RawKeyboardFlags.LeftKey || keyboardData.Keyboard.Flags == (RawKeyboardFlags.LeftKey | RawKeyboardFlags.Up))
{
if (key == KeyboardKey.Enter)
key = KeyboardKey.NumPadEnter;
if (key == KeyboardKey.LeftCtrl)
key = KeyboardKey.RightCtrl;
if (key == KeyboardKey.LeftAlt)
key = KeyboardKey.RightAlt;
}
if (key == KeyboardKey.LeftShift && keyboardData.Keyboard.ScanCode == 54)
key = KeyboardKey.RightShift;
bool isDown = keyboardData.Keyboard.Flags != RawKeyboardFlags.Up &&
keyboardData.Keyboard.Flags != (RawKeyboardFlags.Up | RawKeyboardFlags.LeftKey) &&
keyboardData.Keyboard.Flags != (RawKeyboardFlags.Up | RawKeyboardFlags.RightKey);
OnKeyboardDataReceived(device, key, isDown);
UpdateToggleStatus();
}
private void UpdateToggleStatus()
{
OnKeyboardToggleStatusReceived(new KeyboardToggleStatus(
Keyboard.IsKeyToggled(Key.NumLock),
Keyboard.IsKeyToggled(Key.CapsLock),
Keyboard.IsKeyToggled(Key.Scroll)
));
}
#endregion
#region Mouse
private int _mouseDeltaX;
private int _mouseDeltaY;
private void HandleMouseData(RawInputData data, RawInputMouseData mouseData)
{
// Only submit mouse movement 25 times per second but increment the delta
// This can create a small inaccuracy of course, but Artemis is not a shooter :')
if (mouseData.Mouse.Buttons == RawMouseButtonFlags.None)
{
_mouseDeltaX += mouseData.Mouse.LastX;
_mouseDeltaY += mouseData.Mouse.LastY;
if (DateTime.Now - _lastMouseUpdate < TimeSpan.FromMilliseconds(40))
return;
}
ArtemisDevice device = null;
string identifier = data.Device?.DevicePath;
if (identifier != null)
try
{
device = _inputService.GetDeviceByIdentifier(this, identifier, InputDeviceType.Keyboard);
}
catch (Exception e)
{
_logger.Warning(e, "Failed to retrieve input device by its identifier");
}
// Debug.WriteLine($"Buttons: {data.Mouse.Buttons}, Data: {data.Mouse.ButtonData}, Flags: {data.Mouse.Flags}, XY: {data.Mouse.LastX},{data.Mouse.LastY}");
// Movement
if (mouseData.Mouse.Buttons == RawMouseButtonFlags.None)
{
Win32Point cursorPosition = GetCursorPosition();
OnMouseMoveDataReceived(device, cursorPosition.X, cursorPosition.Y, _mouseDeltaX, _mouseDeltaY);
_mouseDeltaX = 0;
_mouseDeltaY = 0;
_lastMouseUpdate = DateTime.Now;
return;
}
// Now we know its not movement, let the core know there is an identifier so it can store new identifications if applicable
if (identifier != null)
OnIdentifierReceived(identifier, InputDeviceType.Mouse);
// Scrolling
if (mouseData.Mouse.ButtonData != 0)
{
if (mouseData.Mouse.Buttons == RawMouseButtonFlags.MouseWheel)
OnMouseScrollDataReceived(device, MouseScrollDirection.Vertical, mouseData.Mouse.ButtonData);
else if (mouseData.Mouse.Buttons == RawMouseButtonFlags.MouseHorizontalWheel)
OnMouseScrollDataReceived(device, MouseScrollDirection.Horizontal, mouseData.Mouse.ButtonData);
return;
}
// Button presses
MouseButton button = MouseButton.Left;
bool isDown = false;
// Left
if (DetermineMouseButton(mouseData, RawMouseButtonFlags.LeftButtonDown, RawMouseButtonFlags.LeftButtonUp, ref isDown))
button = MouseButton.Left;
// Middle
else if (DetermineMouseButton(mouseData, RawMouseButtonFlags.MiddleButtonDown, RawMouseButtonFlags.MiddleButtonUp, ref isDown))
button = MouseButton.Middle;
// Right
else if (DetermineMouseButton(mouseData, RawMouseButtonFlags.RightButtonDown, RawMouseButtonFlags.RightButtonUp, ref isDown))
button = MouseButton.Right;
// Button 4
else if (DetermineMouseButton(mouseData, RawMouseButtonFlags.Button4Down, RawMouseButtonFlags.Button4Up, ref isDown))
button = MouseButton.Button4;
else if (DetermineMouseButton(mouseData, RawMouseButtonFlags.Button5Down, RawMouseButtonFlags.Button5Up, ref isDown))
button = MouseButton.Button5;
OnMouseButtonDataReceived(device, button, isDown);
}
private bool DetermineMouseButton(RawInputMouseData data, RawMouseButtonFlags downButton, RawMouseButtonFlags upButton, ref bool isDown)
{
if (data.Mouse.Buttons == downButton || data.Mouse.Buttons == upButton)
{
isDown = data.Mouse.Buttons == downButton;
return true;
}
isDown = false;
return false;
}
#endregion
#region Native
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetCursorPos(ref Win32Point pt);
[StructLayout(LayoutKind.Sequential)]
private struct Win32Point
{
public readonly int X;
public readonly int Y;
}
private static Win32Point GetCursorPosition()
{
Win32Point w32Mouse = new();
GetCursorPos(ref w32Mouse);
return w32Mouse;
}
#endregion
}
}