Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 122 additions & 1 deletion src/modules/MouseWithoutBorders/App/Class/Common.WinAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ internal partial class Common
{
private static MyRectangle newDesktopBounds;
private static MyRectangle newPrimaryScreenBounds;
// temporary list used during enumeration
private static List<MyRectangle> newMonitorBounds;
private static string activeDesktop;

internal static string ActiveDesktop => Common.activeDesktop;
Expand All @@ -43,6 +45,10 @@ internal static void SystemEvents_DisplaySettingsChanged(object sender, EventArg

internal static readonly List<Point> SensitivePoints = new();

// List of monitor rectangles (in desktop coordinate space) populated by GetScreenConfig()
// Each entry describes a monitor's bounds: Left/Top/Right/Bottom
internal static List<MyRectangle> MonitorRects = new();

private static bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref NativeMethods.RECT lprcMonitor, IntPtr dwData)
{
// lprcMonitor is wrong!!! => using GetMonitorInfo(...)
Expand Down Expand Up @@ -70,7 +76,26 @@ private static bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref Nati
Logger.Log(e);
}

if (mi.rcMonitor.Left == 0 && mi.rcMonitor.Top == 0 && mi.rcMonitor.Right != 0 && mi.rcMonitor.Bottom != 0)
// MonitorInfoEx.dwFlags will include 1 when monitor is primary.
const uint MONITORINFOF_PRIMARY = 1;

// Add monitor rectangle to the temporary list (for GetScreenConfig to pick up)
if (newMonitorBounds == null)
{
newMonitorBounds = new List<MyRectangle>();
}

var thisRect = new MyRectangle()
{
Left = mi.rcMonitor.Left,
Top = mi.rcMonitor.Top,
Right = mi.rcMonitor.Right,
Bottom = mi.rcMonitor.Bottom
};

newMonitorBounds.Add(thisRect);

if ((mi.dwFlags & MONITORINFOF_PRIMARY) == MONITORINFOF_PRIMARY)
{
// Primary screen
_ = Interlocked.Exchange(ref screenWidth, mi.rcMonitor.Right - mi.rcMonitor.Left);
Expand Down Expand Up @@ -122,6 +147,7 @@ internal static void GetScreenConfig()
Logger.LogDebug("==================== GetScreenConfig started");
newDesktopBounds = new MyRectangle();
newPrimaryScreenBounds = new MyRectangle();
newMonitorBounds = new List<MyRectangle>();
newDesktopBounds.Left = newPrimaryScreenBounds.Left = Screen.PrimaryScreen.Bounds.Left;
newDesktopBounds.Top = newPrimaryScreenBounds.Top = Screen.PrimaryScreen.Bounds.Top;
newDesktopBounds.Right = newPrimaryScreenBounds.Right = Screen.PrimaryScreen.Bounds.Right;
Expand Down Expand Up @@ -165,6 +191,12 @@ internal static void GetScreenConfig()
Interlocked.Exchange(ref MachineStuff.desktopBounds, newDesktopBounds);
Interlocked.Exchange(ref MachineStuff.primaryScreenBounds, newPrimaryScreenBounds);

// Replace the current monitor list with the newly enumerated list
lock (SensitivePoints)
{
MonitorRects = newMonitorBounds ?? new List<MyRectangle>();
}

Logger.Log(string.Format(
CultureInfo.CurrentCulture,
"logon = {0} PrimaryScreenBounds = {1},{2},{3},{4} desktopBounds = {5},{6},{7},{8}",
Expand All @@ -186,6 +218,95 @@ internal static void GetScreenConfig()
}
}

// Return the monitor rect containing the given desktop coordinate point if any
internal static MyRectangle GetMonitorContainingPoint(int x, int y)
{
lock (SensitivePoints)
{
foreach (MyRectangle r in MonitorRects)
{
if (x >= r.Left && x < r.Right && y >= r.Top && y < r.Bottom)
{
return r;
}
}
}

return null;
}

internal static bool MonitorBelowExists(MyRectangle r, int x)
{
if (r == null) return false;
lock (SensitivePoints)
{
foreach (MyRectangle m in MonitorRects)
{
if (m == r) continue;
if (x >= m.Left && x < m.Right && m.Top >= r.Bottom)
{
return true;
}
}
}

return false;
}

internal static bool MonitorAboveExists(MyRectangle r, int x)
{
if (r == null) return false;
lock (SensitivePoints)
{
foreach (MyRectangle m in MonitorRects)
{
if (m == r) continue;
if (x >= m.Left && x < m.Right && m.Bottom <= r.Top)
{
return true;
}
}
}

return false;
}

internal static bool MonitorRightExists(MyRectangle r, int y)
{
if (r == null) return false;
lock (SensitivePoints)
{
foreach (MyRectangle m in MonitorRects)
{
if (m == r) continue;
if (y >= m.Top && y < m.Bottom && m.Left >= r.Right)
{
return true;
}
}
}

return false;
}

internal static bool MonitorLeftExists(MyRectangle r, int y)
{
if (r == null) return false;
lock (SensitivePoints)
{
foreach (MyRectangle m in MonitorRects)
{
if (m == r) continue;
if (y >= m.Top && y < m.Bottom && m.Right <= r.Left)
{
return true;
}
}
}

return false;
}

#if USING_SCREEN_SAVER_ROUTINES
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int PostMessage(IntPtr hWnd, int wMsg, int wParam, int lParam);
Expand Down
36 changes: 28 additions & 8 deletions src/modules/MouseWithoutBorders/App/Core/MachineStuff.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ internal static MachinePool MachinePool

#pragma warning disable SA1306 // Field should begin with a lower-case letter
internal static MouseLocation SwitchLocation = new();
// Helper to allow unit tests to stub monitor detection.
internal static Func<Point, System.Drawing.Rectangle> GetScreenBoundsFromPoint = p => System.Windows.Forms.Screen.FromPoint(p).Bounds;
#pragma warning restore SA1306

internal static ID NewDesMachineID
Expand Down Expand Up @@ -238,19 +240,34 @@ internal static Point MoveToMyNeighbourIfNeeded(int x, int y, ID desMachineID)
* */
if (desMachineID == Common.MachineID)
{
if (x < desktopBounds.Left + SKIP_PIXELS)
// When the current (controller) machine has multiple monitors arranged in a row
// we should check the monitor that actually contains the pointer rather than
// using the union of all monitor bounds (desktopBounds). This fixes a bug
// where moving off the bottom of a monitor with a higher bottom would not
// trigger a switch even though the user was at the bottom of that specific monitor.
// prefer the internal list (populated by GetScreenConfig) for monitor adjacency checks
var screenBounds = GetScreenBoundsFromPoint(new Point(x, y));

MyRectangle activeMon = Common.GetMonitorContainingPoint(x, y);
if (activeMon == null)
{
// fall back to the screen bounds returned by WinForms for tests or when enumeration is unavailable
activeMon = new MyRectangle() { Left = screenBounds.Left, Top = screenBounds.Top, Right = screenBounds.Right, Bottom = screenBounds.Bottom };
}

if (x < activeMon.Left + SKIP_PIXELS && !Common.MonitorLeftExists(activeMon, y))
{
return MoveLeft(x, y);
}
else if (x >= desktopBounds.Right - SKIP_PIXELS)
else if (x >= activeMon.Right - SKIP_PIXELS && !Common.MonitorRightExists(activeMon, y))
{
return MoveRight(x, y);
}
else if (y < desktopBounds.Top + SKIP_PIXELS)
else if (y < activeMon.Top + SKIP_PIXELS && !Common.MonitorAboveExists(activeMon, x))
{
return MoveUp(x, y);
}
else if (y >= desktopBounds.Bottom - SKIP_PIXELS)
else if (y >= activeMon.Bottom - SKIP_PIXELS && !Common.MonitorBelowExists(activeMon, x))
{
return MoveDown(x, y);
}
Expand All @@ -261,19 +278,22 @@ internal static Point MoveToMyNeighbourIfNeeded(int x, int y, ID desMachineID)
* */
else
{
if (x < primaryScreenBounds.Left + SKIP_PIXELS)
// When the mouse position provided is relative to the primary screen
MyRectangle activePrim = Common.GetMonitorContainingPoint(x, y) ?? primaryScreenBounds;

if (x < activePrim.Left + SKIP_PIXELS && !Common.MonitorLeftExists(activePrim, y))
{
return MoveLeft(x, y);
}
else if (x >= primaryScreenBounds.Right - SKIP_PIXELS)
else if (x >= activePrim.Right - SKIP_PIXELS && !Common.MonitorRightExists(activePrim, y))
{
return MoveRight(x, y);
}
else if (y < primaryScreenBounds.Top + SKIP_PIXELS)
else if (y < activePrim.Top + SKIP_PIXELS && !Common.MonitorAboveExists(activePrim, x))
{
return MoveUp(x, y);
}
else if (y >= primaryScreenBounds.Bottom - SKIP_PIXELS)
else if (y >= activePrim.Bottom - SKIP_PIXELS && !Common.MonitorBelowExists(activePrim, x))
{
return MoveDown(x, y);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System.Drawing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseWithoutBorders.Class;
using MouseWithoutBorders.Core;

namespace MouseWithoutBorders.UnitTests.Core;

[TestClass]
public sealed class MoveTests
{
[TestMethod]
public void MoveDown_ShouldSwitchFromTopToBottomBasedOnMonitorBounds()
{
// Arrange: create a 2x2 matrix (A,B on top row; C,D on bottom row)
Setting.Values.PauseInstantSaving = true;
Setting.Values.MatrixOneRow = false; // 2 rows

MachineStuff.MachinePool.Initialize(new string[] { "A", "B", "C", "D" });
MachineStuff.MachineMatrix = new string[] { "A", "B", "C", "D" };

// Assign deterministic IDs so NameFromID/IdFromName work predictably.
_ = MachineStuff.MachinePool.TryUpdateMachineID("A", (ID)1, true);
_ = MachineStuff.MachinePool.TryUpdateMachineID("B", (ID)2, true);
_ = MachineStuff.MachinePool.TryUpdateMachineID("C", (ID)3, true);
_ = MachineStuff.MachinePool.TryUpdateMachineID("D", (ID)4, true);

// Make this test environment the controller machine = A (1)
Setting.Values.MachineId = 1;

// Stub the monitor bounds for the point so the Move* logic checks the monitor's bounds
var stubBounds = new Rectangle(0, 0, 1000, 500);
var original = MachineStuff.GetScreenBoundsFromPoint;
try
{
MachineStuff.GetScreenBoundsFromPoint = p => stubBounds;

// Act: pointer at the bottom edge of the top-left monitor (A) should trigger MoveDown -> C
var result = MachineStuff.MoveToMyNeighbourIfNeeded(10, 500, Common.MachineID);

// Assert: result should be non-empty (indicates a switch) and newDesMachineIdEx should now be C (ID=3)
Assert.IsFalse(result.IsEmpty, "MoveToMyNeighbourIfNeeded returned an empty point; expected a switch.");
Assert.AreEqual((ID)3, MachineStuff.newDesMachineIdEx);
}
finally
{
// restore
MachineStuff.GetScreenBoundsFromPoint = original;
}
}

[TestMethod]
public void MoveDown_ShouldTrigger_When_NoMonitorBelowAtCursorX()
{
// Arrange: top-row monitors with different heights (no monitors below on this machine at these X positions)
Setting.Values.PauseInstantSaving = true;
Setting.Values.MatrixOneRow = false; // 2 rows

MachineStuff.MachinePool.Initialize(new string[] { "A", "B", "C", "D" });
MachineStuff.MachineMatrix = new string[] { "A", "B", "C", "D" };
_ = MachineStuff.MachinePool.TryUpdateMachineID("A", (ID)1, true);
_ = MachineStuff.MachinePool.TryUpdateMachineID("B", (ID)2, true);
_ = MachineStuff.MachinePool.TryUpdateMachineID("C", (ID)3, true);
_ = MachineStuff.MachinePool.TryUpdateMachineID("D", (ID)4, true);
Setting.Values.MachineId = 1;

// Current machine has two monitors in top row with different heights and no monitors below them.
Common.MonitorRects = new System.Collections.Generic.List<MyRectangle>
{
new MyRectangle { Left = 0, Top = 0, Right = 1000, Bottom = 500 },
new MyRectangle { Left = 1000, Top = 0, Right = 2000, Bottom = 300 }
};

// Act: pointer at the bottom edge of the left top monitor should trigger MoveDown -> C
var result = MachineStuff.MoveToMyNeighbourIfNeeded(10, 500, Common.MachineID);

// Assert
Assert.IsFalse(result.IsEmpty);
Assert.AreEqual((ID)3, MachineStuff.newDesMachineIdEx);
}

[TestMethod]
public void MoveDown_ShouldNotTrigger_When_MonitorBelowExistsAtCursorX()
{
// Arrange: two stacked monitors so there IS a monitor below at the cursor's X
Setting.Values.PauseInstantSaving = true;
Setting.Values.MatrixOneRow = false; // 2 rows

MachineStuff.MachinePool.Initialize(new string[] { "A", "B", "C", "D" });
MachineStuff.MachineMatrix = new string[] { "A", "B", "C", "D" };
_ = MachineStuff.MachinePool.TryUpdateMachineID("A", (ID)1, true);
_ = MachineStuff.MachinePool.TryUpdateMachineID("B", (ID)2, true);
_ = MachineStuff.MachinePool.TryUpdateMachineID("C", (ID)3, true);
_ = MachineStuff.MachinePool.TryUpdateMachineID("D", (ID)4, true);
Setting.Values.MachineId = 1;

// Two monitors stacked vertically with the same X range
Common.MonitorRects = new System.Collections.Generic.List<MyRectangle>
{
new MyRectangle { Left = 0, Top = 0, Right = 1000, Bottom = 500 },
new MyRectangle { Left = 0, Top = 500, Right = 1000, Bottom = 1000 }
};

// Act: pointer at the bottom edge of the top monitor should NOT trigger MoveDown since there is a monitor below
var result = MachineStuff.MoveToMyNeighbourIfNeeded(10, 500, Common.MachineID);

// Assert
Assert.IsTrue(result.IsEmpty);
}
}
Loading