Skip to content

Commit

Permalink
added MouseHelper class
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeclayton committed Mar 13, 2023
1 parent 7d7b97f commit 5772ed6
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 139 deletions.
147 changes: 147 additions & 0 deletions src/FancyMouse.Displays/VirtualDisplayManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using FancyMouse.Displays.DeviceManagement;
using System.Diagnostics;
using System.Management;

namespace FancyMouse.Displays;

public sealed class VirtualDisplayManager
{

#region Constructors

public VirtualDisplayManager(string rootFolder, string hardwareId)
{
this.RootFolder = rootFolder ?? throw new ArgumentNullException(nameof(rootFolder));
this.HardwareId = hardwareId ?? throw new ArgumentNullException(nameof(hardwareId));
}

#endregion

#region Properties

public string RootFolder
{
get;
}

public string HardwareId
{
get;
}

#endregion

#region Display Driver

/// <summary>
/// Checks if the Amyuni Technologies USB Mobile Monitor Virtual Display driver is installed.
/// </summary>
public Dictionary<string, object>? GetDisplayDriver()
{
var drivers = VirtualDisplayManager.GetManagementObjects(
string.Join(" ",
$"SELECT * FROM Win32_PnPSignedDriver",
$"WHERE ClassGuid='{DeviceClassGuids.Display}'",
$"AND Manufacturer='Amyuni'",
$"AND DeviceName='USB Mobile Monitor Virtual Display'",
$"AND HardWareID='{this.HardwareId}'"
)
).ToList();
return drivers.SingleOrDefault();
}

/// <summary>
/// </summary>
/// <returns></returns>
public void InstallDisplayDriver()
{
this.ExecuteDeviceInstallerCommand(
new List<string>
{
"install",
Path.Combine(this.RootFolder, "usbmmidd.inf"),
this.HardwareId
}
);
}

/// <summary>
/// </summary>
/// <returns></returns>
public void UninstallDisplayDriver()
{
this.ExecuteDeviceInstallerCommand(
new List<string> { "stop", this.HardwareId }
);
this.ExecuteDeviceInstallerCommand(
new List<string> { "remove", this.HardwareId }
);
}

#endregion

#region Display Adapter

public Dictionary<string, object>? GetDisplayAdapter()
{
var devices = VirtualDisplayManager.GetManagementObjects(
string.Join(" ",
$"SELECT * FROM Win32_PnPEntity",
$"WHERE ClassGuid='{DeviceClassGuids.Display}'",
$"AND Manufacturer='Amyuni'",
$"AND Name='USB Mobile Monitor Virtual Display'"
//$"AND HardWareID='{this.HardwareId}'"
)
).Where(
device => (device["HardwareID"] is string[] hardwareIds) && (hardwareIds?.Contains(this.HardwareId) ?? false)
).OrderBy(d => d["Name"])
.ToList();
return devices.SingleOrDefault();
}

#endregion

#region Helpers

private static List<Dictionary<string, object>> GetManagementObjects(string query)
{
return new ManagementObjectSearcher(query).Get()
.Cast<ManagementObject>()
.Select(
obj => obj.Properties
.Cast<PropertyData>()
.OrderBy(p => p.Name)
.ToDictionary(
p => p.Name,
p => p.Value
)
).ToList();
}

public int ExecuteDeviceInstallerCommand(IEnumerable<string> args)
{
var startInfo = new ProcessStartInfo
{
FileName = Path.Combine(this.RootFolder, "deviceinstaller64.exe"),
UseShellExecute = true,
Verb = "RunAs"
};
foreach(var arg in args)
{
startInfo.ArgumentList.Add(arg);
}
var process = Process.Start(startInfo) ?? throw new InvalidOperationException();
process.WaitForExit();
var exitcode = process.ExitCode;
if (exitcode != 0)
{
throw new InvalidOperationException(
$"installer failed with exit code {exitcode}"
);
}
return exitcode;
}

#endregion

}
60 changes: 0 additions & 60 deletions src/FancyMouse.UnitTests/Helpers/DrawingHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,64 +156,4 @@ public void RunTestCases(TestCase data)
Assert.AreEqual(expected.ActivatedScreen.ToRectangle(), actual.ActivatedScreen.ToRectangle(), "ActivatedScreen.ToRectangle");
}
}

[TestClass]
public class GetJumpLocationTests
{
public class TestCase
{
public TestCase(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds, PointInfo expectedResult)
{
this.PreviewLocation = previewLocation;
this.PreviewSize = previewSize;
this.DesktopBounds = desktopBounds;
this.ExpectedResult = expectedResult;
}

public PointInfo PreviewLocation { get; set; }

public SizeInfo PreviewSize { get; set; }

public RectangleInfo DesktopBounds { get; set; }

public PointInfo ExpectedResult { get; set; }
}

public static IEnumerable<object[]> GetTestCases()
{
// corners and midpoint with a zero origin
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(0, 0, 1600, 1200), new(0, 0)) };
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(0, 0, 1600, 1200), new(1600, 0)) };
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(0, 0, 1600, 1200), new(0, 1200)) };
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(0, 0, 1600, 1200), new(1600, 1200)) };
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(0, 0, 1600, 1200), new(800, 600)) };

// corners and midpoint with a positive origin
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 1000)) };
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 1000)) };
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 2200)) };
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 2200)) };
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(1000, 1000, 1600, 1200), new(1800, 1600)) };

