Files
PowerToys/src/modules/MouseWithoutBorders/App/Class/InputSimulation.cs
Mason Bergstrom dd420509ab [Mouse Without Borders] Adding Horizontal Scrolling Support (#42179)
## Summary of the Pull Request
Added support for horizontal scrolling to Mouse Without Borders, instead
of being a no-op.

## PR Checklist

- [x] Closes: #37037
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

## Detailed Description of the Pull Request / Additional comments
Works in a backward compatible fashion, continuing to be a no-op when
forwarded to an older version, but works once both devices are updated.

## Validation Steps Performed
Built on two separate devices that are paired with each other. First
tested with one device updated and one on the old code, confirming
backwards compatibility support. Second tested both devices updated,
confirming horizontal scroll is now working on remote device.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-23 12:06:33 +08:00

483 lines
18 KiB
C#

// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// <summary>
// Keyboard/Mouse simulation.
// </summary>
// <history>
// 2008 created by Truong Do (ductdo).
// 2009-... modified by Truong Do (TruongDo).
// 2023- Included in PowerToys.
// </history>
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
using System.ServiceProcess;
using System.Threading.Tasks;
using Microsoft.PowerToys.Settings.UI.Library;
using MouseWithoutBorders.Core;
using Windows.UI.Input.Preview.Injection;
using static MouseWithoutBorders.Class.NativeMethods;
[module: SuppressMessage("Microsoft.Portability", "CA1901:PInvokeDeclarationsShouldBePortable", Scope = "member", Target = "MouseWithoutBorders.InputSimulation.#keybd_event(System.Byte,System.Byte,System.UInt32,System.Int32)", MessageId = "3", Justification = "Dotnet port with style preservation")]
[module: SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", Scope = "member", Target = "MouseWithoutBorders.InputSimulation.#InputProcessKeyEx(System.Int32,System.Int32,System.Boolean&)", MessageId = "MouseWithoutBorders.NativeMethods.LockWorkStation", Justification = "Dotnet port with style preservation")]
namespace MouseWithoutBorders.Class
{
internal class InputSimulation
{
public static InputInjector Injector;
private InputSimulation()
{
}
internal static InjectedInputMouseInfo MouseInputToInjectedInputMouseInfo(MOUSEINPUT mouseInput)
{
var injectedInput = new InjectedInputMouseInfo();
injectedInput.DeltaX = mouseInput.dx;
injectedInput.DeltaY = mouseInput.dy;
injectedInput.MouseData = (uint)mouseInput.mouseData;
injectedInput.MouseOptions = (InjectedInputMouseOptions)mouseInput.dwFlags;
injectedInput.TimeOffsetInMilliseconds = (uint)mouseInput.time;
return injectedInput;
}
private static uint SendInputEx(NativeMethods.INPUT input)
{
Common.PaintCount = 0;
uint rv = 0;
if (Common.Is64bitOS)
{
NativeMethods.INPUT64 input64 = default;
input64.type = input.type;
// Keyboard
if (input.type == 1)
{
input64.ki.wVk = input.ki.wVk;
input64.ki.wScan = input.ki.wScan;
input64.ki.dwFlags = input.ki.dwFlags;
input64.ki.time = input.ki.time;
input64.ki.dwExtraInfo = input.ki.dwExtraInfo;
}
// Mouse
else
{
input64.mi.dx = input.mi.dx;
input64.mi.dy = input.mi.dy;
input64.mi.dwFlags = input.mi.dwFlags;
input64.mi.mouseData = input.mi.mouseData;
input64.mi.time = input.mi.time;
input64.mi.dwExtraInfo = input.mi.dwExtraInfo;
}
if (input.type == 0 && (input.mi.dwFlags & (int)NativeMethods.MOUSEEVENTF.MOVE) != 0 && NativeMethods.InjectMouseInputAvailable)
{
Injector.InjectMouseInput(new[] { MouseInputToInjectedInputMouseInfo(input64.mi) });
}
else
{
NativeMethods.INPUT64[] inputs = { input64 };
rv = NativeMethods.SendInput64(1, inputs, Marshal.SizeOf(input64));
}
}
else
{
if (input.type == 0 && (input.mi.dwFlags & (int)NativeMethods.MOUSEEVENTF.MOVE) != 0 && NativeMethods.InjectMouseInputAvailable)
{
Injector.InjectMouseInput(new[] { MouseInputToInjectedInputMouseInfo(input.mi) });
}
else
{
NativeMethods.INPUT[] inputs = { input };
rv = NativeMethods.SendInput(1, inputs, Marshal.SizeOf(input));
}
}
return rv;
}
internal static void SendKey(KEYBDDATA kd)
{
string log = string.Empty;
NativeMethods.KEYEVENTF dwFlags = NativeMethods.KEYEVENTF.KEYDOWN;
uint scanCode = 0;
// http://msdn.microsoft.com/en-us/library/ms644967(VS.85).aspx
if ((kd.dwFlags & (int)Common.LLKHF.UP) == (int)Common.LLKHF.UP)
{
dwFlags = NativeMethods.KEYEVENTF.KEYUP;
}
if ((kd.dwFlags & (int)Common.LLKHF.EXTENDED) == (int)Common.LLKHF.EXTENDED)
{
dwFlags |= NativeMethods.KEYEVENTF.EXTENDEDKEY;
}
scanCode = NativeMethods.MapVirtualKey((uint)kd.wVk, 0);
InputProcessKeyEx(kd.wVk, kd.dwFlags, out bool eatKey);
if (!eatKey)
{
InputHook.RealData = false;
#if !USING_keybd_event
// #if USING_SendInput
NativeMethods.INPUT structInput;
structInput = default;
structInput.type = 1;
structInput.ki.wScan = (short)scanCode;
structInput.ki.time = 0;
structInput.ki.wVk = (short)kd.wVk;
structInput.ki.dwFlags = (int)dwFlags;
structInput.ki.dwExtraInfo = NativeMethods.GetMessageExtraInfo();
Common.DoSomethingInTheInputSimulationThread(() =>
{
// key press simulation
SendInputEx(structInput);
});
#else
keybd_event((byte)kd.wVk, (byte)scanCode, (UInt32)dwFlags, 0);
#endif
InputHook.RealData = true;
}
log += "*"; // ((Keys)kd.wVk).ToString(CultureInfo.InvariantCulture);
Logger.LogDebug(log);
}
// Md.X, Md.Y is from 0 to 65535
internal static uint SendMouse(MOUSEDATA md)
{
uint rv = 0;
NativeMethods.INPUT mouse_input = default;
long w65535 = (MachineStuff.DesktopBounds.Right - MachineStuff.DesktopBounds.Left) * 65535 / Common.ScreenWidth;
long h65535 = (MachineStuff.DesktopBounds.Bottom - MachineStuff.DesktopBounds.Top) * 65535 / Common.ScreenHeight;
long l65535 = MachineStuff.DesktopBounds.Left * 65535 / Common.ScreenWidth;
long t65535 = MachineStuff.DesktopBounds.Top * 65535 / Common.ScreenHeight;
mouse_input.type = 0;
long dx = (md.X * w65535 / 65535) + l65535;
long dy = (md.Y * h65535 / 65535) + t65535;
mouse_input.mi.dx = (int)dx;
mouse_input.mi.dy = (int)dy;
mouse_input.mi.mouseData = md.WheelDelta;
if (md.dwFlags != Common.WM_MOUSEMOVE)
{
Logger.LogDebug($"InputSimulation.SendMouse: x = {md.X}, y = {md.Y}, WheelDelta = {md.WheelDelta}, dwFlags = {md.dwFlags}.");
}
switch (md.dwFlags)
{
case Common.WM_MOUSEMOVE:
mouse_input.mi.dwFlags |= (int)(NativeMethods.MOUSEEVENTF.MOVE | NativeMethods.MOUSEEVENTF.ABSOLUTE);
break;
case Common.WM_LBUTTONDOWN:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.LEFTDOWN;
break;
case Common.WM_LBUTTONUP:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.LEFTUP;
break;
case Common.WM_RBUTTONDOWN:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.RIGHTDOWN;
break;
case Common.WM_RBUTTONUP:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.RIGHTUP;
break;
case Common.WM_MBUTTONDOWN:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.MIDDLEDOWN;
break;
case Common.WM_MBUTTONUP:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.MIDDLEUP;
break;
case Common.WM_MOUSEWHEEL:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.WHEEL;
break;
case Common.WM_MOUSEHWHEEL:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.HWHEEL;
break;
case Common.WM_XBUTTONUP:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.XUP;
break;
case Common.WM_XBUTTONDOWN:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.XDOWN;
break;
default:
break;
}
Common.DoSomethingInTheInputSimulationThread(() =>
{
InputHook.RealData = false;
rv = SendInputEx(mouse_input);
});
if (Common.MainFormVisible && !DragDrop.IsDropping)
{
Helper.MainFormDot();
}
return rv;
}
internal static void MoveMouseEx(int x, int y)
{
NativeMethods.INPUT mouse_input = default;
long w65535 = (MachineStuff.DesktopBounds.Right - MachineStuff.DesktopBounds.Left) * 65535 / Common.ScreenWidth;
long h65535 = (MachineStuff.DesktopBounds.Bottom - MachineStuff.DesktopBounds.Top) * 65535 / Common.ScreenHeight;
long l65535 = MachineStuff.DesktopBounds.Left * 65535 / Common.ScreenWidth;
long t65535 = MachineStuff.DesktopBounds.Top * 65535 / Common.ScreenHeight;
mouse_input.type = 0;
long dx = (x * w65535 / 65535) + l65535;
long dy = (y * h65535 / 65535) + t65535;
mouse_input.mi.dx = (int)dx;
mouse_input.mi.dy = (int)dy;
Logger.LogDebug($"InputSimulation.MoveMouseEx: x = {x}, y = {y}.");
mouse_input.mi.dwFlags |= (int)(NativeMethods.MOUSEEVENTF.MOVE | NativeMethods.MOUSEEVENTF.ABSOLUTE);
Common.DoSomethingInTheInputSimulationThread(() =>
{
InputHook.RealData = false;
SendInputEx(mouse_input);
});
}
// x, y is in pixel
internal static void MoveMouse(int x, int y)
{
// Common.Log("Mouse move: " + x.ToString(CultureInfo.CurrentCulture) + "," + y.ToString(CultureInfo.CurrentCulture));
NativeMethods.INPUT mouse_input = default;
mouse_input.type = 0;
mouse_input.mi.dx = x * 65535 / Common.ScreenWidth;
mouse_input.mi.dy = y * 65535 / Common.ScreenHeight;
mouse_input.mi.mouseData = 0;
mouse_input.mi.dwFlags = (int)(NativeMethods.MOUSEEVENTF.MOVE | NativeMethods.MOUSEEVENTF.ABSOLUTE);
Logger.LogDebug($"InputSimulation.MoveMouse: x = {x}, y = {y}.");
Common.DoSomethingInTheInputSimulationThread(() =>
{
InputHook.RealData = false;
SendInputEx(mouse_input);
// NativeMethods.SetCursorPos(x, y);
});
}
// dx, dy is in pixel, relative
internal static void MoveMouseRelative(int dx, int dy)
{
NativeMethods.INPUT mouse_input = default;
mouse_input.type = 0;
mouse_input.mi.dx = dx; // *65535 / Common.ScreenWidth;
mouse_input.mi.dy = dy; // *65535 / Common.ScreenHeight;
mouse_input.mi.mouseData = 0;
mouse_input.mi.dwFlags = (int)NativeMethods.MOUSEEVENTF.MOVE;
Logger.LogDebug($"InputSimulation.MoveMouseRelative: x = {dx}, y = {dy}.");
Common.DoSomethingInTheInputSimulationThread(() =>
{
InputHook.RealData = false;
SendInputEx(mouse_input);
// NativeMethods.SetCursorPos(x, y);
});
}
internal static void MouseUp()
{
Common.DoSomethingInTheInputSimulationThread(() =>
{
NativeMethods.INPUT input = default;
input.type = 0;
input.mi.dx = 0;
input.mi.dy = 0;
input.mi.mouseData = 0;
input.mi.dwFlags = (int)NativeMethods.MOUSEEVENTF.LEFTUP;
InputHook.SkipMouseUpCount++;
_ = SendInputEx(input);
Logger.LogDebug("MouseUp() called");
});
}
internal static void MouseClickDotForm(int x, int y)
{
_ = Task.Factory.StartNew(
() =>
{
NativeMethods.INPUT input = default;
input.type = 0;
input.mi.dx = 0;
input.mi.dy = 0;
input.mi.mouseData = 0;
InputHook.SkipMouseUpDown = true;
try
{
MoveMouse(x, y);
InputHook.RealData = false;
input.mi.dwFlags = (int)NativeMethods.MOUSEEVENTF.LEFTDOWN;
_ = SendInputEx(input);
InputHook.RealData = false;
input.mi.dwFlags = (int)NativeMethods.MOUSEEVENTF.LEFTUP;
_ = SendInputEx(input);
Logger.LogDebug("MouseClick() called");
Thread.Sleep(200);
}
finally
{
InputHook.SkipMouseUpDown = false;
}
},
System.Threading.CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.Default);
}
private static bool winDown;
private static bool ctrlDown;
private static bool altDown;
private static bool shiftDown;
internal static readonly string[] Args = new string[] { "CAD" };
private static void ResetModifiersState(HotkeySettings matchingHotkey)
{
ctrlDown = ctrlDown && matchingHotkey.Ctrl;
altDown = altDown && matchingHotkey.Alt;
shiftDown = shiftDown && matchingHotkey.Shift;
winDown = winDown && matchingHotkey.Win;
}
private static void InputProcessKeyEx(int vkCode, int flags, out bool eatKey)
{
eatKey = false;
if ((flags & (int)Common.LLKHF.UP) == (int)Common.LLKHF.UP)
{
switch ((VK)vkCode)
{
case VK.LWIN:
case VK.RWIN:
winDown = false;
break;
case VK.LCONTROL:
case VK.RCONTROL:
ctrlDown = false;
break;
case VK.LMENU:
case VK.RMENU:
altDown = false;
break;
case VK.LSHIFT:
case VK.RSHIFT:
shiftDown = false;
break;
default:
break;
}
}
else
{
if (Common.HotkeyMatched(vkCode, winDown, ctrlDown, altDown, shiftDown, Setting.Values.HotKeyLockMachine))
{
if (!Common.RunOnLogonDesktop
&& !Common.RunOnScrSaverDesktop)
{
ResetModifiersState(Setting.Values.HotKeyLockMachine);
eatKey = true;
InitAndCleanup.ReleaseAllKeys();
_ = NativeMethods.LockWorkStation();
}
}
switch ((VK)vkCode)
{
case VK.LWIN:
case VK.RWIN:
winDown = true;
break;
case VK.LCONTROL:
case VK.RCONTROL:
ctrlDown = true;
break;
case VK.LMENU:
case VK.RMENU:
altDown = true;
break;
case VK.LSHIFT:
case VK.RSHIFT:
shiftDown = true;
break;
case VK.DELETE:
if (ctrlDown && altDown)
{
ctrlDown = altDown = false;
eatKey = true;
InitAndCleanup.ReleaseAllKeys();
}
break;
case (VK)'L':
if (winDown)
{
winDown = false;
eatKey = true;
InitAndCleanup.ReleaseAllKeys();
uint rv = NativeMethods.LockWorkStation();
Logger.LogDebug("LockWorkStation returned " + rv.ToString(CultureInfo.CurrentCulture));
}
break;
case VK.END:
if (ctrlDown && altDown)
{
ctrlDown = altDown = false;
new ServiceController("MouseWithoutBordersSvc").Start(Args);
}
break;
default:
break;
}
}
}
internal static void ResetSystemKeyFlags()
{
ctrlDown = winDown = altDown = false;
}
}
}