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/LayoutHelperTests.cs similarity index 62% rename from src/FancyMouse.UnitTests/Helpers/DrawingHelperTests.cs rename to src/FancyMouse.UnitTests/Helpers/LayoutHelperTests.cs index 1aa0f23..1262156 100644 --- a/src/FancyMouse.UnitTests/Helpers/DrawingHelperTests.cs +++ b/src/FancyMouse.UnitTests/Helpers/LayoutHelperTests.cs @@ -1,15 +1,18 @@ -using FancyMouse.Helpers; +using System.Drawing; +using FancyMouse.Helpers; using FancyMouse.Models.Drawing; using FancyMouse.Models.Layout; using FancyMouse.Models.Screen; +using FancyMouse.Models.Settings; using Microsoft.VisualStudio.TestTools.UnitTesting; using static FancyMouse.NativeMethods.Core; namespace FancyMouse.UnitTests.Helpers; [TestClass] -public static class DrawingHelperTests +public static class LayoutHelperTests { + /* [TestClass] public sealed class CalculateLayoutInfoTests { @@ -221,4 +224,132 @@ public void RunTestCases(TestCase data) Assert.AreEqual(expected.ActivatedScreenBounds.ToRectangle(), actual.ActivatedScreenBounds.ToRectangle(), "ActivatedScreen.ToRectangle"); } } + */ + + [TestClass] + public sealed class GetPreviewLayoutTests + { + public sealed class TestCase + { + public TestCase(PreviewSettings previewSettings, List screens, PointInfo activatedLocation, PreviewLayout expectedResult) + { + this.PreviewSettings = previewSettings; + this.Screens = screens; + this.ActivatedLocation = activatedLocation; + this.ExpectedResult = expectedResult; + } + + public PreviewSettings PreviewSettings { 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 + // + // +----------------+ + // | | + // | 0 | + // | | + // +----------------+ + var previewSettings = new PreviewSettings( + size: new( + width: 7 + 2 + 5 + 512 + 2 + 7, + height: 7 + 2 + 384 + 2 + 7), + previewStyle: new( + marginInfo: MarginInfo.Empty, + borderInfo: new( + color: SystemColors.Highlight, + all: 5, + depth: 3), + paddingInfo: new( + all: 1), + backgroundInfo: new( + color1: Color.FromArgb(13, 87, 210), // light blue + color2: Color.FromArgb(3, 68, 192) // darker blue + ) + ), + screenshotStyle: new( + marginInfo: MarginInfo.Empty, + borderInfo: new(Color.Black, 5, 3), + paddingInfo: new(1), + backgroundInfo: BackgroundInfo.Empty + )); + var screens = new List + { + new(HANDLE.Null, true, new(0, 0, 1024, 768), new(0, 0, 1024, 768)), + }; + var activatedLocation = new PointInfo(512, 384); + var previewLayout = new PreviewLayout( + virtualScreen: new(0, 0, 1024, 768), + screens: screens, + activatedScreen: screens[0], + formBounds: new(0, 0, 0, 0), + previewStyle: new BoxStyle( + marginInfo: MarginInfo.Empty, + borderInfo: BorderInfo.Empty, + paddingInfo: PaddingInfo.Empty, + backgroundInfo: BackgroundInfo.Empty), + previewBounds: new( + outerBounds: RectangleInfo.Empty, + marginBounds: RectangleInfo.Empty, + borderBounds: RectangleInfo.Empty, + paddingBounds: RectangleInfo.Empty, + contentBounds: RectangleInfo.Empty), + screenshotStyle: BoxStyle.Empty, + screenshotBounds: Enumerable.Empty()); + yield return new object[] { new TestCase(previewSettings, screens, activatedLocation, previewLayout) }; + } + + [TestMethod] + [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + public void RunTestCases(TestCase data) + { + // note - even if values are within 0.0001M of each other they could + // still round to different values - e.g. + // (int)1279.999999999999 -> 1279 + // vs + // (int)1280.000000000000 -> 1280 + // so we'll compare the raw values, *and* convert to an int-based + // Rectangle to compare rounded values + var actual = LayoutHelper.GetPreviewLayout(data.PreviewSettings, data.Screens, data.ActivatedLocation); + var expected = data.ExpectedResult; + /* form bounds */ + Assert.AreEqual(expected.FormBounds.X, actual.FormBounds.X, 0.00001M, "FormBounds.X"); + Assert.AreEqual(expected.FormBounds.Y, actual.FormBounds.Y, 0.00001M, "FormBounds.Y"); + Assert.AreEqual(expected.FormBounds.Width, actual.FormBounds.Width, 0.00001M, "FormBounds.Width"); + Assert.AreEqual(expected.FormBounds.Height, actual.FormBounds.Height, 0.00001M, "FormBounds.Height"); + Assert.AreEqual(expected.FormBounds.ToRectangle(), actual.FormBounds.ToRectangle(), "FormBounds.ToRectangle"); + /* preview bounds */ + /* + Assert.AreEqual(expected.PreviewBounds.X, actual.PreviewBounds.X, 0.00001M, "PreviewBounds.X"); + Assert.AreEqual(expected.PreviewBounds.Y, actual.PreviewBounds.Y, 0.00001M, "PreviewBounds.Y"); + Assert.AreEqual(expected.PreviewBounds.Width, actual.PreviewBounds.Width, 0.00001M, "PreviewBounds.Width"); + Assert.AreEqual(expected.PreviewBounds.Height, actual.PreviewBounds.Height, 0.00001M, "PreviewBounds.Height"); + Assert.AreEqual(expected.PreviewBounds.ToRectangle(), actual.PreviewBounds.ToRectangle(), "PreviewBounds.ToRectangle"); + */ + /* + Assert.AreEqual(expected.ScreenBounds.Count, actual.ScreenBounds.Count, "ScreenBounds.Count"); + for (var i = 0; i < expected.ScreenBounds.Count; i++) + { + Assert.AreEqual(expected.ScreenBounds[i].X, actual.ScreenBounds[i].X, 0.00001M, $"ScreenBounds[{i}].X"); + Assert.AreEqual(expected.ScreenBounds[i].Y, actual.ScreenBounds[i].Y, 0.00001M, $"ScreenBounds[{i}].Y"); + Assert.AreEqual(expected.ScreenBounds[i].Width, actual.ScreenBounds[i].Width, 0.00001M, $"ScreenBounds[{i}].Width"); + Assert.AreEqual(expected.ScreenBounds[i].Height, actual.ScreenBounds[i].Height, 0.00001M, $"ScreenBounds[{i}].Height"); + Assert.AreEqual(expected.ScreenBounds[i].ToRectangle(), actual.ScreenBounds[i].ToRectangle(), "ActivatedScreen.ToRectangle"); + } + + Assert.AreEqual(expected.ActivatedScreenBounds.X, actual.ActivatedScreenBounds.X, "ActivatedScreen.X"); + Assert.AreEqual(expected.ActivatedScreenBounds.Y, actual.ActivatedScreenBounds.Y, "ActivatedScreen.Y"); + Assert.AreEqual(expected.ActivatedScreenBounds.Width, actual.ActivatedScreenBounds.Width, "ActivatedScreen.Width"); + Assert.AreEqual(expected.ActivatedScreenBounds.Height, actual.ActivatedScreenBounds.Height, "ActivatedScreen.Height"); + Assert.AreEqual(expected.ActivatedScreenBounds.ToRectangle(), actual.ActivatedScreenBounds.ToRectangle(), "ActivatedScreen.ToRectangle"); + */ + } + } } 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..75db391 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; } @@ -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..35e8e59 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..9e43cbb 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 { @@ -18,15 +20,16 @@ public KeyModifiers Modifiers get; } - public static Keystroke Parse(string s) + 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 +56,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..0dde36e 100644 --- a/src/FancyMouse/FancyMouse.csproj +++ b/src/FancyMouse/FancyMouse.csproj @@ -9,29 +9,12 @@ true - - PerMonitorV2 - + Always @@ -41,8 +24,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..14e0bac 100644 --- a/src/FancyMouse/Helpers/DrawingHelper.cs +++ b/src/FancyMouse/Helpers/DrawingHelper.cs @@ -1,5 +1,6 @@ using System.Drawing.Drawing2D; using FancyMouse.Models.Drawing; +using FancyMouse.Models.Layout; using FancyMouse.NativeMethods; using static FancyMouse.NativeMethods.Core; @@ -8,26 +9,125 @@ namespace FancyMouse.Helpers; internal static class DrawingHelper { /// - /// Draw the gradient-filled preview background. + /// Draw a border shape. /// - public static void DrawPreviewBackground( - Graphics previewGraphics, RectangleInfo previewBounds, IEnumerable screenBounds) + public static void DrawBoxBorder( + Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds) { + var borderInfo = boxStyle.BorderInfo; + 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); + } + } + } + + /// + /// Draw a gradient-filled background shape. + /// + public static void DrawBoxBackground( + Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds, IEnumerable excludeBounds) + { + var backgroundBounds = boxBounds.PaddingBounds; + var backgroundInfo = boxStyle.BackgroundInfo; + 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 +168,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 = new HDC(graphics.GetHdc()); var result = Gdi32.SetStretchBltMode(previewHdc, Gdi32.STRETCH_BLT_MODE.STRETCH_HALFTONE); if (result == 0) @@ -86,11 +186,11 @@ 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; } } @@ -99,29 +199,29 @@ public static void FreePreviewDeviceContext(Graphics previewGraphics, ref HDC pr /// Draw 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, IEnumerable 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.BackgroundInfo.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..9470b57 100644 --- a/src/FancyMouse/Helpers/LayoutHelper.cs +++ b/src/FancyMouse/Helpers/LayoutHelper.cs @@ -1,69 +1,133 @@ using FancyMouse.Models.Drawing; using FancyMouse.Models.Layout; +using FancyMouse.Models.Screen; +using FancyMouse.Models.Settings; namespace FancyMouse.Helpers; internal static class LayoutHelper { - public static LayoutInfo CalculateLayoutInfo( - LayoutConfig layoutConfig) + public static PreviewLayout GetPreviewLayout( + PreviewSettings previewSettings, IEnumerable screens, PointInfo activatedLocation) { - if (layoutConfig is null) + if (previewSettings is null) { - throw new ArgumentNullException(nameof(layoutConfig)); + throw new ArgumentNullException(nameof(previewSettings)); } - 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 = allScreens.Skip(1).Aggregate( + seed: allScreens.First().Bounds, + (bounds, screen) => bounds.Union(screen.DisplayArea)); + 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.DisplayArea.Contains(activatedLocation)); + builder.ActivatedScreen = 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.DisplayArea.Size + .Intersect(previewSettings.Size); - // 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(previewSettings.PreviewStyle.MarginInfo) + .Shrink(previewSettings.PreviewStyle.BorderInfo) + .Shrink(previewSettings.PreviewStyle.PaddingInfo); - // 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(previewSettings.PreviewStyle.MarginInfo.Left, previewSettings.PreviewStyle.MarginInfo.Top) + .Offset(previewSettings.PreviewStyle.BorderInfo.Left, previewSettings.PreviewStyle.BorderInfo.Top) + .Offset(previewSettings.PreviewStyle.PaddingInfo.Left, previewSettings.PreviewStyle.PaddingInfo.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 = previewSettings.PreviewStyle; + builder.PreviewBounds = LayoutHelper.GetBoxBoundsFromContentBounds( + contentBounds, + previewSettings.PreviewStyle); - // ... 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.DisplayArea); + 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.ScreenshotStyle = previewSettings.ScreenshotStyle; + 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.Bounds + .Offset(virtualScreen.Location.ToSize().Negate()) + .Scale(scalingRatio) + .Offset(builder.PreviewBounds.ContentBounds.Location.ToSize()), + previewSettings.ScreenshotStyle)) .ToList(); return builder.Build(); } + public static BoxBounds GetBoxBoundsFromContentBounds( + RectangleInfo contentBounds, + BoxStyle boxStyle) + { + var paddingBounds = contentBounds.Enlarge( + boxStyle.PaddingInfo ?? throw new ArgumentException(nameof(boxStyle.PaddingInfo))); + var borderBounds = paddingBounds.Enlarge( + boxStyle.BorderInfo ?? throw new ArgumentException(nameof(boxStyle.BorderInfo))); + var marginBounds = borderBounds.Enlarge( + boxStyle.MarginInfo ?? throw new ArgumentException(nameof(boxStyle.MarginInfo))); + var outerBounds = marginBounds; + return new( + outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds); + } + + public static BoxBounds GetBoxBoundsFromOuterBounds( + RectangleInfo outerBounds, + BoxStyle boxStyle) + { + var marginBounds = outerBounds ?? throw new ArgumentNullException(nameof(outerBounds)); + var borderBounds = marginBounds.Shrink( + boxStyle.MarginInfo ?? throw new ArgumentException(nameof(boxStyle.MarginInfo))); + var paddingBounds = borderBounds.Shrink( + boxStyle.BorderInfo ?? throw new ArgumentException(nameof(boxStyle.BorderInfo))); + var contentBounds = paddingBounds.Shrink( + boxStyle.PaddingInfo ?? throw new ArgumentException(nameof(boxStyle.PaddingInfo))); + return new( + outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds); + } + /// /// Resize and position the specified form. /// diff --git a/src/FancyMouse/Models/Drawing/BackgroundInfo.cs b/src/FancyMouse/Models/Drawing/BackgroundInfo.cs new file mode 100644 index 0000000..e594f07 --- /dev/null +++ b/src/FancyMouse/Models/Drawing/BackgroundInfo.cs @@ -0,0 +1,32 @@ +namespace FancyMouse.Models.Drawing; + +public sealed class BackgroundInfo +{ + public static readonly BackgroundInfo Empty = new(SystemColors.Control, SystemColors.Control); + + public BackgroundInfo( + 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/Drawing/BorderInfo.cs b/src/FancyMouse/Models/Drawing/BorderInfo.cs new file mode 100644 index 0000000..9aa1d51 --- /dev/null +++ b/src/FancyMouse/Models/Drawing/BorderInfo.cs @@ -0,0 +1,69 @@ +namespace FancyMouse.Models.Drawing; + +public sealed class BorderInfo +{ + public static readonly BorderInfo Empty = new(Color.Transparent, 0, 0); + + public BorderInfo(Color color, decimal all, decimal depth) + : this(color, all, all, all, all, depth) + { + } + + public BorderInfo(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; + } + + public decimal Horizontal => this.Left + this.Right; + + 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}" + + "}"; + } +} diff --git a/src/FancyMouse/Models/Drawing/BoxBounds.cs b/src/FancyMouse/Models/Drawing/BoxBounds.cs new file mode 100644 index 0000000..2faa6c2 --- /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 content settings for this layout box. + /// + public RectangleInfo ContentBounds + { + get; + } +} diff --git a/src/FancyMouse/Models/Drawing/BoxStyle.cs b/src/FancyMouse/Models/Drawing/BoxStyle.cs new file mode 100644 index 0000000..be760ce --- /dev/null +++ b/src/FancyMouse/Models/Drawing/BoxStyle.cs @@ -0,0 +1,67 @@ +namespace FancyMouse.Models.Drawing; + +public sealed class BoxStyle +{ + /* + + see https://www.w3schools.com/css/css_boxmodel.asp + + +--------------[bounds]---------------+ + |▒▒▒▒▒▒▒▒▒▒▒▒▒▒[margin]▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| + |▒▒▓▓▓▓▓▓▓▓▓▓▓▓[border]▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒| + |▒▒▓▓░░░░░░░░░░[padding]░░░░░░░░░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ [content] ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▒▒| + |▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒| + |▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| + +-------------------------------------+ + + */ + + public static readonly BoxStyle Empty = new(MarginInfo.Empty, BorderInfo.Empty, PaddingInfo.Empty, BackgroundInfo.Empty); + + internal BoxStyle( + MarginInfo marginInfo, + BorderInfo borderInfo, + PaddingInfo paddingInfo, + BackgroundInfo backgroundInfo) + { + this.MarginInfo = marginInfo ?? throw new ArgumentNullException(nameof(marginInfo)); + this.BorderInfo = borderInfo ?? throw new ArgumentNullException(nameof(borderInfo)); + this.PaddingInfo = paddingInfo ?? throw new ArgumentNullException(nameof(paddingInfo)); + this.BackgroundInfo = backgroundInfo ?? throw new ArgumentNullException(nameof(backgroundInfo)); + } + + /// + /// Gets the margin settings for this layout box. + /// + public MarginInfo MarginInfo + { + get; + } + + /// + /// Gets the border settings for this layout box. + /// + public BorderInfo BorderInfo + { + get; + } + + /// + /// Gets the padding settings for this layout box. + /// + public PaddingInfo PaddingInfo + { + get; + } + + public BackgroundInfo BackgroundInfo + { + get; + } +} diff --git a/src/FancyMouse/Models/Drawing/MarginInfo.cs b/src/FancyMouse/Models/Drawing/MarginInfo.cs new file mode 100644 index 0000000..13d3170 --- /dev/null +++ b/src/FancyMouse/Models/Drawing/MarginInfo.cs @@ -0,0 +1,53 @@ +namespace FancyMouse.Models.Drawing; + +public sealed class MarginInfo +{ + public static readonly MarginInfo Empty = new(0); + + public MarginInfo(decimal all) + : this(all, all, all, all) + { + } + + public MarginInfo(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; + } + + public decimal Horizontal => this.Left + this.Right; + + 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/Drawing/PaddingInfo.cs b/src/FancyMouse/Models/Drawing/PaddingInfo.cs index 065f649..fa4a44f 100644 --- a/src/FancyMouse/Models/Drawing/PaddingInfo.cs +++ b/src/FancyMouse/Models/Drawing/PaddingInfo.cs @@ -5,6 +5,8 @@ /// public sealed class PaddingInfo { + public static readonly PaddingInfo Empty = new(0); + public PaddingInfo(decimal all) : this(all, all, all, all) { @@ -42,6 +44,11 @@ public decimal Bottom public decimal Vertical => this.Top + this.Bottom; + public MarginInfo ToMarginInfo() + { + return new MarginInfo(this.Left, this.Top, this.Right, this.Bottom); + } + public override string ToString() { return "{" + diff --git a/src/FancyMouse/Models/Drawing/RectangleInfo.cs b/src/FancyMouse/Models/Drawing/RectangleInfo.cs index c871921..f68e2cf 100644 --- a/src/FancyMouse/Models/Drawing/RectangleInfo.cs +++ b/src/FancyMouse/Models/Drawing/RectangleInfo.cs @@ -5,6 +5,8 @@ /// 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; @@ -56,19 +58,50 @@ public decimal Height public decimal Bottom => this.Y + this.Height; - public SizeInfo Size => new(this.Width, this.Height); + public decimal Area => this.Width * this.Height; public PointInfo Location => new(this.X, this.Y); - public decimal Area => this.Width * this.Height; + public PointInfo Midpoint => new( + x: this.X + (this.Width / 2), + y: this.Y + (this.Height / 2)); + + 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); + + 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 +110,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(BorderInfo border) => + new( + this.X - border.Left, + this.Y - border.Top, + this.Width + border.Horizontal, + this.Height + border.Vertical); + + public RectangleInfo Enlarge(MarginInfo margin) => + new( + this.X - margin.Left, + this.Y - margin.Top, + this.Width + margin.Horizontal, + this.Height + margin.Vertical); + + public RectangleInfo Enlarge(PaddingInfo 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 +141,39 @@ 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(BorderInfo border) => + new( + this.X + border.Left, + this.Y + border.Top, + this.Width - border.Horizontal, + this.Height - border.Vertical); + + public RectangleInfo Shrink(MarginInfo margin) => + new( + this.X + margin.Left, + this.Y + margin.Top, + this.Width - margin.Horizontal, + this.Height - margin.Vertical); + + public RectangleInfo Shrink(PaddingInfo padding) => + new( + this.X + padding.Left, + this.Y + padding.Top, + this.Width - padding.Horizontal, + this.Height - padding.Vertical); - 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/Drawing/SizeInfo.cs b/src/FancyMouse/Models/Drawing/SizeInfo.cs index 9b15cfd..198a02a 100644 --- a/src/FancyMouse/Models/Drawing/SizeInfo.cs +++ b/src/FancyMouse/Models/Drawing/SizeInfo.cs @@ -26,13 +26,32 @@ public decimal Height get; } - public SizeInfo Negate() => new(-this.Width, -this.Height); + public SizeInfo Enlarge(BorderInfo border) => + new( + this.Width + border.Horizontal, + this.Height + border.Vertical); - public SizeInfo Shrink(PaddingInfo padding) => new(this.Width - padding.Horizontal, this.Height - padding.Vertical); + public SizeInfo Enlarge(PaddingInfo 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 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(BorderInfo border) => + new(this.Width - border.Horizontal, this.Height - border.Vertical); + + public SizeInfo Shrink(MarginInfo margin) => + new(this.Width - margin.Horizontal, this.Height - margin.Vertical); + + public SizeInfo Shrink(PaddingInfo 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 +67,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..b20c1b5 100644 --- a/src/FancyMouse/Models/Layout/PreviewLayout.cs +++ b/src/FancyMouse/Models/Layout/PreviewLayout.cs @@ -1,18 +1,141 @@ -using FancyMouse.Models.Drawing; +using System.Collections.ObjectModel; +using FancyMouse.Models.Drawing; +using FancyMouse.Models.Screen; 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 RectangleInfo? VirtualScreen + { + get; + set; + } + + public List Screens + { + get; + set; + } + + public ScreenInfo? ActivatedScreen + { + get; + set; + } + + public RectangleInfo? FormBounds + { + get; + set; + } + + public BoxStyle? PreviewStyle + { + get; + set; + } + + public BoxBounds? PreviewBounds + { + get; + set; + } + + public BoxStyle? ScreenshotStyle + { + get; + set; + } + + public List ScreenshotBounds + { + get; + set; + } + + public PreviewLayout Build() + { + return new PreviewLayout( + 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)}."), + activatedScreen: this.ActivatedScreen ?? throw new InvalidOperationException($"{nameof(this.ActivatedScreen)} must be initialized before calling {nameof(this.Build)}."), + formBounds: this.FormBounds ?? throw new InvalidOperationException($"{nameof(this.FormBounds)} must be initialized before calling {nameof(this.Build)}."), + previewStyle: this.PreviewStyle ?? throw new InvalidOperationException($"{nameof(this.PreviewStyle)} 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)}."), + screenshotStyle: this.ScreenshotStyle ?? throw new InvalidOperationException($"{nameof(this.ScreenshotStyle)} 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) + RectangleInfo virtualScreen, + IEnumerable screens, + ScreenInfo activatedScreen, + RectangleInfo formBounds, + BoxStyle previewStyle, + BoxBounds previewBounds, + BoxStyle screenshotStyle, + IEnumerable screenshotBounds) + { + this.VirtualScreen = virtualScreen ?? throw new ArgumentNullException(nameof(virtualScreen)); + this.Screens = new( + (screens ?? throw new ArgumentNullException(nameof(screens))) + .ToList()); + this.ActivatedScreen = activatedScreen ?? throw new ArgumentNullException(nameof(activatedScreen)); + this.FormBounds = formBounds ?? throw new ArgumentNullException(nameof(formBounds)); + this.PreviewStyle = previewStyle ?? throw new ArgumentNullException(nameof(previewStyle)); + this.PreviewBounds = previewBounds ?? throw new ArgumentNullException(nameof(previewBounds)); + this.ScreenshotStyle = screenshotStyle ?? throw new ArgumentNullException(nameof(screenshotStyle)); + this.ScreenshotBounds = new( + (screenshotBounds ?? throw new ArgumentNullException(nameof(screenshotBounds))) + .ToList()); + } + + public RectangleInfo VirtualScreen + { + get; + } + + public ReadOnlyCollection Screens + { + get; + } + + public ScreenInfo ActivatedScreen + { + get; + } + + public RectangleInfo FormBounds + { + get; + } + + public BoxStyle PreviewStyle + { + get; + } + + public BoxBounds PreviewBounds { - this.PreviewSize = previewSize ?? throw new ArgumentNullException(nameof(previewSize)); + get; + } + + public BoxStyle ScreenshotStyle + { + get; } - public SizeInfo PreviewSize + public ReadOnlyCollection ScreenshotBounds { get; } diff --git a/src/FancyMouse/Models/Screen/ScreenInfo.cs b/src/FancyMouse/Models/Screen/ScreenInfo.cs index a15f34e..ea863a4 100644 --- a/src/FancyMouse/Models/Screen/ScreenInfo.cs +++ b/src/FancyMouse/Models/Screen/ScreenInfo.cs @@ -1,4 +1,5 @@ using FancyMouse.Models.Drawing; +using FancyMouse.NativeMethods; namespace FancyMouse.Models.Screen; @@ -8,7 +9,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; diff --git a/src/FancyMouse/Models/Settings/AppSettings.cs b/src/FancyMouse/Models/Settings/AppSettings.cs new file mode 100644 index 0000000..2094776 --- /dev/null +++ b/src/FancyMouse/Models/Settings/AppSettings.cs @@ -0,0 +1,25 @@ +using FancyMouse.Models.Layout; +using FancyMouse.WindowsHotKeys; + +namespace FancyMouse.Models.Settings; + +internal sealed class AppSettings +{ + public AppSettings( + Keystroke hotkey, + PreviewSettings preview) + { + this.Hotkey = hotkey ?? throw new ArgumentNullException(nameof(hotkey)); + this.Preview = preview ?? throw new ArgumentNullException(nameof(preview)); + } + + public Keystroke Hotkey + { + get; + } + + public PreviewSettings Preview + { + get; + } +} diff --git a/src/FancyMouse/Models/Settings/PreviewSettings.cs b/src/FancyMouse/Models/Settings/PreviewSettings.cs new file mode 100644 index 0000000..1382851 --- /dev/null +++ b/src/FancyMouse/Models/Settings/PreviewSettings.cs @@ -0,0 +1,64 @@ +using FancyMouse.Models.Drawing; +using FancyMouse.Models.Layout; + +namespace FancyMouse.Models.Settings; + +public class PreviewSettings +{ + public sealed class Builder + { + public SizeInfo? Size + { + get; + set; + } + + public BoxStyle? PreviewStyle + { + get; + set; + } + + public BoxStyle? ScreenshotStyle + { + get; + set; + } + + public PreviewSettings Build() + { + return new PreviewSettings( + size: this.Size ?? throw new InvalidOperationException($"{nameof(this.Size)} must be initialized before calling {nameof(this.Build)}."), + previewStyle: this.PreviewStyle ?? throw new InvalidOperationException($"{nameof(this.PreviewStyle)} must be initialized before calling {nameof(this.Build)}."), + screenshotStyle: this.ScreenshotStyle ?? throw new InvalidOperationException($"{nameof(this.ScreenshotStyle)} must be initialized before calling {nameof(this.Build)}.")); + } + } + + public PreviewSettings( + SizeInfo size, + BoxStyle previewStyle, + BoxStyle screenshotStyle) + { + this.Size = size ?? throw new ArgumentNullException(nameof(size)); + this.PreviewStyle = previewStyle ?? throw new ArgumentNullException(nameof(previewStyle)); + this.ScreenshotStyle = screenshotStyle ?? throw new ArgumentNullException(nameof(screenshotStyle)); + } + + /// + /// Gets the maximum allowed size of the preview image. + /// + public SizeInfo Size + { + get; + } + + public BoxStyle PreviewStyle + { + get; + } + + public BoxStyle ScreenshotStyle + { + get; + } +} diff --git a/src/FancyMouse/Program.cs b/src/FancyMouse/Program.cs index 7df7580..a543e18 100644 --- a/src/FancyMouse/Program.cs +++ b/src/FancyMouse/Program.cs @@ -1,9 +1,13 @@ using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; using FancyMouse.Models.Drawing; +using FancyMouse.Models.Settings; using FancyMouse.UI; using FancyMouse.WindowsHotKeys; using Microsoft.Extensions.Configuration; using NLog; +using Keys = FancyMouse.WindowsHotKeys.Keys; namespace FancyMouse; @@ -34,24 +38,152 @@ 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 options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() }, + }; + var appSettings = JsonSerializer.Deserialize( + File.ReadAllText("appSettings.json"), options) + ?? throw new InvalidOperationException(); + */ + + var legacySettings = new AppSettings( + hotkey: new( + key: Keys.F, + modifiers: KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift + ), + preview: new( + size: new(1600, 1200), + previewStyle: new( + marginInfo: MarginInfo.Empty, + borderInfo: new( + color: SystemColors.Highlight, + all: 5, + depth: 0 + ), + paddingInfo: new(0), + backgroundInfo: new( + color1: Color.FromArgb(13, 87, 210), // light blue + color2: Color.FromArgb(3, 68, 192) // darker blue + ) + ), + screenshotStyle: BoxStyle.Empty + )); + + var defaultSettings = new AppSettings( + hotkey: new( + key: Keys.F, + modifiers: KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift + ), + preview: new( + size: new(1600, 1200), + previewStyle: new( + marginInfo: MarginInfo.Empty, + borderInfo: new( + color: SystemColors.Highlight, + all: 7, + depth: 0 + ), + paddingInfo: new(15), + backgroundInfo: new( + color1: Color.FromArgb(13, 87, 210), // light blue + color2: Color.FromArgb(3, 68, 192) // darker blue + ) + ), + screenshotStyle: new( + marginInfo: new(1), + borderInfo: new( + color: Color.FromArgb(0xFF, 0x22, 0x22, 0x22), // dark grey + all: 15, + depth: 2 + ), + paddingInfo: PaddingInfo.Empty, + backgroundInfo: new( + Color.MidnightBlue, + Color.MidnightBlue + ) + ))); - var preview = (config["Preview"] ?? throw new InvalidOperationException("Missing config value 'Preview'")) - .Split("x").Select(s => int.Parse(s.Trim(), CultureInfo.InvariantCulture)).ToList(); + var spacedSettings = new AppSettings( + hotkey: new( + key: Keys.F, + modifiers: KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift + ), + preview: new( + size: new(800, 600), + previewStyle: new( + marginInfo: MarginInfo.Empty, + borderInfo: new( + color: SystemColors.Highlight, + all: 15, + depth: 4 + ), + paddingInfo: new(15), + backgroundInfo: new( + color1: Color.FromArgb(13, 87, 210), // light blue + color2: Color.FromArgb(3, 68, 192) // darker blue + ) + ), + screenshotStyle: new( + marginInfo: new(25), + borderInfo: BorderInfo.Empty, + paddingInfo: PaddingInfo.Empty, + backgroundInfo: new( + Color.MidnightBlue, + Color.MidnightBlue + ) + ))); + + var gaudy1Settings = new AppSettings( + hotkey: new( + key: Keys.F, + modifiers: KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift + ), + preview: new( + size: new(400, 300), + previewStyle: new( + marginInfo: MarginInfo.Empty, + borderInfo: new( + color: Color.Red, + all: 7, + depth: 2 + ), + paddingInfo: new(15), + backgroundInfo: new( + color1: Color.Yellow, + color2: Color.Green + )), + screenshotStyle: new( + marginInfo: new(1), + borderInfo: new( + color: Color.HotPink, + all: 15, + depth: 4 + ), + paddingInfo: PaddingInfo.Empty, + backgroundInfo: new( + Color.MidnightBlue, + Color.MidnightBlue + ) + ))); + + var appSettings = new[] + { + legacySettings, + defaultSettings, + spacedSettings, + gaudy1Settings, + }.Skip(3).First(); // logger: LogManager.LoadConfiguration(".\\NLog.config").GetCurrentClassLogger(), var dialog = new FancyMouseDialog( new FancyMouseDialogOptions( logger: LogManager.CreateNullLogger(), - maximumThumbnailSize: new SizeInfo( - preview[0], preview[1]))); + previewSettings: appSettings.Preview)); - 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/FancyMouseDialogOptions.cs b/src/FancyMouse/UI/FancyMouseDialogOptions.cs index d317d91..fa48d83 100644 --- a/src/FancyMouse/UI/FancyMouseDialogOptions.cs +++ b/src/FancyMouse/UI/FancyMouseDialogOptions.cs @@ -1,4 +1,4 @@ -using FancyMouse.Models.Drawing; +using FancyMouse.Models.Settings; using NLog; namespace FancyMouse.UI; @@ -7,10 +7,10 @@ internal sealed class FancyMouseDialogOptions { public FancyMouseDialogOptions( ILogger logger, - SizeInfo maximumThumbnailSize) + PreviewSettings previewSettings) { this.Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.MaximumThumbnailImageSize = maximumThumbnailSize; + this.PreviewSettings = previewSettings ?? throw new ArgumentNullException(nameof(previewSettings)); } public ILogger Logger @@ -18,7 +18,7 @@ public ILogger Logger get; } - public SizeInfo MaximumThumbnailImageSize + public PreviewSettings PreviewSettings { 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..737e22f 100644 --- a/src/FancyMouse/UI/FancyMouseForm.cs +++ b/src/FancyMouse/UI/FancyMouseForm.cs @@ -3,6 +3,7 @@ using FancyMouse.Helpers; using FancyMouse.Models.Drawing; using FancyMouse.Models.Layout; +using FancyMouse.Models.Screen; using NLog; using static FancyMouse.NativeMethods.Core; @@ -144,94 +145,42 @@ public void ShowThumbnail() "-----------")); var stopwatch = Stopwatch.StartNew(); - var layoutInfo = FancyMouseForm.GetLayoutInfo(logger, this); - LayoutHelper.PositionForm(this, layoutInfo.FormBounds); - FancyMouseForm.RenderPreview(this, layoutInfo); + + var screens = ScreenHelper.GetAllScreens(); + var activatedLocation = MouseHelper.GetCursorPosition(); + var previewLayout = LayoutHelper.GetPreviewLayout( + previewSettings: this.Options.PreviewSettings, + screens: screens, + activatedLocation: activatedLocation); + + LayoutHelper.PositionForm(this, previewLayout.FormBounds); + FancyMouseForm.RenderPreview(this, previewLayout); 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; - } - private static void RenderPreview( - FancyMouseForm form, LayoutInfo layoutInfo) + FancyMouseForm form, PreviewLayout previewLayout) { - var layoutConfig = layoutInfo.LayoutConfig; - var stopwatch = Stopwatch.StartNew(); // initialize the preview image - var preview = new Bitmap( - (int)layoutInfo.PreviewBounds.Width, - (int)layoutInfo.PreviewBounds.Height, + var previewImage = new Bitmap( + (int)previewLayout.PreviewBounds.OuterBounds.Width, + (int)previewLayout.PreviewBounds.OuterBounds.Height, PixelFormat.Format32bppArgb); - form.Thumbnail.Image = preview; + form.Thumbnail.Image = previewImage; - using var previewGraphics = Graphics.FromImage(preview); + using var previewGraphics = Graphics.FromImage(previewImage); - DrawingHelper.DrawPreviewBackground(previewGraphics, layoutInfo.PreviewBounds, layoutInfo.ScreenBounds); + DrawingHelper.DrawBoxBorder(previewGraphics, previewLayout.PreviewStyle, previewLayout.PreviewBounds); + DrawingHelper.DrawBoxBackground( + previewGraphics, + previewLayout.PreviewStyle, + previewLayout.PreviewBounds, + Enumerable.Empty()); var desktopHwnd = HWND.Null; var desktopHdc = HDC.Null; @@ -241,48 +190,55 @@ private static void RenderPreview( // 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)) + var activatedScreenIndex = previewLayout.Screens.IndexOf(previewLayout.ActivatedScreen); + var sourceScreens = new List { previewLayout.ActivatedScreen } + .Union(previewLayout.Screens.Where((_, idx) => idx != activatedScreenIndex)) .Select(screen => screen.Bounds) .ToList(); - var targetScreens = layoutInfo.ScreenBounds - .Where((_, idx) => idx == layoutConfig.ActivatedScreenIndex) - .Union(layoutInfo.ScreenBounds.Where((_, idx) => idx != layoutConfig.ActivatedScreenIndex)) + var targetScreens = previewLayout.ScreenshotBounds + .Where((_, idx) => idx == activatedScreenIndex) + .Union(previewLayout.ScreenshotBounds.Where((_, idx) => idx != activatedScreenIndex)) .ToList(); DrawingHelper.EnsureDesktopDeviceContext(ref desktopHwnd, ref desktopHdc); - DrawingHelper.EnsurePreviewDeviceContext(previewGraphics, ref previewHdc); + + // draw all the screenshot bezels + foreach (var screenshotBounds in previewLayout.ScreenshotBounds) + { + DrawingHelper.DrawBoxBorder( + previewGraphics, previewLayout.ScreenshotStyle, screenshotBounds); + } var placeholdersDrawn = false; for (var i = 0; i < sourceScreens.Count; i++) { - DrawingHelper.DrawPreviewScreen( + DrawingHelper.EnsurePreviewDeviceContext(previewGraphics, ref previewHdc); + DrawingHelper.DrawScreenshot( 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)) + 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); + 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.ScreenshotStyle, + targetScreens.Where((_, idx) => idx > i).ToList()); + placeholdersDrawn = true; + } + + FancyMouseForm.RefreshPreview(form); } } finally 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..0a76bc9 --- /dev/null +++ b/src/FancyMouse/appSettings.json @@ -0,0 +1,31 @@ +{ + "Hotkey": { + "Key": "F", + "Modifiers": "Control, Alt, Shift" + }, + "Preview": { + "Size": { + "Width": 1600, + "Height": 1200 + }, + "Border": { + "Visible": true, + "Weight": 5, + "Color": "Highlight" + }, + "Padding": { + "Visible": true, + "Width": 1 + }, + "Bezel": { + "Visible": true, + "Width": 10, + "Color": "Grey" + }, + "Background": { + "Visible": true, + "GradientColor1": "LightBlue", + "GradientColor2": "DarkBlue" + } + } +}