Skip to content

Commit

Permalink
#19 - configurable borders
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeclayton committed Jul 9, 2023
1 parent 275f4b9 commit 22964c1
Show file tree
Hide file tree
Showing 31 changed files with 1,376 additions and 541 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
using FancyMouse.Helpers;
using System.Drawing;
using FancyMouse.Helpers;
using FancyMouse.Models.Drawing;
using FancyMouse.Models.Layout;
using FancyMouse.Models.Screen;
using FancyMouse.Models.Settings;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static FancyMouse.NativeMethods.Core;

namespace FancyMouse.UnitTests.Helpers;

[TestClass]
public static class DrawingHelperTests
public static class LayoutHelperTests
{
/*
[TestClass]
public sealed class CalculateLayoutInfoTests
{
Expand Down Expand Up @@ -221,4 +224,132 @@ public void RunTestCases(TestCase data)
Assert.AreEqual(expected.ActivatedScreenBounds.ToRectangle(), actual.ActivatedScreenBounds.ToRectangle(), "ActivatedScreen.ToRectangle");
}
}
*/

[TestClass]
public sealed class GetPreviewLayoutTests
{
public sealed class TestCase
{
public TestCase(PreviewSettings previewSettings, List<ScreenInfo> screens, PointInfo activatedLocation, PreviewLayout expectedResult)
{
this.PreviewSettings = previewSettings;
this.Screens = screens;
this.ActivatedLocation = activatedLocation;
this.ExpectedResult = expectedResult;
}

public PreviewSettings PreviewSettings { get; set; }

public List<ScreenInfo> Screens { get; set; }

public PointInfo ActivatedLocation { get; set; }

public PreviewLayout ExpectedResult { get; set; }
}

public static IEnumerable<object[]> GetTestCases()
{
// happy path - 50% scaling
//
// +----------------+
// | |
// | 0 |
// | |
// +----------------+
var previewSettings = new PreviewSettings(
size: new(
width: 7 + 2 + 5 + 512 + 2 + 7,
height: 7 + 2 + 384 + 2 + 7),
previewStyle: new(
marginInfo: MarginInfo.Empty,
borderInfo: new(
color: SystemColors.Highlight,
all: 5,
depth: 3),
paddingInfo: new(
all: 1),
backgroundInfo: new(
color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue
)
),
screenshotStyle: new(
marginInfo: MarginInfo.Empty,
borderInfo: new(Color.Black, 5, 3),
paddingInfo: new(1),
backgroundInfo: BackgroundInfo.Empty
));
var screens = new List<ScreenInfo>
{
new(HANDLE.Null, true, new(0, 0, 1024, 768), new(0, 0, 1024, 768)),
};
var activatedLocation = new PointInfo(512, 384);
var previewLayout = new PreviewLayout(
virtualScreen: new(0, 0, 1024, 768),
screens: screens,
activatedScreen: screens[0],
formBounds: new(0, 0, 0, 0),
previewStyle: new BoxStyle(
marginInfo: MarginInfo.Empty,
borderInfo: BorderInfo.Empty,
paddingInfo: PaddingInfo.Empty,
backgroundInfo: BackgroundInfo.Empty),
previewBounds: new(
outerBounds: RectangleInfo.Empty,
marginBounds: RectangleInfo.Empty,
borderBounds: RectangleInfo.Empty,
paddingBounds: RectangleInfo.Empty,
contentBounds: RectangleInfo.Empty),
screenshotStyle: BoxStyle.Empty,
screenshotBounds: Enumerable.Empty<BoxBounds>());
yield return new object[] { new TestCase(previewSettings, screens, activatedLocation, previewLayout) };
}

[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
// note - even if values are within 0.0001M of each other they could
// still round to different values - e.g.
// (int)1279.999999999999 -> 1279
// vs
// (int)1280.000000000000 -> 1280
// so we'll compare the raw values, *and* convert to an int-based
// Rectangle to compare rounded values
var actual = LayoutHelper.GetPreviewLayout(data.PreviewSettings, data.Screens, data.ActivatedLocation);
var expected = data.ExpectedResult;
/* form bounds */
Assert.AreEqual(expected.FormBounds.X, actual.FormBounds.X, 0.00001M, "FormBounds.X");
Assert.AreEqual(expected.FormBounds.Y, actual.FormBounds.Y, 0.00001M, "FormBounds.Y");
Assert.AreEqual(expected.FormBounds.Width, actual.FormBounds.Width, 0.00001M, "FormBounds.Width");
Assert.AreEqual(expected.FormBounds.Height, actual.FormBounds.Height, 0.00001M, "FormBounds.Height");
Assert.AreEqual(expected.FormBounds.ToRectangle(), actual.FormBounds.ToRectangle(), "FormBounds.ToRectangle");
/* preview bounds */
/*
Assert.AreEqual(expected.PreviewBounds.X, actual.PreviewBounds.X, 0.00001M, "PreviewBounds.X");
Assert.AreEqual(expected.PreviewBounds.Y, actual.PreviewBounds.Y, 0.00001M, "PreviewBounds.Y");
Assert.AreEqual(expected.PreviewBounds.Width, actual.PreviewBounds.Width, 0.00001M, "PreviewBounds.Width");
Assert.AreEqual(expected.PreviewBounds.Height, actual.PreviewBounds.Height, 0.00001M, "PreviewBounds.Height");
Assert.AreEqual(expected.PreviewBounds.ToRectangle(), actual.PreviewBounds.ToRectangle(), "PreviewBounds.ToRectangle");
*/
/*
Assert.AreEqual(expected.ScreenBounds.Count, actual.ScreenBounds.Count, "ScreenBounds.Count");
for (var i = 0; i < expected.ScreenBounds.Count; i++)
{
Assert.AreEqual(expected.ScreenBounds[i].X, actual.ScreenBounds[i].X, 0.00001M, $"ScreenBounds[{i}].X");
Assert.AreEqual(expected.ScreenBounds[i].Y, actual.ScreenBounds[i].Y, 0.00001M, $"ScreenBounds[{i}].Y");
Assert.AreEqual(expected.ScreenBounds[i].Width, actual.ScreenBounds[i].Width, 0.00001M, $"ScreenBounds[{i}].Width");
Assert.AreEqual(expected.ScreenBounds[i].Height, actual.ScreenBounds[i].Height, 0.00001M, $"ScreenBounds[{i}].Height");
Assert.AreEqual(expected.ScreenBounds[i].ToRectangle(), actual.ScreenBounds[i].ToRectangle(), "ActivatedScreen.ToRectangle");
}
Assert.AreEqual(expected.ActivatedScreenBounds.X, actual.ActivatedScreenBounds.X, "ActivatedScreen.X");
Assert.AreEqual(expected.ActivatedScreenBounds.Y, actual.ActivatedScreenBounds.Y, "ActivatedScreen.Y");
Assert.AreEqual(expected.ActivatedScreenBounds.Width, actual.ActivatedScreenBounds.Width, "ActivatedScreen.Width");
Assert.AreEqual(expected.ActivatedScreenBounds.Height, actual.ActivatedScreenBounds.Height, "ActivatedScreen.Height");
Assert.AreEqual(expected.ActivatedScreenBounds.ToRectangle(), actual.ActivatedScreenBounds.ToRectangle(), "ActivatedScreen.ToRectangle");
*/
}
}
}
8 changes: 7 additions & 1 deletion src/FancyMouse.WindowsHotKeys.Tests/KeystrokeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@ public static class ParseTests
public static void ValidStringsShouldParse(string s, KeyModifiers modifier, Keys key)
{
var expected = new Keystroke(key, modifier);
var actual = Keystroke.Parse(s);
var result = Keystroke.TryParse(s, out var actual);
Assert.Multiple(() =>
{
Assert.IsTrue(result);
if (actual == null)
{
throw new InvalidOperationException();
}

Assert.AreEqual(expected.Key, actual.Key);
Assert.AreEqual(expected.Modifiers, actual.Modifiers);
});
Expand Down
9 changes: 4 additions & 5 deletions src/FancyMouse.WindowsHotKeys/HotKeyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public HotKeyManager(Keystroke hotkey)
{
this.HotKey = hotkey ?? throw new ArgumentNullException(nameof(hotkey));

// cache a window proc delegate so doesn't get garbage-collected
// cache the window proc delegate so it doesn't get garbage-collected
this.WndProc = this.WindowProc;
this.HWnd = HWND.Null;
}
Expand Down Expand Up @@ -109,10 +109,9 @@ public void Start()
lpParam: LPVOID.Null);