// corners and midpoint with a negative origin
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, -1000)) };
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, -1000)) };
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, 200)) };
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, 200)) };
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(-1000, -1000, 1600, 1200), new(-200, -400)) };
}

[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
var actual = DrawingHelper.GetJumpLocation(
data.PreviewLocation,
data.PreviewSize,
data.DesktopBounds);
var expected = data.ExpectedResult;
Assert.AreEqual(expected.X, actual.X);
Assert.AreEqual(expected.Y, actual.Y);
}
}
}
70 changes: 70 additions & 0 deletions src/FancyMouse.UnitTests/Helpers/MouseHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Drawing;
using FancyMouse.Drawing.Models;
using FancyMouse.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace FancyMouse.UnitTests.Helpers;

[TestClass]
public static class MouseHelperTests
{
[TestClass]
public class GetJumpLocationTests
{
public class TestCase
{
public TestCase(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds, PointInfo expectedResult)
{
this.PreviewLocation = previewLocation;
this.PreviewSize = previewSize;
this.DesktopBounds = desktopBounds;
this.ExpectedResult = expectedResult;
}

public PointInfo PreviewLocation { get; set; }

public SizeInfo PreviewSize { get; set; }

public RectangleInfo DesktopBounds { get; set; }

public PointInfo ExpectedResult { get; set; }
}

public static IEnumerable<object[]> GetTestCases()
{
// corners and midpoint with a zero origin
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(0, 0, 1600, 1200), new(0, 0)) };
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(0, 0, 1600, 1200), new(1600, 0)) };
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(0, 0, 1600, 1200), new(0, 1200)) };
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(0, 0, 1600, 1200), new(1600, 1200)) };
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(0, 0, 1600, 1200), new(800, 600)) };

// corners and midpoint with a positive origin
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 1000)) };
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 1000)) };
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 2200)) };
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 2200)) };
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(1000, 1000, 1600, 1200), new(1800, 1600)) };

// corners and midpoint with a negative origin
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, -1000)) };
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, -1000)) };
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, 200)) };
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, 200)) };
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(-1000, -1000, 1600, 1200), new(-200, -400)) };
}

[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
var actual = MouseHelper.GetJumpLocation(
data.PreviewLocation,
data.PreviewSize,
data.DesktopBounds);
var expected = data.ExpectedResult;
Assert.AreEqual(expected.X, actual.X);
Assert.AreEqual(expected.Y, actual.Y);
}
}
}
75 changes: 0 additions & 75 deletions src/FancyMouse/Helpers/DrawingHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,79 +264,4 @@ public static void DrawPreviewScreens(
NativeMethods.Gdi32.ROP_CODE.SRCCOPY);
}
}

/// <summary>
/// Calculates where to move the cursor to by projecting a point from
/// the preview image onto the desktop and using that as the target location.
/// </summary>
/// <remarks>
/// The preview image origin is (0, 0) but the desktop origin may be non-zero,
/// or even negative if the primary monitor is not the at the top-left of the
/// entire desktop rectangle, so results may contain negative coordinates.
/// </remarks>
public static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds)
{
return previewLocation
.Scale(previewSize.ScaleToFitRatio(desktopBounds.Size))
.Offset(desktopBounds.Location);
}

/// <summary>
/// Moves the cursor to the specified location.
/// </summary>
/// <remarks>
/// See https://github.com/mikeclayton/FancyMouse/pull/3
/// </remarks>
public static void JumpCursor(PointInfo location)
{
// set the new cursor position *twice* - the cursor sometimes end up in
// the wrong place if we try to cross the dead space between non-aligned
// monitors - e.g. when trying to move the cursor from (a) to (b) we can
// *sometimes* - for no clear reason - end up at (c) instead.
//
// +----------------+
// |(c) (b) |
// | |
// | |
// | |
// +---------+ |
// | (a) | |
// +---------+----------------+
//
// setting the position a second time seems to fix this and moves the
// cursor to the expected location (b) - for more details see
var point = location.ToPoint();
Cursor.Position = point;
Cursor.Position = point;
}

/// <summary>
/// Sends an input simulating an absolute mouse move to the new location.
/// </summary>
/// <remarks>
/// See https://github.com/microsoft/PowerToys/issues/24523
/// https://github.com/microsoft/PowerToys/pull/24527
/// </remarks>
public static void SimulateMouseMovementEvent(Point location)
{
var mouseMoveInput = new NativeMethods.NativeMethods.INPUT
{
type = NativeMethods.NativeMethods.INPUTTYPE.INPUT_MOUSE,
data = new NativeMethods.NativeMethods.InputUnion
{
mi = new NativeMethods.NativeMethods.MOUSEINPUT
{
dx = NativeMethods.NativeMethods.CalculateAbsoluteCoordinateX(location.X),
dy = NativeMethods.NativeMethods.CalculateAbsoluteCoordinateY(location.Y),
mouseData = 0,
dwFlags = (uint)NativeMethods.NativeMethods.MOUSE_INPUT_FLAGS.MOUSEEVENTF_MOVE
| (uint)NativeMethods.NativeMethods.MOUSE_INPUT_FLAGS.MOUSEEVENTF_ABSOLUTE,
time = 0,
dwExtraInfo = 0,
},
},
};
var inputs = new NativeMethods.NativeMethods.INPUT[] { mouseMoveInput };
_ = NativeMethods.NativeMethods.SendInput(1, inputs, NativeMethods.NativeMethods.INPUT.Size);
}
}
Loading

0 comments on commit 5772ed6

Please sign in to comment.