mirror of
https://github.com/immense/Remotely.git
synced 2025-10-26 11:27:15 +00:00
381 lines
15 KiB
C#
381 lines
15 KiB
C#
using Remotely.Desktop.Core.Enums;
|
|
using Remotely.Desktop.Core.Interfaces;
|
|
using Remotely.Desktop.Core.Services;
|
|
using Remotely.Shared.Utilities;
|
|
using Remotely.Shared.Win32;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using static Remotely.Shared.Win32.User32;
|
|
|
|
namespace Remotely.Desktop.Win.Services
|
|
{
|
|
public class KeyboardMouseInputWin : IKeyboardMouseInput
|
|
{
|
|
private volatile bool _inputBlocked;
|
|
private Thread _inputProcessingThread;
|
|
|
|
private CancellationTokenSource CancelTokenSource { get; set; }
|
|
|
|
private ConcurrentQueue<Action> InputActions { get; } = new ConcurrentQueue<Action>();
|
|
|
|
public Tuple<double, double> GetAbsolutePercentFromRelativePercent(double percentX, double percentY, IScreenCapturer capturer)
|
|
{
|
|
var screenBounds = capturer.CurrentScreenBounds;
|
|
|
|
var absoluteX = (screenBounds.Width * percentX) + screenBounds.Left - capturer.GetVirtualScreenBounds().Left;
|
|
var absoluteY = (screenBounds.Height * percentY) + screenBounds.Top - capturer.GetVirtualScreenBounds().Top;
|
|
return new Tuple<double, double>(absoluteX / capturer.GetVirtualScreenBounds().Width, absoluteY / capturer.GetVirtualScreenBounds().Height);
|
|
}
|
|
|
|
public Tuple<double, double> GetAbsolutePointFromRelativePercent(double percentX, double percentY, IScreenCapturer capturer)
|
|
{
|
|
var screenBounds = capturer.CurrentScreenBounds;
|
|
|
|
var absoluteX = (screenBounds.Width * percentX) + screenBounds.Left;
|
|
var absoluteY = (screenBounds.Height * percentY) + screenBounds.Top;
|
|
return new Tuple<double, double>(absoluteX, absoluteY);
|
|
}
|
|
|
|
public void Init()
|
|
{
|
|
App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
App.Current.Exit -= App_Exit;
|
|
App.Current.Exit += App_Exit;
|
|
});
|
|
}
|
|
|
|
public void SendKeyDown(string key)
|
|
{
|
|
TryOnInputDesktop(() =>
|
|
{
|
|
var keyCode = ConvertJavaScriptKeyToVirtualKey(key);
|
|
var union = new InputUnion()
|
|
{
|
|
ki = new KEYBDINPUT()
|
|
{
|
|
wVk = keyCode,
|
|
wScan = (ScanCodeShort)MapVirtualKeyEx((uint)keyCode, VkMapType.MAPVK_VK_TO_VSC, GetKeyboardLayout()),
|
|
time = 0,
|
|
dwExtraInfo = GetMessageExtraInfo()
|
|
}
|
|
};
|
|
var input = new INPUT() { type = InputType.KEYBOARD, U = union };
|
|
SendInput(1, new INPUT[] { input }, INPUT.Size);
|
|
});
|
|
}
|
|
|
|
public void SendKeyUp(string key)
|
|
{
|
|
TryOnInputDesktop(() =>
|
|
{
|
|
var keyCode = ConvertJavaScriptKeyToVirtualKey(key);
|
|
var union = new InputUnion()
|
|
{
|
|
ki = new KEYBDINPUT()
|
|
{
|
|
wVk = keyCode,
|
|
wScan = (ScanCodeShort)MapVirtualKeyEx((uint)keyCode, VkMapType.MAPVK_VK_TO_VSC, GetKeyboardLayout()),
|
|
time = 0,
|
|
dwFlags = KEYEVENTF.KEYUP,
|
|
dwExtraInfo = GetMessageExtraInfo()
|
|
}
|
|
};
|
|
var input = new INPUT() { type = InputType.KEYBOARD, U = union };
|
|
SendInput(1, new INPUT[] { input }, INPUT.Size);
|
|
});
|
|
}
|
|
|
|
public void SendMouseButtonAction(int button, ButtonAction buttonAction, double percentX, double percentY, Viewer viewer)
|
|
{
|
|
TryOnInputDesktop(() =>
|
|
{
|
|
MOUSEEVENTF mouseEvent;
|
|
switch (button)
|
|
{
|
|
case 0:
|
|
switch (buttonAction)
|
|
{
|
|
case ButtonAction.Down:
|
|
mouseEvent = MOUSEEVENTF.LEFTDOWN;
|
|
break;
|
|
case ButtonAction.Up:
|
|
mouseEvent = MOUSEEVENTF.LEFTUP;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
break;
|
|
case 1:
|
|
switch (buttonAction)
|
|
{
|
|
case ButtonAction.Down:
|
|
mouseEvent = MOUSEEVENTF.MIDDLEDOWN;
|
|
break;
|
|
case ButtonAction.Up:
|
|
mouseEvent = MOUSEEVENTF.MIDDLEUP;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
break;
|
|
case 2:
|
|
switch (buttonAction)
|
|
{
|
|
case ButtonAction.Down:
|
|
mouseEvent = MOUSEEVENTF.RIGHTDOWN;
|
|
break;
|
|
case ButtonAction.Up:
|
|
mouseEvent = MOUSEEVENTF.RIGHTUP;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, viewer.Capturer);
|
|
// Coordinates must be normalized. The bottom-right coordinate is mapped to 65535.
|
|
var normalizedX = xyPercent.Item1 * 65535D;
|
|
var normalizedY = xyPercent.Item2 * 65535D;
|
|
var union = new InputUnion() { mi = new MOUSEINPUT() { dwFlags = MOUSEEVENTF.ABSOLUTE | mouseEvent | MOUSEEVENTF.VIRTUALDESK, dx = (int)normalizedX, dy = (int)normalizedY, time = 0, mouseData = 0, dwExtraInfo = GetMessageExtraInfo() } };
|
|
var input = new INPUT() { type = InputType.MOUSE, U = union };
|
|
SendInput(1, new INPUT[] { input }, INPUT.Size);
|
|
});
|
|
}
|
|
|
|
public void SendMouseMove(double percentX, double percentY, Viewer viewer)
|
|
{
|
|
TryOnInputDesktop(() =>
|
|
{
|
|
var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, viewer.Capturer);
|
|
// Coordinates must be normalized. The bottom-right coordinate is mapped to 65535.
|
|
var normalizedX = xyPercent.Item1 * 65535D;
|
|
var normalizedY = xyPercent.Item2 * 65535D;
|
|
var union = new InputUnion() { mi = new MOUSEINPUT() { dwFlags = MOUSEEVENTF.ABSOLUTE | MOUSEEVENTF.MOVE | MOUSEEVENTF.VIRTUALDESK, dx = (int)normalizedX, dy = (int)normalizedY, time = 0, mouseData = 0, dwExtraInfo = GetMessageExtraInfo() } };
|
|
var input = new INPUT() { type = InputType.MOUSE, U = union };
|
|
SendInput(1, new INPUT[] { input }, INPUT.Size);
|
|
});
|
|
}
|
|
|
|
public void SendMouseWheel(int deltaY)
|
|
{
|
|
TryOnInputDesktop(() =>
|
|
{
|
|
if (deltaY < 0)
|
|
{
|
|
deltaY = -120;
|
|
}
|
|
else if (deltaY > 0)
|
|
{
|
|
deltaY = 120;
|
|
}
|
|
var union = new InputUnion() { mi = new MOUSEINPUT() { dwFlags = MOUSEEVENTF.WHEEL, dx = 0, dy = 0, time = 0, mouseData = deltaY, dwExtraInfo = GetMessageExtraInfo() } };
|
|
var input = new INPUT() { type = InputType.MOUSE, U = union };
|
|
SendInput(1, new INPUT[] { input }, INPUT.Size);
|
|
});
|
|
}
|
|
|
|
public void SendText(string transferText)
|
|
{
|
|
TryOnInputDesktop(() =>
|
|
{
|
|
SendKeys.SendWait(transferText);
|
|
});
|
|
}
|
|
|
|
public void SetKeyStatesUp()
|
|
{
|
|
TryOnInputDesktop(() =>
|
|
{
|
|
var thread = new Thread(() =>
|
|
{
|
|
foreach (VirtualKey key in Enum.GetValues(typeof(VirtualKey)))
|
|
{
|
|
try
|
|
{
|
|
var state = GetKeyState(key);
|
|
if (state == -127)
|
|
{
|
|
var union = new InputUnion()
|
|
{
|
|
ki = new KEYBDINPUT()
|
|
{
|
|
wVk = key,
|
|
wScan = 0,
|
|
time = 0,
|
|
dwFlags = KEYEVENTF.KEYUP,
|
|
dwExtraInfo = GetMessageExtraInfo()
|
|
}
|
|
};
|
|
var input = new INPUT() { type = InputType.KEYBOARD, U = union };
|
|
SendInput(1, new INPUT[] { input }, INPUT.Size);
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
});
|
|
thread.SetApartmentState(ApartmentState.STA);
|
|
thread.Start();
|
|
});
|
|
}
|
|
|
|
public void ToggleBlockInput(bool toggleOn)
|
|
{
|
|
InputActions.Enqueue(() =>
|
|
{
|
|
_inputBlocked = toggleOn;
|
|
var result = BlockInput(toggleOn);
|
|
Logger.Write($"Result of ToggleBlockInput set to {toggleOn}: {result}");
|
|
|
|
if (!toggleOn)
|
|
{
|
|
CancelTokenSource.Cancel();
|
|
}
|
|
});
|
|
|
|
if (toggleOn)
|
|
{
|
|
StartInputProcessingThread();
|
|
}
|
|
}
|
|
|
|
private void App_Exit(object sender, System.Windows.ExitEventArgs e)
|
|
{
|
|
CancelTokenSource?.Cancel();
|
|
}
|
|
private void CheckQueue(CancellationToken cancelToken)
|
|
{
|
|
while (!cancelToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
if (InputActions.TryDequeue(out var action))
|
|
{
|
|
action();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Thread.Sleep(1);
|
|
}
|
|
}
|
|
|
|
Logger.Write($"Stopping input processing on thread {Thread.CurrentThread.ManagedThreadId}.");
|
|
}
|
|
|
|
private VirtualKey ConvertJavaScriptKeyToVirtualKey(string key)
|
|
{
|
|
var keyCode = key switch
|
|
{
|
|
"Down" or "ArrowDown" => VirtualKey.DOWN,
|
|
"Up" or "ArrowUp" => VirtualKey.UP,
|
|
"Left" or "ArrowLeft" => VirtualKey.LEFT,
|
|
"Right" or "ArrowRight" => VirtualKey.RIGHT,
|
|
"Enter" => VirtualKey.RETURN,
|
|
"Esc" or "Escape" => VirtualKey.ESCAPE,
|
|
"Alt" => VirtualKey.MENU,
|
|
"Control" => VirtualKey.CONTROL,
|
|
"Shift" => VirtualKey.SHIFT,
|
|
"PAUSE" => VirtualKey.PAUSE,
|
|
"BREAK" => VirtualKey.PAUSE,
|
|
"Backspace" => VirtualKey.BACK,
|
|
"Tab" => VirtualKey.TAB,
|
|
"CapsLock" => VirtualKey.CAPITAL,
|
|
"Delete" => VirtualKey.DELETE,
|
|
"Home" => VirtualKey.HOME,
|
|
"End" => VirtualKey.END,
|
|
"PageUp" => VirtualKey.PRIOR,
|
|
"PageDown" => VirtualKey.NEXT,
|
|
"NumLock" => VirtualKey.NUMLOCK,
|
|
"Insert" => VirtualKey.INSERT,
|
|
"ScrollLock" => VirtualKey.SCROLL,
|
|
"F1" => VirtualKey.F1,
|
|
"F2" => VirtualKey.F2,
|
|
"F3" => VirtualKey.F3,
|
|
"F4" => VirtualKey.F4,
|
|
"F5" => VirtualKey.F5,
|
|
"F6" => VirtualKey.F6,
|
|
"F7" => VirtualKey.F7,
|
|
"F8" => VirtualKey.F8,
|
|
"F9" => VirtualKey.F9,
|
|
"F10" => VirtualKey.F10,
|
|
"F11" => VirtualKey.F11,
|
|
"F12" => VirtualKey.F12,
|
|
"Meta" => VirtualKey.LWIN,
|
|
"ContextMenu" => VirtualKey.MENU,
|
|
_ => (VirtualKey)VkKeyScan(Convert.ToChar(key)),
|
|
};
|
|
return keyCode;
|
|
}
|
|
private void StartInputProcessingThread()
|
|
{
|
|
try
|
|
{
|
|
CancelTokenSource?.Cancel();
|
|
CancelTokenSource?.Dispose();
|
|
}
|
|
catch { }
|
|
|
|
// After BlockInput is enabled, only simulated input coming from the same thread
|
|
// will work. So we have to start a new thread that runs continuously and
|
|
// processes a queue of input events.
|
|
_inputProcessingThread = new Thread(() =>
|
|
{
|
|
Logger.Write($"New input processing thread started on thread {Thread.CurrentThread.ManagedThreadId}.");
|
|
CancelTokenSource = new CancellationTokenSource();
|
|
|
|
if (_inputBlocked)
|
|
{
|
|
ToggleBlockInput(true);
|
|
}
|
|
CheckQueue(CancelTokenSource.Token);
|
|
});
|
|
|
|
_inputProcessingThread.SetApartmentState(ApartmentState.STA);
|
|
_inputProcessingThread.Start();
|
|
}
|
|
|
|
private void TryOnInputDesktop(Action inputAction)
|
|
{
|
|
if (!_inputBlocked)
|
|
{
|
|
var inputThread = new Thread(() =>
|
|
{
|
|
Win32Interop.SwitchToInputDesktop();
|
|
inputAction();
|
|
});
|
|
inputThread.SetApartmentState(ApartmentState.STA);
|
|
inputThread.Start();
|
|
}
|
|
else
|
|
{
|
|
InputActions.Enqueue(() =>
|
|
{
|
|
try
|
|
{
|
|
if (!Win32Interop.SwitchToInputDesktop())
|
|
{
|
|
Logger.Write("Desktop switch failed during input processing.");
|
|
|
|
// Thread likely has hooks in current desktop. SendKeys will create one with no way to unhook it.
|
|
// Start a new thread for processing input.
|
|
StartInputProcessingThread();
|
|
return;
|
|
}
|
|
inputAction();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Write(ex);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|