this.MessageLoop = new MessageLoop(
name: "FancyMouseMessageLoop",
isBackground: true);
name: "FancyMouseMessageLoop");

this.MessageLoop.Run();
this.MessageLoop.Start();

_ = Win32Wrappers.RegisterHotKey(
hWnd: this.HWnd,
Expand All @@ -126,7 +125,7 @@ public void Stop()
_ = Win32Wrappers.UnregisterHotKey(
hWnd: this.HWnd,
id: this._id);
this.MessageLoop?.Exit();
this.MessageLoop?.Stop();
}

private void OnHotKeyPressed(HotKeyEventArgs e)
Expand Down
79 changes: 48 additions & 31 deletions src/FancyMouse.WindowsHotKeys/Internal/MessageLoop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,69 +6,79 @@ namespace FancyMouse.WindowsHotKeys.Internal;

internal sealed class MessageLoop
{
public MessageLoop(string name, bool isBackground)
public MessageLoop(string name)
{
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.IsBackground = isBackground;
this.RunningSemaphore = new SemaphoreSlim(1);
this.CancellationTokenSource = new CancellationTokenSource();
}

private string Name
{
get;
}

private bool IsBackground
/// <summary>
/// Gets a semaphore that can be waited on until the message loop has stopped.
/// </summary>
private SemaphoreSlim RunningSemaphore
{
get;
}

private Thread? ManagedThread
/// <summary>
/// Gets a cancellation token that can be used to signal the internal message loop thread to stop.
/// </summary>
private CancellationTokenSource CancellationTokenSource
{
get;
set;
}

private DWORD NativeThreadId
private Thread? ManagedThread
{
get;
set;
}

private CancellationTokenSource? CancellationTokenSource
private DWORD NativeThreadId
{
get;
set;
}

public void Run()
public void Start()
{
if (this.ManagedThread is not null)
// make sure we're not already running the internal message loop
if (!this.RunningSemaphore.Wait(0))
{
throw new InvalidOperationException();
}

this.CancellationTokenSource = new CancellationTokenSource();
// reset the internal message loop cancellation token
if (!this.CancellationTokenSource.TryReset())
{
throw new InvalidOperationException();
}

// start a new internal message loop thread
this.ManagedThread = new Thread(() =>
{
this.NativeThreadId = Kernel32.GetCurrentThreadId();
this.RunInternal();
this.RunMessageLoop();
})
{
Name = this.Name,
IsBackground = this.IsBackground,
IsBackground = true,
};

this.ManagedThread.Start();
}

private void RunInternal()
private void RunMessageLoop()
{
var quitMessagePosted = false;

var lpMsg = new LPMSG(
new MSG(
hwnd: HWND.Null,
message: MESSAGE_TYPE.WM_QUIT,
message: MESSAGE_TYPE.WM_NULL,
wParam: new(0),
lParam: new(0),
time: new(0),
Expand All @@ -81,57 +91,64 @@ private void RunInternal()
// https://devblogs.microsoft.com/oldnewthing/20050406-57/?p=35963
while (true)
{
// check if the cancellation token is signalling that we should stop the message loop
if (this.CancellationTokenSource.IsCancellationRequested)
{
break;
}

var result = User32.GetMessageW(
lpMsg: lpMsg,
hWnd: HWND.Null,
wMsgFilterMin: 0,
wMsgFilterMax: 0);

if (result.Value == -1)
{
continue;
}

var msg = lpMsg.ToStructure();

if (msg.message == MESSAGE_TYPE.WM_QUIT)
{
break;
}

_ = User32.TranslateMessage(msg);
_ = User32.DispatchMessageW(msg);

if ((this.CancellationTokenSource?.IsCancellationRequested ?? false) && !quitMessagePosted)
{
User32.PostQuitMessage(0);
quitMessagePosted = true;
}
}

// clean up
this.ManagedThread = null;
this.NativeThreadId = 0;
(this.CancellationTokenSource ?? throw new InvalidOperationException())
.Dispose();

// the message loop is no longer running
this.RunningSemaphore.Release(1);
}

public void Exit()
public void Stop()
{
if (this.ManagedThread is null)
// make sure we're actually running the internal message loop
if (this.RunningSemaphore.CurrentCount != 0)
{
throw new InvalidOperationException();
}

// signal to the internal message loop that it should stop
(this.CancellationTokenSource ?? throw new InvalidOperationException())
.Cancel();

// post a null message just to nudge the message loop and pump it - it'll notice that we've
// set the cancellation token, then post a quit message to itself and exit the loop
// see https://devblogs.microsoft.com/oldnewthing/20050405-46/?p=35973
// post a null message just in case GetMessageW needs a nudge to stop blocking the
// message loop. the loop will then notice that we've set the cancellation token,
// and exit the loop...
// (see https://devblogs.microsoft.com/oldnewthing/20050405-46/?p=35973)
_ = Win32Wrappers.PostThreadMessageW(
idThread: this.NativeThreadId,
Msg: MESSAGE_TYPE.WM_NULL,
wParam: WPARAM.Null,
lParam: LPARAM.Null);

// wait for the internal message loop to actually stop
this.RunningSemaphore.Wait();
}
}
Loading

0 comments on commit 22964c1

Please sign in to comment.