diff --git a/.gitignore b/.gitignore index 59e0d3b..d501859 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,6 @@ obj releases -*.csproj.user +*.user wiki/anim \ No newline at end of file diff --git a/readme.md b/readme.md index 0920b41..5260957 100644 --- a/readme.md +++ b/readme.md @@ -13,6 +13,13 @@ I'll continue to maintain the FancyMouse repo as an incubator for new features g FancyMouse is a Windows utility for quickly moving the mouse large distances on high-res desktops. +See links for additional details: + +* [Basic Configuration](./wiki/config/basic_config.md) +* [Advancedc Configuration](./wiki/config/advanced_config.md) + +--- + ## The Problem On a modern laptop that uses an Ultra-Wide external monitor you could easily have a desktop in the region of 8000+ pixels wide, and that's a lot of ground for your mouse to cover. @@ -21,6 +28,8 @@ What tends to happen is you end up swiping the physical mouse as far as it will FancyMouse helps by letting you click a scaled-down preview of your entire desktop and "teleport" the mouse there in an instant. +--- + ## Swiping Here's an animation showing the old slow way of swiping the mouse multiple times. @@ -29,6 +38,8 @@ Imagine you're happily working on a spreadsheet on your ultra-wide external moni ![Animation of swiping a mouse multiple times to move across a large monitor setup](wiki/images/swipe.gif) +--- + ## FancyMouse And here's the same thing using FancyMouse. A hotkey or spare mouse button can be configured to activate the FancyMouse popup, and the pointer only needs to be moved a tiny amount on the preview thumbnail. A single mouse click then teleports the pointer to that location on the full-size desktop. diff --git a/src/FancyMouse.NativeMethods/Core/ATOM.cs b/src/FancyMouse.NativeMethods/Core/ATOM.cs index 5d10511..1279e4b 100644 --- a/src/FancyMouse.NativeMethods/Core/ATOM.cs +++ b/src/FancyMouse.NativeMethods/Core/ATOM.cs @@ -24,7 +24,7 @@ public ATOM(ushort value) public static implicit operator ushort(ATOM value) => value.Value; - public static implicit operator ATOM(ushort value) => new(value); + public static explicit operator ATOM(ushort value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/GUID.cs b/src/FancyMouse.NativeMethods/Core/GUID.cs new file mode 100644 index 0000000..18a72aa --- /dev/null +++ b/src/FancyMouse.NativeMethods/Core/GUID.cs @@ -0,0 +1,53 @@ +// 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. + +namespace FancyMouse.NativeMethods; + +internal static partial class Core +{ + /// + /// A 32-bit signed integer.The range is -2147483648 through 2147483647 decimal. + /// This type is declared in WinNT.h as follows: + /// typedef long LONG; + /// + /// + /// See https://learn.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid + /// + internal readonly struct GUID + { + public readonly ulong Data1; + public readonly short Data2; + public readonly short Data3; + public readonly char[] Data4; + + public GUID(Guid value) + { + this.Data1 = 0; + this.Data2 = 0; + this.Data3 = 0; + this.Data4 = new[] { (char)0, (char)0, (char)0, (char)0, (char)0, (char)0, (char)0, (char)0 }; + } + + public GUID(ulong data1, short data2, short data3, char[] data4) + { + this.Data1 = data1; + this.Data2 = data2; + this.Data3 = data3; + this.Data4 = data4; + } + + public static implicit operator Guid(GUID value) => Guid.NewGuid(); + + public static implicit operator GUID(Guid value) => new( + data1: 0, + data2: 0, + data3: 0, + data4: new[] { (char)0, (char)0, (char)0, (char)0, (char)0, (char)0, (char)0, (char)0 }); + + public override string ToString() + { + return $"{this.GetType().Name}"; + } + } +} diff --git a/src/FancyMouse.NativeMethods/Core/HANDLE.cs b/src/FancyMouse.NativeMethods/Core/HANDLE.cs index 446a853..3eedc64 100644 --- a/src/FancyMouse.NativeMethods/Core/HANDLE.cs +++ b/src/FancyMouse.NativeMethods/Core/HANDLE.cs @@ -25,7 +25,7 @@ public HANDLE(IntPtr value) public static implicit operator IntPtr(HANDLE value) => value.Value; - public static implicit operator HANDLE(IntPtr value) => new(value); + public static explicit operator HANDLE(IntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/HBITMAP.cs b/src/FancyMouse.NativeMethods/Core/HBITMAP.cs new file mode 100644 index 0000000..2de491a --- /dev/null +++ b/src/FancyMouse.NativeMethods/Core/HBITMAP.cs @@ -0,0 +1,39 @@ +namespace FancyMouse.NativeMethods; + +internal static partial class Core +{ + /// + /// A handle to a bitmap. + /// This type is declared in WinDef.h as follows: + /// typedef HANDLE HBITMAP; + /// + /// + /// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types + /// + internal readonly struct HBITMAP + { + public static readonly HBITMAP Null = new(IntPtr.Zero); + + public readonly IntPtr Value; + + public HBITMAP(IntPtr value) + { + this.Value = value; + } + + public bool IsNull => this.Value == HBITMAP.Null.Value; + + public static implicit operator IntPtr(HBITMAP value) => value.Value; + + public static explicit operator HBITMAP(IntPtr value) => new(value); + + public static explicit operator HBITMAP(HGDIOBJ value) => new(value.Value); + + public static implicit operator HGDIOBJ(HBITMAP value) => new(value.Value); + + public override string ToString() + { + return $"{this.GetType().Name}({this.Value})"; + } + } +} diff --git a/src/FancyMouse.NativeMethods/Core/HBRUSH.cs b/src/FancyMouse.NativeMethods/Core/HBRUSH.cs index b0a6ff5..f37b832 100644 --- a/src/FancyMouse.NativeMethods/Core/HBRUSH.cs +++ b/src/FancyMouse.NativeMethods/Core/HBRUSH.cs @@ -25,7 +25,7 @@ public HBRUSH(IntPtr value) public static implicit operator IntPtr(HBRUSH value) => value.Value; - public static implicit operator HBRUSH(IntPtr value) => new(value); + public static explicit operator HBRUSH(IntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/HDC.cs b/src/FancyMouse.NativeMethods/Core/HDC.cs index 9b1c83f..7dc778a 100644 --- a/src/FancyMouse.NativeMethods/Core/HDC.cs +++ b/src/FancyMouse.NativeMethods/Core/HDC.cs @@ -23,6 +23,10 @@ public HDC(IntPtr value) public bool IsNull => this.Value == HDC.Null.Value; + public static implicit operator IntPtr(HDC value) => value.Value; + + public static explicit operator HDC(IntPtr value) => new(value); + public override string ToString() { return $"{this.GetType().Name}({this.Value})"; diff --git a/src/FancyMouse.NativeMethods/Core/HDESK.cs b/src/FancyMouse.NativeMethods/Core/HDESK.cs index 05e6056..9341ef5 100644 --- a/src/FancyMouse.NativeMethods/Core/HDESK.cs +++ b/src/FancyMouse.NativeMethods/Core/HDESK.cs @@ -25,11 +25,11 @@ public HDESK(IntPtr value) public static implicit operator IntPtr(HDESK value) => value.Value; - public static implicit operator HDESK(IntPtr value) => new(value); + public static explicit operator HDESK(IntPtr value) => new(value); public static implicit operator HANDLE(HDESK value) => new(value.Value); - public static implicit operator HDESK(HANDLE value) => new(value.Value); + public static explicit operator HDESK(HANDLE value) => new(value.Value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/HGDIOBJ.cs b/src/FancyMouse.NativeMethods/Core/HGDIOBJ.cs new file mode 100644 index 0000000..9892e6a --- /dev/null +++ b/src/FancyMouse.NativeMethods/Core/HGDIOBJ.cs @@ -0,0 +1,35 @@ +namespace FancyMouse.NativeMethods; + +internal static partial class Core +{ + /// + /// A handle to a GDI object. + /// This type is declared in WinDef.h as follows: + /// typedef HANDLE HGDIOBJ; + /// + /// + /// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types + /// + internal readonly struct HGDIOBJ + { + public static readonly HGDIOBJ Null = new(IntPtr.Zero); + + public readonly IntPtr Value; + + public HGDIOBJ(IntPtr value) + { + this.Value = value; + } + + public bool IsNull => this.Value == HGDIOBJ.Null.Value; + + public static implicit operator IntPtr(HGDIOBJ value) => value.Value; + + public static explicit operator HGDIOBJ(IntPtr value) => new(value); + + public override string ToString() + { + return $"{this.GetType().Name}({this.Value})"; + } + } +} diff --git a/src/FancyMouse.NativeMethods/Core/HINSTANCE.cs b/src/FancyMouse.NativeMethods/Core/HINSTANCE.cs index b7e2154..8c762c9 100644 --- a/src/FancyMouse.NativeMethods/Core/HINSTANCE.cs +++ b/src/FancyMouse.NativeMethods/Core/HINSTANCE.cs @@ -26,7 +26,7 @@ public HINSTANCE(IntPtr value) public static implicit operator IntPtr(HINSTANCE value) => value.Value; - public static implicit operator HINSTANCE(IntPtr value) => new(value); + public static explicit operator HINSTANCE(IntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/HMODULE.cs b/src/FancyMouse.NativeMethods/Core/HMODULE.cs index 2c4b67c..6d5bfaa 100644 --- a/src/FancyMouse.NativeMethods/Core/HMODULE.cs +++ b/src/FancyMouse.NativeMethods/Core/HMODULE.cs @@ -26,7 +26,7 @@ public HMODULE(IntPtr value) public static implicit operator IntPtr(HMODULE value) => value.Value; - public static implicit operator HMODULE(IntPtr value) => new(value); + public static explicit operator HMODULE(IntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/HMONITOR.cs b/src/FancyMouse.NativeMethods/Core/HMONITOR.cs index cd528c4..b22093b 100644 --- a/src/FancyMouse.NativeMethods/Core/HMONITOR.cs +++ b/src/FancyMouse.NativeMethods/Core/HMONITOR.cs @@ -25,15 +25,15 @@ public HMONITOR(IntPtr value) public static implicit operator int(HMONITOR value) => value.Value.ToInt32(); - public static implicit operator HMONITOR(int value) => new(value); + public static explicit operator HMONITOR(int value) => new(value); public static implicit operator IntPtr(HMONITOR value) => value.Value; - public static implicit operator HMONITOR(IntPtr value) => new(value); + public static explicit operator HMONITOR(IntPtr value) => new(value); public static implicit operator HANDLE(HMONITOR value) => new(value.Value); - public static implicit operator HMONITOR(HANDLE value) => new(value.Value); + public static explicit operator HMONITOR(HANDLE value) => new(value.Value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/HWINSTA.cs b/src/FancyMouse.NativeMethods/Core/HWINSTA.cs index 20217cc..8407fb6 100644 --- a/src/FancyMouse.NativeMethods/Core/HWINSTA.cs +++ b/src/FancyMouse.NativeMethods/Core/HWINSTA.cs @@ -25,7 +25,7 @@ public HWINSTA(IntPtr value) public static implicit operator IntPtr(HWINSTA value) => value.Value; - public static implicit operator HWINSTA(IntPtr value) => new(value); + public static explicit operator HWINSTA(IntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/LPARAM.cs b/src/FancyMouse.NativeMethods/Core/LPARAM.cs index a0548b0..b629134 100644 --- a/src/FancyMouse.NativeMethods/Core/LPARAM.cs +++ b/src/FancyMouse.NativeMethods/Core/LPARAM.cs @@ -21,9 +21,11 @@ public LPARAM(IntPtr value) this.Value = value; } + public bool IsNull => this.Value == LPARAM.Null.Value; + public static implicit operator IntPtr(LPARAM value) => value.Value; - public static implicit operator LPARAM(IntPtr value) => new(value); + public static explicit operator LPARAM(IntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/LPCRECT.cs b/src/FancyMouse.NativeMethods/Core/LPCRECT.cs index 151ed7d..a43e77e 100644 --- a/src/FancyMouse.NativeMethods/Core/LPCRECT.cs +++ b/src/FancyMouse.NativeMethods/Core/LPCRECT.cs @@ -20,6 +20,8 @@ public LPCRECT(CRECT value) this.Value = LPCRECT.ToPtr(value); } + public bool IsNull => this.Value == LPCRECT.Null.Value; + private static IntPtr ToPtr(CRECT value) { var ptr = Marshal.AllocHGlobal(CRECT.Size); @@ -34,7 +36,7 @@ public void Free() public static implicit operator IntPtr(LPCRECT value) => value.Value; - public static implicit operator LPCRECT(IntPtr value) => new(value); + public static explicit operator LPCRECT(IntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/LPCWSTR.cs b/src/FancyMouse.NativeMethods/Core/LPCWSTR.cs index 6e4ea5c..c8814fa 100644 --- a/src/FancyMouse.NativeMethods/Core/LPCWSTR.cs +++ b/src/FancyMouse.NativeMethods/Core/LPCWSTR.cs @@ -32,7 +32,7 @@ public LPCWSTR(string value) public static implicit operator IntPtr(LPCWSTR value) => value.Value; - public static implicit operator LPCWSTR(IntPtr value) => new(value); + public static explicit operator LPCWSTR(IntPtr value) => new(value); public static implicit operator string?(LPCWSTR value) => Marshal.PtrToStringUni(value.Value); diff --git a/src/FancyMouse.NativeMethods/Core/LPDWORD.cs b/src/FancyMouse.NativeMethods/Core/LPDWORD.cs index f18de27..278c4c9 100644 --- a/src/FancyMouse.NativeMethods/Core/LPDWORD.cs +++ b/src/FancyMouse.NativeMethods/Core/LPDWORD.cs @@ -28,6 +28,8 @@ public LPDWORD(DWORD value) this.Value = LPDWORD.ToPtr(value); } + public bool IsNull => this.Value == LPDWORD.Null.Value; + private static IntPtr ToPtr(DWORD value) { var ptr = Marshal.AllocHGlobal(DWORD.Size); @@ -42,7 +44,7 @@ public void Free() public static implicit operator IntPtr(LPDWORD value) => value.Value; - public static implicit operator LPDWORD(IntPtr value) => new(value); + public static explicit operator LPDWORD(IntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/LPPOINT.cs b/src/FancyMouse.NativeMethods/Core/LPPOINT.cs index 407e05a..5e829ae 100644 --- a/src/FancyMouse.NativeMethods/Core/LPPOINT.cs +++ b/src/FancyMouse.NativeMethods/Core/LPPOINT.cs @@ -20,6 +20,8 @@ public LPPOINT(POINT value) this.Value = LPPOINT.ToPtr(value); } + public bool IsNull => this.Value == LPPOINT.Null.Value; + private static IntPtr ToPtr(POINT value) { var ptr = Marshal.AllocHGlobal(POINT.Size); @@ -39,7 +41,7 @@ public void Free() public static implicit operator IntPtr(LPPOINT value) => value.Value; - public static implicit operator LPPOINT(IntPtr value) => new(value); + public static explicit operator LPPOINT(IntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/LPRECT.cs b/src/FancyMouse.NativeMethods/Core/LPRECT.cs index a64d265..ac59c66 100644 --- a/src/FancyMouse.NativeMethods/Core/LPRECT.cs +++ b/src/FancyMouse.NativeMethods/Core/LPRECT.cs @@ -20,6 +20,8 @@ public LPRECT(RECT value) this.Value = LPRECT.ToPtr(value); } + public bool IsNull => this.Value == LPRECT.Null.Value; + private static IntPtr ToPtr(RECT value) { var ptr = Marshal.AllocHGlobal(RECT.Size); @@ -34,7 +36,7 @@ public void Free() public static implicit operator IntPtr(LPRECT value) => value.Value; - public static implicit operator LPRECT(IntPtr value) => new(value); + public static explicit operator LPRECT(IntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/LPTSTR.cs b/src/FancyMouse.NativeMethods/Core/LPTSTR.cs index e0cccb6..3d947a3 100644 --- a/src/FancyMouse.NativeMethods/Core/LPTSTR.cs +++ b/src/FancyMouse.NativeMethods/Core/LPTSTR.cs @@ -33,9 +33,11 @@ public LPTSTR(string value) this.Value = Marshal.StringToHGlobalAuto(value); } + public bool IsNull => this.Value == LPTSTR.Null.Value; + public static implicit operator IntPtr(LPTSTR value) => value.Value; - public static implicit operator LPTSTR(IntPtr value) => new(value); + public static explicit operator LPTSTR(IntPtr value) => new(value); public static implicit operator string?(LPTSTR value) => Marshal.PtrToStringUni(value.Value); diff --git a/src/FancyMouse.NativeMethods/Core/LPVOID.cs b/src/FancyMouse.NativeMethods/Core/LPVOID.cs index c433123..b3f9b04 100644 --- a/src/FancyMouse.NativeMethods/Core/LPVOID.cs +++ b/src/FancyMouse.NativeMethods/Core/LPVOID.cs @@ -25,7 +25,7 @@ public LPVOID(IntPtr value) public static implicit operator IntPtr(LPVOID value) => value.Value; - public static implicit operator LPVOID(IntPtr value) => new(value); + public static explicit operator LPVOID(IntPtr value) => new(value); public static LPVOID Allocate(int length) { diff --git a/src/FancyMouse.NativeMethods/Core/LRESULT.cs b/src/FancyMouse.NativeMethods/Core/LRESULT.cs index 1e75957..60a1a4b 100644 --- a/src/FancyMouse.NativeMethods/Core/LRESULT.cs +++ b/src/FancyMouse.NativeMethods/Core/LRESULT.cs @@ -23,7 +23,7 @@ public LRESULT(IntPtr value) public static implicit operator IntPtr(LRESULT value) => value.Value; - public static implicit operator LRESULT(IntPtr value) => new(value); + public static explicit operator LRESULT(IntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/PCWSTR.cs b/src/FancyMouse.NativeMethods/Core/PCWSTR.cs index 2451283..cdb4665 100644 --- a/src/FancyMouse.NativeMethods/Core/PCWSTR.cs +++ b/src/FancyMouse.NativeMethods/Core/PCWSTR.cs @@ -30,7 +30,7 @@ public PCWSTR(string value) public static implicit operator IntPtr(PCWSTR value) => value.Value; - public static implicit operator PCWSTR(IntPtr value) => new(value); + public static explicit operator PCWSTR(IntPtr value) => new(value); public static implicit operator string?(PCWSTR value) => Marshal.PtrToStringUni(value.Value); diff --git a/src/FancyMouse.NativeMethods/Core/PVOID.cs b/src/FancyMouse.NativeMethods/Core/PVOID.cs index cbdd157..3595201 100644 --- a/src/FancyMouse.NativeMethods/Core/PVOID.cs +++ b/src/FancyMouse.NativeMethods/Core/PVOID.cs @@ -25,7 +25,7 @@ public PVOID(IntPtr value) public static implicit operator IntPtr(PVOID value) => value.Value; - public static implicit operator PVOID(IntPtr value) => new(value); + public static explicit operator PVOID(IntPtr value) => new(value); public static PVOID Allocate(int length) { diff --git a/src/FancyMouse.NativeMethods/Core/ULONG_PTR.cs b/src/FancyMouse.NativeMethods/Core/ULONG_PTR.cs index 7413a87..157526a 100644 --- a/src/FancyMouse.NativeMethods/Core/ULONG_PTR.cs +++ b/src/FancyMouse.NativeMethods/Core/ULONG_PTR.cs @@ -28,7 +28,7 @@ public ULONG_PTR(UIntPtr value) public static implicit operator UIntPtr(ULONG_PTR value) => value.Value; - public static implicit operator ULONG_PTR(UIntPtr value) => new(value); + public static explicit operator ULONG_PTR(UIntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/Core/WCHAR.cs b/src/FancyMouse.NativeMethods/Core/WCHAR.cs new file mode 100644 index 0000000..cfd4013 --- /dev/null +++ b/src/FancyMouse.NativeMethods/Core/WCHAR.cs @@ -0,0 +1,31 @@ +namespace FancyMouse.NativeMethods; + +internal static partial class Core +{ + /// + /// A 16-bit Unicode character.For more information, see Character Sets Used By Fonts. + /// This type is declared in WinNT.h as follows: + /// typedef wchar_t WCHAR; + /// + /// + /// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types + /// + internal readonly struct WCHAR + { + public readonly char Value; + + public WCHAR(char value) + { + this.Value = value; + } + + public static implicit operator char(WCHAR value) => value.Value; + + public static implicit operator WCHAR(char value) => new(value); + + public override string ToString() + { + return $"{this.GetType().Name}({this.Value})"; + } + } +} diff --git a/src/FancyMouse.NativeMethods/Core/WPARAM.cs b/src/FancyMouse.NativeMethods/Core/WPARAM.cs index 7b49f2a..0b23c13 100644 --- a/src/FancyMouse.NativeMethods/Core/WPARAM.cs +++ b/src/FancyMouse.NativeMethods/Core/WPARAM.cs @@ -23,7 +23,7 @@ public WPARAM(UIntPtr value) public static implicit operator UIntPtr(WPARAM value) => value.Value; - public static implicit operator WPARAM(UIntPtr value) => new(value); + public static explicit operator WPARAM(UIntPtr value) => new(value); public override string ToString() { diff --git a/src/FancyMouse.NativeMethods/User32/UI/WindowsAndMessaging/User32.GetMessage.cs b/src/FancyMouse.NativeMethods/User32/UI/WindowsAndMessaging/User32.GetMessageW.cs similarity index 100% rename from src/FancyMouse.NativeMethods/User32/UI/WindowsAndMessaging/User32.GetMessage.cs rename to src/FancyMouse.NativeMethods/User32/UI/WindowsAndMessaging/User32.GetMessageW.cs diff --git a/src/FancyMouse.UnitTests/Helpers/DrawingHelperTests.cs b/src/FancyMouse.UnitTests/Helpers/DrawingHelperTests.cs index 1aa0f23..8ae88eb 100644 --- a/src/FancyMouse.UnitTests/Helpers/DrawingHelperTests.cs +++ b/src/FancyMouse.UnitTests/Helpers/DrawingHelperTests.cs @@ -1,9 +1,11 @@ -using FancyMouse.Helpers; +using System.Drawing; +using System.Drawing.Imaging; +using FancyMouse.Helpers; using FancyMouse.Models.Drawing; -using FancyMouse.Models.Layout; -using FancyMouse.Models.Screen; +using FancyMouse.Models.Settings; +using FancyMouse.Models.Styles; +using FancyMouse.NativeMethods; using Microsoft.VisualStudio.TestTools.UnitTesting; -using static FancyMouse.NativeMethods.Core; namespace FancyMouse.UnitTests.Helpers; @@ -11,214 +13,106 @@ namespace FancyMouse.UnitTests.Helpers; public static class DrawingHelperTests { [TestClass] - public sealed class CalculateLayoutInfoTests + public sealed class GetPreviewLayoutTests { - public sealed class TestCase + private static Bitmap DrawDesktopImage(List<(RectangleInfo Bounds, Color Color)> screens) { - public TestCase(LayoutConfig layoutConfig, LayoutInfo expectedResult) + // created a "desktop" bitmap with the given screen areas drawn the specified colors. + var desktopBounds = LayoutHelper.GetCombinedScreenBounds( + screens.Select(s => s.Bounds).ToList()); + + // we can only create bitmaps with non-negative coordinates + if (desktopBounds.X < 0 || desktopBounds.Y < 0) { - this.LayoutConfig = layoutConfig; - this.ExpectedResult = expectedResult; + throw new InvalidOperationException(); } - public LayoutConfig LayoutConfig { get; set; } + // create the bitmap + var bitmap = new Bitmap((int)desktopBounds.Width, (int)desktopBounds.Height, PixelFormat.Format32bppArgb); + + // draw the rectangles + using var graphics = Graphics.FromImage(bitmap); + foreach (var screen in screens) + { + var screenBounds = screen.Bounds.ToRectangle(); + /* + using var pen = new Pen(screen.Color); + graphics.DrawRectangle( + pen, screenBounds.X, screenBounds.Y, screenBounds.Width - 1, screenBounds.Height - 1); + */ + using var brush = new SolidBrush(screen.Color); + graphics.FillRectangle(brush, screenBounds); + } - public LayoutInfo ExpectedResult { get; set; } + return bitmap; } - public static IEnumerable GetTestCases() + public sealed class TestCase { - // happy path - check the preview form is shown - // at the correct size and position on a single screen - // - // +----------------+ - // | | - // | 0 | - // | | - // +----------------+ - var layoutConfig = new LayoutConfig( - virtualScreenBounds: new(0, 0, 5120, 1440), - screens: new List - { - new(HMONITOR.Null, false, new(0, 0, 5120, 1440), new(0, 0, 5120, 1440)), - }, - activatedLocation: new(5120M / 2, 1440M / 2), - activatedScreenIndex: 0, - activatedScreenNumber: 1, - maximumFormSize: new(1600, 1200), - formPadding: new(5, 5, 5, 5), - previewPadding: new(0, 0, 0, 0)); - var layoutInfo = new LayoutInfo( - layoutConfig: layoutConfig, - formBounds: new(1760, 491.40625M, 1600, 457.1875M), - previewBounds: new(0, 0, 1590, 447.1875M), - screenBounds: new List - { - new(0, 0, 1590, 447.1875M), - }, - activatedScreenBounds: new(0, 0, 5120, 1440)); - yield return new object[] { new TestCase(layoutConfig, layoutInfo) }; + public TestCase(PreviewStyle previewStyle, List<(RectangleInfo Bounds, Color Color)> screens, PointInfo activatedLocation) + { + this.PreviewStyle = previewStyle; + this.Screens = screens; + this.ActivatedLocation = activatedLocation; + } - // primary monitor not topmost / leftmost - if there are screens - // that are further left or higher than the primary monitor - // they'll have negative coordinates which has caused some - // issues with calculations in the past. this test will make - // sure we handle negative coordinates gracefully - // - // +-------+ - // | 0 +----------------+ - // +-------+ | - // | 1 | - // | | - // +----------------+ - layoutConfig = new LayoutConfig( - virtualScreenBounds: new(-1920, -472, 7040, 1912), - screens: new List - { - new(HMONITOR.Null, false, new(-1920, -472, 1920, 1080), new(-1920, -472, 1920, 1080)), - new(HMONITOR.Null, false, new(0, 0, 5120, 1440), new(0, 0, 5120, 1440)), - }, - activatedLocation: new(-960, -236), - activatedScreenIndex: 0, - activatedScreenNumber: 1, - maximumFormSize: new(1600, 1200), - formPadding: new(5, 5, 5, 5), - previewPadding: new(0, 0, 0, 0)); - layoutInfo = new LayoutInfo( - layoutConfig: layoutConfig, - formBounds: new( - -1760, - -456.91477M, // -236 - (((decimal)(1600-10) / 7040 * 1912) + 10) / 2 - 1600, - 441.829545M // ((decimal)(1600-10) / 7040 * 1912) + 10 - ), - previewBounds: new(0, 0, 1590, 431.829545M), - screenBounds: new List - { - new(0, 0, 433.63636M, 243.92045M), - new(433.63636M, 106.602270M, 1156.36363M, 325.22727M), - }, - activatedScreenBounds: new(-1920, -472, 1920, 1080)); - yield return new object[] { new TestCase(layoutConfig, layoutInfo) }; + public PreviewStyle PreviewStyle { get; } - // check we handle rounding errors in scaling the preview form - // that might make the form *larger* than the current screen - - // e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768 - // with a 5px form padding border: - // - // ((decimal)1014 / 7168) * 7168 = 1014.0000000000000000000000002 - // - // +----------------+ - // | | - // | 1 +-------+ - // | | 0 | - // +----------------+-------+ - layoutConfig = new LayoutConfig( - virtualScreenBounds: new(0, 0, 7168, 1440), - screens: new List - { - new(HMONITOR.Null, false, new(6144, 0, 1024, 768), new(6144, 0, 1024, 768)), - new(HMONITOR.Null, false, new(0, 0, 6144, 1440), new(0, 0, 6144, 1440)), - }, - activatedLocation: new(6656, 384), - activatedScreenIndex: 0, - activatedScreenNumber: 1, - maximumFormSize: new(1600, 1200), - formPadding: new(5, 5, 5, 5), - previewPadding: new(0, 0, 0, 0)); - layoutInfo = new LayoutInfo( - layoutConfig: layoutConfig, - formBounds: new(6144, 277.14732M, 1024, 213.70535M), - previewBounds: new(0, 0, 1014, 203.70535M), - screenBounds: new List - { - new(869.14285M, 0, 144.85714M, 108.642857M), - new(0, 0, 869.142857M, 203.705357M), - }, - activatedScreenBounds: new(6144, 0, 1024, 768)); - yield return new object[] { new TestCase(layoutConfig, layoutInfo) }; + public List<(RectangleInfo Bounds, Color Color)> Screens { get; } - // check we handle rounding errors in scaling the preview form - // that might make the form a pixel *smaller* than the current screen - - // e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768 - // with a 5px form padding border: - // - // ((decimal)1280 / 7424) * 7424 = 1279.9999999999999999999999999 - // - // +----------------+ - // | | - // | 1 +-------+ - // | | 0 | - // +----------------+-------+ - layoutConfig = new LayoutConfig( - virtualScreenBounds: new(0, 0, 7424, 1440), - screens: new List - { - new(HMONITOR.Null, false, new(6144, 0, 1280, 768), new(6144, 0, 1280, 768)), - new(HMONITOR.Null, false, new(0, 0, 6144, 1440), new(0, 0, 6144, 1440)), - }, - activatedLocation: new(6784, 384), - activatedScreenIndex: 0, - activatedScreenNumber: 1, - maximumFormSize: new(1600, 1200), - formPadding: new(5, 5, 5, 5), - previewPadding: new(0, 0, 0, 0)); - layoutInfo = new LayoutInfo( - layoutConfig: layoutConfig, - formBounds: new( - 6144, - 255.83189M, // (768 - (((decimal)(1280-10) / 7424 * 1440) + 10)) / 2 - 1280, - 256.33620M // ((decimal)(1280 - 10) / 7424 * 1440) + 10 - ), - previewBounds: new(0, 0, 1270, 246.33620M), - screenBounds: new List - { - new(1051.03448M, 0, 218.96551M, 131.37931M), - new(0, 0M, 1051.03448M, 246.33620M), - }, - activatedScreenBounds: new(6144, 0, 1280, 768)); - yield return new object[] { new TestCase(layoutConfig, layoutInfo) }; + public PointInfo ActivatedLocation { get; } + } + + public static IEnumerable GetTestCases() + { + yield return new object[] + { + new TestCase( + previewStyle: AppSettings.DefaultSettings.PreviewStyle, + screens: new() + { + new() + { + Bounds = new RectangleInfo(0, 0, 500, 500), + Color = Color.Red, + }, + new() + { + Bounds = new RectangleInfo(500, 0, 500, 500), + Color = Color.Orange, + }, + new() + { + Bounds = new RectangleInfo(500, 500, 500, 500), + Color = Color.Yellow, + }, + new() + { + Bounds = new RectangleInfo(0, 500, 500, 500), + Color = Color.Green, + }, + }, + new(x: 50, y: 50)), + }; } [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.CalculateLayoutInfo(data.LayoutConfig); - var expected = data.ExpectedResult; - 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"); - 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"); - } + // generate the fake desktop image + using var desktop = GetPreviewLayoutTests.DrawDesktopImage(data.Screens); + + // var desktop = Bitmap.FromFile(".\\desktop.png"); + using var graphics = Graphics.FromImage(desktop); + var desktopHdc = new Core.HDC(graphics.GetHdc()); - 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"); + // draw the preview image + var previewLayout = LayoutHelper.GetPreviewLayout( + previewStyle: data.PreviewStyle, + screens: data.Screens.Select(s => s.Bounds).ToList(), + activatedLocation: data.ActivatedLocation); + using var actual = DrawingHelper.RenderPreview(previewLayout, desktopHdc); } } } diff --git a/src/FancyMouse.UnitTests/Helpers/LayoutHelperTests.cs b/src/FancyMouse.UnitTests/Helpers/LayoutHelperTests.cs new file mode 100644 index 0000000..73c38b5 --- /dev/null +++ b/src/FancyMouse.UnitTests/Helpers/LayoutHelperTests.cs @@ -0,0 +1,445 @@ +using System.Drawing; +using System.Text.Json; +using FancyMouse.Helpers; +using FancyMouse.Models.Drawing; +using FancyMouse.Models.Layout; +using FancyMouse.Models.Styles; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace FancyMouse.UnitTests.Helpers; + +[TestClass] +public static class LayoutHelperTests +{ + /* + [TestClass] + public sealed class OldLayoutTests + { + + public static IEnumerable GetTestCases() + { + // check we handle rounding errors in scaling the preview form + // that might make the form *larger* than the current screen - + // e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768 + // with a 5px form padding border: + // + // ((decimal)1014 / 7168) * 7168 = 1014.0000000000000000000000002 + // + // +----------------+ + // | | + // | 1 +-------+ + // | | 0 | + // +----------------+-------+ + layoutConfig = new LayoutConfig( + virtualScreenBounds: new(0, 0, 7168, 1440), + screens: new List + { + new(HMONITOR.Null, false, new(6144, 0, 1024, 768), new(6144, 0, 1024, 768)), + new(HMONITOR.Null, false, new(0, 0, 6144, 1440), new(0, 0, 6144, 1440)), + }, + activatedLocation: new(6656, 384), + activatedScreenIndex: 0, + activatedScreenNumber: 1, + maximumFormSize: new(1600, 1200), + formPadding: new(5, 5, 5, 5), + previewPadding: new(0, 0, 0, 0)); + layoutInfo = new LayoutInfo( + layoutConfig: layoutConfig, + formBounds: new(6144, 277.14732M, 1024, 213.70535M), + previewBounds: new(0, 0, 1014, 203.70535M), + screenBounds: new List + { + new(869.14285M, 0, 144.85714M, 108.642857M), + new(0, 0, 869.142857M, 203.705357M), + }, + activatedScreenBounds: new(6144, 0, 1024, 768)); + yield return new object[] { new TestCase(layoutConfig, layoutInfo) }; + + // check we handle rounding errors in scaling the preview form + // that might make the form a pixel *smaller* than the current screen - + // e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768 + // with a 5px form padding border: + // + // ((decimal)1280 / 7424) * 7424 = 1279.9999999999999999999999999 + // + // +----------------+ + // | | + // | 1 +-------+ + // | | 0 | + // +----------------+-------+ + layoutConfig = new LayoutConfig( + virtualScreenBounds: new(0, 0, 7424, 1440), + screens: new List + { + new(HMONITOR.Null, false, new(6144, 0, 1280, 768), new(6144, 0, 1280, 768)), + new(HMONITOR.Null, false, new(0, 0, 6144, 1440), new(0, 0, 6144, 1440)), + }, + activatedLocation: new(6784, 384), + activatedScreenIndex: 0, + activatedScreenNumber: 1, + maximumFormSize: new(1600, 1200), + formPadding: new(5, 5, 5, 5), + previewPadding: new(0, 0, 0, 0)); + layoutInfo = new LayoutInfo( + layoutConfig: layoutConfig, + formBounds: new( + 6144, + 255.83189M, // (768 - (((decimal)(1280-10) / 7424 * 1440) + 10)) / 2 + 1280, + 256.33620M // ((decimal)(1280 - 10) / 7424 * 1440) + 10 + ), + previewBounds: new(0, 0, 1270, 246.33620M), + screenBounds: new List + { + new(1051.03448M, 0, 218.96551M, 131.37931M), + new(0, 0M, 1051.03448M, 246.33620M), + }, + activatedScreenBounds: new(6144, 0, 1280, 768)); + yield return new object[] { new TestCase(layoutConfig, layoutInfo) }; + } + } + */ + + [TestClass] + public sealed class GetPreviewLayoutTests + { + public sealed class TestCase + { + public TestCase(PreviewStyle previewStyle, List screens, PointInfo activatedLocation, PreviewLayout expectedResult) + { + this.PreviewStyle = previewStyle; + this.Screens = screens; + this.ActivatedLocation = activatedLocation; + this.ExpectedResult = expectedResult; + } + + public PreviewStyle PreviewStyle { get; set; } + + public List Screens { get; set; } + + public PointInfo ActivatedLocation { get; set; } + + public PreviewLayout ExpectedResult { get; set; } + } + + public static IEnumerable GetTestCases() + { + // happy path - 50% scaling, *has* preview borders but *no* screenshot borders + // + // +----------------+ + // | | + // | 0 | + // | | + // +----------------+ + var previewStyle = new PreviewStyle( + canvasSize: new( + width: 524, + height: 396 + ), + canvasStyle: new( + marginStyle: MarginStyle.Empty, + borderStyle: new( + color: SystemColors.Highlight, + all: 5, + depth: 3), + paddingStyle: new( + all: 1), + backgroundStyle: new( + color1: Color.FromArgb(13, 87, 210), // light blue + color2: Color.FromArgb(3, 68, 192) // darker blue + ) + ), + screenshotStyle: BoxStyle.Empty); + var screens = new List + { + new(0, 0, 1024, 768), + }; + var activatedLocation = new PointInfo(512, 384); + var previewLayout = new PreviewLayout( + virtualScreen: new(0, 0, 1024, 768), + screens: screens, + activatedScreenIndex: 0, + formBounds: new(250, 186, 524, 396), + previewStyle: previewStyle, + previewBounds: new( + outerBounds: new(0, 0, 524, 396), + marginBounds: new(0, 0, 524, 396), + borderBounds: new(0, 0, 524, 396), + paddingBounds: new(5, 5, 514, 386), + contentBounds: new(6, 6, 512, 384) + ), + screenshotBounds: new() + { + new( + outerBounds: new(6, 6, 512, 384), + marginBounds: new(6, 6, 512, 384), + borderBounds: new(6, 6, 512, 384), + paddingBounds: new(6, 6, 512, 384), + contentBounds: new(6, 6, 512, 384) + ), + }); + yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, previewLayout) }; + + // happy path - 50% scaling, *no* preview borders but *has* screenshot borders + // + // +----------------+ + // | | + // | 0 | + // | | + // +----------------+ + previewStyle = new PreviewStyle( + canvasSize: new( + width: 512, + height: 384 + ), + canvasStyle: BoxStyle.Empty, + screenshotStyle: new( + marginStyle: new( + all: 1), + borderStyle: new( + color: SystemColors.Highlight, + all: 5, + depth: 3), + paddingStyle: PaddingStyle.Empty, + backgroundStyle: new( + color1: Color.FromArgb(13, 87, 210), // light blue + color2: Color.FromArgb(3, 68, 192) // darker blue + ) + )); + screens = new List + { + new(0, 0, 1024, 768), + }; + activatedLocation = new PointInfo(512, 384); + previewLayout = new PreviewLayout( + virtualScreen: new(0, 0, 1024, 768), + screens: screens, + activatedScreenIndex: 0, + formBounds: new(256, 192, 512, 384), + previewStyle: previewStyle, + previewBounds: new( + outerBounds: new(0, 0, 512, 384), + marginBounds: new(0, 0, 512, 384), + borderBounds: new(0, 0, 512, 384), + paddingBounds: new(0, 0, 512, 384), + contentBounds: new(0, 0, 512, 384) + ), + screenshotBounds: new() + { + new( + outerBounds: new(0, 0, 512, 384), + marginBounds: new(0, 0, 512, 384), + borderBounds: new(1, 1, 510, 382), + paddingBounds: new(6, 6, 500, 372), + contentBounds: new(6, 6, 500, 372) + ), + }); + yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, previewLayout) }; + + // primary monitor not topmost / leftmost - if there are screens + // that are further left or higher up than the primary monitor + // they'll have negative coordinates which has caused some + // issues with calculations in the past. this test will make + // sure we handle screens with negative coordinates gracefully + // + // +-------+ + // | 0 +----------------+ + // +-------+ | + // | 1 | + // | | + // +----------------+ + previewStyle = new PreviewStyle( + canvasSize: new( + width: 716, + height: 204 + ), + canvasStyle: new( + marginStyle: MarginStyle.Empty, + borderStyle: new( + color: SystemColors.Highlight, + all: 5, + depth: 3), + paddingStyle: new( + all: 1), + backgroundStyle: new( + color1: Color.FromArgb(13, 87, 210), // light blue + color2: Color.FromArgb(3, 68, 192) // darker blue + ) + ), + screenshotStyle: new( + marginStyle: new( + all: 1), + borderStyle: new( + color: SystemColors.Highlight, + all: 5, + depth: 3), + paddingStyle: PaddingStyle.Empty, + backgroundStyle: new( + color1: Color.FromArgb(13, 87, 210), // light blue + color2: Color.FromArgb(3, 68, 192) // darker blue + ) + )); + screens = new List + { + new(-1920, -480, 1920, 1080), + new(0, 0, 5120, 1440), + }; + activatedLocation = new(-960, 60); + previewLayout = new PreviewLayout( + virtualScreen: new(-1920, -480, 7040, 1920), + screens: screens, + activatedScreenIndex: 0, + formBounds: new(-1318, -42, 716, 204), + previewStyle: previewStyle, + previewBounds: new( + outerBounds: new(0, 0, 716, 204), + marginBounds: new(0, 0, 716, 204), + borderBounds: new(0, 0, 716, 204), + paddingBounds: new(5, 5, 706, 194), + contentBounds: new(6, 6, 704, 192) + ), + screenshotBounds: new() + { + new( + outerBounds: new(6, 6, 192, 108), + marginBounds: new(6, 6, 192, 108), + borderBounds: new(7, 7, 190, 106), + paddingBounds: new(12, 12, 180, 96), + contentBounds: new(12, 12, 180, 96) + ), + new( + outerBounds: new(198, 54, 512, 144), + marginBounds: new(198, 54, 512, 144), + borderBounds: new(199, 55, 510, 142), + paddingBounds: new(204, 60, 500, 132), + contentBounds: new(204, 60, 500, 132) + ), + }); + yield return new object[] { new TestCase(previewStyle, 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.PreviewStyle, data.Screens, data.ActivatedLocation); + var expected = data.ExpectedResult; + var options = new JsonSerializerOptions + { + WriteIndented = true, + }; + Assert.AreEqual( + JsonSerializer.Serialize(expected, options), + JsonSerializer.Serialize(actual, options)); + } + } + + [TestClass] + public sealed class GetBoxBoundsFromContentBoundsTests + { + public sealed class TestCase + { + public TestCase(RectangleInfo contentBounds, BoxStyle boxStyle, BoxBounds expectedResult) + { + this.ContentBounds = contentBounds; + this.BoxStyle = boxStyle; + this.ExpectedResult = expectedResult; + } + + public RectangleInfo ContentBounds { get; set; } + + public BoxStyle BoxStyle { get; set; } + + public BoxBounds ExpectedResult { get; set; } + } + + public static IEnumerable GetTestCases() + { + yield return new[] + { + new TestCase( + contentBounds: new(100, 100, 800, 600), + boxStyle: new( + marginStyle: new(3), + borderStyle: new(Color.Red, 5, 0), + paddingStyle: new(7), + backgroundStyle: BackgroundStyle.Empty), + expectedResult: new( + outerBounds: new(85, 85, 830, 630), + marginBounds: new(85, 85, 830, 630), + borderBounds: new(88, 88, 824, 624), + paddingBounds: new(93, 93, 814, 614), + contentBounds: new(100, 100, 800, 600))), + }; + } + + [TestMethod] + [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + public void RunTestCases(TestCase data) + { + var actual = LayoutHelper.GetBoxBoundsFromContentBounds(data.ContentBounds, data.BoxStyle); + var expected = data.ExpectedResult; + Assert.AreEqual( + JsonSerializer.Serialize(expected), + JsonSerializer.Serialize(actual)); + } + } + + [TestClass] + public sealed class GetBoxBoundsFromOuterBoundsTests + { + public sealed class TestCase + { + public TestCase(RectangleInfo outerBounds, BoxStyle boxStyle, BoxBounds expectedResult) + { + this.OuterBounds = outerBounds; + this.BoxStyle = boxStyle; + this.ExpectedResult = expectedResult; + } + + public RectangleInfo OuterBounds { get; set; } + + public BoxStyle BoxStyle { get; set; } + + public BoxBounds ExpectedResult { get; set; } + } + + public static IEnumerable GetTestCases() + { + yield return new[] + { + new TestCase( + outerBounds: new(85, 85, 830, 630), + boxStyle: new( + marginStyle: new(3), + borderStyle: new(Color.Red, 5, 0), + paddingStyle: new(7), + backgroundStyle: BackgroundStyle.Empty), + expectedResult: new( + outerBounds: new(85, 85, 830, 630), + marginBounds: new(85, 85, 830, 630), + borderBounds: new(88, 88, 824, 624), + paddingBounds: new(93, 93, 814, 614), + contentBounds: new(100, 100, 800, 600))), + }; + } + + [TestMethod] + [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + public void RunTestCases(TestCase data) + { + var actual = LayoutHelper.GetBoxBoundsFromOuterBounds(data.OuterBounds, data.BoxStyle); + var expected = data.ExpectedResult; + Assert.AreEqual( + JsonSerializer.Serialize(expected), + JsonSerializer.Serialize(actual)); + } + } +} diff --git a/src/FancyMouse.UnitTests/Helpers/MouseHelperTests.cs b/src/FancyMouse.UnitTests/Helpers/MouseHelperTests.cs index a15cd3e..0bf7a8d 100644 --- a/src/FancyMouse.UnitTests/Helpers/MouseHelperTests.cs +++ b/src/FancyMouse.UnitTests/Helpers/MouseHelperTests.cs @@ -20,13 +20,13 @@ public TestCase(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo this.ExpectedResult = expectedResult; } - public PointInfo PreviewLocation { get; set; } + public PointInfo PreviewLocation { get; } - public SizeInfo PreviewSize { get; set; } + public SizeInfo PreviewSize { get; } - public RectangleInfo DesktopBounds { get; set; } + public RectangleInfo DesktopBounds { get; } - public PointInfo ExpectedResult { get; set; } + public PointInfo ExpectedResult { get; } } public static IEnumerable GetTestCases() diff --git a/src/FancyMouse.UnitTests/Models/Drawing/RectangleInfoTests.cs b/src/FancyMouse.UnitTests/Models/Drawing/RectangleInfoTests.cs index 422d518..aa6716d 100644 --- a/src/FancyMouse.UnitTests/Models/Drawing/RectangleInfoTests.cs +++ b/src/FancyMouse.UnitTests/Models/Drawing/RectangleInfoTests.cs @@ -18,11 +18,11 @@ public TestCase(RectangleInfo rectangle, PointInfo point, RectangleInfo expected this.ExpectedResult = expectedResult; } - public RectangleInfo Rectangle { get; set; } + public RectangleInfo Rectangle { get; } - public PointInfo Point { get; set; } + public PointInfo Point { get; } - public RectangleInfo ExpectedResult { get; set; } + public RectangleInfo ExpectedResult { get; } } public static IEnumerable GetTestCases() @@ -69,11 +69,11 @@ public TestCase(RectangleInfo inner, RectangleInfo outer, RectangleInfo expected this.ExpectedResult = expectedResult; } - public RectangleInfo Inner { get; set; } + public RectangleInfo Inner { get; } - public RectangleInfo Outer { get; set; } + public RectangleInfo Outer { get; } - public RectangleInfo ExpectedResult { get; set; } + public RectangleInfo ExpectedResult { get; } } public static IEnumerable GetTestCases() diff --git a/src/FancyMouse.UnitTests/Models/Drawing/SizeInfoTests.cs b/src/FancyMouse.UnitTests/Models/Drawing/SizeInfoTests.cs index 1ecf315..dfb901e 100644 --- a/src/FancyMouse.UnitTests/Models/Drawing/SizeInfoTests.cs +++ b/src/FancyMouse.UnitTests/Models/Drawing/SizeInfoTests.cs @@ -18,11 +18,11 @@ public TestCase(SizeInfo obj, SizeInfo bounds, SizeInfo expectedResult) this.ExpectedResult = expectedResult; } - public SizeInfo Obj { get; set; } + public SizeInfo Obj { get; } - public SizeInfo Bounds { get; set; } + public SizeInfo Bounds { get; } - public SizeInfo ExpectedResult { get; set; } + public SizeInfo ExpectedResult { get; } } public static IEnumerable GetTestCases() @@ -65,11 +65,11 @@ public TestCase(SizeInfo obj, SizeInfo bounds, decimal expectedResult) this.ExpectedResult = expectedResult; } - public SizeInfo Obj { get; set; } + public SizeInfo Obj { get; } - public SizeInfo Bounds { get; set; } + public SizeInfo Bounds { get; } - public decimal ExpectedResult { get; set; } + public decimal ExpectedResult { get; } } public static IEnumerable GetTestCases() diff --git a/src/FancyMouse.UnitTests/Models/Settings/AppSettingsReaderTests.cs b/src/FancyMouse.UnitTests/Models/Settings/AppSettingsReaderTests.cs new file mode 100644 index 0000000..f168016 --- /dev/null +++ b/src/FancyMouse.UnitTests/Models/Settings/AppSettingsReaderTests.cs @@ -0,0 +1,320 @@ +using System.Diagnostics; +using System.Drawing; +using System.Text.Json; +using FancyMouse.Models.Settings; +using FancyMouse.Models.Styles; +using FancyMouse.UnitTests.TestUtils; +using FancyMouse.WindowsHotKeys; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace FancyMouse.UnitTests.Models.Settings; + +[TestClass] +public sealed class AppSettingsReaderTests +{ + [TestClass] + public sealed class ParseJsonTests + { + [TestMethod] + public void NullJsonShouldReturnDefaultSettings() + { + var actual = AppSettingsReader.ParseJson(null); + var expected = AppSettings.DefaultSettings; + Assert.AreSame(expected, actual); + } + + [TestMethod] + public void EmptyJsonShouldReturnDefaultSettings() + { + var actual = AppSettingsReader.ParseJson(string.Empty); + var expected = AppSettings.DefaultSettings; + Assert.AreSame(expected, actual); + } + + [TestMethod] + public void InvalidJsonShouldReturnDefaultSettings() + { + var actual = AppSettingsReader.ParseJson("xxx"); + var expected = AppSettings.DefaultSettings; + Assert.AreSame(expected, actual); + } + + [TestMethod] + public void MissingVersionShouldBeTreatedAsVersion1() + { + var actual = AppSettingsReader.ParseJson("{}"); + var expected = AppSettings.DefaultSettings; + Assert.AreEqual( + JsonSerializer.Serialize(expected), + JsonSerializer.Serialize(actual)); + } + + [TestMethod] + public void EmptyVersion1ShouldParse() + { + var json = SerializationUtils.SerializeAnonymousType( + new + { + version = 1, + }); + var actual = AppSettingsReader.ParseJson(json); + var expected = AppSettings.DefaultSettings; + Assert.AreEqual( + JsonSerializer.Serialize(expected), + JsonSerializer.Serialize(actual)); + } + + [TestMethod] + public void Version1WithNullRootKeysShouldParse() + { + var json = SerializationUtils.SerializeAnonymousType( + new + { + version = 1, + fancymouse = (object?)null, + }); + var actual = AppSettingsReader.ParseJson(json); + var expected = AppSettings.DefaultSettings; + Assert.AreEqual( + JsonSerializer.Serialize(expected), + JsonSerializer.Serialize(actual)); + } + + [TestMethod] + public void Version1WithNullChildKeysShouldParse() + { + var json = SerializationUtils.SerializeAnonymousType( + new + { + version = 1, + fancymouse = new + { + hotkey = (string?)null, + }, + }); + var actual = AppSettingsReader.ParseJson(json); + var expected = AppSettings.DefaultSettings; + Assert.AreEqual( + JsonSerializer.Serialize(expected), + JsonSerializer.Serialize(actual)); + } + + [TestMethod] + public void Version1WithAllValuesShouldParse() + { + var json = SerializationUtils.SerializeAnonymousType( + new + { + version = 1, + fancymouse = new + { + hotkey = "CTRL + ALT + X", + preview = "800 x 600", + }, + }); + var actual = AppSettingsReader.ParseJson(json); + var expected = new AppSettings( + hotkey: new( + key: Keys.X, + modifiers: KeyModifiers.Control | KeyModifiers.Alt + ), + previewStyle: new( + canvasSize: new( + width: 800, + height: 600 + ), + canvasStyle: AppSettings.DefaultSettings.PreviewStyle.CanvasStyle, + screenshotStyle: AppSettings.DefaultSettings.PreviewStyle.ScreenshotStyle + )); + Assert.AreEqual( + JsonSerializer.Serialize(expected), + JsonSerializer.Serialize(actual)); + } + + [TestMethod] + public void Version2WithNullRootKeysShouldParse() + { + var json = SerializationUtils.SerializeAnonymousType( + new + { + version = 2, + hotkey = (object?)null, + preview = (object?)null, + }); + var actual = AppSettingsReader.ParseJson(json); + var expected = AppSettings.DefaultSettings; + Assert.AreEqual( + JsonSerializer.Serialize(expected), + JsonSerializer.Serialize(actual)); + } + + [TestMethod] + public void Version2WithAllValuesShouldParse() + { + var json = SerializationUtils.SerializeAnonymousType( + new + { + version = 2, + hotkey = "CTRL + ALT + X", + preview = new + { + size = new + { + width = 800, + height = 600, + }, + canvas = new + { + border = new + { + color = $"{nameof(SystemColors)}.{nameof(SystemColors.Control)}", + width = 5, + depth = 2, + }, + padding = new + { + width = 2, + }, + background = new + { + color1 = $"{nameof(Color)}.{nameof(Color.Green)}", + color2 = $"{nameof(Color)}.{nameof(Color.Blue)}", + }, + }, + screenshot = new + { + margin = new + { + width = 10, + }, + border = new + { + color = $"{nameof(SystemColors)}.{nameof(SystemColors.Control)}", + width = 5, + depth = 2, + }, + background = new + { + color1 = $"{nameof(Color)}.{nameof(Color.Yellow)}", + color2 = $"{nameof(Color)}.{nameof(Color.Pink)}", + }, + }, + }, + }); + var actual = AppSettingsReader.ParseJson(json); + var expected = new AppSettings( + hotkey: new( + key: Keys.X, + modifiers: KeyModifiers.Control | KeyModifiers.Alt + ), + previewStyle: new( + canvasSize: new( + width: 800, + height: 600 + ), + canvasStyle: new( + marginStyle: MarginStyle.Empty, + borderStyle: new( + color: SystemColors.Control, + all: 5, + depth: 2 + ), + paddingStyle: new( + all: 2 + ), + backgroundStyle: new( + color1: Color.Green, + color2: Color.Blue + ) + ), + screenshotStyle: new( + marginStyle: new( + all: 10 + ), + borderStyle: new( + color: SystemColors.Control, + all: 5, + depth: 2 + ), + paddingStyle: PaddingStyle.Empty, + backgroundStyle: new( + color1: Color.Yellow, + color2: Color.Pink + ) + ) + )); + Assert.AreEqual( + JsonSerializer.Serialize(expected), + JsonSerializer.Serialize(actual)); + } + + [TestMethod] + public void PerformanceTest() + { + var json = SerializationUtils.SerializeAnonymousType( + new + { + version = 2, + hotkey = "CTRL + ALT + X", + preview = new + { + size = new + { + width = 800, + height = 600, + }, + canvas = new + { + border = new + { + color = $"{nameof(SystemColors)}.{nameof(SystemColors.Control)}", + width = 5, + depth = 2, + }, + padding = new + { + width = 2, + }, + background = new + { + color1 = $"{nameof(Color)}.{nameof(Color.Green)}", + color2 = $"{nameof(Color)}.{nameof(Color.Blue)}", + }, + }, + screenshot = new + { + margin = new + { + width = 10, + }, + border = new + { + color = $"{nameof(SystemColors)}.{nameof(SystemColors.Control)}", + width = 5, + depth = 2, + }, + background = new + { + color1 = $"{nameof(Color)}.{nameof(Color.Yellow)}", + color2 = $"{nameof(Color)}.{nameof(Color.Pink)}", + }, + }, + }, + }); + var times = new List(); + for (var i = 0; i < 10000; i++) + { + var stopwatch = Stopwatch.StartNew(); + AppSettingsReader.ParseJson(json); + stopwatch.Stop(); + times.Add(stopwatch.ElapsedTicks); + } + + const int ticksPerMs = 10000; + var averageMs = (decimal)times.Sum() / times.Count / ticksPerMs; + Console.WriteLine($"{averageMs} ms"); + + Assert.IsTrue(averageMs < 1.5M); + } + } +} diff --git a/src/FancyMouse.UnitTests/TestUtils/SerializationUtils.cs b/src/FancyMouse.UnitTests/TestUtils/SerializationUtils.cs new file mode 100644 index 0000000..7dc5fe7 --- /dev/null +++ b/src/FancyMouse.UnitTests/TestUtils/SerializationUtils.cs @@ -0,0 +1,14 @@ +using System.Text.Json; + +namespace FancyMouse.UnitTests.TestUtils; +internal static class SerializationUtils +{ + public static string SerializeAnonymousType(T value) + { + var options = new JsonSerializerOptions + { + WriteIndented = true, + }; + return JsonSerializer.Serialize(value, options); + } +} diff --git a/src/FancyMouse.WindowsHotKeys.Tests/KeystrokeTests.cs b/src/FancyMouse.WindowsHotKeys.Tests/KeystrokeTests.cs index e3d9e78..975dc28 100644 --- a/src/FancyMouse.WindowsHotKeys.Tests/KeystrokeTests.cs +++ b/src/FancyMouse.WindowsHotKeys.Tests/KeystrokeTests.cs @@ -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); }); diff --git a/src/FancyMouse.WindowsHotKeys/HotKeyManager.cs b/src/FancyMouse.WindowsHotKeys/HotKeyManager.cs index ccf7f81..abbf379 100644 --- a/src/FancyMouse.WindowsHotKeys/HotKeyManager.cs +++ b/src/FancyMouse.WindowsHotKeys/HotKeyManager.cs @@ -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; } @@ -70,7 +70,7 @@ private LRESULT WindowProc(HWND hWnd, MESSAGE_TYPE msg, WPARAM wParam, LPARAM lP public void Start() { // see https://learn.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues - var hInstance = Process.GetCurrentProcess().Handle; + var hInstance = (HINSTANCE)Process.GetCurrentProcess().Handle; // see https://stackoverflow.com/a/30992796/3156906 var wndClass = new WNDCLASSEXW( @@ -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, @@ -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) diff --git a/src/FancyMouse.WindowsHotKeys/Internal/MessageLoop.cs b/src/FancyMouse.WindowsHotKeys/Internal/MessageLoop.cs index 2eea6cf..3f66687 100644 --- a/src/FancyMouse.WindowsHotKeys/Internal/MessageLoop.cs +++ b/src/FancyMouse.WindowsHotKeys/Internal/MessageLoop.cs @@ -6,10 +6,11 @@ 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 @@ -17,58 +18,67 @@ private string Name get; } - private bool IsBackground + /// + /// Gets a semaphore that can be waited on until the message loop has stopped. + /// + private SemaphoreSlim RunningSemaphore { get; } - private Thread? ManagedThread + /// + /// Gets a cancellation token that can be used to signal the internal message loop thread to stop. + /// + 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), @@ -81,18 +91,24 @@ 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; @@ -100,38 +116,39 @@ private void RunInternal() _ = 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(); } } diff --git a/src/FancyMouse.WindowsHotKeys/Keystroke.cs b/src/FancyMouse.WindowsHotKeys/Keystroke.cs index 82d8436..56aed43 100644 --- a/src/FancyMouse.WindowsHotKeys/Keystroke.cs +++ b/src/FancyMouse.WindowsHotKeys/Keystroke.cs @@ -1,4 +1,6 @@ -namespace FancyMouse.WindowsHotKeys; +using System.Diagnostics.CodeAnalysis; + +namespace FancyMouse.WindowsHotKeys; public sealed class Keystroke { @@ -19,14 +21,25 @@ public KeyModifiers Modifiers } public static Keystroke Parse(string s) + { + if (!Keystroke.TryParse(s, out var result)) + { + throw new ArgumentException("Invalid argument format.", nameof(s)); + } + + return result; + } + + public static bool TryParse(string s, [NotNullWhen(true)] out Keystroke? result) { // see https://github.com/microsoft/terminal/blob/14919073a12fc0ecb4a9805cc183fdd68d30c4b6/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp#L124 // for an alternate implementation // e.g. "CTRL + ALT + SHIFT + F" - if (s == null) + if (string.IsNullOrEmpty(s)) { - throw new ArgumentNullException(nameof(s)); + result = null; + return false; } var parts = s @@ -53,12 +66,20 @@ public static Keystroke Parse(string s) keystroke.Modifiers |= KeyModifiers.Windows; break; default: - keystroke.Keys = Enum.Parse(part); + if (!Enum.TryParse(part, out var key)) + { + result = null; + return false; + } + + keystroke.Keys = key; break; } } - return new Keystroke(keystroke.Keys, keystroke.Modifiers); + result = new Keystroke( + keystroke.Keys, keystroke.Modifiers); + return true; } public override string ToString() diff --git a/src/FancyMouse/FancyMouse.csproj b/src/FancyMouse/FancyMouse.csproj index 6f2486d..ab34664 100644 --- a/src/FancyMouse/FancyMouse.csproj +++ b/src/FancyMouse/FancyMouse.csproj @@ -9,29 +9,16 @@ true - - - - PerMonitorV2 - - + Always @@ -41,8 +28,10 @@ + + diff --git a/src/FancyMouse/FancyMouse.json b/src/FancyMouse/FancyMouse.json deleted file mode 100644 index ee0c0ee..0000000 --- a/src/FancyMouse/FancyMouse.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "FancyMouse": { - "Hotkey": "CTRL + ALT + SHIFT + F", - "Preview": "1600 x 1200" - } -} diff --git a/src/FancyMouse/Helpers/DrawingHelper.cs b/src/FancyMouse/Helpers/DrawingHelper.cs index 79a4fa3..69fc79a 100644 --- a/src/FancyMouse/Helpers/DrawingHelper.cs +++ b/src/FancyMouse/Helpers/DrawingHelper.cs @@ -1,5 +1,9 @@ -using System.Drawing.Drawing2D; +using System.Diagnostics; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; using FancyMouse.Models.Drawing; +using FancyMouse.Models.Layout; +using FancyMouse.Models.Styles; using FancyMouse.NativeMethods; using static FancyMouse.NativeMethods.Core; @@ -7,27 +11,224 @@ namespace FancyMouse.Helpers; internal static class DrawingHelper { + internal static Bitmap RenderPreview( + PreviewLayout previewLayout, + HDC sourceHdc) + { + return DrawingHelper.RenderPreview(previewLayout, sourceHdc, null, null); + } + + internal static Bitmap RenderPreview( + PreviewLayout previewLayout, + HDC sourceHdc, + Action? previewImageCreatedCallback, + Action? previewImageUpdatedCallback) + { + var stopwatch = Stopwatch.StartNew(); + + // initialize the preview image + var previewBounds = previewLayout.PreviewBounds.OuterBounds.ToRectangle(); + var previewImage = new Bitmap(previewBounds.Width, previewBounds.Height, PixelFormat.Format32bppArgb); + var previewGraphics = Graphics.FromImage(previewImage); + previewImageCreatedCallback?.Invoke(previewImage); + + DrawingHelper.DrawBoxBorder(previewGraphics, previewLayout.PreviewStyle.CanvasStyle, previewLayout.PreviewBounds); + DrawingHelper.DrawBoxBackground( + previewGraphics, + previewLayout.PreviewStyle.CanvasStyle, + previewLayout.PreviewBounds, + Enumerable.Empty()); + + // sort the source and target screen areas, putting the activated screen first + // (we need to capture and draw the activated screen before we show the form + // because otherwise we'll capture the form as part of the screenshot!) + var sourceScreens = new List { previewLayout.Screens[previewLayout.ActivatedScreenIndex] } + .Union(previewLayout.Screens.Where((_, idx) => idx != previewLayout.ActivatedScreenIndex)) + .ToList(); + var targetScreens = new List { previewLayout.ScreenshotBounds[previewLayout.ActivatedScreenIndex] } + .Union(previewLayout.ScreenshotBounds.Where((_, idx) => idx != previewLayout.ActivatedScreenIndex)) + .ToList(); + + // draw all the screenshot bezels + foreach (var screenshotBounds in previewLayout.ScreenshotBounds) + { + DrawingHelper.DrawBoxBorder( + previewGraphics, previewLayout.PreviewStyle.ScreenshotStyle, screenshotBounds); + } + + var previewHdc = HDC.Null; + var imageUpdated = false; + try + { + var placeholdersDrawn = false; + for (var i = 0; i < sourceScreens.Count; i++) + { + DrawingHelper.EnsurePreviewDeviceContext(previewGraphics, ref previewHdc); + DrawingHelper.DrawScreenshot( + sourceHdc, previewHdc, sourceScreens[i], targetScreens[i]); + imageUpdated = true; + + // show the placeholder images and show the form if it looks like it might take + // a while to capture the remaining screenshot images (but only if there are any) + if ((i >= (sourceScreens.Count - 1)) || (stopwatch.ElapsedMilliseconds <= 250)) + { + continue; + } + + // we need to release the device context handle before we draw anything + // using the Graphics object otherwise we'll get an error from GDI saying + // "Object is currently in use elsewhere" + DrawingHelper.FreePreviewDeviceContext(previewGraphics, ref previewHdc); + + if (!placeholdersDrawn) + { + // draw placeholders for any undrawn screens + DrawingHelper.DrawPlaceholders( + previewGraphics, + previewLayout.PreviewStyle.ScreenshotStyle, + targetScreens.GetRange(i + 1, targetScreens.Count - i - 1)); + placeholdersDrawn = true; + } + + previewImageUpdatedCallback?.Invoke(); + imageUpdated = false; + } + } + finally + { + DrawingHelper.FreePreviewDeviceContext(previewGraphics, ref previewHdc); + } + + if (imageUpdated) + { + previewImageUpdatedCallback?.Invoke(); + } + + stopwatch.Stop(); + + return previewImage; + } + /// - /// Draw the gradient-filled preview background. + /// Draws a border shape with an optional 3d highlight and shadow effect. /// - public static void DrawPreviewBackground( - Graphics previewGraphics, RectangleInfo previewBounds, IEnumerable screenBounds) + public static void DrawBoxBorder( + Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds) { + var borderInfo = boxStyle.BorderStyle; + if (borderInfo is { Horizontal: 0, Vertical: 0 }) + { + return; + } + + // draw the preview border + using var borderBrush = new SolidBrush(borderInfo.Color); + var borderRegion = new Region(boxBounds.BorderBounds.ToRectangle()); + borderRegion.Exclude(boxBounds.PaddingBounds.ToRectangle()); + graphics.FillRegion(borderBrush, borderRegion); + + // draw the highlight and shadow + var bounds = boxBounds.BorderBounds.ToRectangle(); + using var highlight = new Pen(Color.FromArgb(0x44, 0xFF, 0xFF, 0xFF)); + using var shadow = new Pen(Color.FromArgb(0x44, 0x00, 0x00, 0x00)); + + for (var i = 0; i < borderInfo.Depth; i++) + { + // left edge + if (borderInfo.Left >= i * 2) + { + graphics.DrawLine( + highlight, + bounds.Left + i, + bounds.Top + i, + bounds.Left + i, + bounds.Bottom - 1 - i); + graphics.DrawLine( + shadow, + bounds.Left + (int)borderInfo.Left - 1 - i, + bounds.Top + (int)borderInfo.Top - 1 - i, + bounds.Left + (int)borderInfo.Left - 1 - i, + bounds.Bottom - (int)borderInfo.Bottom + i); + } + + // top edge + if (borderInfo.Top >= i * 2) + { + graphics.DrawLine( + highlight, + bounds.Left + i, + bounds.Top + i, + bounds.Right - 1 - i, + bounds.Top + i); + graphics.DrawLine( + shadow, + bounds.Left + (int)borderInfo.Left - 1 - i, + bounds.Top + (int)borderInfo.Top - 1 - i, + bounds.Right - (int)borderInfo.Right + i, + bounds.Top + (int)borderInfo.Bottom - 1 - i); + } + + // right edge + if (borderInfo.Right >= i * 2) + { + graphics.DrawLine( + highlight, + bounds.Right - (int)borderInfo.Right + i, + bounds.Top + (int)borderInfo.Top - 1 - i, + bounds.Right - (int)borderInfo.Right + i, + bounds.Bottom - (int)borderInfo.Bottom + i); + graphics.DrawLine( + shadow, + bounds.Right - 1 - i, + bounds.Top + i, + bounds.Right - 1 - i, + bounds.Bottom - 1 - i); + } + + // bottom edge + if (borderInfo.Bottom >= i * 2) + { + graphics.DrawLine( + highlight, + bounds.Left + (int)borderInfo.Left - 1 - i, + bounds.Bottom - (int)borderInfo.Bottom + i, + bounds.Right - (int)borderInfo.Right + i, + bounds.Bottom - (int)borderInfo.Bottom + i); + graphics.DrawLine( + shadow, + bounds.Left + i, + bounds.Bottom - 1 - i, + bounds.Right - 1 - i, + bounds.Bottom - 1 - i); + } + } + } + + /// + /// Draws a gradient-filled background shape. + /// + public static void DrawBoxBackground( + Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds, IEnumerable excludeBounds) + { + var backgroundBounds = boxBounds.PaddingBounds; + var backgroundInfo = boxStyle.BackgroundStyle; + using var backgroundBrush = new LinearGradientBrush( - previewBounds.Location.ToPoint(), - previewBounds.Size.ToPoint(), - Color.FromArgb(13, 87, 210), // light blue - Color.FromArgb(3, 68, 192)); // darker blue + backgroundBounds.ToRectangle(), + backgroundInfo.Color1, + backgroundInfo.Color2, + LinearGradientMode.ForwardDiagonal); + + var backgroundRegion = new Region(backgroundBounds.ToRectangle()); // it's faster to build a region with the screen areas excluded // and fill that than it is to fill the entire bounding rectangle - var backgroundRegion = new Region(previewBounds.ToRectangle()); - foreach (var screen in screenBounds) + foreach (var exclude in excludeBounds) { - backgroundRegion.Exclude(screen.ToRectangle()); + backgroundRegion.Exclude(exclude.ToRectangle()); } - previewGraphics.FillRegion(backgroundBrush, backgroundRegion); + graphics.FillRegion(backgroundBrush, backgroundRegion); } public static void EnsureDesktopDeviceContext(ref HWND desktopHwnd, ref HDC desktopHdc) @@ -68,11 +269,11 @@ public static void FreeDesktopDeviceContext(ref HWND desktopHwnd, ref HDC deskto /// Checks if the device context handle exists, and creates a new one from the /// specified Graphics object if not. /// - public static void EnsurePreviewDeviceContext(Graphics previewGraphics, ref HDC previewHdc) + public static void EnsurePreviewDeviceContext(Graphics graphics, ref HDC previewHdc) { if (previewHdc.IsNull) { - previewHdc = new HDC(previewGraphics.GetHdc()); + previewHdc = (HDC)graphics.GetHdc(); var result = Gdi32.SetStretchBltMode(previewHdc, Gdi32.STRETCH_BLT_MODE.STRETCH_HALFTONE); if (result == 0) @@ -86,42 +287,42 @@ public static void EnsurePreviewDeviceContext(Graphics previewGraphics, ref HDC /// /// Free the specified device context handle if it exists. /// - public static void FreePreviewDeviceContext(Graphics previewGraphics, ref HDC previewHdc) + public static void FreePreviewDeviceContext(Graphics graphics, ref HDC previewHdc) { - if ((previewGraphics is not null) && !previewHdc.IsNull) + if ((graphics is not null) && !previewHdc.IsNull) { - previewGraphics.ReleaseHdc(previewHdc.Value); + graphics.ReleaseHdc(previewHdc.Value); previewHdc = HDC.Null; } } /// - /// Draw placeholder images for any non-activated screens on the preview. + /// Draws placeholder images for any non-activated screens on the preview. /// Will release the specified device context handle if it needs to draw anything. /// - public static void DrawPreviewScreenPlaceholders( - Graphics previewGraphics, List screenBounds) + public static void DrawPlaceholders( + Graphics graphics, BoxStyle screenStyle, IList screenBounds) { // we can exclude the activated screen because we've already draw // the screen capture image for that one on the preview if (screenBounds.Any()) { - var brush = Brushes.Black; - previewGraphics.FillRectangles(brush, screenBounds.Select(screen => screen.ToRectangle()).ToArray()); + var brush = new SolidBrush(screenStyle.BackgroundStyle.Color1); + graphics.FillRectangles(brush, screenBounds.Select(bounds => bounds.PaddingBounds.ToRectangle()).ToArray()); } } /// /// Draws a screen capture from the specified desktop handle onto the target device context. /// - public static void DrawPreviewScreen( + public static void DrawScreenshot( HDC sourceHdc, HDC targetHdc, RectangleInfo sourceBounds, - RectangleInfo targetBounds) + BoxBounds targetBounds) { var source = sourceBounds.ToRectangle(); - var target = targetBounds.ToRectangle(); + var target = targetBounds.ContentBounds.ToRectangle(); var result = Gdi32.StretchBlt( targetHdc, target.X, diff --git a/src/FancyMouse/Helpers/LayoutHelper.cs b/src/FancyMouse/Helpers/LayoutHelper.cs index 92879e5..1a736fd 100644 --- a/src/FancyMouse/Helpers/LayoutHelper.cs +++ b/src/FancyMouse/Helpers/LayoutHelper.cs @@ -1,84 +1,134 @@ using FancyMouse.Models.Drawing; using FancyMouse.Models.Layout; +using FancyMouse.Models.Styles; namespace FancyMouse.Helpers; internal static class LayoutHelper { - public static LayoutInfo CalculateLayoutInfo( - LayoutConfig layoutConfig) + public static PreviewLayout GetPreviewLayout( + PreviewStyle previewStyle, IEnumerable screens, PointInfo activatedLocation) { - if (layoutConfig is null) + if (previewStyle is null) { - throw new ArgumentNullException(nameof(layoutConfig)); + throw new ArgumentNullException(nameof(previewStyle)); } - var builder = new LayoutInfo.Builder + if (screens is null) { - LayoutConfig = layoutConfig, - }; + throw new ArgumentNullException(nameof(screens)); + } + + var allScreens = screens.ToList(); + if (allScreens.Count == 0) + { + throw new ArgumentException("Value must contain at least one item.", nameof(screens)); + } - builder.ActivatedScreenBounds = layoutConfig.Screens[layoutConfig.ActivatedScreenIndex].Bounds; + var builder = new PreviewLayout.Builder(); + + // calculate the bounding rectangle for the virtual screen + var virtualScreen = LayoutHelper.GetCombinedScreenBounds(allScreens); + builder.VirtualScreen = virtualScreen; + + builder.Screens = allScreens; + + // find the screen that contains the activated location - this is the + // one we'll show the preview form on + var activatedScreen = allScreens.Single( + screen => screen.Contains(activatedLocation)); + builder.ActivatedScreenIndex = allScreens.IndexOf(activatedScreen); // work out the maximum *constrained* form size // * can't be bigger than the activated screen // * can't be bigger than the max form size - var maxFormSize = builder.ActivatedScreenBounds.Size - .Intersect(layoutConfig.MaximumFormSize); - - // the drawing area for screen images is inside the - // form border and inside the preview border - var maxDrawingSize = maxFormSize - .Shrink(layoutConfig.FormPadding) - .Shrink(layoutConfig.PreviewPadding); + var maxPreviewSize = activatedScreen.Size + .Intersect(previewStyle.CanvasSize); - // scale the virtual screen to fit inside the drawing bounds - var scalingRatio = layoutConfig.VirtualScreenBounds.Size - .ScaleToFitRatio(maxDrawingSize); + // the drawing area for screenshots is inside the + // preview border and inside the preview padding (if any) + var maxContentSize = maxPreviewSize + .Shrink(previewStyle.CanvasStyle.MarginStyle) + .Shrink(previewStyle.CanvasStyle.BorderStyle) + .Shrink(previewStyle.CanvasStyle.PaddingStyle); - // position the drawing bounds inside the preview border - var drawingBounds = layoutConfig.VirtualScreenBounds.Size - .ScaleToFit(maxDrawingSize) - .PlaceAt(layoutConfig.PreviewPadding.Left, layoutConfig.PreviewPadding.Top); + // position the drawing area on the preview image, offset to + // allow for any borders and padding + var contentBounds = virtualScreen.Size + .ScaleToFit(maxContentSize) + .Floor() + .PlaceAt(0, 0) + .Offset(previewStyle.CanvasStyle.MarginStyle.Left, previewStyle.CanvasStyle.MarginStyle.Top) + .Offset(previewStyle.CanvasStyle.BorderStyle.Left, previewStyle.CanvasStyle.BorderStyle.Top) + .Offset(previewStyle.CanvasStyle.PaddingStyle.Left, previewStyle.CanvasStyle.PaddingStyle.Top); - // now we know the size of the drawing area we can work out the preview size - builder.PreviewBounds = drawingBounds.Enlarge(layoutConfig.PreviewPadding); + // now we know the size of the content area we can work out the background bounds + builder.PreviewStyle = previewStyle; + builder.PreviewBounds = LayoutHelper.GetBoxBoundsFromContentBounds( + contentBounds, + previewStyle.CanvasStyle); - // ... and the form size + // ... and the form bounds // * center the form to the activated position, but nudge it back // inside the visible area of the activated screen if it falls outside - builder.FormBounds = builder.PreviewBounds - .Enlarge(layoutConfig.FormPadding) - .Center(layoutConfig.ActivatedLocation) - .Clamp(builder.ActivatedScreenBounds); + var formBounds = builder.PreviewBounds.OuterBounds + .Center(activatedLocation) + .Clamp(activatedScreen); + builder.FormBounds = formBounds; - // now calculate the positions of each of the screen images on the preview - builder.ScreenBounds = layoutConfig.Screens + // scale the virtual screen to fit inside the preview content bounds + var scalingRatio = builder.VirtualScreen.Size + .ScaleToFitRatio(contentBounds.Size); + + // now calculate the positions of each of the screenshot images on the preview + builder.ScreenshotBounds = allScreens .Select( - screen => screen.Bounds - .Offset(layoutConfig.VirtualScreenBounds.Location.ToSize().Negate()) - .Scale(scalingRatio) - .Offset(layoutConfig.PreviewPadding.Left, layoutConfig.PreviewPadding.Top)) + screen => LayoutHelper.GetBoxBoundsFromOuterBounds( + screen + .Offset(virtualScreen.Location.ToSize().Negate()) + .Scale(scalingRatio) + .Offset(builder.PreviewBounds.ContentBounds.Location.ToSize()) + .Truncate(), + previewStyle.ScreenshotStyle)) .ToList(); return builder.Build(); } - /// - /// Resize and position the specified form. - /// - public static void PositionForm( - Form form, RectangleInfo formBounds) + public static RectangleInfo GetCombinedScreenBounds(List screens) + { + return screens.Skip(1).Aggregate( + seed: screens.First(), + (bounds, screen) => bounds.Union(screen)); + } + + public static BoxBounds GetBoxBoundsFromContentBounds( + RectangleInfo contentBounds, + BoxStyle boxStyle) + { + var paddingBounds = contentBounds.Enlarge( + boxStyle.PaddingStyle ?? throw new ArgumentException(nameof(boxStyle.PaddingStyle))); + var borderBounds = paddingBounds.Enlarge( + boxStyle.BorderStyle ?? throw new ArgumentException(nameof(boxStyle.BorderStyle))); + var marginBounds = borderBounds.Enlarge( + boxStyle.MarginStyle ?? throw new ArgumentException(nameof(boxStyle.MarginStyle))); + var outerBounds = marginBounds; + return new( + outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds); + } + + public static BoxBounds GetBoxBoundsFromOuterBounds( + RectangleInfo outerBounds, + BoxStyle boxStyle) { - // note - do this in two steps rather than "this.Bounds = formBounds" as there - // appears to be an issue in WinForms with dpi scaling even when using PerMonitorV2, - // where the form scaling uses either the *primary* screen scaling or the *previous* - // screen's scaling when the form is moved to a different screen. i've got no idea - // *why*, but the exact sequence of calls below seems to be a workaround... - // see https://github.com/mikeclayton/FancyMouse/issues/2 - var bounds = formBounds.ToRectangle(); - form.Location = bounds.Location; - _ = form.PointToScreen(Point.Empty); - form.Size = bounds.Size; + var marginBounds = outerBounds ?? throw new ArgumentNullException(nameof(outerBounds)); + var borderBounds = marginBounds.Shrink( + boxStyle.MarginStyle ?? throw new ArgumentException(nameof(boxStyle.MarginStyle))); + var paddingBounds = borderBounds.Shrink( + boxStyle.BorderStyle ?? throw new ArgumentException(nameof(boxStyle.BorderStyle))); + var contentBounds = paddingBounds.Shrink( + boxStyle.PaddingStyle ?? throw new ArgumentException(nameof(boxStyle.PaddingStyle))); + return new( + outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds); } } diff --git a/src/FancyMouse/Helpers/MouseHelper.cs b/src/FancyMouse/Helpers/MouseHelper.cs index ffc3a22..356f4ad 100644 --- a/src/FancyMouse/Helpers/MouseHelper.cs +++ b/src/FancyMouse/Helpers/MouseHelper.cs @@ -44,6 +44,27 @@ public static PointInfo GetCursorPosition() point.x, point.y); } + public static PointInfo GetCursorPosition(PointInfo location) + { + var pos = new POINT(0, 0); + var lpPoint = new LPPOINT(pos); + + if (!User32.GetCursorPos(lpPoint)) + { + throw new InvalidOperationException(); + } + + if (lpPoint.IsNull) + { + throw new InvalidOperationException(); + } + + pos = lpPoint.ToStructure(); + lpPoint.Free(); + + return new PointInfo(pos.x, pos.y); + } + /// /// Moves the cursor to the specified location. /// @@ -68,16 +89,29 @@ public static void SetCursorPosition(PointInfo location) // // setting the position a second time seems to fix this and moves the // cursor to the expected location (b) - var point = location.ToPoint(); + var target = location.ToPoint(); for (var i = 0; i < 2; i++) { - var result = User32.SetCursorPos(point.X, point.Y); + var result = User32.SetCursorPos(target.X, target.Y); if (!result) { throw new Win32Exception( Marshal.GetLastWin32Error()); } + + var current = MouseHelper.GetCursorPosition(); + if ((current.X == target.X) || (current.Y == target.Y)) + { + break; + } + } + + /* + if ((pos.x != target.X) || (pos.y != target.Y)) + { + throw new InvalidOperationException(); } + */ // temporary workaround for issue #1273 MouseHelper.SimulateMouseMovementEvent(location); diff --git a/src/FancyMouse/Helpers/ScreenHelper.cs b/src/FancyMouse/Helpers/ScreenHelper.cs index 09c4c1d..12c6ef7 100644 --- a/src/FancyMouse/Helpers/ScreenHelper.cs +++ b/src/FancyMouse/Helpers/ScreenHelper.cs @@ -1,6 +1,5 @@ using System.ComponentModel; using FancyMouse.Models.Drawing; -using FancyMouse.Models.Screen; using FancyMouse.NativeMethods; using static FancyMouse.NativeMethods.Core; diff --git a/src/FancyMouse/Models/Drawing/BoxBounds.cs b/src/FancyMouse/Models/Drawing/BoxBounds.cs new file mode 100644 index 0000000..e88f2b4 --- /dev/null +++ b/src/FancyMouse/Models/Drawing/BoxBounds.cs @@ -0,0 +1,69 @@ +namespace FancyMouse.Models.Drawing; + +public sealed class BoxBounds +{ + /* + + see https://www.w3schools.com/css/css_boxmodel.asp + + +--------------[bounds]---------------+ + |▒▒▒▒▒▒▒▒▒▒▒▒▒▒[margin]▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| + |▒▒▓▓▓▓▓▓▓▓▓▓▓▓[border]▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒| + |▒▒▓▓░░░░░░░░░░[padding]░░░░░░░░░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ [content] ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▒▒| + |▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒| + |▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| + +-------------------------------------+ + + */ + + internal BoxBounds( + RectangleInfo outerBounds, + RectangleInfo marginBounds, + RectangleInfo borderBounds, + RectangleInfo paddingBounds, + RectangleInfo contentBounds) + { + this.OuterBounds = outerBounds ?? throw new ArgumentNullException(nameof(outerBounds)); + this.MarginBounds = marginBounds ?? throw new ArgumentNullException(nameof(marginBounds)); + this.BorderBounds = borderBounds ?? throw new ArgumentNullException(nameof(borderBounds)); + this.PaddingBounds = paddingBounds ?? throw new ArgumentNullException(nameof(paddingBounds)); + this.ContentBounds = contentBounds ?? throw new ArgumentNullException(nameof(contentBounds)); + } + + /// + /// Gets the outer bounds of this layout box. + /// + public RectangleInfo OuterBounds + { + get; + } + + public RectangleInfo MarginBounds + { + get; + } + + public RectangleInfo BorderBounds + { + get; + } + + public RectangleInfo PaddingBounds + { + get; + } + + /// + /// Gets the bounds of the content area for this layout box. + /// + public RectangleInfo ContentBounds + { + get; + } +} diff --git a/src/FancyMouse/Models/Drawing/PointInfo.cs b/src/FancyMouse/Models/Drawing/PointInfo.cs index 40548a1..79f68d6 100644 --- a/src/FancyMouse/Models/Drawing/PointInfo.cs +++ b/src/FancyMouse/Models/Drawing/PointInfo.cs @@ -26,9 +26,14 @@ public decimal Y get; } - public SizeInfo ToSize() + /// + /// Moves this PointInfo inside the specified RectangleInfo. + /// + public PointInfo Clamp(RectangleInfo outer) { - return new((int)this.X, (int)this.Y); + return new( + x: Math.Clamp(this.X, outer.X, outer.Right), + y: Math.Clamp(this.Y, outer.Y, outer.Bottom)); } public PointInfo Scale(decimal scalingFactor) => new(this.X * scalingFactor, this.Y * scalingFactor); @@ -37,6 +42,27 @@ public SizeInfo ToSize() public Point ToPoint() => new((int)this.X, (int)this.Y); + public SizeInfo ToSize() + { + return new((int)this.X, (int)this.Y); + } + + /// + /// Stretches the point to the same proportional position in targetBounds as + /// it currently is in sourceBounds + /// + public PointInfo Stretch(RectangleInfo source, RectangleInfo target) + { + return new PointInfo( + x: ((this.X - source.X) / source.Width * target.Width) + target.X, + y: ((this.Y - source.Y) / source.Height * target.Height) + target.Y); + } + + public PointInfo Truncate() => + new( + (int)this.X, + (int)this.Y); + public override string ToString() { return "{" + diff --git a/src/FancyMouse/Models/Drawing/RectangleInfo.cs b/src/FancyMouse/Models/Drawing/RectangleInfo.cs index c871921..75fc657 100644 --- a/src/FancyMouse/Models/Drawing/RectangleInfo.cs +++ b/src/FancyMouse/Models/Drawing/RectangleInfo.cs @@ -1,10 +1,15 @@ -namespace FancyMouse.Models.Drawing; +using System.Text.Json.Serialization; +using FancyMouse.Models.Styles; + +namespace FancyMouse.Models.Drawing; /// /// Immutable version of a System.Drawing.Rectangle object with some extra utility methods. /// public sealed class RectangleInfo { + public static readonly RectangleInfo Empty = new(0, 0, 0, 0); + public RectangleInfo(decimal x, decimal y, decimal width, decimal height) { this.X = x; @@ -48,27 +53,69 @@ public decimal Height get; } + [JsonIgnore] public decimal Left => this.X; + [JsonIgnore] public decimal Top => this.Y; + [JsonIgnore] public decimal Right => this.X + this.Width; + [JsonIgnore] public decimal Bottom => this.Y + this.Height; - public SizeInfo Size => new(this.Width, this.Height); + [JsonIgnore] + public decimal Area => this.Width * this.Height; + [JsonIgnore] public PointInfo Location => new(this.X, this.Y); - public decimal Area => this.Width * this.Height; + [JsonIgnore] + public PointInfo Midpoint => new( + x: this.X + (this.Width / 2), + y: this.Y + (this.Height / 2)); + + [JsonIgnore] + public SizeInfo Size => new(this.Width, this.Height); + + public RectangleInfo Center(PointInfo point) => new( + x: point.X - (this.Width / 2), + y: point.Y - (this.Height / 2), + width: this.Width, + height: this.Height); + + /// + /// Moves this RectangleInfo inside the specified RectangleInfo. + /// + public RectangleInfo Clamp(RectangleInfo outer) + { + if ((this.Width > outer.Width) || (this.Height > outer.Height)) + { + throw new ArgumentException($"Value cannot be larger than {nameof(outer)}."); + } + + return new( + x: Math.Clamp(this.X, outer.X, outer.Right - this.Width), + y: Math.Clamp(this.Y, outer.Y, outer.Bottom - this.Height), + width: this.Width, + height: this.Height); + } /// /// Adapted from https://github.com/dotnet/runtime /// See https://github.com/dotnet/runtime/blob/dfd618dc648ba9b11dd0f8034f78113d69f223cd/src/libraries/System.Drawing.Primitives/src/System/Drawing/Rectangle.cs /// - public bool Contains(int x, int y) => + public bool Contains(decimal x, decimal y) => this.X <= x && x < this.X + this.Width && this.Y <= y && y < this.Y + this.Height; + /// + /// Adapted from https://github.com/dotnet/runtime + /// See https://github.com/dotnet/runtime/blob/dfd618dc648ba9b11dd0f8034f78113d69f223cd/src/libraries/System.Drawing.Primitives/src/System/Drawing/Rectangle.cs + /// + public bool Contains(PointInfo pt) => + this.Contains(pt.X, pt.Y); + /// /// Adapted from https://github.com/dotnet/runtime /// See https://github.com/dotnet/runtime/blob/dfd618dc648ba9b11dd0f8034f78113d69f223cd/src/libraries/System.Drawing.Primitives/src/System/Drawing/Rectangle.cs @@ -77,11 +124,26 @@ public bool Contains(RectangleInfo rect) => (this.X <= rect.X) && (rect.X + rect.Width <= this.X + this.Width) && (this.Y <= rect.Y) && (rect.Y + rect.Height <= this.Y + this.Height); - public RectangleInfo Enlarge(PaddingInfo padding) => new( - this.X + padding.Left, - this.Y + padding.Top, - this.Width + padding.Horizontal, - this.Height + padding.Vertical); + public RectangleInfo Enlarge(Styles.BorderStyle border) => + new( + this.X - border.Left, + this.Y - border.Top, + this.Width + border.Horizontal, + this.Height + border.Vertical); + + public RectangleInfo Enlarge(MarginStyle margin) => + new( + this.X - margin.Left, + this.Y - margin.Top, + this.Width + margin.Horizontal, + this.Height + margin.Vertical); + + public RectangleInfo Enlarge(PaddingStyle padding) => + new( + this.X - padding.Left, + this.Y - padding.Top, + this.Width + padding.Horizontal, + this.Height + padding.Vertical); public RectangleInfo Offset(SizeInfo amount) => this.Offset(amount.Width, amount.Height); @@ -93,28 +155,46 @@ public bool Contains(RectangleInfo rect) => this.Width * scalingFactor, this.Height * scalingFactor); - public RectangleInfo Center(PointInfo point) => new( - x: point.X - (this.Width / 2), - y: point.Y - (this.Height / 2), - width: this.Width, - height: this.Height); - - public PointInfo Midpoint => new( - x: this.X + (this.Width / 2), - y: this.Y + (this.Height / 2)); + public RectangleInfo Shrink(Styles.BorderStyle border) => + new( + this.X + border.Left, + this.Y + border.Top, + this.Width - border.Horizontal, + this.Height - border.Vertical); + + public RectangleInfo Shrink(MarginStyle margin) => + new( + this.X + margin.Left, + this.Y + margin.Top, + this.Width - margin.Horizontal, + this.Height - margin.Vertical); + + public RectangleInfo Shrink(PaddingStyle padding) => + new( + this.X + padding.Left, + this.Y + padding.Top, + this.Width - padding.Horizontal, + this.Height - padding.Vertical); + + public RectangleInfo Truncate() => + new( + (int)this.X, + (int)this.Y, + (int)this.Width, + (int)this.Height); - public RectangleInfo Clamp(RectangleInfo outer) + /// + /// Adapted from https://github.com/dotnet/runtime + /// See https://github.com/dotnet/runtime/blob/dfd618dc648ba9b11dd0f8034f78113d69f223cd/src/libraries/System.Drawing.Primitives/src/System/Drawing/Rectangle.cs + /// + public RectangleInfo Union(RectangleInfo rect) { - if ((this.Width > outer.Width) || (this.Height > outer.Height)) - { - throw new ArgumentException($"Value cannot be larger than {nameof(outer)}."); - } + var x1 = Math.Min(this.X, rect.X); + var x2 = Math.Max(this.X + this.Width, rect.X + rect.Width); + var y1 = Math.Min(this.Y, rect.Y); + var y2 = Math.Max(this.Y + this.Height, rect.Y + rect.Height); - return new( - x: Math.Clamp(this.X, outer.X, outer.Right - this.Width), - y: Math.Clamp(this.Y, outer.Y, outer.Bottom - this.Height), - width: this.Width, - height: this.Height); + return new RectangleInfo(x1, y1, x2 - x1, y2 - y1); } public Rectangle ToRectangle() => new((int)this.X, (int)this.Y, (int)this.Width, (int)this.Height); diff --git a/src/FancyMouse/Models/Screen/ScreenInfo.cs b/src/FancyMouse/Models/Drawing/ScreenInfo.cs similarity index 74% rename from src/FancyMouse/Models/Screen/ScreenInfo.cs rename to src/FancyMouse/Models/Drawing/ScreenInfo.cs index a15f34e..96c22da 100644 --- a/src/FancyMouse/Models/Screen/ScreenInfo.cs +++ b/src/FancyMouse/Models/Drawing/ScreenInfo.cs @@ -1,6 +1,6 @@ -using FancyMouse.Models.Drawing; +using FancyMouse.NativeMethods; -namespace FancyMouse.Models.Screen; +namespace FancyMouse.Models.Drawing; /// /// Immutable version of a System.Windows.Forms.Screen object so we don't need to @@ -8,7 +8,7 @@ namespace FancyMouse.Models.Screen; /// public sealed class ScreenInfo { - internal ScreenInfo(int handle, bool primary, RectangleInfo displayArea, RectangleInfo workingArea) + internal ScreenInfo(Core.HMONITOR handle, bool primary, RectangleInfo displayArea, RectangleInfo workingArea) { this.Handle = handle; this.Primary = primary; @@ -31,9 +31,6 @@ public RectangleInfo DisplayArea get; } - public RectangleInfo Bounds => - this.DisplayArea; - public RectangleInfo WorkingArea { get; diff --git a/src/FancyMouse/Models/Drawing/SizeInfo.cs b/src/FancyMouse/Models/Drawing/SizeInfo.cs index 9b15cfd..c4943b7 100644 --- a/src/FancyMouse/Models/Drawing/SizeInfo.cs +++ b/src/FancyMouse/Models/Drawing/SizeInfo.cs @@ -1,4 +1,6 @@ -namespace FancyMouse.Models.Drawing; +using FancyMouse.Models.Styles; + +namespace FancyMouse.Models.Drawing; /// /// Immutable version of a System.Drawing.Size object with some extra utility methods. @@ -26,13 +28,32 @@ public decimal Height get; } - public SizeInfo Negate() => new(-this.Width, -this.Height); + public SizeInfo Enlarge(Styles.BorderStyle border) => + new( + this.Width + border.Horizontal, + this.Height + border.Vertical); + + public SizeInfo Enlarge(PaddingStyle padding) => + new( + this.Width + padding.Horizontal, + this.Height + padding.Vertical); + + public SizeInfo Intersect(SizeInfo size) => + new( + Math.Min(this.Width, size.Width), + Math.Min(this.Height, size.Height)); + + public SizeInfo Negate() => + new(-this.Width, -this.Height); - public SizeInfo Shrink(PaddingInfo padding) => new(this.Width - padding.Horizontal, this.Height - padding.Vertical); + public SizeInfo Shrink(Styles.BorderStyle border) => + new(this.Width - border.Horizontal, this.Height - border.Vertical); - public SizeInfo Intersect(SizeInfo size) => new( - Math.Min(this.Width, size.Width), - Math.Min(this.Height, size.Height)); + public SizeInfo Shrink(MarginStyle margin) => + new(this.Width - margin.Horizontal, this.Height - margin.Vertical); + + public SizeInfo Shrink(PaddingStyle padding) => + new(this.Width - padding.Horizontal, this.Height - padding.Vertical); public RectangleInfo PlaceAt(decimal x, decimal y) => new(x, y, this.Width, this.Height); @@ -48,6 +69,13 @@ public SizeInfo ScaleToFit(SizeInfo bounds) }; } + public SizeInfo Floor() + { + return new SizeInfo( + Math.Floor(this.Width), + Math.Floor(this.Height)); + } + /// /// Get the scaling ratio to scale obj by so that it fits inside the specified bounds /// without distorting the aspect ratio. diff --git a/src/FancyMouse/Models/Layout/BorderLayout.cs b/src/FancyMouse/Models/Layout/BorderLayout.cs deleted file mode 100644 index 635f790..0000000 --- a/src/FancyMouse/Models/Layout/BorderLayout.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace FancyMouse.Models.Layout; - -internal sealed class BorderLayout -{ - public BorderLayout( - bool visible, - int width, - Color color) - { - this.Visible = visible; - this.Width = width; - this.Color = color; - } - - public bool Visible - { - get; - } - - public int Width - { - get; - } - - public Color Color - { - get; - } -} diff --git a/src/FancyMouse/Models/Layout/LayoutConfig.cs b/src/FancyMouse/Models/Layout/LayoutConfig.cs deleted file mode 100644 index 8e18eee..0000000 --- a/src/FancyMouse/Models/Layout/LayoutConfig.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Collections.ObjectModel; -using FancyMouse.Models.Drawing; -using FancyMouse.Models.Screen; - -namespace FancyMouse.Models.Layout; - -/// -/// Represents a collection of values needed for calculating the MainForm layout. -/// -public sealed class LayoutConfig -{ - public LayoutConfig( - RectangleInfo virtualScreenBounds, - List screens, - PointInfo activatedLocation, - int activatedScreenIndex, - int activatedScreenNumber, - SizeInfo maximumFormSize, - PaddingInfo formPadding, - PaddingInfo previewPadding) - { - // make sure the virtual screen entirely contains all of the individual screen bounds - ArgumentNullException.ThrowIfNull(virtualScreenBounds); - ArgumentNullException.ThrowIfNull(screens); - if (screens.Any(screen => !virtualScreenBounds.Contains(screen.Bounds))) - { - throw new ArgumentException($"'{nameof(virtualScreenBounds)}' must contain all of the screens in '{nameof(screens)}'", nameof(virtualScreenBounds)); - } - - this.VirtualScreenBounds = virtualScreenBounds; - this.Screens = new(screens.ToList()); - this.ActivatedLocation = activatedLocation; - this.ActivatedScreenIndex = activatedScreenIndex; - this.ActivatedScreenNumber = activatedScreenNumber; - this.MaximumFormSize = maximumFormSize; - this.FormPadding = formPadding; - this.PreviewPadding = previewPadding; - } - - /// - /// Gets the coordinates of the entire virtual screen. - /// - /// - /// The Virtual Screen is the bounding rectangle of all the monitors. - /// https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen - /// - public RectangleInfo VirtualScreenBounds - { - get; - } - - /// - /// Gets a collection containing the individual screens connected to the system. - /// - public ReadOnlyCollection Screens - { - get; - } - - /// - /// Gets the point where the cursor was located when the form was activated. - /// - /// - /// The preview form will be centered on this location unless there are any - /// constraints such as being too close to edge of a screen, in which case - /// the form will be displayed centered as close as possible to this location. - /// - public PointInfo ActivatedLocation - { - get; - } - - /// - /// Gets the index of the screen the cursor was on when the form was activated. - /// The value is an index into the ScreenBounds array and is 0-indexed as a result. - /// - public int ActivatedScreenIndex - { - get; - } - - /// - /// Gets the screen number the cursor was on when the form was activated. - /// The value matches the screen numbering scheme in the "Display Settings" dialog - /// and is 1-indexed as a result. - /// - public int ActivatedScreenNumber - { - get; - } - - /// - /// Gets the maximum size of the screen preview form. - /// - public SizeInfo MaximumFormSize - { - get; - } - - /// - /// Gets the padding border around the screen preview form. - /// - public PaddingInfo FormPadding - { - get; - } - - /// - /// Gets the padding border inside the screen preview image. - /// - public PaddingInfo PreviewPadding - { - get; - } -} diff --git a/src/FancyMouse/Models/Layout/LayoutInfo.cs b/src/FancyMouse/Models/Layout/LayoutInfo.cs deleted file mode 100644 index 325edf8..0000000 --- a/src/FancyMouse/Models/Layout/LayoutInfo.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.ObjectModel; -using FancyMouse.Models.Drawing; - -namespace FancyMouse.Models.Layout; - -public sealed class LayoutInfo -{ - public sealed class Builder - { - public Builder() - { - this.ScreenBounds = new(); - } - - public LayoutConfig? LayoutConfig - { - get; - set; - } - - public RectangleInfo? FormBounds - { - get; - set; - } - - public RectangleInfo? PreviewBounds - { - get; - set; - } - - public List ScreenBounds - { - get; - set; - } - - public RectangleInfo? ActivatedScreenBounds - { - get; - set; - } - - public LayoutInfo Build() - { - return new LayoutInfo( - layoutConfig: this.LayoutConfig ?? throw new InvalidOperationException(), - formBounds: this.FormBounds ?? throw new InvalidOperationException(), - previewBounds: this.PreviewBounds ?? throw new InvalidOperationException(), - screenBounds: this.ScreenBounds ?? throw new InvalidOperationException(), - activatedScreenBounds: this.ActivatedScreenBounds ?? throw new InvalidOperationException()); - } - } - - public LayoutInfo( - LayoutConfig layoutConfig, - RectangleInfo formBounds, - RectangleInfo previewBounds, - IEnumerable screenBounds, - RectangleInfo activatedScreenBounds) - { - this.LayoutConfig = layoutConfig ?? throw new ArgumentNullException(nameof(layoutConfig)); - this.FormBounds = formBounds ?? throw new ArgumentNullException(nameof(formBounds)); - this.PreviewBounds = previewBounds ?? throw new ArgumentNullException(nameof(previewBounds)); - this.ScreenBounds = new( - (screenBounds ?? throw new ArgumentNullException(nameof(screenBounds))) - .ToList()); - this.ActivatedScreenBounds = activatedScreenBounds ?? throw new ArgumentNullException(nameof(activatedScreenBounds)); - } - - /// - /// Gets the original LayoutConfig settings used to calculate coordinates. - /// - public LayoutConfig LayoutConfig - { - get; - } - - /// - /// Gets the size and location of the preview form. - /// - public RectangleInfo FormBounds - { - get; - } - - /// - /// Gets the size and location of the preview image. - /// - public RectangleInfo PreviewBounds - { - get; - } - - public ReadOnlyCollection ScreenBounds - { - get; - } - - public RectangleInfo ActivatedScreenBounds - { - get; - } -} diff --git a/src/FancyMouse/Models/Layout/PreviewLayout.cs b/src/FancyMouse/Models/Layout/PreviewLayout.cs index 842accc..a63ad2c 100644 --- a/src/FancyMouse/Models/Layout/PreviewLayout.cs +++ b/src/FancyMouse/Models/Layout/PreviewLayout.cs @@ -1,18 +1,127 @@ -using FancyMouse.Models.Drawing; +using System.Collections.ObjectModel; +using FancyMouse.Models.Drawing; +using FancyMouse.Models.Styles; namespace FancyMouse.Models.Layout; -internal sealed class PreviewLayout +public sealed class PreviewLayout { + public sealed class Builder + { + public Builder() + { + this.Screens = new(); + this.ScreenshotBounds = new(); + } + + public PreviewStyle? PreviewStyle + { + get; + set; + } + + public RectangleInfo? VirtualScreen + { + get; + set; + } + + public List Screens + { + get; + set; + } + + public int ActivatedScreenIndex + { + get; + set; + } + + public RectangleInfo? FormBounds + { + get; + set; + } + + public BoxBounds? PreviewBounds + { + get; + set; + } + + public List ScreenshotBounds + { + get; + set; + } + + public PreviewLayout Build() + { + return new PreviewLayout( + previewStyle: this.PreviewStyle ?? throw new InvalidOperationException($"{nameof(this.PreviewStyle)} must be initialized before calling {nameof(this.Build)}."), + virtualScreen: this.VirtualScreen ?? throw new InvalidOperationException($"{nameof(this.VirtualScreen)} must be initialized before calling {nameof(this.Build)}."), + screens: this.Screens ?? throw new InvalidOperationException($"{nameof(this.Screens)} must be initialized before calling {nameof(this.Build)}."), + activatedScreenIndex: this.ActivatedScreenIndex, + formBounds: this.FormBounds ?? throw new InvalidOperationException($"{nameof(this.FormBounds)} must be initialized before calling {nameof(this.Build)}."), + previewBounds: this.PreviewBounds ?? throw new InvalidOperationException($"{nameof(this.PreviewBounds)} must be initialized before calling {nameof(this.Build)}."), + screenshotBounds: this.ScreenshotBounds ?? throw new InvalidOperationException($"{nameof(this.ScreenshotBounds)} must be initialized before calling {nameof(this.Build)}.")); + } + } + public PreviewLayout( - SizeInfo previewSize, - RectangleInfo borderCoords, - Region backgroundRegion) + PreviewStyle previewStyle, + RectangleInfo virtualScreen, + List screens, + int activatedScreenIndex, + RectangleInfo formBounds, + BoxBounds previewBounds, + List screenshotBounds) + { + this.PreviewStyle = previewStyle ?? throw new ArgumentNullException(nameof(previewStyle)); + this.VirtualScreen = virtualScreen ?? throw new ArgumentNullException(nameof(virtualScreen)); + this.Screens = new( + (screens ?? throw new ArgumentNullException(nameof(screens))) + .ToList()); + this.ActivatedScreenIndex = activatedScreenIndex; + this.FormBounds = formBounds ?? throw new ArgumentNullException(nameof(formBounds)); + this.PreviewBounds = previewBounds ?? throw new ArgumentNullException(nameof(previewBounds)); + this.ScreenshotBounds = new( + (screenshotBounds ?? throw new ArgumentNullException(nameof(screenshotBounds))) + .ToList()); + } + + public PreviewStyle PreviewStyle + { + get; + } + + public RectangleInfo VirtualScreen { - this.PreviewSize = previewSize ?? throw new ArgumentNullException(nameof(previewSize)); + get; + } + + public ReadOnlyCollection Screens + { + get; + } + + public int ActivatedScreenIndex + { + get; + } + + public RectangleInfo FormBounds + { + get; + } + + public BoxBounds PreviewBounds + { + get; } - public SizeInfo PreviewSize + public ReadOnlyCollection ScreenshotBounds { get; } diff --git a/src/FancyMouse/Models/Settings/AppSettings.cs b/src/FancyMouse/Models/Settings/AppSettings.cs new file mode 100644 index 0000000..6c26bbf --- /dev/null +++ b/src/FancyMouse/Models/Settings/AppSettings.cs @@ -0,0 +1,74 @@ +using FancyMouse.Models.Styles; +using FancyMouse.WindowsHotKeys; +using Keys = FancyMouse.WindowsHotKeys.Keys; + +namespace FancyMouse.Models.Settings; + +/// +/// Represents the settings used to control application behaviour. +/// This is different to the AppConfig class that is used to +/// serialize / deserialize settings into the application config file. +/// +internal sealed class AppSettings +{ + public static readonly AppSettings DefaultSettings = new( + hotkey: new( + key: Keys.F, + modifiers: KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift + ), + previewStyle: new( + canvasSize: new( + width: 1600, + height: 1200 + ), + canvasStyle: new( + marginStyle: MarginStyle.Empty, + borderStyle: new( + color: SystemColors.Highlight, + all: 6, + depth: 0 + ), + paddingStyle: new( + all: 4 + ), + backgroundStyle: new( + color1: Color.FromArgb(0xFF, 0x0D, 0x57, 0xD2), + color2: Color.FromArgb(0xFF, 0x03, 0x44, 0xC0) + ) + ), + screenshotStyle: new( + marginStyle: new( + all: 4 + ), + borderStyle: new( + color: Color.FromArgb(0xFF, 0x22, 0x22, 0x22), + all: 10, + depth: 3 + ), + paddingStyle: PaddingStyle.Empty, + backgroundStyle: new( + color1: Color.MidnightBlue, + color2: Color.MidnightBlue + ) + ) + ) + ); + + public AppSettings( + Keystroke hotkey, + PreviewStyle previewStyle) + { + this.Hotkey = hotkey ?? throw new ArgumentNullException(nameof(hotkey)); + this.PreviewStyle = previewStyle ?? throw new ArgumentNullException(nameof(previewStyle)); + } + + public Keystroke Hotkey + { + get; + } + + public PreviewStyle PreviewStyle + { + get; + } +} diff --git a/src/FancyMouse/Models/Settings/AppSettingsReader.cs b/src/FancyMouse/Models/Settings/AppSettingsReader.cs new file mode 100644 index 0000000..fb7547a --- /dev/null +++ b/src/FancyMouse/Models/Settings/AppSettingsReader.cs @@ -0,0 +1,43 @@ +using System.Text.Json.Nodes; + +namespace FancyMouse.Models.Settings; + +internal static class AppSettingsReader +{ + public static AppSettings ReadFile(string filename) + { + // determine the version of the config file so we know which converter to use + var configJson = File.ReadAllText(filename); + return AppSettingsReader.ParseJson(configJson); + } + + public static AppSettings ParseJson(string? configJson) + { + if (configJson is null) + { + return AppSettings.DefaultSettings; + } + + // determine the version of the config file so we know which converter to use + int configVersion; + try + { + var configNode = JsonNode.Parse(configJson); + + // if the version isn't specified we'll default to v1 + configVersion = configNode?["version"]?.GetValue() ?? 1; + } + catch + { + return AppSettings.DefaultSettings; + } + + var appSettings = configVersion switch + { + 1 => V1.SettingsConverter.ParseAppSettings(configJson), + 2 => V2.SettingsConverter.ParseAppSettings(configJson), + _ => AppSettings.DefaultSettings, + }; + return appSettings; + } +} diff --git a/src/FancyMouse/Models/Settings/V1/AppConfig.cs b/src/FancyMouse/Models/Settings/V1/AppConfig.cs new file mode 100644 index 0000000..1729713 --- /dev/null +++ b/src/FancyMouse/Models/Settings/V1/AppConfig.cs @@ -0,0 +1,20 @@ +namespace FancyMouse.Models.Settings.V1; + +/// +/// Represents the configuration file format to allow for easier +/// serialization / deserialization. This needs to be converted +/// into an AppSettings object for the main application to use. +/// +internal sealed class AppConfig +{ + public AppConfig( + FancyMouseSettings? fancymouse) + { + this.FancyMouse = fancymouse; + } + + public FancyMouseSettings? FancyMouse + { + get; + } +} diff --git a/src/FancyMouse/Models/Settings/V1/FancyMouseSettings.cs b/src/FancyMouse/Models/Settings/V1/FancyMouseSettings.cs new file mode 100644 index 0000000..ff9b2f4 --- /dev/null +++ b/src/FancyMouse/Models/Settings/V1/FancyMouseSettings.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace FancyMouse.Models.Settings.V1; + +public sealed class FancyMouseSettings +{ + public FancyMouseSettings( + string? hotkey, + string? previewSize) + { + this.Hotkey = hotkey; + this.PreviewSize = previewSize; + } + + [JsonPropertyName("hotkey")] + public string? Hotkey + { + get; + } + + [JsonPropertyName("preview")] + public string? PreviewSize + { + get; + } +} diff --git a/src/FancyMouse/Models/Settings/V1/SettingsConverter.cs b/src/FancyMouse/Models/Settings/V1/SettingsConverter.cs new file mode 100644 index 0000000..e0ec3bd --- /dev/null +++ b/src/FancyMouse/Models/Settings/V1/SettingsConverter.cs @@ -0,0 +1,52 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using FancyMouse.Models.Styles; +using FancyMouse.WindowsHotKeys; + +namespace FancyMouse.Models.Settings.V1; + +internal static class SettingsConverter +{ + public static AppSettings ParseAppSettings(string json) + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() }, + }; + var appConfig = JsonSerializer.Deserialize(json, options) + ?? throw new InvalidOperationException(); + var hotkey = SettingsConverter.ConvertToKeystroke(appConfig?.FancyMouse?.Hotkey); + var previewStyle = SettingsConverter.ConvertToPreviewStyle(appConfig?.FancyMouse?.PreviewSize); + var appSettings = new AppSettings(hotkey, previewStyle); + return appSettings; + } + + public static Keystroke ConvertToKeystroke(string? hotkey) + { + return (hotkey == null) + ? AppSettings.DefaultSettings.Hotkey + : Keystroke.Parse(hotkey); + } + + public static PreviewStyle ConvertToPreviewStyle(string? previewSize) + { + if (previewSize is null) + { + return AppSettings.DefaultSettings.PreviewStyle; + } + + var parts = previewSize.Split("x") + .Select(part => int.Parse(part.Trim(), CultureInfo.InvariantCulture)) + .ToList(); + + return new PreviewStyle( + canvasSize: new( + width: parts[0], + height: parts[1] + ), + canvasStyle: AppSettings.DefaultSettings.PreviewStyle.CanvasStyle, + screenshotStyle: AppSettings.DefaultSettings.PreviewStyle.ScreenshotStyle); + } +} diff --git a/src/FancyMouse/Models/Settings/V2/AppConfig.cs b/src/FancyMouse/Models/Settings/V2/AppConfig.cs new file mode 100644 index 0000000..75bbaaf --- /dev/null +++ b/src/FancyMouse/Models/Settings/V2/AppConfig.cs @@ -0,0 +1,27 @@ +namespace FancyMouse.Models.Settings.V2; + +/// +/// Represents the configuration file format to allow for easier +/// serialization / deserialization. This needs to be converted +/// into an AppSettings object for the main application to use. +/// +internal sealed class AppConfig +{ + public AppConfig( + string? hotkey, + PreviewSettings? preview) + { + this.Hotkey = hotkey; + this.Preview = preview; + } + + public string? Hotkey + { + get; + } + + public PreviewSettings? Preview + { + get; + } +} diff --git a/src/FancyMouse/Models/Settings/V2/BackgroundStyleSettings.cs b/src/FancyMouse/Models/Settings/V2/BackgroundStyleSettings.cs new file mode 100644 index 0000000..796b6ec --- /dev/null +++ b/src/FancyMouse/Models/Settings/V2/BackgroundStyleSettings.cs @@ -0,0 +1,33 @@ +namespace FancyMouse.Models.Settings.V2; + +/// +/// Represents the background fill style for a drawing object. +/// +public sealed class BackgroundStyleSettings +{ + public BackgroundStyleSettings( + string color1, + string color2) + { + this.Color1 = color1; + this.Color2 = color2; + } + + public string Color1 + { + get; + } + + public string Color2 + { + get; + } + + public override string ToString() + { + return "{" + + $"{nameof(this.Color1)}={this.Color1}," + + $"{nameof(this.Color2)}={this.Color2}" + + "}"; + } +} diff --git a/src/FancyMouse/Models/Settings/V2/BorderStyleSettings.cs b/src/FancyMouse/Models/Settings/V2/BorderStyleSettings.cs new file mode 100644 index 0000000..85660f8 --- /dev/null +++ b/src/FancyMouse/Models/Settings/V2/BorderStyleSettings.cs @@ -0,0 +1,41 @@ +namespace FancyMouse.Models.Settings.V2; + +/// +/// Represents the border style for a drawing object. +/// +public sealed class BorderStyleSettings +{ + public BorderStyleSettings(string color, decimal width, decimal depth) + { + this.Color = color; + this.Width = width; + this.Depth = depth; + } + + public string Color + { + get; + } + + public decimal Width + { + get; + } + + /// + /// Gets the "depth" of the 3d highlight and shadow effect on the border. + /// + public decimal Depth + { + get; + } + + public override string ToString() + { + return "{" + + $"{nameof(this.Color)}={this.Color}," + + $"{nameof(this.Width)}={this.Width}," + + $"{nameof(this.Depth)}={this.Depth}" + + "}"; + } +} diff --git a/src/FancyMouse/Models/Settings/V2/CanvasSizeSettings.cs b/src/FancyMouse/Models/Settings/V2/CanvasSizeSettings.cs new file mode 100644 index 0000000..7c191bf --- /dev/null +++ b/src/FancyMouse/Models/Settings/V2/CanvasSizeSettings.cs @@ -0,0 +1,22 @@ +namespace FancyMouse.Models.Settings.V2; + +public sealed class CanvasSizeSettings +{ + public CanvasSizeSettings( + int width, + int height) + { + this.Width = width; + this.Height = height; + } + + public int Width + { + get; + } + + public int Height + { + get; + } +} diff --git a/src/FancyMouse/Models/Settings/V2/CanvasStyleSettings.cs b/src/FancyMouse/Models/Settings/V2/CanvasStyleSettings.cs new file mode 100644 index 0000000..cace5b6 --- /dev/null +++ b/src/FancyMouse/Models/Settings/V2/CanvasStyleSettings.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; + +namespace FancyMouse.Models.Settings.V2; + +/// +/// Doesn't have a MarginStyle setting like the BoxStyle class does - we don't +/// support configuring this in app settings. +/// > +public sealed class CanvasStyleSettings +{ + public CanvasStyleSettings( + BorderStyleSettings borderStyle, + PaddingStyleSettings paddingStyle, + BackgroundStyleSettings backgroundStyle) + { + this.BorderStyle = borderStyle ?? throw new ArgumentNullException(nameof(borderStyle)); + this.PaddingStyle = paddingStyle ?? throw new ArgumentNullException(nameof(paddingStyle)); + this.BackgroundStyle = backgroundStyle ?? throw new ArgumentNullException(nameof(backgroundStyle)); + } + + [JsonPropertyName("border")] + public BorderStyleSettings BorderStyle + { + get; + } + + [JsonPropertyName("padding")] + public PaddingStyleSettings PaddingStyle + { + get; + } + + [JsonPropertyName("background")] + public BackgroundStyleSettings BackgroundStyle + { + get; + } +} diff --git a/src/FancyMouse/Models/Settings/V2/MarginStyleSettings.cs b/src/FancyMouse/Models/Settings/V2/MarginStyleSettings.cs new file mode 100644 index 0000000..2523029 --- /dev/null +++ b/src/FancyMouse/Models/Settings/V2/MarginStyleSettings.cs @@ -0,0 +1,24 @@ +namespace FancyMouse.Models.Settings.V2; + +/// +/// Represents the margin style for a drawing object. +/// +public sealed class MarginStyleSettings +{ + public MarginStyleSettings(decimal width) + { + this.Width = width; + } + + public decimal Width + { + get; + } + + public override string ToString() + { + return "{" + + $"{nameof(this.Width)}={this.Width}" + + "}"; + } +} diff --git a/src/FancyMouse/Models/Settings/V2/PaddingStyleSettings.cs b/src/FancyMouse/Models/Settings/V2/PaddingStyleSettings.cs new file mode 100644 index 0000000..2f5e82b --- /dev/null +++ b/src/FancyMouse/Models/Settings/V2/PaddingStyleSettings.cs @@ -0,0 +1,24 @@ +namespace FancyMouse.Models.Settings.V2; + +/// +/// Represents the margin style for a drawing object. +/// +public sealed class PaddingStyleSettings +{ + public PaddingStyleSettings(decimal width) + { + this.Width = width; + } + + public decimal Width + { + get; + } + + public override string ToString() + { + return "{" + + $"{nameof(this.Width)}={this.Width}" + + "}"; + } +} diff --git a/src/FancyMouse/Models/Settings/V2/PreviewSettings.cs b/src/FancyMouse/Models/Settings/V2/PreviewSettings.cs new file mode 100644 index 0000000..46cde5d --- /dev/null +++ b/src/FancyMouse/Models/Settings/V2/PreviewSettings.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace FancyMouse.Models.Settings.V2; + +public class PreviewSettings +{ + public PreviewSettings( + CanvasSizeSettings? canvasSize, + CanvasStyleSettings? canvasStyle, + ScreenshotStyleSettings? screenshotStyle) + { + this.CanvasSize = canvasSize; + this.CanvasStyle = canvasStyle; + this.ScreenshotStyle = screenshotStyle; + } + + [JsonPropertyName("size")] + public CanvasSizeSettings? CanvasSize + { + get; + } + + [JsonPropertyName("canvas")] + public CanvasStyleSettings? CanvasStyle + { + get; + } + + [JsonPropertyName("screenshot")] + public ScreenshotStyleSettings? ScreenshotStyle + { + get; + } +} diff --git a/src/FancyMouse/Models/Settings/V2/ScreenshotStyleSettings.cs b/src/FancyMouse/Models/Settings/V2/ScreenshotStyleSettings.cs new file mode 100644 index 0000000..d582485 --- /dev/null +++ b/src/FancyMouse/Models/Settings/V2/ScreenshotStyleSettings.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; + +namespace FancyMouse.Models.Settings.V2; + +/// +/// Doesn't have a PaddingStyle setting like the BoxStyle class does - we don't +/// support configuring this in app settings. +/// > +public sealed class ScreenshotStyleSettings +{ + public ScreenshotStyleSettings( + MarginStyleSettings marginStyle, + BorderStyleSettings borderStyle, + BackgroundStyleSettings backgroundStyle) + { + this.MarginStyle = marginStyle ?? throw new ArgumentNullException(nameof(marginStyle)); + this.BorderStyle = borderStyle ?? throw new ArgumentNullException(nameof(borderStyle)); + this.BackgroundStyle = backgroundStyle ?? throw new ArgumentNullException(nameof(backgroundStyle)); + } + + [JsonPropertyName("margin")] + public MarginStyleSettings MarginStyle + { + get; + } + + [JsonPropertyName("border")] + public BorderStyleSettings BorderStyle + { + get; + } + + [JsonPropertyName("background")] + public BackgroundStyleSettings BackgroundStyle + { + get; + } +} diff --git a/src/FancyMouse/Models/Settings/V2/SettingsConverter.cs b/src/FancyMouse/Models/Settings/V2/SettingsConverter.cs new file mode 100644 index 0000000..b1265ac --- /dev/null +++ b/src/FancyMouse/Models/Settings/V2/SettingsConverter.cs @@ -0,0 +1,150 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using FancyMouse.Models.Styles; +using BorderStyle = FancyMouse.Models.Styles.BorderStyle; + +namespace FancyMouse.Models.Settings.V2; + +internal static class SettingsConverter +{ + public static AppSettings ParseAppSettings(string json) + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() }, + }; + var appConfig = JsonSerializer.Deserialize(json, options) + ?? throw new InvalidOperationException(); + var hotkey = V1.SettingsConverter.ConvertToKeystroke(appConfig.Hotkey); + var previewStyle = SettingsConverter.ConvertToPreviewStyle(appConfig.Preview); + var appSettings = new AppSettings(hotkey, previewStyle); + return appSettings; + } + + public static PreviewStyle ConvertToPreviewStyle(PreviewSettings? previewSettings) + { + if (previewSettings is null) + { + return AppSettings.DefaultSettings.PreviewStyle; + } + + var defaultStyle = AppSettings.DefaultSettings.PreviewStyle; + var canvasStyle = AppSettings.DefaultSettings.PreviewStyle.CanvasStyle; + var screenshotStyle = AppSettings.DefaultSettings.PreviewStyle.ScreenshotStyle; + return new PreviewStyle( + canvasSize: new( + width: Math.Clamp( + value: previewSettings?.CanvasSize?.Width ?? defaultStyle.CanvasSize.Width, + min: 50, + max: 99999), + height: Math.Clamp( + value: previewSettings?.CanvasSize?.Height ?? defaultStyle.CanvasSize.Height, + min: 50, + max: 99999) + ), + canvasStyle: new( + marginStyle: MarginStyle.Empty, + borderStyle: SettingsConverter.ConvertToBorderStyle(previewSettings?.CanvasStyle?.BorderStyle, defaultStyle.CanvasStyle.BorderStyle), + paddingStyle: new( + all: Math.Clamp( + value: previewSettings?.CanvasStyle?.PaddingStyle?.Width ?? canvasStyle.PaddingStyle.Top, + min: 0, + max: 99) + ), + backgroundStyle: new( + color1: SettingsConverter.ParseColorSettings( + value: previewSettings?.CanvasStyle?.BackgroundStyle?.Color1, + defaultValue: canvasStyle.BackgroundStyle.Color1), + color2: SettingsConverter.ParseColorSettings( + value: previewSettings?.CanvasStyle?.BackgroundStyle?.Color2, + defaultValue: canvasStyle.BackgroundStyle.Color2) + ) + ), + screenshotStyle: new( + marginStyle: new( + Math.Clamp( + value: previewSettings?.ScreenshotStyle?.MarginStyle?.Width ?? screenshotStyle.MarginStyle.Top, + min: 0, + max: 99) + ), + borderStyle: SettingsConverter.ConvertToBorderStyle(previewSettings?.ScreenshotStyle?.BorderStyle, defaultStyle.CanvasStyle.BorderStyle), + paddingStyle: PaddingStyle.Empty, + backgroundStyle: new( + color1: SettingsConverter.ParseColorSettings( + previewSettings?.ScreenshotStyle?.BackgroundStyle?.Color1, + defaultValue: screenshotStyle.BackgroundStyle.Color1), + color2: SettingsConverter.ParseColorSettings( + value: previewSettings?.ScreenshotStyle?.BackgroundStyle?.Color2, + defaultValue: screenshotStyle.BackgroundStyle.Color2) + ) + )); + } + + private static BorderStyle ConvertToBorderStyle(BorderStyleSettings? settings, BorderStyle defaultStyle) + { + return new( + color: settings?.Color is null + ? defaultStyle.Color + : SettingsConverter.ParseColorSettings( + value: settings.Color, + defaultValue: defaultStyle.Color), + all: Math.Clamp( + value: settings?.Width ?? defaultStyle.Top, + min: 0, + max: 99), + depth: Math.Clamp( + value: settings?.Depth ?? defaultStyle.Depth, + min: 0, + max: 99) + ); + } + + private static Color ParseColorSettings(string? value, Color defaultValue) + { + if (string.IsNullOrEmpty(value)) + { + return defaultValue; + } + + var comparison = StringComparison.InvariantCulture; + if (value.StartsWith("#", comparison)) + { + var culture = CultureInfo.InvariantCulture; + if ((value.Length == 7) + && int.TryParse(value[1..3], NumberStyles.HexNumber, culture, out var r) + && int.TryParse(value[3..5], NumberStyles.HexNumber, culture, out var g) + && int.TryParse(value[5..7], NumberStyles.HexNumber, culture, out var b)) + { + return Color.FromArgb(0xff, r, g, b); + } + } + + if (value.StartsWith("Color.", comparison)) + { + var propertyName = value["Color.".Length..]; + var property = typeof(Color).GetProperties() + .SingleOrDefault(property => property.Name == propertyName); + if (property is not null) + { + var propertyValue = property.GetValue(null, null); + return (propertyValue is null) ? defaultValue : (Color)propertyValue; + } + } + + if (value.StartsWith("SystemColors.", comparison)) + { + var propertyName = value["SystemColors.".Length..]; + var property = typeof(SystemColors).GetProperties() + .SingleOrDefault(property => property.Name == propertyName); + if (property is not null) + { + var propertyValue = property.GetValue(null, null); + return (propertyValue is null) ? defaultValue : (Color)propertyValue; + } + } + + return defaultValue; + } +} diff --git a/src/FancyMouse/Models/Styles/BackgroundStyle.cs b/src/FancyMouse/Models/Styles/BackgroundStyle.cs new file mode 100644 index 0000000..a10f0c7 --- /dev/null +++ b/src/FancyMouse/Models/Styles/BackgroundStyle.cs @@ -0,0 +1,35 @@ +namespace FancyMouse.Models.Styles; + +/// +/// Represents the background fill style for a drawing object. +/// +public sealed class BackgroundStyle +{ + public static readonly BackgroundStyle Empty = new(SystemColors.Control, SystemColors.Control); + + public BackgroundStyle( + Color color1, + Color color2) + { + this.Color1 = color1; + this.Color2 = color2; + } + + public Color Color1 + { + get; + } + + public Color Color2 + { + get; + } + + public override string ToString() + { + return "{" + + $"{nameof(this.Color1)}={this.Color1}," + + $"{nameof(this.Color2)}={this.Color2}" + + "}"; + } +} diff --git a/src/FancyMouse/Models/Styles/BorderStyle.cs b/src/FancyMouse/Models/Styles/BorderStyle.cs new file mode 100644 index 0000000..508b0c6 --- /dev/null +++ b/src/FancyMouse/Models/Styles/BorderStyle.cs @@ -0,0 +1,77 @@ +using System.Text.Json.Serialization; + +namespace FancyMouse.Models.Styles; + +/// +/// Represents the border style for a drawing object. +/// +public sealed class BorderStyle +{ + public static readonly BorderStyle Empty = new(Color.Transparent, 0, 0); + + public BorderStyle(Color color, decimal all, decimal depth) + : this(color, all, all, all, all, depth) + { + } + + public BorderStyle(Color color, decimal left, decimal top, decimal right, decimal bottom, decimal depth) + { + this.Color = color; + this.Left = left; + this.Top = top; + this.Right = right; + this.Bottom = bottom; + this.Depth = depth; + } + + public Color Color + { + get; + } + + public decimal Left + { + get; + } + + public decimal Top + { + get; + } + + public decimal Right + { + get; + } + + public decimal Bottom + { + get; + } + + /// + /// Gets the "depth" of the 3d highlight and shadow effect on the border. + /// + public decimal Depth + { + get; + } + + [JsonIgnore] + public decimal Horizontal => this.Left + this.Right; + + [JsonIgnore] + public decimal Vertical => this.Top + this.Bottom; + + public override string ToString() + { + return "{" + + $"{nameof(this.Color)}={this.Color}," + + $"{nameof(this.Left)}={this.Left}," + + $"{nameof(this.Top)}={this.Top}," + + $"{nameof(this.Right)}={this.Right}," + + $"{nameof(this.Bottom)}={this.Bottom}," + + $"{nameof(this.Depth)}={this.Depth}" + + "}"; + } +} diff --git a/src/FancyMouse/Models/Styles/BoxStyle.cs b/src/FancyMouse/Models/Styles/BoxStyle.cs new file mode 100644 index 0000000..1cc2af6 --- /dev/null +++ b/src/FancyMouse/Models/Styles/BoxStyle.cs @@ -0,0 +1,73 @@ +namespace FancyMouse.Models.Styles; + +/// +/// Represents the styles to apply to a simple box-layout based drawing object. +/// +public sealed class BoxStyle +{ + /* + + see https://www.w3schools.com/css/css_boxmodel.asp + + +--------------[bounds]---------------+ + |▒▒▒▒▒▒▒▒▒▒▒▒▒▒[margin]▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| + |▒▒▓▓▓▓▓▓▓▓▓▓▓▓[border]▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒| + |▒▒▓▓░░░░░░░░░░[padding]░░░░░░░░░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ [content] ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▒▒| + |▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒| + |▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| + +-------------------------------------+ + + */ + + public static readonly BoxStyle Empty = new(MarginStyle.Empty, BorderStyle.Empty, PaddingStyle.Empty, BackgroundStyle.Empty); + + public BoxStyle( + MarginStyle marginStyle, + BorderStyle borderStyle, + PaddingStyle paddingStyle, + BackgroundStyle backgroundStyle) + { + this.MarginStyle = marginStyle ?? throw new ArgumentNullException(nameof(marginStyle)); + this.BorderStyle = borderStyle ?? throw new ArgumentNullException(nameof(borderStyle)); + this.PaddingStyle = paddingStyle ?? throw new ArgumentNullException(nameof(paddingStyle)); + this.BackgroundStyle = backgroundStyle ?? throw new ArgumentNullException(nameof(backgroundStyle)); + } + + /// + /// Gets the margin style for this layout box. + /// + public MarginStyle MarginStyle + { + get; + } + + /// + /// Gets the border style for this layout box. + /// + public BorderStyle BorderStyle + { + get; + } + + /// + /// Gets the padding style for this layout box. + /// + public PaddingStyle PaddingStyle + { + get; + } + + /// + /// Gets the background fill style for the content area of this layout box. + /// + public BackgroundStyle BackgroundStyle + { + get; + } +} diff --git a/src/FancyMouse/Models/Drawing/PaddingInfo.cs b/src/FancyMouse/Models/Styles/MarginStyle.cs similarity index 70% rename from src/FancyMouse/Models/Drawing/PaddingInfo.cs rename to src/FancyMouse/Models/Styles/MarginStyle.cs index 065f649..f0bb777 100644 --- a/src/FancyMouse/Models/Drawing/PaddingInfo.cs +++ b/src/FancyMouse/Models/Styles/MarginStyle.cs @@ -1,16 +1,20 @@ -namespace FancyMouse.Models.Drawing; +using System.Text.Json.Serialization; + +namespace FancyMouse.Models.Styles; /// -/// Immutable version of a System.Windows.Forms.Padding object with some extra utility methods. +/// Represents the margin style for a drawing object. /// -public sealed class PaddingInfo +public sealed class MarginStyle { - public PaddingInfo(decimal all) + public static readonly MarginStyle Empty = new(0); + + public MarginStyle(decimal all) : this(all, all, all, all) { } - public PaddingInfo(decimal left, decimal top, decimal right, decimal bottom) + public MarginStyle(decimal left, decimal top, decimal right, decimal bottom) { this.Left = left; this.Top = top; @@ -38,8 +42,10 @@ public decimal Bottom get; } + [JsonIgnore] public decimal Horizontal => this.Left + this.Right; + [JsonIgnore] public decimal Vertical => this.Top + this.Bottom; public override string ToString() diff --git a/src/FancyMouse/Models/Styles/PaddingStyle.cs b/src/FancyMouse/Models/Styles/PaddingStyle.cs new file mode 100644 index 0000000..f2e5c20 --- /dev/null +++ b/src/FancyMouse/Models/Styles/PaddingStyle.cs @@ -0,0 +1,60 @@ +using System.Text.Json.Serialization; + +namespace FancyMouse.Models.Styles; + +/// +/// Represents the margin style for a drawing object. +/// +public sealed class PaddingStyle +{ + public static readonly PaddingStyle Empty = new(0); + + public PaddingStyle(decimal all) + : this(all, all, all, all) + { + } + + public PaddingStyle(decimal left, decimal top, decimal right, decimal bottom) + { + this.Left = left; + this.Top = top; + this.Right = right; + this.Bottom = bottom; + } + + public decimal Left + { + get; + } + + public decimal Top + { + get; + } + + public decimal Right + { + get; + } + + public decimal Bottom + { + get; + } + + [JsonIgnore] + public decimal Horizontal => this.Left + this.Right; + + [JsonIgnore] + public decimal Vertical => this.Top + this.Bottom; + + public override string ToString() + { + return "{" + + $"{nameof(this.Left)}={this.Left}," + + $"{nameof(this.Top)}={this.Top}," + + $"{nameof(this.Right)}={this.Right}," + + $"{nameof(this.Bottom)}={this.Bottom}" + + "}"; + } +} diff --git a/src/FancyMouse/Models/Styles/PreviewStyle.cs b/src/FancyMouse/Models/Styles/PreviewStyle.cs new file mode 100644 index 0000000..7f101dd --- /dev/null +++ b/src/FancyMouse/Models/Styles/PreviewStyle.cs @@ -0,0 +1,31 @@ +using FancyMouse.Models.Drawing; + +namespace FancyMouse.Models.Styles; + +public class PreviewStyle +{ + public PreviewStyle( + SizeInfo canvasSize, + BoxStyle canvasStyle, + BoxStyle screenshotStyle) + { + this.CanvasSize = canvasSize ?? throw new ArgumentNullException(nameof(canvasSize)); + this.CanvasStyle = canvasStyle ?? throw new ArgumentNullException(nameof(canvasStyle)); + this.ScreenshotStyle = screenshotStyle ?? throw new ArgumentNullException(nameof(screenshotStyle)); + } + + public SizeInfo CanvasSize + { + get; + } + + public BoxStyle CanvasStyle + { + get; + } + + public BoxStyle ScreenshotStyle + { + get; + } +} diff --git a/src/FancyMouse/Program.cs b/src/FancyMouse/Program.cs index 7df7580..71bb446 100644 --- a/src/FancyMouse/Program.cs +++ b/src/FancyMouse/Program.cs @@ -1,8 +1,6 @@ -using System.Globalization; -using FancyMouse.Models.Drawing; +using FancyMouse.Models.Settings; using FancyMouse.UI; using FancyMouse.WindowsHotKeys; -using Microsoft.Extensions.Configuration; using NLog; namespace FancyMouse; @@ -34,24 +32,14 @@ private static void Main() // create the notify icon for the application var notifyForm = new FancyMouseNotify(); - var config = new ConfigurationBuilder() - .AddJsonFile("FancyMouse.json") - .Build() - .GetSection("FancyMouse"); - - var preview = (config["Preview"] ?? throw new InvalidOperationException("Missing config value 'Preview'")) - .Split("x").Select(s => int.Parse(s.Trim(), CultureInfo.InvariantCulture)).ToList(); + var appSettings = AppSettingsReader.ReadFile(".\\appSettings.json"); // logger: LogManager.LoadConfiguration(".\\NLog.config").GetCurrentClassLogger(), var dialog = new FancyMouseDialog( new FancyMouseDialogOptions( - logger: LogManager.CreateNullLogger(), - maximumThumbnailSize: new SizeInfo( - preview[0], preview[1]))); + logger: LogManager.CreateNullLogger())); - var hotkey = Keystroke.Parse( - config["HotKey"] ?? throw new InvalidOperationException("Missing config value 'HotKey'")); - var hotKeyManager = new HotKeyManager(hotkey); + var hotKeyManager = new HotKeyManager(appSettings.Hotkey); hotKeyManager.HotKeyPressed += (_, _) => { diff --git a/src/FancyMouse/UI/FancyMouseDialog.cs b/src/FancyMouse/UI/FancyMouseDialog.cs index d66eaf6..a0d663b 100644 --- a/src/FancyMouse/UI/FancyMouseDialog.cs +++ b/src/FancyMouse/UI/FancyMouseDialog.cs @@ -16,8 +16,7 @@ private FancyMouseForm Form public void Show() { var form = this.Form; - form.Visible = false; - form.ShowThumbnail(); + form.ShowPreview(); // GC.Collect(); } diff --git a/src/FancyMouse/UI/FancyMouseDialogOptions.cs b/src/FancyMouse/UI/FancyMouseDialogOptions.cs index d317d91..27bf85e 100644 --- a/src/FancyMouse/UI/FancyMouseDialogOptions.cs +++ b/src/FancyMouse/UI/FancyMouseDialogOptions.cs @@ -1,25 +1,17 @@ -using FancyMouse.Models.Drawing; -using NLog; +using NLog; namespace FancyMouse.UI; internal sealed class FancyMouseDialogOptions { public FancyMouseDialogOptions( - ILogger logger, - SizeInfo maximumThumbnailSize) + ILogger logger) { this.Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.MaximumThumbnailImageSize = maximumThumbnailSize; } public ILogger Logger { get; } - - public SizeInfo MaximumThumbnailImageSize - { - get; - } } diff --git a/src/FancyMouse/UI/FancyMouseForm.Designer.cs b/src/FancyMouse/UI/FancyMouseForm.Designer.cs index bfeebca..a791356 100644 --- a/src/FancyMouse/UI/FancyMouseForm.Designer.cs +++ b/src/FancyMouse/UI/FancyMouseForm.Designer.cs @@ -1,6 +1,7 @@ namespace FancyMouse.UI; -partial class FancyMouseForm { +partial class FancyMouseForm +{ /// /// Required designer variable. @@ -11,8 +12,10 @@ partial class FancyMouseForm { /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) { - if (disposing && (components != null)) { + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { components.Dispose(); } base.Dispose(disposing); @@ -24,7 +27,8 @@ protected override void Dispose(bool disposing) { /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// - private void InitializeComponent() { + private void InitializeComponent() + { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FancyMouseForm)); panel1 = new Panel(); Thumbnail = new PictureBox(); @@ -39,7 +43,6 @@ private void InitializeComponent() { panel1.Dock = DockStyle.Fill; panel1.Location = new Point(0, 0); panel1.Name = "panel1"; - panel1.Padding = new Padding(5); panel1.Size = new Size(800, 450); panel1.TabIndex = 1; // @@ -47,9 +50,9 @@ private void InitializeComponent() { // Thumbnail.BackColor = SystemColors.ControlDarkDark; Thumbnail.Dock = DockStyle.Fill; - Thumbnail.Location = new Point(5, 5); + Thumbnail.Location = new Point(0, 0); Thumbnail.Name = "Thumbnail"; - Thumbnail.Size = new Size(790, 440); + Thumbnail.Size = new Size(800, 450); Thumbnail.SizeMode = PictureBoxSizeMode.StretchImage; Thumbnail.TabIndex = 1; Thumbnail.TabStop = false; diff --git a/src/FancyMouse/UI/FancyMouseForm.cs b/src/FancyMouse/UI/FancyMouseForm.cs index ee5a07b..899d0b0 100644 --- a/src/FancyMouse/UI/FancyMouseForm.cs +++ b/src/FancyMouse/UI/FancyMouseForm.cs @@ -1,9 +1,8 @@ using System.Diagnostics; -using System.Drawing.Imaging; using FancyMouse.Helpers; using FancyMouse.Models.Drawing; using FancyMouse.Models.Layout; -using NLog; +using FancyMouse.Models.Settings; using static FancyMouse.NativeMethods.Core; namespace FancyMouse.UI; @@ -21,6 +20,12 @@ private FancyMouseDialogOptions Options get; } + private PreviewLayout? PreviewLayout + { + get; + set; + } + private void FancyMouseForm_Load(object sender, EventArgs e) { } @@ -84,7 +89,7 @@ private void FancyMouseForm_KeyDown(object sender, KeyEventArgs e) if (targetScreenNumber.HasValue) { MouseHelper.SetCursorPosition( - screens[targetScreenNumber.Value - 1].Screen.Bounds.Midpoint); + screens[targetScreenNumber.Value - 1].Screen.DisplayArea.Midpoint); this.OnDeactivate(EventArgs.Empty); } } @@ -92,13 +97,7 @@ private void FancyMouseForm_KeyDown(object sender, KeyEventArgs e) private void FancyMouseForm_Deactivate(object sender, EventArgs e) { this.Hide(); - - if (this.Thumbnail.Image is not null) - { - var tmp = this.Thumbnail.Image; - this.Thumbnail.Image = null; - tmp.Dispose(); - } + this.ClearPreview(); } private void Thumbnail_Click(object sender, EventArgs e) @@ -120,188 +119,137 @@ private void Thumbnail_Click(object sender, EventArgs e) if (mouseEventArgs.Button == MouseButtons.Left) { - // plain click - move mouse pointer - var virtualScreen = ScreenHelper.GetVirtualScreen(); - var scaledLocation = MouseHelper.GetJumpLocation( - new PointInfo(mouseEventArgs.X, mouseEventArgs.Y), - new SizeInfo(this.Thumbnail.Size), - virtualScreen); - logger.Info($"scaled location = {scaledLocation}"); - MouseHelper.SetCursorPosition(scaledLocation); + // work out which screenshot was clicked + var clickedScreenshot = (this.PreviewLayout ?? throw new InvalidOperationException()) + .ScreenshotBounds + .SingleOrDefault( + box => box.BorderBounds.Contains(mouseEventArgs.X, mouseEventArgs.Y)); + if (clickedScreenshot is null) + { + return; + } + + // scale up the click onto the physical screen - the aspect ratio of the screenshot + // might be distorted compared to the physical screen due to the borders around the + // screenshot, so we need to work out the target location on the physical screen first + var clickedScreen = + this.PreviewLayout.Screens[this.PreviewLayout.ScreenshotBounds.IndexOf(clickedScreenshot)]; + var clickedLocation = new PointInfo(mouseEventArgs.Location) + .Stretch( + source: clickedScreenshot.ContentBounds, + target: clickedScreen) + .Clamp( + new( + x: clickedScreen.X + 1, + y: clickedScreen.Y + 1, + width: clickedScreen.Width - 1, + height: clickedScreen.Height - 1 + )) + .Truncate(); + + // move mouse pointer + logger.Info($"clicked location = {clickedLocation}"); + MouseHelper.SetCursorPosition(clickedLocation); } this.OnDeactivate(EventArgs.Empty); } - public void ShowThumbnail() + public void ShowPreview() { var logger = this.Options.Logger; logger.Info(string.Join( '\n', "-----------", - nameof(FancyMouseForm.ShowThumbnail), + nameof(FancyMouseForm.ShowPreview), "-----------")); - var stopwatch = Stopwatch.StartNew(); - var layoutInfo = FancyMouseForm.GetLayoutInfo(logger, this); - LayoutHelper.PositionForm(this, layoutInfo.FormBounds); - FancyMouseForm.RenderPreview(this, layoutInfo); - stopwatch.Stop(); - - // we have to activate the form to make sure the deactivate event fires - this.Activate(); - } - - private static LayoutInfo GetLayoutInfo( - ILogger logger, FancyMouseForm form) - { - // map screens to their screen number in "System > Display" - var screens = ScreenHelper.GetAllScreens() - .Select((screen, index) => new { Screen = screen, Index = index, Number = index + 1 }) - .ToList(); - foreach (var screen in screens) - { - logger.Info(string.Join( - '\n', - $"screen[{screen.Number}]", - $"\tprimary = {screen.Screen.Primary}", - $"\tdisplay area = {screen.Screen.DisplayArea}", - $"\tworking area = {screen.Screen.WorkingArea}")); - } - - // collect together some values that we need for calculating layout - var activatedLocation = MouseHelper.GetCursorPosition(); - var activatedScreenHandle = ScreenHelper.MonitorFromPoint(activatedLocation); - var activatedScreenIndex = screens - .Single(item => item.Screen.Handle == activatedScreenHandle.Value) - .Index; - - var layoutConfig = new LayoutConfig( - virtualScreenBounds: ScreenHelper.GetVirtualScreen(), - screens: screens.Select(item => item.Screen).ToList(), - activatedLocation: activatedLocation, - activatedScreenIndex: activatedScreenIndex, - activatedScreenNumber: activatedScreenIndex + 1, - maximumFormSize: form.Options.MaximumThumbnailImageSize, - formPadding: new( - form.panel1.Padding.Left, - form.panel1.Padding.Top, - form.panel1.Padding.Right, - form.panel1.Padding.Bottom), - previewPadding: new(0)); - logger.Info(string.Join( - '\n', - "Layout config", - "-------------", - $"virtual screen = {layoutConfig.VirtualScreenBounds}", - $"activated location = {layoutConfig.ActivatedLocation}", - $"activated screen index = {layoutConfig.ActivatedScreenIndex}", - $"activated screen number = {layoutConfig.ActivatedScreenNumber}", - $"maximum form size = {layoutConfig.MaximumFormSize}", - $"form padding = {layoutConfig.FormPadding}", - $"preview padding = {layoutConfig.PreviewPadding}")); - - // calculate the layout coordinates for everything - var layoutInfo = LayoutHelper.CalculateLayoutInfo(layoutConfig); - logger.Info(string.Join( - '\n', - "Layout info", - "-----------", - $"form bounds = {layoutInfo.FormBounds}", - $"preview bounds = {layoutInfo.PreviewBounds}", - $"activated screen = {layoutInfo.ActivatedScreenBounds}")); - - return layoutInfo; - } + // hide the form while we redraw it... + this.Visible = false; - private static void RenderPreview( - FancyMouseForm form, LayoutInfo layoutInfo) - { - var layoutConfig = layoutInfo.LayoutConfig; + // load the config so it can be changed without having to restart the app + var appSettings = AppSettingsReader.ReadFile(".\\appSettings.json"); var stopwatch = Stopwatch.StartNew(); - // initialize the preview image - var preview = new Bitmap( - (int)layoutInfo.PreviewBounds.Width, - (int)layoutInfo.PreviewBounds.Height, - PixelFormat.Format32bppArgb); - form.Thumbnail.Image = preview; - - using var previewGraphics = Graphics.FromImage(preview); + var screens = ScreenHelper.GetAllScreens().Select(screen => screen.DisplayArea).ToList(); + var activatedLocation = MouseHelper.GetCursorPosition(); + this.PreviewLayout = LayoutHelper.GetPreviewLayout( + previewStyle: appSettings.PreviewStyle, + screens: screens, + activatedLocation: activatedLocation); - DrawingHelper.DrawPreviewBackground(previewGraphics, layoutInfo.PreviewBounds, layoutInfo.ScreenBounds); + this.PositionForm(this.PreviewLayout.FormBounds); var desktopHwnd = HWND.Null; var desktopHdc = HDC.Null; - var previewHdc = HDC.Null; + DrawingHelper.EnsureDesktopDeviceContext(ref desktopHwnd, ref desktopHdc); try { - // sort the source and target screen areas, putting the activated screen first - // (we need to capture and draw the activated screen before we show the form - // because otherwise we'll capture the form as part of the screenshot!) - var sourceScreens = layoutConfig.Screens - .Where((_, idx) => idx == layoutConfig.ActivatedScreenIndex) - .Union(layoutConfig.Screens.Where((_, idx) => idx != layoutConfig.ActivatedScreenIndex)) - .Select(screen => screen.Bounds) - .ToList(); - var targetScreens = layoutInfo.ScreenBounds - .Where((_, idx) => idx == layoutConfig.ActivatedScreenIndex) - .Union(layoutInfo.ScreenBounds.Where((_, idx) => idx != layoutConfig.ActivatedScreenIndex)) - .ToList(); - - DrawingHelper.EnsureDesktopDeviceContext(ref desktopHwnd, ref desktopHdc); - DrawingHelper.EnsurePreviewDeviceContext(previewGraphics, ref previewHdc); - - var placeholdersDrawn = false; - for (var i = 0; i < sourceScreens.Count; i++) - { - DrawingHelper.DrawPreviewScreen( - desktopHdc, previewHdc, sourceScreens[i], targetScreens[i]); - - // show the placeholder images and show the form if it looks like it might take - // a while to capture the remaining screenshot images (but only if there are any) - if ((i < (sourceScreens.Count - 1)) && (stopwatch.ElapsedMilliseconds > 250)) - { - // we need to release the device context handle before we draw the placeholders - // using the Graphics object otherwise we'll get an error from GDI saying - // "Object is currently in use elsewhere" - DrawingHelper.FreePreviewDeviceContext(previewGraphics, ref previewHdc); - - if (!placeholdersDrawn) - { - // draw placeholders for any undrawn screens - DrawingHelper.DrawPreviewScreenPlaceholders( - previewGraphics, - targetScreens.Where((_, idx) => idx > i).ToList()); - placeholdersDrawn = true; - } - - FancyMouseForm.RefreshPreview(form); - - // we've still got more screens to draw so open the device context again - DrawingHelper.EnsurePreviewDeviceContext(previewGraphics, ref previewHdc); - } - } + DrawingHelper.RenderPreview( + this.PreviewLayout, + desktopHdc, + this.OnPreviewImageCreated, + this.OnPreviewImageUpdated); } finally { DrawingHelper.FreeDesktopDeviceContext(ref desktopHwnd, ref desktopHdc); - DrawingHelper.FreePreviewDeviceContext(previewGraphics, ref previewHdc); } - FancyMouseForm.RefreshPreview(form); stopwatch.Stop(); + + // we have to activate the form to make sure the deactivate event fires + this.Activate(); + } + + private void ClearPreview() + { + if (this.Thumbnail.Image is null) + { + return; + } + + var tmp = this.Thumbnail.Image; + this.Thumbnail.Image = null; + tmp.Dispose(); + + // force preview image memory to be released, otherwise + // all the disposed images can pile up without being GC'ed + GC.Collect(); + } + + /// + /// Resize and position the specified form. + /// + private void PositionForm(RectangleInfo bounds) + { + // note - do this in two steps rather than "this.Bounds = formBounds" as there + // appears to be an issue in WinForms with dpi scaling even when using PerMonitorV2, + // where the form scaling uses either the *primary* screen scaling or the *previous* + // screen's scaling when the form is moved to a different screen. i've got no idea + // *why*, but the exact sequence of calls below seems to be a workaround... + // see https://github.com/mikeclayton/FancyMouse/issues/2 + var rect = bounds.ToRectangle(); + this.Location = rect.Location; + _ = this.PointToScreen(Point.Empty); + this.Size = rect.Size; + } + + private void OnPreviewImageCreated(Bitmap preview) + { + this.ClearPreview(); + this.Thumbnail.Image = preview; } - private static void RefreshPreview(FancyMouseForm form) + private void OnPreviewImageUpdated() { - if (!form.Visible) + if (!this.Visible) { - form.Show(); + this.Show(); } - form.Thumbnail.Refresh(); + this.Thumbnail.Refresh(); } } diff --git a/src/FancyMouse/UI/FancyMouseForm.resx b/src/FancyMouse/UI/FancyMouseForm.resx index e33d2ec..214c25a 100644 --- a/src/FancyMouse/UI/FancyMouseForm.resx +++ b/src/FancyMouse/UI/FancyMouseForm.resx @@ -1,4 +1,64 @@ - + + + diff --git a/src/FancyMouse/appSettings.json b/src/FancyMouse/appSettings.json new file mode 100644 index 0000000..89480c3 --- /dev/null +++ b/src/FancyMouse/appSettings.json @@ -0,0 +1,10 @@ +{ + + "versions": 2, + + "hotkey": { + "key": "F", + "modifiers": "Control, Alt, Shift" + } + +} diff --git a/wiki/config/advanced_config.md b/wiki/config/advanced_config.md new file mode 100644 index 0000000..2aff6f7 --- /dev/null +++ b/wiki/config/advanced_config.md @@ -0,0 +1,99 @@ +# Advanced Config + +**FancyMouse** supports a set of style and layout configuration settings that follow a simplified version of the [W3C Box Model](https://www.w3.org/TR/CSS2/box.html). These can be used to configure the spacing and appearance of elements in the preview popup, for example: + +``` + +--------------[bounds]---------------+ + |▒▒▒▒▒▒▒▒▒▒▒▒▒▒[margin]▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| + |▒▒▓▓▓▓▓▓▓▓▓▓▓▓[border]▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒| + |▒▒▓▓░░░░░░░░░░[padding]░░░░░░░░░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ [content] ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▒▒| + |▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒| + |▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| + +-------------------------------------+ +``` + + +* **margin** - the size of the gap between the outer boundary of the element and the border +* **border** - the thickness and colour of the border around the element +* **padding** - the size of the gap between the border and the content +* **content** - the area at the center of the element that can contain child elements + +There are two sets of box configurations that can be defined - one for the main pewview popup, and one for the miniature screenshots shown in the preview popup. These can be combined to make a variety of effects. + +## Gallery + +If you just want to dive into some examples to see how it works, here's some pre-made config files that you can experiment with... + +| Preview | Name | Description | +| ------- | ---- | ----------- | +| ![](./default_v2_config_t.png) | **[Default](./default_v2_config.md)** | The current default preview style +| ![](./legacy_v1_config_t.png) | **[Legacy](./legacy_v1_config.md)** | Config settings that emulate the legacy preview style +| ![](./gaudy_v2_config_t.png) | **[Gaudy](./gaudy_v2_config.md)** | A ghastly assaault on the eyes + +> **Note:** If you design an interesting style feel free to share it by raising an [issue](https://github.com/mikeclayton/FancyMouse/issues) with a screenshot and the contents of the config file and I'll and some of them to this gallery. + +## Reference + +The table below annotates a sample config file, but check out one of the gallery links above for a cut & pasteable version that you can copy into your own config file. + +| Key | Description | +| --- | ----------- | +| ```{``` | +|     ```"version": 2,``` | Indicates the **version** of the config file format used in this file. DIfferent versions will support different settings and this is used where possible to read old formats and apply them to the application's behaviour and visual style. +|     ```"hotkey": "CTRL + ALT + SHIFT + F",``` | The key combination that activates the preview popup. You can potentially bind this combination to spare button on your mouse if your manufacturer's softwatr supports it to make it easier to trigger. +|     ```"preview: {``` | +|         ```"name": "default",``` | Specifies an arbitray **name** for the style in this config file to make it easier to distinguish it when comparing to other config files. +|         ```"size": {``` | +|             ```"width": 1600,``` | The maximum **width** in pixels of the preview popup. The actual popup width may be smaller depending on the aspect ratio of the entire screen, and / or the size of the monitor the preview popup is activated on. +|             ```"height": 1200``` | The maximum **height** in pixels of the preview popup. The actual popup height may be smaller depending on the aspect ratio of the entire screen, and / or the size of the monitor the preview popup is activated on. +|         ```},``` | +|         ```"canvas": {``` | Contains style settings that define the visual appearance of the main **preview popup** window (as opposed to the screenshot thumbnails). This is called the ```canvas``` for the sake of gving it a unique name that isn't heavily overloaded elsewhere (e.g. ```preview```). +|             ```"border": {``` | +|                 ```"color": "SystemColors.Highlight",``` | The **color** of the border around the ```canvas```. See below for the format of valid ```color``` strings. +|                 ```"width": 6,``` | The **width** in pixels of the border around the ```canvas```. +|                 ```"depth": 0``` | The **depth** (i.e. thickness in pixels) of the 3D highlight and shadow effect on the border around the ```canvas```. +|             ```},``` | +|             ```"padding": {``` | +|                 ```"width": 6``` | The **width** in pixels of the spacing between the ```canvas``` border and the boundaries of the screenshot boxes. +|             ```},``` | +|             ```"background": {``` | +|                 ```"color1": "#0D57D2",``` | The **primary color** of the background gradient fill drawn on visible parts of the ```canvas``` - e.g. the padding area, or the margin on screenshot boxes. See below for the format of valid ```color``` strings. +|                 ```"color2": "#0344C0"``` | The **secondary color** of the background gradient fill drawn on visible parts of the ```canvas``` - e.g. the padding area, or the margin on screenshot boxes. See below for the format of valid ```color``` strings. +|             ```}``` | +|         ```},``` | +|         ```"screenshot": {``` | Contains style settings that define the visual appearance of the **screenshot** boxes on the preview popup window. +|             ```"margin": {``` | +|                 ```"width": 2``` | The **width** of the margin around each screenshot box. +|             ```},``` | +|             ```"border": {``` | +|                 ```"color": "#222222",``` | The **color** of the border around each screenshot box. See below for the format of valid ```color``` strings. +|                 ```"width": 10,``` | The **width** in pixels of the border around each screenshot box. +|                 ```"depth": 3``` | The **depth** (i.e. thickness in pixels) of the 3D highlight and shadow effect on the border around each screeenshot box. +|             ```},``` | +|             ```"background": {``` | +|                 ```"color1": "Color.MidnightBlue",``` | The **primary color** of the background gradient fill drawn inside each screesnhot box instead of the screesnhot image if the form takes too long to load. See below for the format of valid ```color``` strings. +|                 ```"color2": "Color.MidnightBlue"``` | The **secondary color** of the background gradient fill drawn inside each screesnhot box instead of the screesnhot image if the form takes too long to load. See below for the format of valid ```color``` strings. +|             ```}``` | +|         ```}``` | +|     ```}``` | +| ```}``` | + +## Color Strings + +Color settings can be specified in three different formats: + +| Format | Description | +| ------ | ----------- | +| Named colors | Any of the named colors in the [Colors]() enumeration - e.g. ```"color1": "Color.Red"```. +| System colors | These follow the current operating system color scheme and can be any of the values in the [SystemColors](https://learn.microsoft.com/en-us/dotnet/api/system.drawing.systemcolors?view=net-7.0) enumeration - e.g. ```"color1": "SystemColors.Highlight"```. +| Hex color | A hexadecimal RGB color code - e.g. ```"color": "#FFA500"``` ( orange) + +### See also + +* [Basic Config](./basic_config.md) \ No newline at end of file diff --git a/wiki/config/basic_config.md b/wiki/config/basic_config.md new file mode 100644 index 0000000..7e990c9 --- /dev/null +++ b/wiki/config/basic_config.md @@ -0,0 +1,35 @@ +## Basic Config + +**FancyMouse** stores its configuration in a json file called ```appSettings.json``` alongside the main ```FancyMouse.exe```. + +In the absence of this file (or if any settings is not specified in the file), FancyMouse uses hard-coded built-in defaults. + +Below are the config settings you're most likely to want to change: + +```json +{ + "version": 2, + + "hotkey": "CTRL + ALT + SHIFT + F", + + "preview": { + "size": { + "width": 1600, + "height": 1200 + } + } + +} +``` + +| Key | Description | +|-----|-------------| +| **version** | The version of config format used in this config file. Used for detemining how to read the rest of the file. This should be set to ```2``` for the latest version of the config format, and where possible you should manually reformat older config files to the latest version. +| **hotkey** | The activation key combination. +| **preview** | Style settings for the preview thumbnail +| **preview.size.width** | The maximum width of the preview popup in pixels +| **preview.size.height** | The maximum height of the preview popup in pixels + +### See also + +* [Advanced Config](./advanced_config.md) \ No newline at end of file diff --git a/wiki/config/default_v2_config.md b/wiki/config/default_v2_config.md new file mode 100644 index 0000000..3adc1c7 --- /dev/null +++ b/wiki/config/default_v2_config.md @@ -0,0 +1,64 @@ +# Default Config V2 Sample + +## Description + +The Default Config V2 style places a thin, flat border around the main preview popup, and then adde a margin and border with a 3d effect to each screenshot to make it appear as if it's being displayed on a monitor bezel. + +## Preview + +![](./default_v2_config.png) + +## Config + +The text below shows the config json used to display the above preview. + +```json +{ + + "version": 2, + + "hotkey": "CTRL + ALT + SHIFT + F", + + "preview": { + "name": "default_v2", + + "size": { + "width": 1600, + "height": 1200 + }, + + "canvas": { + "border": { + "color": "SystemColors.Highlight", + "width": 6, + "depth": 0 + }, + "padding": { + "width": 6 + }, + "background": { + "color1": "#0D57D2", + "color2": "#0344C0" + } + }, + + "screenshot": { + "margin": { + "width": 2 + }, + "border": { + "color": "#222222", + "width": 10, + "depth": 3 + }, + "background": { + "color1": "Color.MidnightBlue", + "color2": "Color.MidnightBlue" + } + } + + } + +} + +``` diff --git a/wiki/config/default_v2_config.pdn b/wiki/config/default_v2_config.pdn new file mode 100644 index 0000000..c7e7bfc Binary files /dev/null and b/wiki/config/default_v2_config.pdn differ diff --git a/wiki/config/default_v2_config.png b/wiki/config/default_v2_config.png new file mode 100644 index 0000000..35b461c Binary files /dev/null and b/wiki/config/default_v2_config.png differ diff --git a/wiki/config/default_v2_config_t.png b/wiki/config/default_v2_config_t.png new file mode 100644 index 0000000..62ed9fb Binary files /dev/null and b/wiki/config/default_v2_config_t.png differ diff --git a/wiki/config/gaudy_v2_config.md b/wiki/config/gaudy_v2_config.md new file mode 100644 index 0000000..141b686 --- /dev/null +++ b/wiki/config/gaudy_v2_config.md @@ -0,0 +1,67 @@ +# Gaudy V2 Config Sample + +## Description + +As with any styling system, the FancyMouse config file can be abused to create some truly horrific visual appearances. This **Gaudy V2 Config** sample is one of them... + +> Note this *might* actually be useful if you want to use FancyMouse as a kind of mouse-finder that shows a nright eye-catching image on one of the screens so it draws your eyes to it). + +## Preview + +![](./gaudy_v2_config.png) + +## Config + +The text below shows the config json used to display the above preview. + +```json +{ + + "version": 2, + + "hotkey": "CTRL + ALT + SHIFT + F", + + + "preview": { + "name": "gaudy_v2", + + "size": { + "width": 1600, + "height": 1200 + }, + + "canvas": { + "border": { + "color": "Color.Red", + "width": 15, + "depth": 5 + }, + "padding": { + "width": 15 + }, + "background": { + "color1": "Color.Yellow", + "color2": "Color.Green" + } + }, + + "screenshot": { + "margin": { + "width": 15 + }, + "border": { + "color": "Color.HotPink", + "width": 15, + "depth": 6 + }, + "background": { + "color1": "Color.MidnightBlue", + "color2": "Color.MidnightBlue" + } + } + + } + +} + +``` diff --git a/wiki/config/gaudy_v2_config.pdn b/wiki/config/gaudy_v2_config.pdn new file mode 100644 index 0000000..86815ec Binary files /dev/null and b/wiki/config/gaudy_v2_config.pdn differ diff --git a/wiki/config/gaudy_v2_config.png b/wiki/config/gaudy_v2_config.png new file mode 100644 index 0000000..5683863 Binary files /dev/null and b/wiki/config/gaudy_v2_config.png differ diff --git a/wiki/config/gaudy_v2_config_t.png b/wiki/config/gaudy_v2_config_t.png new file mode 100644 index 0000000..f2a3252 Binary files /dev/null and b/wiki/config/gaudy_v2_config_t.png differ diff --git a/wiki/config/legacy_v1_config.md b/wiki/config/legacy_v1_config.md new file mode 100644 index 0000000..e6de6b0 --- /dev/null +++ b/wiki/config/legacy_v1_config.md @@ -0,0 +1,65 @@ +# Legacy V1 Config Sample + +## Description + +The **Legacy V1 Config** style can be simulated using the config file below. This places a thin, flat border around the main preview popup, and then clears all margin, border and padding settings on the screenshot box. + +## Preview + +![](./legacy_v1_config.png) + +## Config + +The text below shows the config json used to display the above preview. + +```json +{ + + "version": 2, + + "hotkey": "CTRL + ALT + SHIFT + F", + + "preview_l": { + "name": "legacy_v1", + + "size": { + "width": 1600, + "height": 1200 + }, + + "canvas": { + "border": { + "color": "SystemColors.Highlight", + "width": 7, + "depth": 0 + }, + "padding": { + "width": 0 + }, + "background": { + "color1": "#0D57D2", + "color2": "#0344C0" + } + }, + + "screenshot": { + "margin": { + "width": 0 + }, + "border": { + "color": "#222222", + "width": 0, + "depth": 0 + }, + "background": { + "color1": "Color.MidnightBlue", + "color2": "Color.MidnightBlue" + } + + } + + } + +} + +``` diff --git a/wiki/config/legacy_v1_config.pdn b/wiki/config/legacy_v1_config.pdn new file mode 100644 index 0000000..1deda21 Binary files /dev/null and b/wiki/config/legacy_v1_config.pdn differ diff --git a/wiki/config/legacy_v1_config.png b/wiki/config/legacy_v1_config.png new file mode 100644 index 0000000..6226d94 Binary files /dev/null and b/wiki/config/legacy_v1_config.png differ diff --git a/wiki/config/legacy_v1_config_t.png b/wiki/config/legacy_v1_config_t.png new file mode 100644 index 0000000..30cff76 Binary files /dev/null and b/wiki/config/legacy_v1_config_t.png differ