diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 33c5b20c01..a28081d341 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -124,6 +124,7 @@ bootstrapper BOOTSTRAPPERINSTALLFOLDER bostrot BOTTOMALIGN +boxmodel BPBF bpmf bpp diff --git a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/DrawingHelperTests.cs b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/DrawingHelperTests.cs new file mode 100644 index 0000000000..65af07098b --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/DrawingHelperTests.cs @@ -0,0 +1,152 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MouseJumpUI.Common.Helpers; +using MouseJumpUI.Common.Imaging; +using MouseJumpUI.Common.Models.Drawing; +using MouseJumpUI.Common.Models.Styles; +using MouseJumpUI.Helpers; + +namespace MouseJumpUI.UnitTests.Common.Helpers; + +[TestClass] +public static class DrawingHelperTests +{ + [TestClass] + public sealed class GetPreviewLayoutTests + { + public sealed class TestCase + { + public TestCase(PreviewStyle previewStyle, List screens, PointInfo activatedLocation, string desktopImageFilename, string expectedImageFilename) + { + this.PreviewStyle = previewStyle; + this.Screens = screens; + this.ActivatedLocation = activatedLocation; + this.DesktopImageFilename = desktopImageFilename; + this.ExpectedImageFilename = expectedImageFilename; + } + + public PreviewStyle PreviewStyle { get; } + + public List Screens { get; } + + public PointInfo ActivatedLocation { get; } + + public string DesktopImageFilename { get; } + + public string ExpectedImageFilename { get; } + } + + public static IEnumerable GetTestCases() + { + /* 4-grid */ + yield return new object[] + { + new TestCase( + previewStyle: StyleHelper.DefaultPreviewStyle, + screens: new List() + { + new(0, 0, 500, 500), + new(500, 0, 500, 500), + new(500, 500, 500, 500), + new(0, 500, 500, 500), + }, + activatedLocation: new(x: 50, y: 50), + desktopImageFilename: "Common/Helpers/_test-4grid-desktop.png", + expectedImageFilename: "Common/Helpers/_test-4grid-expected.png"), + }; + /* win 11 */ + yield return new object[] + { + new TestCase( + previewStyle: StyleHelper.DefaultPreviewStyle, + screens: new List() + { + new(5120, 349, 1920, 1080), + new(0, 0, 5120, 1440), + }, + activatedLocation: new(x: 50, y: 50), + desktopImageFilename: "Common/Helpers/_test-win11-desktop.png", + expectedImageFilename: "Common/Helpers/_test-win11-expected.png"), + }; + } + + [TestMethod] + [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + public void RunTestCases(TestCase data) + { + // load the fake desktop image + using var desktopImage = GetPreviewLayoutTests.LoadImageResource(data.DesktopImageFilename); + + // draw the preview image + var previewLayout = LayoutHelper.GetPreviewLayout( + previewStyle: data.PreviewStyle, + screens: data.Screens, + activatedLocation: data.ActivatedLocation); + var imageCopyService = new StaticImageRegionCopyService(desktopImage); + using var actual = DrawingHelper.RenderPreview(previewLayout, imageCopyService); + + // load the expected image + var expected = GetPreviewLayoutTests.LoadImageResource(data.ExpectedImageFilename); + + // compare the images + var screens = System.Windows.Forms.Screen.AllScreens; + AssertImagesEqual(expected, actual); + } + + private static Bitmap LoadImageResource(string filename) + { + var assembly = Assembly.GetExecutingAssembly(); + var assemblyName = new AssemblyName(assembly.FullName ?? throw new InvalidOperationException()); + var resourceName = $"Microsoft.{assemblyName.Name}.{filename.Replace("/", ".")}"; + var resourceNames = assembly.GetManifestResourceNames(); + if (!resourceNames.Contains(resourceName)) + { + throw new InvalidOperationException($"Embedded resource '{resourceName}' does not exist."); + } + + var stream = assembly.GetManifestResourceStream(resourceName) + ?? throw new InvalidOperationException(); + var image = (Bitmap)Image.FromStream(stream); + return image; + } + + /// + /// Naive / brute force image comparison - we can optimise this later :-) + /// + private static void AssertImagesEqual(Bitmap expected, Bitmap actual) + { + Assert.AreEqual( + expected.Width, + actual.Width, + $"expected width: {expected.Width}, actual width: {actual.Width}"); + Assert.AreEqual( + expected.Height, + actual.Height, + $"expected height: {expected.Height}, actual height: {actual.Height}"); + for (var y = 0; y < expected.Height; y++) + { + for (var x = 0; x < expected.Width; x++) + { + var expectedPixel = expected.GetPixel(x, y); + var actualPixel = actual.GetPixel(x, y); + + // allow a small tolerance for rounding differences in gdi + Assert.IsTrue( + (Math.Abs(expectedPixel.A - actualPixel.A) <= 1) && + (Math.Abs(expectedPixel.R - actualPixel.R) <= 1) && + (Math.Abs(expectedPixel.G - actualPixel.G) <= 1) && + (Math.Abs(expectedPixel.B - actualPixel.B) <= 1), + $"images differ at pixel ({x}, {y}) - expected: {expectedPixel}, actual: {actualPixel}"); + } + } + } + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/LayoutHelperTests.cs b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/LayoutHelperTests.cs new file mode 100644 index 0000000000..f4645c248f --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/LayoutHelperTests.cs @@ -0,0 +1,452 @@ +// 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. + +using System.Collections.Generic; +using System.Drawing; +using System.Text.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MouseJumpUI.Common.Helpers; +using MouseJumpUI.Common.Models.Drawing; +using MouseJumpUI.Common.Models.Layout; +using MouseJumpUI.Common.Models.Styles; + +namespace MouseJumpUI.UnitTests.Common.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; } + + public List Screens { get; } + + public PointInfo ActivatedLocation { get; } + + public PreviewLayout ExpectedResult { get; } + } + + public static IEnumerable GetTestCases() + { + // happy path - single screen with 50% scaling, + // *has* a 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 + ) + ), + screenStyle: 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 - single screen with 50% scaling, + // *no* preview borders but *has* screenshot borders + // + // +----------------+ + // | | + // | 0 | + // | | + // +----------------+ + previewStyle = new PreviewStyle( + canvasSize: new( + width: 512, + height: 384 + ), + canvasStyle: BoxStyle.Empty, + screenStyle: 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 + ) + ), + screenStyle: 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/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/MouseHelperTests.cs b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/MouseHelperTests.cs new file mode 100644 index 0000000000..a61a915137 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/MouseHelperTests.cs @@ -0,0 +1,74 @@ +// 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. + +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MouseJumpUI.Common.Helpers; +using MouseJumpUI.Common.Models.Drawing; + +namespace MouseJumpUI.UnitTests.Common.Helpers; + +[TestClass] +public static class MouseHelperTests +{ + [TestClass] + public sealed class GetJumpLocationTests + { + public sealed class TestCase + { + public TestCase(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds, PointInfo expectedResult) + { + this.PreviewLocation = previewLocation; + this.PreviewSize = previewSize; + this.DesktopBounds = desktopBounds; + this.ExpectedResult = expectedResult; + } + + public PointInfo PreviewLocation { get; } + + public SizeInfo PreviewSize { get; } + + public RectangleInfo DesktopBounds { get; } + + public PointInfo ExpectedResult { get; } + } + + public static IEnumerable GetTestCases() + { + // screen corners and midpoint with a zero origin + yield return new object[] { new TestCase(new(0, 0), new(160, 120), new(0, 0, 1600, 1200), new(0, 0)) }; + yield return new object[] { new TestCase(new(160, 0), new(160, 120), new(0, 0, 1600, 1200), new(1600, 0)) }; + yield return new object[] { new TestCase(new(0, 120), new(160, 120), new(0, 0, 1600, 1200), new(0, 1200)) }; + yield return new object[] { new TestCase(new(160, 120), new(160, 120), new(0, 0, 1600, 1200), new(1600, 1200)) }; + yield return new object[] { new TestCase(new(80, 60), new(160, 120), new(0, 0, 1600, 1200), new(800, 600)) }; + + // screen corners and midpoint with a positive origin + yield return new object[] { new TestCase(new(0, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 1000)) }; + yield return new object[] { new TestCase(new(160, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 1000)) }; + yield return new object[] { new TestCase(new(0, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 2200)) }; + yield return new object[] { new TestCase(new(160, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 2200)) }; + yield return new object[] { new TestCase(new(80, 60), new(160, 120), new(1000, 1000, 1600, 1200), new(1800, 1600)) }; + + // screen corners and midpoint with a negative origin + yield return new object[] { new TestCase(new(0, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, -1000)) }; + yield return new object[] { new TestCase(new(160, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, -1000)) }; + yield return new object[] { new TestCase(new(0, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, 200)) }; + yield return new object[] { new TestCase(new(160, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, 200)) }; + yield return new object[] { new TestCase(new(80, 60), new(160, 120), new(-1000, -1000, 1600, 1200), new(-200, -400)) }; + } + + [TestMethod] + [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + public void RunTestCases(TestCase data) + { + var actual = MouseHelper.GetJumpLocation( + data.PreviewLocation, + data.PreviewSize, + data.DesktopBounds); + var expected = data.ExpectedResult; + Assert.AreEqual(expected.X, actual.X); + Assert.AreEqual(expected.Y, actual.Y); + } + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/_test-4grid-desktop.png b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/_test-4grid-desktop.png new file mode 100644 index 0000000000..7ad69a7d8d Binary files /dev/null and b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/_test-4grid-desktop.png differ diff --git a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/_test-4grid-expected.png b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/_test-4grid-expected.png new file mode 100644 index 0000000000..de74caa55f Binary files /dev/null and b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/_test-4grid-expected.png differ diff --git a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/_test-win11-desktop.png b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/_test-win11-desktop.png new file mode 100644 index 0000000000..b874606bd3 Binary files /dev/null and b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/_test-win11-desktop.png differ diff --git a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/_test-win11-expected.png b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/_test-win11-expected.png new file mode 100644 index 0000000000..9d4b1612d0 Binary files /dev/null and b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Helpers/_test-win11-expected.png differ diff --git a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Models/Drawing/RectangleInfoTests.cs b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Models/Drawing/RectangleInfoTests.cs similarity index 68% rename from src/modules/MouseUtils/MouseJumpUI.UnitTests/Models/Drawing/RectangleInfoTests.cs rename to src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Models/Drawing/RectangleInfoTests.cs index 24d3bac4e1..48503006fa 100644 --- a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Models/Drawing/RectangleInfoTests.cs +++ b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Models/Drawing/RectangleInfoTests.cs @@ -4,9 +4,9 @@ using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MouseJumpUI.Models.Drawing; +using MouseJumpUI.Common.Models.Drawing; -namespace MouseJumpUI.UnitTests.Models.Drawing; +namespace MouseJumpUI.UnitTests.Common.Models.Drawing; [TestClass] public static class RectangleInfoTests @@ -23,30 +23,30 @@ public static class RectangleInfoTests 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() { // zero-sized - yield return new[] { new TestCase(new(0, 0, 0, 0), new(0, 0), new(0, 0, 0, 0)), }; + yield return new object[] { new TestCase(new(0, 0, 0, 0), new(0, 0), new(0, 0, 0, 0)), }; // zero-origin - yield return new[] { new TestCase(new(0, 0, 200, 200), new(100, 100), new(0, 0, 200, 200)), }; - yield return new[] { new TestCase(new(0, 0, 200, 200), new(500, 500), new(400, 400, 200, 200)), }; - yield return new[] { new TestCase(new(0, 0, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), }; + yield return new object[] { new TestCase(new(0, 0, 200, 200), new(100, 100), new(0, 0, 200, 200)), }; + yield return new object[] { new TestCase(new(0, 0, 200, 200), new(500, 500), new(400, 400, 200, 200)), }; + yield return new object[] { new TestCase(new(0, 0, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), }; // non-zero origin - yield return new[] { new TestCase(new(1000, 2000, 200, 200), new(100, 100), new(0, 0, 200, 200)), }; - yield return new[] { new TestCase(new(1000, 2000, 200, 200), new(500, 500), new(400, 400, 200, 200)), }; - yield return new[] { new TestCase(new(1000, 2000, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), }; + yield return new object[] { new TestCase(new(1000, 2000, 200, 200), new(100, 100), new(0, 0, 200, 200)), }; + yield return new object[] { new TestCase(new(1000, 2000, 200, 200), new(500, 500), new(400, 400, 200, 200)), }; + yield return new object[] { new TestCase(new(1000, 2000, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), }; // negative result - yield return new[] { new TestCase(new(0, 0, 1000, 1200), new(300, 300), new(-200, -300, 1000, 1200)), }; + yield return new object[] { new TestCase(new(0, 0, 1000, 1200), new(300, 300), new(-200, -300, 1000, 1200)), }; } [TestMethod] @@ -74,53 +74,53 @@ public static class RectangleInfoTests 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() { // already inside - obj fills bounds exactly - yield return new[] + yield return new object[] { new TestCase(new(0, 0, 100, 100), new(0, 0, 100, 100), new(0, 0, 100, 100)), }; // already inside - obj exactly in each corner - yield return new[] + yield return new object[] { new TestCase(new(0, 0, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)), }; - yield return new[] + yield return new object[] { new TestCase(new(100, 0, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)), }; - yield return new[] + yield return new object[] { new TestCase(new(0, 100, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)), }; - yield return new[] + yield return new object[] { new TestCase(new(100, 100, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)), }; // move inside - obj outside each corner - yield return new[] + yield return new object[] { new TestCase(new(-50, -50, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)), }; - yield return new[] + yield return new object[] { new TestCase(new(250, -50, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)), }; - yield return new[] + yield return new object[] { new TestCase(new(-50, 250, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)), }; - yield return new[] + yield return new object[] { new TestCase(new(150, 150, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)), }; diff --git a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Models/Drawing/SizeInfoTests.cs b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Models/Drawing/SizeInfoTests.cs similarity index 58% rename from src/modules/MouseUtils/MouseJumpUI.UnitTests/Models/Drawing/SizeInfoTests.cs rename to src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Models/Drawing/SizeInfoTests.cs index 5e0bd3d4fe..8c0113a9a2 100644 --- a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Models/Drawing/SizeInfoTests.cs +++ b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Common/Models/Drawing/SizeInfoTests.cs @@ -4,9 +4,9 @@ using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MouseJumpUI.Models.Drawing; +using MouseJumpUI.Common.Models.Drawing; -namespace MouseJumpUI.UnitTests.Drawing; +namespace MouseJumpUI.UnitTests.Common.Models.Drawing; [TestClass] public static class SizeInfoTests @@ -23,28 +23,28 @@ public static class SizeInfoTests 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() { // identity tests - yield return new[] { new TestCase(new(512, 384), new(512, 384), new(512, 384)), }; - yield return new[] { new TestCase(new(1024, 768), new(1024, 768), new(1024, 768)), }; + yield return new object[] { new TestCase(new(512, 384), new(512, 384), new(512, 384)), }; + yield return new object[] { new TestCase(new(1024, 768), new(1024, 768), new(1024, 768)), }; // general tests - yield return new[] { new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536)), }; - yield return new[] { new TestCase(new(2048, 1536), new(1024, 768), new(1024, 768)), }; + yield return new object[] { new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536)), }; + yield return new object[] { new TestCase(new(2048, 1536), new(1024, 768), new(1024, 768)), }; // scale to fit width - yield return new[] { new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)), }; + yield return new object[] { new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)), }; // scale to fit height - yield return new[] { new TestCase(new(512, 384), new(4096, 1536), new(2048, 1536)), }; + yield return new object[] { new TestCase(new(512, 384), new(4096, 1536), new(2048, 1536)), }; } [TestMethod] @@ -70,28 +70,28 @@ public static class SizeInfoTests 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() { // identity tests - yield return new[] { new TestCase(new(512, 384), new(512, 384), 1), }; - yield return new[] { new TestCase(new(1024, 768), new(1024, 768), 1), }; + yield return new object[] { new TestCase(new(512, 384), new(512, 384), 1), }; + yield return new object[] { new TestCase(new(1024, 768), new(1024, 768), 1), }; // general tests - yield return new[] { new TestCase(new(512, 384), new(2048, 1536), 4), }; - yield return new[] { new TestCase(new(2048, 1536), new(1024, 768), 0.5M), }; + yield return new object[] { new TestCase(new(512, 384), new(2048, 1536), 4), }; + yield return new object[] { new TestCase(new(2048, 1536), new(1024, 768), 0.5M), }; // scale to fit width - yield return new[] { new TestCase(new(512, 384), new(2048, 3072), 4), }; + yield return new object[] { new TestCase(new(512, 384), new(2048, 3072), 4), }; // scale to fit height - yield return new[] { new TestCase(new(512, 384), new(4096, 1536), 4), }; + yield return new object[] { new TestCase(new(512, 384), new(4096, 1536), 4), }; } [TestMethod] diff --git a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Helpers/DrawingHelperTests.cs b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Helpers/DrawingHelperTests.cs deleted file mode 100644 index 7579780ebc..0000000000 --- a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Helpers/DrawingHelperTests.cs +++ /dev/null @@ -1,229 +0,0 @@ -// 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. - -using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MouseJumpUI.Helpers; -using MouseJumpUI.Models.Drawing; -using MouseJumpUI.Models.Layout; -using MouseJumpUI.Models.Screen; -using static MouseJumpUI.NativeMethods.Core; - -namespace MouseJumpUI.UnitTests.Helpers; - -[TestClass] -public static class DrawingHelperTests -{ - [TestClass] - public sealed class CalculateLayoutInfoTests - { - public sealed class TestCase - { - public TestCase(LayoutConfig layoutConfig, LayoutInfo expectedResult) - { - this.LayoutConfig = layoutConfig; - this.ExpectedResult = expectedResult; - } - - public LayoutConfig LayoutConfig { get; set; } - - public LayoutInfo ExpectedResult { get; set; } - } - - public static IEnumerable GetTestCases() - { - // 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 ScreenInfo(HMONITOR.Null, false, new(0, 0, 5120, 1440), new(0, 0, 5120, 1440)), - }, - activatedLocation: new(5120 / 2, 1440 / 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[] { new TestCase(layoutConfig, layoutInfo) }; - - // 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 ScreenInfo(HMONITOR.Null, false, new(-1920, -472, 1920, 1080), new(-1920, -472, 1920, 1080)), - new ScreenInfo(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[] { new TestCase(layoutConfig, layoutInfo) }; - - // 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 ScreenInfo(HMONITOR.Null, false, new(6144, 0, 1024, 768), new(6144, 0, 1024, 768)), - new ScreenInfo(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[] { 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 ScreenInfo(HMONITOR.Null, false, new(6144, 0, 1280, 768), new(6144, 0, 1280, 768)), - new ScreenInfo(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[] { new TestCase(layoutConfig, layoutInfo) }; - } - - [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"); - } - - 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/modules/MouseUtils/MouseJumpUI.UnitTests/Helpers/MouseHelperTests.cs b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Helpers/MouseHelperTests.cs deleted file mode 100644 index e0267d4787..0000000000 --- a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Helpers/MouseHelperTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -// 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. - -using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MouseJumpUI.Helpers; -using MouseJumpUI.Models.Drawing; - -namespace MouseJumpUI.UnitTests.Helpers; - -[TestClass] -public static class MouseHelperTests -{ - [TestClass] - public sealed class GetJumpLocationTests - { - public sealed class TestCase - { - public TestCase(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds, PointInfo expectedResult) - { - this.PreviewLocation = previewLocation; - this.PreviewSize = previewSize; - this.DesktopBounds = desktopBounds; - this.ExpectedResult = expectedResult; - } - - public PointInfo PreviewLocation { get; set; } - - public SizeInfo PreviewSize { get; set; } - - public RectangleInfo DesktopBounds { get; set; } - - public PointInfo ExpectedResult { get; set; } - } - - public static IEnumerable GetTestCases() - { - // screen corners and midpoint with a zero origin - yield return new[] { new TestCase(new(0, 0), new(160, 120), new(0, 0, 1600, 1200), new(0, 0)) }; - yield return new[] { new TestCase(new(160, 0), new(160, 120), new(0, 0, 1600, 1200), new(1600, 0)) }; - yield return new[] { new TestCase(new(0, 120), new(160, 120), new(0, 0, 1600, 1200), new(0, 1200)) }; - yield return new[] { new TestCase(new(160, 120), new(160, 120), new(0, 0, 1600, 1200), new(1600, 1200)) }; - yield return new[] { new TestCase(new(80, 60), new(160, 120), new(0, 0, 1600, 1200), new(800, 600)) }; - - // screen corners and midpoint with a positive origin - yield return new[] { new TestCase(new(0, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 1000)) }; - yield return new[] { new TestCase(new(160, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 1000)) }; - yield return new[] { new TestCase(new(0, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 2200)) }; - yield return new[] { new TestCase(new(160, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 2200)) }; - yield return new[] { new TestCase(new(80, 60), new(160, 120), new(1000, 1000, 1600, 1200), new(1800, 1600)) }; - - // screen corners and midpoint with a negative origin - yield return new[] { new TestCase(new(0, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, -1000)) }; - yield return new[] { new TestCase(new(160, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, -1000)) }; - yield return new[] { new TestCase(new(0, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, 200)) }; - yield return new[] { new TestCase(new(160, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, 200)) }; - yield return new[] { new TestCase(new(80, 60), new(160, 120), new(-1000, -1000, 1600, 1200), new(-200, -400)) }; - } - - [TestMethod] - [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] - public void RunTestCases(TestCase data) - { - var actual = MouseHelper.GetJumpLocation( - data.PreviewLocation, - data.PreviewSize, - data.DesktopBounds); - var expected = data.ExpectedResult; - Assert.AreEqual(expected.X, actual.X); - Assert.AreEqual(expected.Y, actual.Y); - } - } -} diff --git a/src/modules/MouseUtils/MouseJumpUI.UnitTests/MouseJumpUI.UnitTests.csproj b/src/modules/MouseUtils/MouseJumpUI.UnitTests/MouseJumpUI.UnitTests.csproj index 66b91f1443..a2fab9dc7e 100644 --- a/src/modules/MouseUtils/MouseJumpUI.UnitTests/MouseJumpUI.UnitTests.csproj +++ b/src/modules/MouseUtils/MouseJumpUI.UnitTests/MouseJumpUI.UnitTests.csproj @@ -28,6 +28,13 @@ + + + + + + + diff --git a/src/modules/MouseUtils/MouseJumpUI/Common/Helpers/DrawingHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Helpers/DrawingHelper.cs new file mode 100644 index 0000000000..9537fca82e --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Helpers/DrawingHelper.cs @@ -0,0 +1,248 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Linq; +using MouseJumpUI.Common.Imaging; +using MouseJumpUI.Common.Models.Drawing; +using MouseJumpUI.Common.Models.Layout; +using MouseJumpUI.Common.Models.Styles; + +namespace MouseJumpUI.Common.Helpers; + +internal static class DrawingHelper +{ + public static Bitmap RenderPreview( + PreviewLayout previewLayout, + IImageRegionCopyService imageCopyService, + Action? previewImageCreatedCallback = null, + Action? previewImageUpdatedCallback = null) + { + var stopwatch = Stopwatch.StartNew(); + + // initialize the preview image + var previewBounds = previewLayout.PreviewBounds.OuterBounds.ToRectangle(); + var previewImage = new Bitmap(previewBounds.Width, previewBounds.Height, PixelFormat.Format32bppPArgb); + var previewGraphics = Graphics.FromImage(previewImage); + previewImageCreatedCallback?.Invoke(previewImage); + + DrawingHelper.DrawRaisedBorder(previewGraphics, previewLayout.PreviewStyle.CanvasStyle, previewLayout.PreviewBounds); + DrawingHelper.DrawBackgroundFill( + previewGraphics, + previewLayout.PreviewStyle.CanvasStyle, + previewLayout.PreviewBounds, + []); + + // sort the source and target screen areas into the order we want to + // draw them, 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] } + .Concat(previewLayout.Screens.Where((_, idx) => idx != previewLayout.ActivatedScreenIndex)) + .ToList(); + var targetScreens = new List { previewLayout.ScreenshotBounds[previewLayout.ActivatedScreenIndex] } + .Concat(previewLayout.ScreenshotBounds.Where((_, idx) => idx != previewLayout.ActivatedScreenIndex)) + .ToList(); + + // draw all the screenshot bezels + foreach (var screenshotBounds in previewLayout.ScreenshotBounds) + { + DrawingHelper.DrawRaisedBorder( + previewGraphics, previewLayout.PreviewStyle.ScreenStyle, screenshotBounds); + } + + var refreshRequired = false; + var placeholdersDrawn = false; + for (var i = 0; i < sourceScreens.Count; i++) + { + imageCopyService.CopyImageRegion(previewGraphics, sourceScreens[i], targetScreens[i].ContentBounds); + refreshRequired = 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 (stopwatch.ElapsedMilliseconds > 250) + { + // draw placeholder backgrounds for any undrawn screens + if (!placeholdersDrawn) + { + DrawingHelper.DrawScreenPlaceholders( + previewGraphics, + previewLayout.PreviewStyle.ScreenStyle, + targetScreens.GetRange(i + 1, targetScreens.Count - i - 1)); + placeholdersDrawn = true; + } + + previewImageUpdatedCallback?.Invoke(); + refreshRequired = false; + } + } + + if (refreshRequired) + { + previewImageUpdatedCallback?.Invoke(); + } + + stopwatch.Stop(); + + return previewImage; + } + + /// + /// Draws a border shape with an optional raised 3d highlight and shadow effect. + /// + private static void DrawRaisedBorder( + Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds) + { + var borderStyle = boxStyle.BorderStyle; + if ((borderStyle.Horizontal == 0) || (borderStyle.Vertical == 0)) + { + return; + } + + // draw the main box border + using var borderBrush = new SolidBrush(borderStyle.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)); + + var outer = ( + Left: bounds.Left, + Top: bounds.Top, + Right: bounds.Right - 1, + Bottom: bounds.Bottom - 1 + ); + var inner = ( + Left: bounds.Left + (int)borderStyle.Left - 1, + Top: bounds.Top + (int)borderStyle.Top - 1, + Right: bounds.Right - (int)borderStyle.Right, + Bottom: bounds.Bottom - (int)borderStyle.Bottom + ); + + for (var i = 0; i < borderStyle.Depth; i++) + { + // left edge + if (borderStyle.Left >= i * 2) + { + graphics.DrawLine(highlight, outer.Left, outer.Top, outer.Left, outer.Bottom); + graphics.DrawLine(shadow, inner.Left, inner.Top, inner.Left, inner.Bottom); + } + + // top edge + if (borderStyle.Top >= i * 2) + { + graphics.DrawLine(highlight, outer.Left, outer.Top, outer.Right, outer.Top); + graphics.DrawLine(shadow, inner.Left, inner.Top, inner.Right, inner.Top); + } + + // right edge + if (borderStyle.Right >= i * 2) + { + graphics.DrawLine(highlight, inner.Right, inner.Top, inner.Right, inner.Bottom); + graphics.DrawLine(shadow, outer.Right, outer.Top, outer.Right, outer.Bottom); + } + + // bottom edge + if (borderStyle.Bottom >= i * 2) + { + graphics.DrawLine(highlight, inner.Left, inner.Bottom, inner.Right, inner.Bottom); + graphics.DrawLine(shadow, outer.Left, outer.Bottom, outer.Right, outer.Bottom); + } + + // shrink the outer border for the next iteration + outer = ( + outer.Left + 1, + outer.Top + 1, + outer.Right - 1, + outer.Bottom - 1 + ); + + // enlarge the inner border for the next iteration + inner = ( + inner.Left - 1, + inner.Top - 1, + inner.Right + 1, + inner.Bottom + 1 + ); + } + } + + /// + /// Draws a gradient-filled background shape. + /// + private static void DrawBackgroundFill( + Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds, IEnumerable excludeBounds) + { + var backgroundBounds = boxBounds.PaddingBounds; + + using var backgroundBrush = DrawingHelper.GetBackgroundStyleBrush(boxStyle.BackgroundStyle, backgroundBounds); + if (backgroundBrush == null) + { + return; + } + + // 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(backgroundBounds.ToRectangle()); + foreach (var exclude in excludeBounds) + { + backgroundRegion.Exclude(exclude.ToRectangle()); + } + + graphics.FillRegion(backgroundBrush, backgroundRegion); + } + + /// + /// Draws placeholder background images for the specified screens on the preview. + /// + private static void DrawScreenPlaceholders( + Graphics graphics, BoxStyle screenStyle, IList screenBounds) + { + if (screenBounds.Count == 0) + { + return; + } + + if (screenStyle?.BackgroundStyle?.Color1 == null) + { + return; + } + + using var brush = new SolidBrush(screenStyle.BackgroundStyle.Color1.Value); + graphics.FillRectangles(brush, screenBounds.Select(bounds => bounds.PaddingBounds.ToRectangle()).ToArray()); + } + + private static Brush? GetBackgroundStyleBrush(BackgroundStyle backgroundStyle, RectangleInfo backgroundBounds) + { + var backgroundBrush = backgroundStyle switch + { + { Color1: not null, Color2: not null } => + /* draw a gradient fill if both colors are specified */ + new LinearGradientBrush( + backgroundBounds.ToRectangle(), + backgroundStyle.Color1.Value, + backgroundStyle.Color2.Value, + LinearGradientMode.ForwardDiagonal), + { Color1: not null } => + /* draw a solid fill if only one color is specified */ + new SolidBrush( + backgroundStyle.Color1.Value), + { Color2: not null } => + /* draw a solid fill if only one color is specified */ + new SolidBrush( + backgroundStyle.Color2.Value), + _ => (Brush?)null, + }; + return backgroundBrush; + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Common/Helpers/LayoutHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Helpers/LayoutHelper.cs new file mode 100644 index 0000000000..e5a57ee567 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Helpers/LayoutHelper.cs @@ -0,0 +1,159 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Linq; +using MouseJumpUI.Common.Models.Drawing; +using MouseJumpUI.Common.Models.Layout; +using MouseJumpUI.Common.Models.Styles; + +namespace MouseJumpUI.Common.Helpers; + +internal static class LayoutHelper +{ + public static PreviewLayout GetPreviewLayout( + PreviewStyle previewStyle, List screens, PointInfo activatedLocation) + { + ArgumentNullException.ThrowIfNull(previewStyle); + ArgumentNullException.ThrowIfNull(screens); + + if (screens.Count == 0) + { + throw new ArgumentException("Value must contain at least one item.", nameof(screens)); + } + + var builder = new PreviewLayout.Builder(); + builder.Screens = screens.ToList(); + + // calculate the bounding rectangle for the virtual screen + builder.VirtualScreen = LayoutHelper.GetCombinedScreenBounds(builder.Screens); + + // find the screen that contains the activated location - this is the + // one we'll show the preview form on + var activatedScreen = builder.Screens.Single( + screen => screen.Contains(activatedLocation)); + builder.ActivatedScreenIndex = builder.Screens.IndexOf(activatedScreen); + + // work out the maximum allowed size of the preview form: + // * can't be bigger than the activated screen + // * can't be bigger than the configured canvas size + var maxPreviewSize = activatedScreen.Size + .Intersect(previewStyle.CanvasSize); + + // the "content area" (i.e. 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); + + // scale the virtual screen to fit inside the content area + var screenScalingRatio = builder.VirtualScreen.Size + .ScaleToFitRatio(maxContentSize); + + // work out the actual size of the "content area" by scaling the virtual screen + // to fit inside the maximum content area while maintaining its aspect ration. + // we'll also offset it to allow for any margins, borders and padding + var contentBounds = builder.VirtualScreen.Size + .Scale(screenScalingRatio) + .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 actual size of the content area we can work outwards to + // get the size of the background bounds including margins, borders and padding + builder.PreviewStyle = previewStyle; + builder.PreviewBounds = LayoutHelper.GetBoxBoundsFromContentBounds( + contentBounds, + previewStyle.CanvasStyle); + + // ... and then the size and position of the preview form on the activated screen + // * center the form to the activated position, but nudge it back + // inside the visible area of the activated screen if it falls outside + var formBounds = builder.PreviewBounds.OuterBounds + .Center(activatedLocation) + .Clamp(activatedScreen); + builder.FormBounds = formBounds; + + // now calculate the positions of each of the screenshot images on the preview + builder.ScreenshotBounds = builder.Screens + .Select( + screen => LayoutHelper.GetBoxBoundsFromOuterBounds( + screen + .Offset(builder.VirtualScreen.Location.ToSize().Invert()) + .Scale(screenScalingRatio) + .Offset(builder.PreviewBounds.ContentBounds.Location.ToSize()) + .Truncate(), + previewStyle.ScreenStyle)) + .ToList(); + + return builder.Build(); + } + + internal static RectangleInfo GetCombinedScreenBounds(List screens) + { + return screens.Skip(1).Aggregate( + seed: screens.First(), + (bounds, screen) => bounds.Union(screen)); + } + + /// + /// Calculates the bounds of the various areas of a box, given the content bounds and the box style. + /// Starts with the content bounds and works outward, enlarging the content bounds by the padding, border, and margin sizes to calculate the outer bounds of the box. + /// + /// The content bounds of the box. + /// The style of the box, which includes the sizes of the margin, border, and padding areas. + /// A object that represents the bounds of the different areas of the box. + /// Thrown when or is null. + /// Thrown when any of the styles in is null. + internal static BoxBounds GetBoxBoundsFromContentBounds( + RectangleInfo contentBounds, + BoxStyle boxStyle) + { + ArgumentNullException.ThrowIfNull(contentBounds); + ArgumentNullException.ThrowIfNull(boxStyle); + if (boxStyle.PaddingStyle == null || boxStyle.BorderStyle == null || boxStyle.MarginStyle == null) + { + throw new ArgumentException(null, nameof(boxStyle)); + } + + var paddingBounds = contentBounds.Enlarge(boxStyle.PaddingStyle); + var borderBounds = paddingBounds.Enlarge(boxStyle.BorderStyle); + var marginBounds = borderBounds.Enlarge(boxStyle.MarginStyle); + var outerBounds = marginBounds; + return new( + outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds); + } + + /// + /// Calculates the bounds of the various areas of a box, given the outer bounds and the box style. + /// This method starts with the outer bounds and works inward, shrinking the outer bounds by the margin, border, and padding sizes to calculate the content bounds of the box. + /// + /// The outer bounds of the box. + /// The style of the box, which includes the sizes of the margin, border, and padding areas. + /// A object that represents the bounds of the different areas of the box. + /// Thrown when or is null. + /// Thrown when any of the styles in is null. + internal static BoxBounds GetBoxBoundsFromOuterBounds( + RectangleInfo outerBounds, + BoxStyle boxStyle) + { + ArgumentNullException.ThrowIfNull(outerBounds); + ArgumentNullException.ThrowIfNull(boxStyle); + if (outerBounds == null || boxStyle.MarginStyle == null || boxStyle.BorderStyle == null || boxStyle.PaddingStyle == null) + { + throw new ArgumentException(null, nameof(boxStyle)); + } + + var marginBounds = outerBounds; + var borderBounds = marginBounds.Shrink(boxStyle.MarginStyle); + var paddingBounds = borderBounds.Shrink(boxStyle.BorderStyle); + var contentBounds = paddingBounds.Shrink(boxStyle.PaddingStyle); + return new( + outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds); + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/MouseHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Helpers/MouseHelper.cs similarity index 74% rename from src/modules/MouseUtils/MouseJumpUI/Helpers/MouseHelper.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/Helpers/MouseHelper.cs index 78eb9a496d..f06be27646 100644 --- a/src/modules/MouseUtils/MouseJumpUI/Helpers/MouseHelper.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Helpers/MouseHelper.cs @@ -4,12 +4,12 @@ using System.ComponentModel; using System.Runtime.InteropServices; -using System.Windows.Forms; -using MouseJumpUI.Models.Drawing; -using MouseJumpUI.NativeMethods; -using static MouseJumpUI.NativeMethods.Core; +using MouseJumpUI.Common.Models.Drawing; +using MouseJumpUI.Common.NativeMethods; +using static MouseJumpUI.Common.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.User32; -namespace MouseJumpUI.Helpers; +namespace MouseJumpUI.Common.Helpers; internal static class MouseHelper { @@ -22,7 +22,7 @@ internal static class MouseHelper /// or even negative if the primary monitor is not the at the top-left of the /// entire desktop rectangle, so results may contain negative coordinates. /// - public static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds) + internal static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds) { return previewLocation .Scale(previewSize.ScaleToFitRatio(desktopBounds.Size)) @@ -32,7 +32,7 @@ internal static class MouseHelper /// /// Get the current position of the cursor. /// - public static PointInfo GetCursorPosition() + internal static PointInfo GetCursorPosition() { var lpPoint = new LPPOINT(new POINT(0, 0)); var result = User32.GetCursorPos(lpPoint); @@ -55,7 +55,7 @@ internal static class MouseHelper /// /// See https://github.com/mikeclayton/FancyMouse/pull/3 /// - public static void SetCursorPosition(PointInfo location) + internal static void SetCursorPosition(PointInfo location) { // set the new cursor position *twice* - the cursor sometimes end up in // the wrong place if we try to cross the dead space between non-aligned @@ -73,15 +73,21 @@ internal static class MouseHelper // // 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; + } } // temporary workaround for issue #1273 @@ -95,25 +101,25 @@ internal static class MouseHelper /// See https://github.com/microsoft/PowerToys/issues/24523 /// https://github.com/microsoft/PowerToys/pull/24527 /// - public static void SimulateMouseMovementEvent(PointInfo location) + internal static void SimulateMouseMovementEvent(PointInfo location) { var inputs = new User32.INPUT[] { new( - type: User32.INPUT_TYPE.INPUT_MOUSE, - data: new User32.INPUT.DUMMYUNIONNAME( - mi: new User32.MOUSEINPUT( + type: INPUT_TYPE.INPUT_MOUSE, + data: new INPUT.DUMMYUNIONNAME( + mi: new MOUSEINPUT( dx: (int)MouseHelper.CalculateAbsoluteCoordinateX(location.X), dy: (int)MouseHelper.CalculateAbsoluteCoordinateY(location.Y), mouseData: 0, - dwFlags: User32.MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | User32.MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE, + dwFlags: MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE, time: 0, dwExtraInfo: ULONG_PTR.Null))), }; var result = User32.SendInput( - (uint)inputs.Length, - new User32.LPINPUT(inputs), - User32.INPUT.Size * inputs.Length); + (UINT)inputs.Length, + new LPINPUT(inputs), + INPUT.Size * inputs.Length); if (result != inputs.Length) { throw new Win32Exception( @@ -125,13 +131,13 @@ internal static class MouseHelper { // If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535. // see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput - return (x * 65535) / User32.GetSystemMetrics(User32.SYSTEM_METRICS_INDEX.SM_CXSCREEN); + return (x * 65535) / User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN); } - internal static decimal CalculateAbsoluteCoordinateY(decimal y) + private static decimal CalculateAbsoluteCoordinateY(decimal y) { // If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535. // see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput - return (y * 65535) / User32.GetSystemMetrics(User32.SYSTEM_METRICS_INDEX.SM_CYSCREEN); + return (y * 65535) / User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN); } } diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/ScreenHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Helpers/ScreenHelper.cs similarity index 73% rename from src/modules/MouseUtils/MouseJumpUI/Helpers/ScreenHelper.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/Helpers/ScreenHelper.cs index 4a61e6a6b3..a280b59a75 100644 --- a/src/modules/MouseUtils/MouseJumpUI/Helpers/ScreenHelper.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Helpers/ScreenHelper.cs @@ -5,13 +5,13 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using MouseJumpUI.Models.Drawing; -using MouseJumpUI.Models.Screen; -using MouseJumpUI.NativeMethods; -using static MouseJumpUI.NativeMethods.Core; -using static MouseJumpUI.NativeMethods.User32; +using System.Linq; +using MouseJumpUI.Common.Models.Drawing; +using MouseJumpUI.Common.NativeMethods; +using static MouseJumpUI.Common.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.User32; -namespace MouseJumpUI.Helpers; +namespace MouseJumpUI.Common.Helpers; internal static class ScreenHelper { @@ -28,22 +28,21 @@ internal static class ScreenHelper User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYVIRTUALSCREEN)); } - public static IEnumerable GetAllScreens() + internal static IEnumerable GetAllScreens() { // enumerate the monitors attached to the system var hMonitors = new List(); - var result = User32.EnumDisplayMonitors( - HDC.Null, - LPCRECT.Null, - (unnamedParam1, unnamedParam2, unnamedParam3, unnamedParam4) => + var callback = new User32.MONITORENUMPROC( + (hMonitor, hdcMonitor, lprcMonitor, dwData) => { - hMonitors.Add(unnamedParam1); + hMonitors.Add(hMonitor); return true; - }, - LPARAM.Null); + }); + var result = User32.EnumDisplayMonitors(HDC.Null, LPCRECT.Null, callback, LPARAM.Null); if (!result) { throw new Win32Exception( + result.Value, $"{nameof(User32.EnumDisplayMonitors)} failed with return code {result.Value}"); } @@ -51,11 +50,12 @@ internal static class ScreenHelper foreach (var hMonitor in hMonitors) { var monitorInfoPtr = new LPMONITORINFO( - new MONITORINFO((uint)MONITORINFO.Size, RECT.Empty, RECT.Empty, 0)); + new MONITORINFO((DWORD)MONITORINFO.Size, RECT.Empty, RECT.Empty, 0)); result = User32.GetMonitorInfoW(hMonitor, monitorInfoPtr); if (!result) { throw new Win32Exception( + result.Value, $"{nameof(User32.GetMonitorInfoW)} failed with return code {result.Value}"); } @@ -78,9 +78,11 @@ internal static class ScreenHelper } } - public static HMONITOR MonitorFromPoint( + internal static ScreenInfo GetScreenFromPoint( + List screens, PointInfo pt) { + // get the monitor handle from the point var hMonitor = User32.MonitorFromPoint( new((int)pt.X, (int)pt.Y), User32.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST); @@ -89,6 +91,9 @@ internal static class ScreenHelper throw new InvalidOperationException($"no monitor found for point {pt}"); } - return hMonitor; + // find the screen with the given monitor handle + var screen = screens + .Single(item => item.Handle == hMonitor); + return screen; } } diff --git a/src/modules/MouseUtils/MouseJumpUI/Common/Imaging/DesktopImageRegionCopyService.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Imaging/DesktopImageRegionCopyService.cs new file mode 100644 index 0000000000..7b3653d837 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Imaging/DesktopImageRegionCopyService.cs @@ -0,0 +1,123 @@ +// 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. + +using System; +using System.Diagnostics; +using System.Drawing; +using MouseJumpUI.Common.Models.Drawing; +using MouseJumpUI.Common.NativeMethods; +using static MouseJumpUI.Common.NativeMethods.Core; + +namespace MouseJumpUI.Common.Imaging; + +/// +/// Implements an IImageRegionCopyService that uses the current desktop window as the copy source. +/// This is used during the main application runtime to generate preview images of the desktop. +/// +internal sealed class DesktopImageRegionCopyService : IImageRegionCopyService +{ + /// + /// Copies the source region from the current desktop window + /// to the target region on the specified Graphics object. + /// + public void CopyImageRegion( + Graphics targetGraphics, + RectangleInfo sourceBounds, + RectangleInfo targetBounds) + { + var stopwatch = Stopwatch.StartNew(); + var (desktopHwnd, desktopHdc) = DesktopImageRegionCopyService.GetDesktopDeviceContext(); + var previewHdc = DesktopImageRegionCopyService.GetGraphicsDeviceContext( + targetGraphics, Gdi32.STRETCH_BLT_MODE.STRETCH_HALFTONE); + stopwatch.Stop(); + + var source = sourceBounds.ToRectangle(); + var target = targetBounds.ToRectangle(); + var result = Gdi32.StretchBlt( + previewHdc, + target.X, + target.Y, + target.Width, + target.Height, + desktopHdc, + source.X, + source.Y, + source.Width, + source.Height, + Gdi32.ROP_CODE.SRCCOPY); + if (!result) + { + throw new InvalidOperationException( + $"{nameof(Gdi32.StretchBlt)} returned {result.Value}"); + } + + // we need to release the graphics device context handle before anything + // else tries to use the Graphics object otherwise it'll give an error + // from GDI saying "Object is currently in use elsewhere" + DesktopImageRegionCopyService.FreeGraphicsDeviceContext(targetGraphics, ref previewHdc); + + DesktopImageRegionCopyService.FreeDesktopDeviceContext(ref desktopHwnd, ref desktopHdc); + } + + private static (HWND DesktopHwnd, HDC DesktopHdc) GetDesktopDeviceContext() + { + var desktopHwnd = User32.GetDesktopWindow(); + var desktopHdc = User32.GetWindowDC(desktopHwnd); + if (desktopHdc.IsNull) + { + throw new InvalidOperationException( + $"{nameof(User32.GetWindowDC)} returned null"); + } + + return (desktopHwnd, desktopHdc); + } + + private static void FreeDesktopDeviceContext(ref HWND desktopHwnd, ref HDC desktopHdc) + { + if (!desktopHwnd.IsNull && !desktopHdc.IsNull) + { + var result = User32.ReleaseDC(desktopHwnd, desktopHdc); + if (result == 0) + { + throw new InvalidOperationException( + $"{nameof(User32.ReleaseDC)} returned {result}"); + } + } + + desktopHwnd = HWND.Null; + desktopHdc = HDC.Null; + } + + /// + /// Checks if the target device context handle exists, and creates a new one from the + /// specified Graphics object if not. + /// + private static HDC GetGraphicsDeviceContext(Graphics graphics, Gdi32.STRETCH_BLT_MODE mode) + { + var graphicsHdc = (HDC)graphics.GetHdc(); + + var result = Gdi32.SetStretchBltMode(graphicsHdc, mode); + if (result == 0) + { + throw new InvalidOperationException( + $"{nameof(Gdi32.SetStretchBltMode)} returned {result}"); + } + + return graphicsHdc; + } + + /// + /// Free the specified device context handle if it exists. + /// + private static void FreeGraphicsDeviceContext(Graphics graphics, ref HDC graphicsHdc) + { + if (graphicsHdc.IsNull) + { + return; + } + + graphics.ReleaseHdc(graphicsHdc.Value); + graphicsHdc = HDC.Null; + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Common/Imaging/IImageRegionCopyService.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Imaging/IImageRegionCopyService.cs new file mode 100644 index 0000000000..24c33766d5 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Imaging/IImageRegionCopyService.cs @@ -0,0 +1,24 @@ +// 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. + +using System.Drawing; +using MouseJumpUI.Common.Models.Drawing; + +namespace MouseJumpUI.Common.Imaging; + +internal interface IImageRegionCopyService +{ + /// + /// Copies the source region from the provider's source image (e.g. the interactive desktop, + /// a static image, etc) to the target region on the specified Graphics object. + /// + /// + /// Implementations of this interface are used to capture regions of the interactive desktop + /// during runtime, or to capture regions of a static reference image during unit tests. + /// + void CopyImageRegion( + Graphics targetGraphics, + RectangleInfo sourceBounds, + RectangleInfo targetBounds); +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Common/Imaging/StaticImageRegionCopyService.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Imaging/StaticImageRegionCopyService.cs new file mode 100644 index 0000000000..a7557c52f3 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Imaging/StaticImageRegionCopyService.cs @@ -0,0 +1,42 @@ +// 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. + +using System; +using System.Drawing; +using MouseJumpUI.Common.Models.Drawing; + +namespace MouseJumpUI.Common.Imaging; + +/// +/// Implements an IImageRegionCopyService that uses the specified image as the copy source. +/// This is used for testing the DrawingHelper rather than as part of the main application. +/// +internal sealed class StaticImageRegionCopyService : IImageRegionCopyService +{ + public StaticImageRegionCopyService(Image sourceImage) + { + this.SourceImage = sourceImage ?? throw new ArgumentNullException(nameof(sourceImage)); + } + + private Image SourceImage + { + get; + } + + /// + /// Copies the source region from the static source image + /// to the target region on the specified Graphics object. + /// + public void CopyImageRegion( + Graphics targetGraphics, + RectangleInfo sourceBounds, + RectangleInfo targetBounds) + { + targetGraphics.DrawImage( + image: this.SourceImage, + destRect: targetBounds.ToRectangle(), + srcRect: sourceBounds.ToRectangle(), + srcUnit: GraphicsUnit.Pixel); + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/BoxBounds.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/BoxBounds.cs new file mode 100644 index 0000000000..8b04ab9d28 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/BoxBounds.cs @@ -0,0 +1,75 @@ +// 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. + +using System; + +namespace MouseJumpUI.Common.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/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/PointInfo.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/PointInfo.cs new file mode 100644 index 0000000000..41ba60eec4 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/PointInfo.cs @@ -0,0 +1,80 @@ +// 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. + +using System; +using System.Drawing; + +namespace MouseJumpUI.Common.Models.Drawing; + +/// +/// Immutable version of a System.Drawing.Point object with some extra utility methods. +/// +public sealed class PointInfo +{ + public PointInfo(decimal x, decimal y) + { + this.X = x; + this.Y = y; + } + + public PointInfo(Point point) + : this(point.X, point.Y) + { + } + + public decimal X + { + get; + } + + public decimal Y + { + get; + } + + /// + /// Moves this PointInfo inside the specified RectangleInfo. + /// + public PointInfo Clamp(RectangleInfo outer) + { + 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); + + public PointInfo Offset(PointInfo amount) => new(this.X + amount.X, this.Y + amount.Y); + + 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 "{" + + $"{nameof(this.X)}={this.X}," + + $"{nameof(this.Y)}={this.Y}" + + "}"; + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/RectangleInfo.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/RectangleInfo.cs new file mode 100644 index 0000000000..8118138db4 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/RectangleInfo.cs @@ -0,0 +1,300 @@ +// 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. + +using System; +using System.Drawing; +using System.Text.Json.Serialization; +using MouseJumpUI.Common.Models.Styles; +using BorderStyle = MouseJumpUI.Common.Models.Styles.BorderStyle; + +namespace MouseJumpUI.Common.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; + this.Y = y; + this.Width = width; + this.Height = height; + } + + public RectangleInfo(Rectangle rectangle) + : this(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height) + { + } + + public RectangleInfo(Point location, SizeInfo size) + : this(location.X, location.Y, size.Width, size.Height) + { + } + + public RectangleInfo(SizeInfo size) + : this(0, 0, size.Width, size.Height) + { + } + + public decimal X + { + get; + } + + public decimal Y + { + get; + } + + public decimal Width + { + get; + } + + 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; + + [JsonIgnore] + public decimal Area => + this.Width * this.Height; + + [JsonIgnore] + public PointInfo Location => + new(this.X, this.Y); + + [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); + + /// + /// Centers the rectangle around a specified point. + /// + /// The around which the rectangle will be centered. + /// A new that is centered around the specified point. + public RectangleInfo Center(PointInfo point) => + new( + x: point.X - (this.Width / 2), + y: point.Y - (this.Height / 2), + width: this.Width, + height: this.Height); + + /// + /// Returns a new that is moved within the bounds of the specified outer rectangle. + /// If the current rectangle is larger than the outer rectangle, an exception is thrown. + /// + /// The outer within which to confine this rectangle. + /// A new that is the result of moving this rectangle within the bounds of the outer rectangle. + /// Thrown when the current rectangle is larger than the outer rectangle. + 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(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 + /// + 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); + + /// + /// Returns a new that is larger than the current rectangle. + /// The dimensions of the new rectangle are calculated by enlarging the current rectangle's dimensions by the size of the border. + /// + /// The that specifies the amount to enlarge the rectangle on each side. + /// A new that is larger than the current rectangle by the specified border amounts. + public RectangleInfo Enlarge(BorderStyle border) => + new( + this.X - border.Left, + this.Y - border.Top, + this.Width + border.Horizontal, + this.Height + border.Vertical); + + /// + /// Returns a new that is larger than the current rectangle. + /// The dimensions of the new rectangle are calculated by enlarging the current rectangle's dimensions by the size of the margin. + /// + /// The that specifies the amount to enlarge the rectangle on each side. + /// A new that is larger than the current rectangle by the specified margin amounts. + public RectangleInfo Enlarge(MarginStyle margin) => + new( + this.X - margin.Left, + this.Y - margin.Top, + this.Width + margin.Horizontal, + this.Height + margin.Vertical); + + /// + /// Returns a new that is larger than the current rectangle. + /// The dimensions of the new rectangle are calculated by enlarging the current rectangle's dimensions by the size of the padding. + /// + /// The that specifies the amount to enlarge the rectangle on each side. + /// A new that is larger than the current rectangle by the specified padding amounts. + public RectangleInfo Enlarge(PaddingStyle padding) => + new( + this.X - padding.Left, + this.Y - padding.Top, + this.Width + padding.Horizontal, + this.Height + padding.Vertical); + + /// + /// Returns a new that is offset by the specified amount. + /// + /// The representing the amount to offset in both the X and Y directions. + /// A new that is offset by the specified amount. + public RectangleInfo Offset(SizeInfo amount) => + this.Offset(amount.Width, amount.Height); + + /// + /// Returns a new that is offset by the specified X and Y distances. + /// + /// The distance to offset the rectangle along the X-axis. + /// The distance to offset the rectangle along the Y-axis. + /// A new that is offset by the specified X and Y distances. + public RectangleInfo Offset(decimal dx, decimal dy) => + new(this.X + dx, this.Y + dy, this.Width, this.Height); + + /// + /// Returns a new that is a scaled version of the current rectangle. + /// The dimensions of the new rectangle are calculated by multiplying the current rectangle's dimensions by the scaling factor. + /// + /// The factor by which to scale the rectangle's dimensions. + /// A new that is a scaled version of the current rectangle. + public RectangleInfo Scale(decimal scalingFactor) => + new( + this.X * scalingFactor, + this.Y * scalingFactor, + this.Width * scalingFactor, + this.Height * scalingFactor); + + /// + /// Returns a new that is smaller than the current rectangle. + /// The dimensions of the new rectangle are calculated by shrinking the current rectangle's dimensions by the size of the border. + /// + /// The that specifies the amount to shrink the rectangle on each side. + /// A new that is smaller than the current rectangle by the specified border amounts. + public RectangleInfo Shrink(BorderStyle border) => + new( + this.X + border.Left, + this.Y + border.Top, + this.Width - border.Horizontal, + this.Height - border.Vertical); + + /// + /// Returns a new that is smaller than the current rectangle. + /// The dimensions of the new rectangle are calculated by shrinking the current rectangle's dimensions by the size of the margin. + /// + /// The that specifies the amount to shrink the rectangle on each side. + /// A new that is smaller than the current rectangle by the specified margin amounts. + public RectangleInfo Shrink(MarginStyle margin) => + new( + this.X + margin.Left, + this.Y + margin.Top, + this.Width - margin.Horizontal, + this.Height - margin.Vertical); + + /// + /// Returns a new that is smaller than the current rectangle. + /// The dimensions of the new rectangle are calculated by shrinking the current rectangle's dimensions by the size of the padding. + /// + /// The that specifies the amount to shrink the rectangle on each side. + /// A new that is smaller than the current rectangle by the specified padding amounts. + public RectangleInfo Shrink(PaddingStyle padding) => + new( + this.X + padding.Left, + this.Y + padding.Top, + this.Width - padding.Horizontal, + this.Height - padding.Vertical); + + /// + /// Returns a new where the X, Y, Width, and Height properties of the current rectangle are truncated to integers. + /// + /// A new with the X, Y, Width, and Height properties of the current rectangle truncated to integers. + public RectangleInfo Truncate() => + new( + (int)this.X, + (int)this.Y, + (int)this.Width, + (int)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 RectangleInfo Union(RectangleInfo rect) + { + 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 RectangleInfo(x1, y1, x2 - x1, y2 - y1); + } + + public Rectangle ToRectangle() => + new( + (int)this.X, + (int)this.Y, + (int)this.Width, + (int)this.Height); + + public override string ToString() + { + return "{" + + $"{nameof(this.Left)}={this.Left}," + + $"{nameof(this.Top)}={this.Top}," + + $"{nameof(this.Width)}={this.Width}," + + $"{nameof(this.Height)}={this.Height}" + + "}"; + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/ScreenInfo.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/ScreenInfo.cs new file mode 100644 index 0000000000..66990a5e6e --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/ScreenInfo.cs @@ -0,0 +1,43 @@ +// 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. + +using System; +using static MouseJumpUI.Common.NativeMethods.Core; + +namespace MouseJumpUI.Common.Models.Drawing; + +/// +/// Immutable version of a System.Windows.Forms.Screen object so we don't need to +/// take a dependency on WinForms just for screen info. +/// +internal sealed class ScreenInfo +{ + internal ScreenInfo(HMONITOR handle, bool primary, RectangleInfo displayArea, RectangleInfo workingArea) + { + this.Handle = handle; + this.Primary = primary; + this.DisplayArea = displayArea ?? throw new ArgumentNullException(nameof(displayArea)); + this.WorkingArea = workingArea ?? throw new ArgumentNullException(nameof(workingArea)); + } + + public int Handle + { + get; + } + + public bool Primary + { + get; + } + + public RectangleInfo DisplayArea + { + get; + } + + public RectangleInfo WorkingArea + { + get; + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/SizeInfo.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/SizeInfo.cs new file mode 100644 index 0000000000..a3e2954834 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Drawing/SizeInfo.cs @@ -0,0 +1,156 @@ +// 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. + +using System; +using System.Drawing; +using MouseJumpUI.Common.Models.Styles; +using BorderStyle = MouseJumpUI.Common.Models.Styles.BorderStyle; + +namespace MouseJumpUI.Common.Models.Drawing; + +/// +/// Immutable version of a System.Drawing.Size object with some extra utility methods. +/// +public sealed class SizeInfo +{ + public SizeInfo(decimal width, decimal height) + { + this.Width = width; + this.Height = height; + } + + public SizeInfo(Size size) + : this(size.Width, size.Height) + { + } + + public decimal Width + { + get; + } + + public decimal Height + { + get; + } + + public SizeInfo Enlarge(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); + + /// + /// Calculates the intersection of this size with another size, resulting in a size that represents + /// the overlapping dimensions. Both sizes must be non-negative. + /// + /// The size to intersect with this instance. + /// A new instance representing the intersection of the two sizes. + /// Thrown when either this size or the specified size has negative dimensions. + public SizeInfo Intersect(SizeInfo size) + { + if ((this.Width < 0) || (this.Height < 0) || (size.Width < 0) || (size.Height < 0)) + { + throw new ArgumentException("Sizes must be non-negative"); + } + + return new( + Math.Min(this.Width, size.Width), + Math.Min(this.Height, size.Height)); + } + + /// + /// Creates a new instance with the width and height negated, effectively inverting its dimensions. + /// + /// A new instance with inverted dimensions. + public SizeInfo Invert() => + new(-this.Width, -this.Height); + + public SizeInfo Scale(decimal scalingFactor) => new( + this.Width * scalingFactor, + this.Height * scalingFactor); + + public SizeInfo Shrink(BorderStyle border) => + new(this.Width - border.Horizontal, this.Height - border.Vertical); + + 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); + + /// + /// Creates a new instance representing a rectangle with this size, + /// positioned at the specified coordinates. + /// + /// The x-coordinate of the upper-left corner of the rectangle. + /// The y-coordinate of the upper-left corner of the rectangle. + /// A new instance representing the positioned rectangle. + public RectangleInfo PlaceAt(decimal x, decimal y) => + new(x, y, this.Width, this.Height); + + /// + /// Scales this size to fit within the bounds of another size, while maintaining the aspect ratio. + /// + /// The size to fit this size into. + /// A new instance representing the scaled size. + public SizeInfo ScaleToFit(SizeInfo bounds) + { + var widthRatio = bounds.Width / this.Width; + var heightRatio = bounds.Height / this.Height; + return widthRatio.CompareTo(heightRatio) switch + { + < 0 => new(bounds.Width, this.Height * widthRatio), + 0 => bounds, + > 0 => new(this.Width * heightRatio, bounds.Height), + }; + } + + /// + /// Rounds down the width and height of this size to the nearest whole number. + /// + /// A new instance with floored dimensions. + public SizeInfo Floor() + { + return new SizeInfo( + Math.Floor(this.Width), + Math.Floor(this.Height)); + } + + /// + /// Calculates the scaling ratio needed to fit this size within the bounds of another size without distorting the aspect ratio. + /// + /// The size to fit this size into. + /// The scaling ratio as a decimal. + /// Thrown if the width or height of the bounds is zero. + public decimal ScaleToFitRatio(SizeInfo bounds) + { + if (bounds.Width == 0 || bounds.Height == 0) + { + throw new ArgumentException($"{nameof(bounds.Width)} or {nameof(bounds.Height)} cannot be zero", nameof(bounds)); + } + + var widthRatio = bounds.Width / this.Width; + var heightRatio = bounds.Height / this.Height; + var scalingRatio = Math.Min(widthRatio, heightRatio); + + return scalingRatio; + } + + public Size ToSize() => new((int)this.Width, (int)this.Height); + + public Point ToPoint() => new((int)this.Width, (int)this.Height); + + public override string ToString() + { + return "{" + + $"{nameof(this.Width)}={this.Width}," + + $"{nameof(this.Height)}={this.Height}" + + "}"; + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Common/Models/Layout/PreviewLayout.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Layout/PreviewLayout.cs new file mode 100644 index 0000000000..9cf95b6eea --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Layout/PreviewLayout.cs @@ -0,0 +1,133 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using MouseJumpUI.Common.Models.Drawing; +using MouseJumpUI.Common.Models.Styles; + +namespace MouseJumpUI.Common.Models.Layout; + +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( + 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 = (screens ?? throw new ArgumentNullException(nameof(screens))) + .ToList().AsReadOnly(); + this.ActivatedScreenIndex = activatedScreenIndex; + this.FormBounds = formBounds ?? throw new ArgumentNullException(nameof(formBounds)); + this.PreviewBounds = previewBounds ?? throw new ArgumentNullException(nameof(previewBounds)); + this.ScreenshotBounds = (screenshotBounds ?? throw new ArgumentNullException(nameof(screenshotBounds))) + .ToList().AsReadOnly(); + } + + public PreviewStyle PreviewStyle + { + get; + } + + public RectangleInfo VirtualScreen + { + get; + } + + public ReadOnlyCollection Screens + { + get; + } + + public int ActivatedScreenIndex + { + get; + } + + public RectangleInfo FormBounds + { + get; + } + + public BoxBounds PreviewBounds + { + get; + } + + public ReadOnlyCollection ScreenshotBounds + { + get; + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/BackgroundStyle.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/BackgroundStyle.cs new file mode 100644 index 0000000000..332a55fcbe --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/BackgroundStyle.cs @@ -0,0 +1,44 @@ +// 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. + +using System.Drawing; + +namespace MouseJumpUI.Common.Models.Styles; + +/// +/// Represents the background fill style for a drawing object. +/// +public sealed class BackgroundStyle +{ + public static readonly BackgroundStyle Empty = new( + Color.Transparent, + Color.Transparent + ); + + 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/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/BorderStyle.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/BorderStyle.cs new file mode 100644 index 0000000000..3c5b870e60 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/BorderStyle.cs @@ -0,0 +1,79 @@ +// 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. + +using System.Drawing; + +namespace MouseJumpUI.Common.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; + } + + 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}," + + $"{nameof(this.Depth)}={this.Depth}" + + "}"; + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/BoxStyle.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/BoxStyle.cs new file mode 100644 index 0000000000..dac908b05c --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/BoxStyle.cs @@ -0,0 +1,79 @@ +// 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. + +using System; + +namespace MouseJumpUI.Common.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/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/MarginStyle.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/MarginStyle.cs new file mode 100644 index 0000000000..6dce63f57b --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/MarginStyle.cs @@ -0,0 +1,60 @@ +// 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 MouseJumpUI.Common.Models.Styles; + +/// +/// Represents the margin style for a drawing object. +/// +public sealed class MarginStyle +{ + public static readonly MarginStyle Empty = new(0); + + public MarginStyle(decimal all) + : this(all, all, all, all) + { + } + + public MarginStyle(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/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/PaddingStyle.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/PaddingStyle.cs new file mode 100644 index 0000000000..6ac7bc6b11 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/PaddingStyle.cs @@ -0,0 +1,60 @@ +// 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 MouseJumpUI.Common.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; + } + + 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/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/PreviewStyle.cs b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/PreviewStyle.cs new file mode 100644 index 0000000000..10443a4a0e --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Common/Models/Styles/PreviewStyle.cs @@ -0,0 +1,36 @@ +// 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. + +using System; +using MouseJumpUI.Common.Models.Drawing; + +namespace MouseJumpUI.Common.Models.Styles; + +public sealed class PreviewStyle +{ + public PreviewStyle( + SizeInfo canvasSize, + BoxStyle canvasStyle, + BoxStyle screenStyle) + { + this.CanvasSize = canvasSize ?? throw new ArgumentNullException(nameof(canvasSize)); + this.CanvasStyle = canvasStyle ?? throw new ArgumentNullException(nameof(canvasStyle)); + this.ScreenStyle = screenStyle ?? throw new ArgumentNullException(nameof(screenStyle)); + } + + public SizeInfo CanvasSize + { + get; + } + + public BoxStyle CanvasStyle + { + get; + } + + public BoxStyle ScreenStyle + { + get; + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/BOOL.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/BOOL.cs similarity index 96% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/BOOL.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/BOOL.cs index 851cb77a46..ec8849d952 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/BOOL.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/BOOL.cs @@ -2,7 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/CRECT.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/CRECT.cs similarity index 84% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/CRECT.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/CRECT.cs index a44da78727..3b02b3bf5e 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/CRECT.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/CRECT.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -25,6 +25,15 @@ internal static partial class Core public readonly LONG right; public readonly LONG bottom; + public CRECT( + int left, int top, int right, int bottom) + { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + public CRECT( LONG left, LONG top, LONG right, LONG bottom) { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/DWORD.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/DWORD.cs similarity index 75% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/DWORD.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/DWORD.cs index 9009b63c86..900b5bfb77 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/DWORD.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/DWORD.cs @@ -2,7 +2,9 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MouseJumpUI.NativeMethods; +using System.Runtime.InteropServices; + +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -23,10 +25,17 @@ internal static partial class Core this.Value = value; } + public static int Size => + Marshal.SizeOf(typeof(DWORD)); + public static implicit operator uint(DWORD value) => value.Value; public static implicit operator DWORD(uint value) => new(value); + public static explicit operator int(DWORD value) => (int)value.Value; + + public static explicit operator DWORD(int value) => new((uint)value); + public override string ToString() { return $"{this.GetType().Name}({this.Value})"; diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HANDLE.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/HANDLE.cs similarity index 90% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HANDLE.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/HANDLE.cs index 1df2624ed5..c89132e0be 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HANDLE.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/HANDLE.cs @@ -4,7 +4,7 @@ using System; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -31,7 +31,7 @@ internal static partial class Core 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/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HDC.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/HDC.cs similarity index 82% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HDC.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/HDC.cs index b02a811c06..0996030b01 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HDC.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/HDC.cs @@ -4,7 +4,7 @@ using System; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -29,6 +29,10 @@ internal static partial class Core 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/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HMONITOR.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/HMONITOR.cs similarity index 84% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HMONITOR.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/HMONITOR.cs index 047c654f6f..5baa77bfc5 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HMONITOR.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/HMONITOR.cs @@ -4,7 +4,7 @@ using System; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -31,15 +31,15 @@ internal static partial class Core 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/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HWND.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/HWND.cs similarity index 75% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HWND.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/HWND.cs index e12f2846f3..e421a1ca91 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HWND.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/HWND.cs @@ -3,8 +3,9 @@ // See the LICENSE file in the project root for more information. using System; +using System.Diagnostics.CodeAnalysis; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -20,6 +21,9 @@ internal static partial class Core { public static readonly HWND Null = new(IntPtr.Zero); + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Name and value taken from Win32Api")] + public static readonly HWND HWND_MESSAGE = new(-3); + public readonly IntPtr Value; public HWND(IntPtr value) diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LONG.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LONG.cs similarity index 95% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LONG.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LONG.cs index 2d2dd82283..7fe4b6cb99 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LONG.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LONG.cs @@ -2,7 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LPARAM.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LPARAM.cs similarity index 84% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LPARAM.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LPARAM.cs index 6a227f5e21..f214e53a6f 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LPARAM.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LPARAM.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -26,9 +26,11 @@ internal static partial class Core 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/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LPCRECT.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LPCRECT.cs similarity index 86% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LPCRECT.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LPCRECT.cs index d19abd9c39..c23dbbfb85 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LPCRECT.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LPCRECT.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.InteropServices; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -25,6 +25,8 @@ internal static partial class Core 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); @@ -39,7 +41,7 @@ internal static partial class Core 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/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LPPOINT.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LPPOINT.cs similarity index 87% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LPPOINT.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LPPOINT.cs index db3423decd..c68d8b336b 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LPPOINT.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LPPOINT.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.InteropServices; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -25,6 +25,8 @@ internal static partial class Core 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); @@ -44,7 +46,7 @@ internal static partial class Core 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/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LPRECT.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LPRECT.cs similarity index 86% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LPRECT.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LPRECT.cs index 087e22abe3..66f22da181 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/LPRECT.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/LPRECT.cs @@ -4,7 +4,7 @@ using System; using System.Runtime.InteropServices; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -24,6 +24,8 @@ internal static partial class Core 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); @@ -38,7 +40,7 @@ internal static partial class Core 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/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/POINT.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/POINT.cs similarity index 88% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/POINT.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/POINT.cs index d4513d6740..6077174ce6 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/POINT.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/POINT.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -28,6 +28,14 @@ internal static partial class Core /// public readonly LONG y; + public POINT( + int x, + int y) + { + this.x = x; + this.y = y; + } + public POINT( LONG x, LONG y) diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/RECT.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/RECT.cs similarity index 84% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/RECT.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/RECT.cs index ff107f6ad8..084616cc77 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/RECT.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/RECT.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -25,6 +25,15 @@ internal static partial class Core public readonly LONG right; public readonly LONG bottom; + public RECT( + int left, int top, int right, int bottom) + { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + public RECT( LONG left, LONG top, LONG right, LONG bottom) { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/UINT.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/UINT.cs similarity index 82% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/UINT.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/UINT.cs index e135212ec9..d2ce9a2e51 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/UINT.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/UINT.cs @@ -1,7 +1,8 @@ // 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 MouseJumpUI.NativeMethods; + +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -26,6 +27,10 @@ internal static partial class Core public static implicit operator UINT(uint value) => new(value); + public static explicit operator int(UINT value) => (int)value.Value; + + public static explicit operator UINT(int value) => new((uint)value); + public override string ToString() { return $"{this.GetType().Name}({this.Value})"; diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/ULONG_PTR.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/ULONG_PTR.cs similarity index 91% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/ULONG_PTR.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/ULONG_PTR.cs index 76d99d88be..ed26092cb1 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/ULONG_PTR.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/ULONG_PTR.cs @@ -4,7 +4,7 @@ using System; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { @@ -34,7 +34,7 @@ internal static partial class Core 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/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/WORD.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/WORD.cs similarity index 95% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/WORD.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/WORD.cs index 642d0b0514..987b532f29 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/WORD.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Core/WORD.cs @@ -2,7 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Core { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.ROP_CODE.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.ROP_CODE.cs similarity index 96% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.ROP_CODE.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.ROP_CODE.cs index 994eeb7165..1cb5261967 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.ROP_CODE.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.ROP_CODE.cs @@ -2,7 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Gdi32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.STRETCH_BLT_MODE.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.STRETCH_BLT_MODE.cs similarity index 94% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.STRETCH_BLT_MODE.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.STRETCH_BLT_MODE.cs index 575e092982..f1417941d3 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.STRETCH_BLT_MODE.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.STRETCH_BLT_MODE.cs @@ -2,7 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Gdi32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.SetStretchBltMode.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.SetStretchBltMode.cs similarity index 90% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.SetStretchBltMode.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.SetStretchBltMode.cs index 410af308d4..014efd3f5b 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.SetStretchBltMode.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.SetStretchBltMode.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Gdi32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.StretchBlt.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.StretchBlt.cs similarity index 92% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.StretchBlt.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.StretchBlt.cs index 345391d666..97cb7a941b 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.StretchBlt.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.StretchBlt.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class Gdi32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Libraries.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Libraries.cs similarity index 87% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/Libraries.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Libraries.cs index 59947db8fd..e648f9b01b 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/Libraries.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/Libraries.cs @@ -2,7 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static class Libraries { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.EnumDisplayMonitors.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.EnumDisplayMonitors.cs similarity index 93% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.EnumDisplayMonitors.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.EnumDisplayMonitors.cs index 6d5a0687bb..9002973536 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.EnumDisplayMonitors.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.EnumDisplayMonitors.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.GetMonitorInfoW.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.GetMonitorInfoW.cs similarity index 89% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.GetMonitorInfoW.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.GetMonitorInfoW.cs index ad55845cf9..be50c5c8ea 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.GetMonitorInfoW.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.GetMonitorInfoW.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.GetWindowDC.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.GetWindowDC.cs similarity index 93% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.GetWindowDC.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.GetWindowDC.cs index ea0b79db9b..33afc19c59 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.GetWindowDC.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.GetWindowDC.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.LPMONITORINFO.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.LPMONITORINFO.cs similarity index 96% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.LPMONITORINFO.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.LPMONITORINFO.cs index f44a2c7972..3896962d71 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.LPMONITORINFO.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.LPMONITORINFO.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.InteropServices; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MONITORENUMPROC .cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MONITORENUMPROC .cs similarity index 87% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MONITORENUMPROC .cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MONITORENUMPROC .cs index 0e366c8163..169bf3e567 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MONITORENUMPROC .cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MONITORENUMPROC .cs @@ -2,9 +2,9 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MONITORINFO.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MONITORINFO.cs similarity index 90% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MONITORINFO.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MONITORINFO.cs index 24fb270938..750222cd43 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MONITORINFO.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MONITORINFO.cs @@ -4,9 +4,9 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { @@ -34,6 +34,6 @@ internal static partial class User32 } public static int Size => - Marshal.SizeOf(typeof(INPUT)); + Marshal.SizeOf(typeof(MONITORINFO)); } } diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_FROM_FLAGS.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_FROM_FLAGS.cs similarity index 95% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_FROM_FLAGS.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_FROM_FLAGS.cs index 5d5fc3b035..c566193874 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_FROM_FLAGS.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_FROM_FLAGS.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; [SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")] internal static partial class User32 diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_INFO_FLAGS.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_INFO_FLAGS.cs similarity index 94% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_INFO_FLAGS.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_INFO_FLAGS.cs index f20e6a2f58..8bfa14eb55 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_INFO_FLAGS.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_INFO_FLAGS.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; [SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")] internal static partial class User32 diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MonitorFromPoint .cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MonitorFromPoint .cs similarity index 90% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MonitorFromPoint .cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MonitorFromPoint .cs index 93878544e5..86ed2c2efc 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.MonitorFromPoint .cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.MonitorFromPoint .cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.ReleaseDC.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.ReleaseDC.cs similarity index 91% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.ReleaseDC.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.ReleaseDC.cs index a6f6caa355..f7c7db68ed 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/Graphics/Gdi/User32.ReleaseDC.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/Graphics/Gdi/User32.ReleaseDC.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.HARDWAREINPUT.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.HARDWAREINPUT.cs similarity index 92% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.HARDWAREINPUT.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.HARDWAREINPUT.cs index 254b60eecd..2e98098916 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.HARDWAREINPUT.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.HARDWAREINPUT.cs @@ -4,9 +4,9 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT.cs similarity index 94% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT.cs index 1a4bcf8546..247b7eb4d5 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT.cs @@ -4,9 +4,9 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT_TYPE.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT_TYPE.cs similarity index 94% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT_TYPE.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT_TYPE.cs index 89e8f60218..b065fdfc43 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT_TYPE.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT_TYPE.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; [SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")] internal static partial class User32 diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.KEYBDINPUT.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.KEYBDINPUT.cs similarity index 93% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.KEYBDINPUT.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.KEYBDINPUT.cs index a5c9bc782b..f7d472e329 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.KEYBDINPUT.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.KEYBDINPUT.cs @@ -4,9 +4,9 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.LPINPUT.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.LPINPUT.cs similarity index 94% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.LPINPUT.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.LPINPUT.cs index ff689a5931..e803fe3d85 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.LPINPUT.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.LPINPUT.cs @@ -6,7 +6,7 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { @@ -52,7 +52,7 @@ internal static partial class User32 var size = INPUT.Size; foreach (var value in values) { - Marshal.StructureToPtr(value, ptr, true); + Marshal.StructureToPtr(value, ptr, false); ptr += size; } diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSEINPUT.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSEINPUT.cs similarity index 93% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSEINPUT.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSEINPUT.cs index 80f9bbe2fc..dc2ee4707e 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSEINPUT.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSEINPUT.cs @@ -4,9 +4,9 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSE_EVENT_FLAGS.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSE_EVENT_FLAGS.cs similarity index 96% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSE_EVENT_FLAGS.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSE_EVENT_FLAGS.cs index b6f3cba549..6bfe6d3bbe 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSE_EVENT_FLAGS.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSE_EVENT_FLAGS.cs @@ -5,7 +5,7 @@ using System; using System.Diagnostics.CodeAnalysis; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; [SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")] internal static partial class User32 diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.SendInput.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.SendInput.cs similarity index 91% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.SendInput.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.SendInput.cs index b7bfba63bf..d1dd0b620a 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.SendInput.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.SendInput.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetCursorPos.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetCursorPos.cs similarity index 89% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetCursorPos.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetCursorPos.cs index f6cbfa75bc..e2f9021c15 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetCursorPos.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetCursorPos.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetDesktopWindow.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetDesktopWindow.cs similarity index 89% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetDesktopWindow.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetDesktopWindow.cs index 4d16b95e15..7172fa0788 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetDesktopWindow.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetDesktopWindow.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetSystemMetrics.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetSystemMetrics.cs similarity index 95% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetSystemMetrics.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetSystemMetrics.cs index 7791c22963..0865d5af3d 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetSystemMetrics.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetSystemMetrics.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.SYSTEM_METRICS_INDEX.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SYSTEM_METRICS_INDEX.cs similarity index 98% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.SYSTEM_METRICS_INDEX.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SYSTEM_METRICS_INDEX.cs index b2ca6b3b68..5891c53551 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.SYSTEM_METRICS_INDEX.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SYSTEM_METRICS_INDEX.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; [SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")] internal static partial class User32 diff --git a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.SetCursorPos.cs b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SetCursorPos.cs similarity index 91% rename from src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.SetCursorPos.cs rename to src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SetCursorPos.cs index b548148925..ee400474e3 100644 --- a/src/modules/MouseUtils/MouseJumpUI/NativeMethods/User32/UI/WindowsAndMessaging/User32.SetCursorPos.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SetCursorPos.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; -using static MouseJumpUI.NativeMethods.Core; +using static MouseJumpUI.Common.NativeMethods.Core; -namespace MouseJumpUI.NativeMethods; +namespace MouseJumpUI.Common.NativeMethods; internal static partial class User32 { diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/DrawingHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/DrawingHelper.cs deleted file mode 100644 index 225ed68b2d..0000000000 --- a/src/modules/MouseUtils/MouseJumpUI/Helpers/DrawingHelper.cs +++ /dev/null @@ -1,184 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Linq; -using MouseJumpUI.Models.Drawing; -using MouseJumpUI.NativeMethods; -using static MouseJumpUI.NativeMethods.Core; - -namespace MouseJumpUI.Helpers; - -internal static class DrawingHelper -{ - /// - /// Draw the gradient-filled preview background. - /// - public static void DrawPreviewBackground( - Graphics previewGraphics, RectangleInfo previewBounds, IEnumerable screenBounds) - { - 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 - - // 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) - { - backgroundRegion.Exclude(screen.ToRectangle()); - } - - previewGraphics.FillRegion(backgroundBrush, backgroundRegion); - } - - public static void EnsureDesktopDeviceContext(ref HWND desktopHwnd, ref HDC desktopHdc) - { - if (desktopHwnd.IsNull) - { - desktopHwnd = User32.GetDesktopWindow(); - } - - if (desktopHdc.IsNull) - { - desktopHdc = User32.GetWindowDC(desktopHwnd); - if (desktopHdc.IsNull) - { - throw new InvalidOperationException( - $"{nameof(User32.GetWindowDC)} returned null"); - } - } - } - - public static void FreeDesktopDeviceContext(ref HWND desktopHwnd, ref HDC desktopHdc) - { - if (!desktopHwnd.IsNull && !desktopHdc.IsNull) - { - var result = User32.ReleaseDC(desktopHwnd, desktopHdc); - if (result == 0) - { - throw new InvalidOperationException( - $"{nameof(User32.ReleaseDC)} returned {result}"); - } - } - - desktopHwnd = HWND.Null; - desktopHdc = HDC.Null; - } - - /// - /// 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) - { - if (previewHdc.IsNull) - { - previewHdc = new HDC(previewGraphics.GetHdc()); - var result = Gdi32.SetStretchBltMode(previewHdc, Gdi32.STRETCH_BLT_MODE.STRETCH_HALFTONE); - - if (result == 0) - { - throw new InvalidOperationException( - $"{nameof(Gdi32.SetStretchBltMode)} returned {result}"); - } - } - } - - /// - /// Free the specified device context handle if it exists. - /// - public static void FreePreviewDeviceContext(Graphics previewGraphics, ref HDC previewHdc) - { - if ((previewGraphics is not null) && !previewHdc.IsNull) - { - previewGraphics.ReleaseHdc(previewHdc.Value); - previewHdc = HDC.Null; - } - } - - /// - /// 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, 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()); - } - } - - /// - /// Draws a screen capture from the specified desktop handle onto the target device context. - /// - public static void DrawPreviewScreen( - HDC sourceHdc, - HDC targetHdc, - RectangleInfo sourceBounds, - RectangleInfo targetBounds) - { - var source = sourceBounds.ToRectangle(); - var target = targetBounds.ToRectangle(); - var result = Gdi32.StretchBlt( - targetHdc, - target.X, - target.Y, - target.Width, - target.Height, - sourceHdc, - source.X, - source.Y, - source.Width, - source.Height, - Gdi32.ROP_CODE.SRCCOPY); - if (!result) - { - throw new InvalidOperationException( - $"{nameof(Gdi32.StretchBlt)} returned {result.Value}"); - } - } - - /// - /// Draws screen captures from the specified desktop handle onto the target device context. - /// - public static void DrawPreviewScreens( - HDC sourceHdc, - HDC targetHdc, - IList sourceBounds, - IList targetBounds) - { - for (var i = 0; i < sourceBounds.Count; i++) - { - var source = sourceBounds[i].ToRectangle(); - var target = targetBounds[i].ToRectangle(); - var result = Gdi32.StretchBlt( - targetHdc, - target.X, - target.Y, - target.Width, - target.Height, - sourceHdc, - source.X, - source.Y, - source.Width, - source.Height, - Gdi32.ROP_CODE.SRCCOPY); - if (!result) - { - throw new InvalidOperationException( - $"{nameof(Gdi32.StretchBlt)} returned {result.Value}"); - } - } - } -} diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/LayoutHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/LayoutHelper.cs deleted file mode 100644 index 025bba1d6e..0000000000 --- a/src/modules/MouseUtils/MouseJumpUI/Helpers/LayoutHelper.cs +++ /dev/null @@ -1,89 +0,0 @@ -// 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. - -using System; -using System.Drawing; -using System.Linq; -using System.Windows.Forms; -using MouseJumpUI.Models.Drawing; -using MouseJumpUI.Models.Layout; - -namespace MouseJumpUI.Helpers; - -internal static class LayoutHelper -{ - public static LayoutInfo CalculateLayoutInfo( - LayoutConfig layoutConfig) - { - ArgumentNullException.ThrowIfNull(layoutConfig); - - var builder = new LayoutInfo.Builder - { - LayoutConfig = layoutConfig, - }; - - builder.ActivatedScreenBounds = layoutConfig.Screens[layoutConfig.ActivatedScreenIndex].Bounds; - - // 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); - - // scale the virtual screen to fit inside the drawing bounds - var scalingRatio = layoutConfig.VirtualScreenBounds.Size - .ScaleToFitRatio(maxDrawingSize); - - // position the drawing bounds inside the preview border - var drawingBounds = layoutConfig.VirtualScreenBounds.Size - .ScaleToFit(maxDrawingSize) - .PlaceAt(layoutConfig.PreviewPadding.Left, layoutConfig.PreviewPadding.Top); - - // now we know the size of the drawing area we can work out the preview size - builder.PreviewBounds = drawingBounds.Enlarge(layoutConfig.PreviewPadding); - - // ... and the form size - // * 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); - - // now calculate the positions of each of the screen images on the preview - builder.ScreenBounds = layoutConfig.Screens - .Select( - screen => screen.Bounds - .Offset(layoutConfig.VirtualScreenBounds.Location.ToSize().Negate()) - .Scale(scalingRatio) - .Offset(layoutConfig.PreviewPadding.Left, layoutConfig.PreviewPadding.Top)) - .ToList(); - - return builder.Build(); - } - - /// - /// Resize and position the specified form. - /// - public static void PositionForm( - Form form, RectangleInfo formBounds) - { - // 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; - } -} diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/StyleHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/StyleHelper.cs new file mode 100644 index 0000000000..bc9ff705d0 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Helpers/StyleHelper.cs @@ -0,0 +1,103 @@ +// 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. + +using System; +using System.Drawing; +using MouseJumpUI.Common.Models.Drawing; +using MouseJumpUI.Common.Models.Styles; + +namespace MouseJumpUI.Helpers; + +internal static class StyleHelper +{ + /// + /// Default v2 preview style + /// + public static readonly PreviewStyle DefaultPreviewStyle = 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) + ) + ), + screenStyle: new( + marginStyle: new( + all: 4 + ), + borderStyle: new( + color: Color.FromArgb(0xFF, 0x22, 0x22, 0x22), + all: 12, + depth: 4 + ), + paddingStyle: PaddingStyle.Empty, + backgroundStyle: new( + color1: Color.MidnightBlue, + color2: Color.MidnightBlue + ) + ) + ); + + /// + /// Legacy preview style + /// + public static readonly PreviewStyle LegacyPreviewStyle = new( + canvasSize: new( + width: 1600, + height: 1200 + ), + canvasStyle: new( + marginStyle: MarginStyle.Empty, + borderStyle: new( + color: SystemColors.Highlight, + all: 6, + depth: 0 + ), + paddingStyle: new( + all: 0 + ), + backgroundStyle: new( + color1: Color.FromArgb(0xFF, 0x0D, 0x57, 0xD2), + color2: Color.FromArgb(0xFF, 0x03, 0x44, 0xC0) + ) + ), + screenStyle: new( + marginStyle: new( + all: 0 + ), + borderStyle: new( + color: Color.FromArgb(0xFF, 0x22, 0x22, 0x22), + all: 0, + depth: 0 + ), + paddingStyle: PaddingStyle.Empty, + backgroundStyle: new( + color1: Color.MidnightBlue, + color2: Color.MidnightBlue + ) + ) + ); + + public static PreviewStyle WithCanvasSize(this PreviewStyle previewStyle, SizeInfo canvasSize) + { + ArgumentNullException.ThrowIfNull(previewStyle); + ArgumentNullException.ThrowIfNull(canvasSize); + return new PreviewStyle( + canvasSize: canvasSize, + canvasStyle: previewStyle.CanvasStyle, + screenStyle: previewStyle.ScreenStyle); + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.Designer.cs b/src/modules/MouseUtils/MouseJumpUI/MainForm.Designer.cs index 8a8fa9110a..6300590e73 100644 --- a/src/modules/MouseUtils/MouseJumpUI/MainForm.Designer.cs +++ b/src/modules/MouseUtils/MouseJumpUI/MainForm.Designer.cs @@ -49,7 +49,6 @@ partial class MainForm panel1.Dock = DockStyle.Fill; panel1.Location = new System.Drawing.Point(0, 0); panel1.Name = "panel1"; - panel1.Padding = new Padding(5); panel1.Size = new System.Drawing.Size(800, 450); panel1.TabIndex = 1; // @@ -59,7 +58,7 @@ partial class MainForm Thumbnail.Dock = DockStyle.Fill; Thumbnail.Location = new System.Drawing.Point(5, 5); Thumbnail.Name = "Thumbnail"; - Thumbnail.Size = new System.Drawing.Size(790, 440); + Thumbnail.Size = new System.Drawing.Size(800, 450); Thumbnail.SizeMode = PictureBoxSizeMode.Normal; Thumbnail.TabIndex = 1; Thumbnail.TabStop = false; diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs index 2b055aa862..546571d1ab 100644 --- a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs +++ b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs @@ -5,15 +5,14 @@ using System; using System.Diagnostics; using System.Drawing; -using System.Drawing.Imaging; using System.Linq; using System.Windows.Forms; using ManagedCommon; -using Microsoft.PowerToys.Settings.UI.Library; +using MouseJumpUI.Common.Helpers; +using MouseJumpUI.Common.Imaging; +using MouseJumpUI.Common.Models.Drawing; +using MouseJumpUI.Common.Models.Layout; using MouseJumpUI.Helpers; -using MouseJumpUI.Models.Drawing; -using MouseJumpUI.Models.Layout; -using static MouseJumpUI.NativeMethods.Core; namespace MouseJumpUI; @@ -25,6 +24,12 @@ internal partial class MainForm : Form this.SettingsHelper = settingsHelper ?? throw new ArgumentNullException(nameof(settingsHelper)); } + private PreviewLayout? PreviewLayout + { + get; + set; + } + public SettingsHelper SettingsHelper { get; @@ -42,61 +47,68 @@ internal partial class MainForm : Form return; } - // 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(); + var screens = ScreenHelper.GetAllScreens().ToList(); + if (screens.Count == 0) + { + return; + } var currentLocation = MouseHelper.GetCursorPosition(); - var currentScreenHandle = ScreenHelper.MonitorFromPoint(currentLocation); - var currentScreen = screens - .Single(item => item.Screen.Handle == currentScreenHandle.Value); - var targetScreenNumber = default(int?); + var currentScreen = ScreenHelper.GetScreenFromPoint(screens, currentLocation); + var currentScreenIndex = screens.IndexOf(currentScreen); + var targetScreen = default(ScreenInfo?); - if (((e.KeyCode >= Keys.D1) && (e.KeyCode <= Keys.D9)) - || ((e.KeyCode >= Keys.NumPad1) && (e.KeyCode <= Keys.NumPad9))) + switch (e.KeyCode) { - // number keys 1-9 or numpad keys 1-9 - move to the numbered screen - var screenNumber = e.KeyCode - Keys.D0; - if (screenNumber <= screens.Count) - { - targetScreenNumber = screenNumber; - } - } - else if (e.KeyCode == Keys.P) - { - // "P" - move to the primary screen - targetScreenNumber = screens.Single(item => item.Screen.Primary).Number; - } - else if (e.KeyCode == Keys.Left) - { - // move to the previous screen - targetScreenNumber = currentScreen.Number == 1 - ? screens.Count - : currentScreen.Number - 1; - } - else if (e.KeyCode == Keys.Right) - { - // move to the next screen - targetScreenNumber = currentScreen.Number == screens.Count - ? 1 - : currentScreen.Number + 1; - } - else if (e.KeyCode == Keys.Home) - { - // move to the first screen - targetScreenNumber = 1; - } - else if (e.KeyCode == Keys.End) - { - // move to the last screen - targetScreenNumber = screens.Count; + case >= Keys.D1 and <= Keys.D9: + { + // number keys 1-9 - move to the numbered screen + var screenNumber = e.KeyCode - Keys.D0; + /* note - screen *numbers* are 1-based, screen *indexes* are 0-based */ + targetScreen = (screenNumber <= screens.Count) + ? targetScreen = screens[screenNumber - 1] + : null; + break; + } + + case >= Keys.NumPad1 and <= Keys.NumPad9: + { + // numpad keys 1-9 - move to the numbered screen + var screenNumber = e.KeyCode - Keys.NumPad0; + /* note - screen *numbers* are 1-based, screen *indexes* are 0-based */ + targetScreen = (screenNumber <= screens.Count) + ? targetScreen = screens[screenNumber - 1] + : null; + break; + } + + case Keys.P: + // "P" - move to the primary screen + targetScreen = screens.Single(screen => screen.Primary); + break; + case Keys.Left: + // move to the previous screen, looping back to the end if needed + var prevIndex = (currentScreenIndex - 1 + screens.Count) % screens.Count; + targetScreen = screens[prevIndex]; + break; + case Keys.Right: + // move to the next screen, looping round to the start if needed + var nextIndex = (currentScreenIndex + 1) % screens.Count; + targetScreen = screens[nextIndex]; + break; + case Keys.Home: + // move to the first screen + targetScreen = screens.First(); + break; + case Keys.End: + // move to the last screen + targetScreen = screens.Last(); + break; } - if (targetScreenNumber.HasValue) + if (targetScreen is not null) { - MouseHelper.SetCursorPosition( - screens[targetScreenNumber.Value - 1].Screen.Bounds.Midpoint); + MouseHelper.SetCursorPosition(targetScreen.DisplayArea.Midpoint); this.OnDeactivate(EventArgs.Empty); } } @@ -118,15 +130,42 @@ internal partial class MainForm : Form 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.LogInfo($"scaled location = {scaledLocation}"); - MouseHelper.SetCursorPosition(scaledLocation); - Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent()); + if (this.PreviewLayout is null) + { + // there's no layout data so we can't work out what screen was clicked + return; + } + + // work out which screenshot was clicked + var clickedScreenshot = this.PreviewLayout.ScreenshotBounds + .FirstOrDefault( + 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.LogInfo($"clicked location = {clickedLocation}"); + MouseHelper.SetCursorPosition(clickedLocation); } this.OnDeactivate(EventArgs.Empty); @@ -138,162 +177,34 @@ internal partial class MainForm : Form this.Visible = false; var stopwatch = Stopwatch.StartNew(); - var layoutInfo = MainForm.GetLayoutInfo(this); - LayoutHelper.PositionForm(this, layoutInfo.FormBounds); - MainForm.RenderPreview(this, layoutInfo); + + var appSettings = this.SettingsHelper.CurrentSettings ?? throw new InvalidOperationException(); + var screens = ScreenHelper.GetAllScreens().Select(screen => screen.DisplayArea).ToList(); + var activatedLocation = MouseHelper.GetCursorPosition(); + this.PreviewLayout = LayoutHelper.GetPreviewLayout( + previewStyle: StyleHelper.LegacyPreviewStyle.WithCanvasSize( + new( + appSettings.Properties.ThumbnailSize.Width, + appSettings.Properties.ThumbnailSize.Height + )), + screens: screens, + activatedLocation: activatedLocation); + + this.PositionForm(this.PreviewLayout.FormBounds); + + var imageCopyService = new DesktopImageRegionCopyService(); + DrawingHelper.RenderPreview( + this.PreviewLayout, + imageCopyService, + this.OnPreviewImageCreated, + this.OnPreviewImageUpdated); + stopwatch.Stop(); // we have to activate the form to make sure the deactivate event fires - Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpShowEvent()); this.Activate(); } - private static LayoutInfo GetLayoutInfo(MainForm 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.LogInfo(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; - - // avoid a race condition - cache the current settings in case they change - var currentSettings = form.SettingsHelper.CurrentSettings; - - var layoutConfig = new LayoutConfig( - virtualScreenBounds: ScreenHelper.GetVirtualScreen(), - screens: screens.Select(item => item.Screen).ToList(), - activatedLocation: activatedLocation, - activatedScreenIndex: activatedScreenIndex, - activatedScreenNumber: activatedScreenIndex + 1, - maximumFormSize: new( - currentSettings.Properties.ThumbnailSize.Width, - currentSettings.Properties.ThumbnailSize.Height), - /* - don't read the panel padding values because they are affected by dpi scaling - and can give wrong values when moving between monitors with different dpi scaling - */ - formPadding: new(5, 5, 5, 5), - previewPadding: new(0)); - Logger.LogInfo(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.LogInfo(string.Join( - '\n', - $"Layout info", - $"-----------", - $"form bounds = {layoutInfo.FormBounds}", - $"preview bounds = {layoutInfo.PreviewBounds}", - $"activated screen = {layoutInfo.ActivatedScreenBounds}")); - - return layoutInfo; - } - - private static void RenderPreview( - MainForm form, LayoutInfo layoutInfo) - { - var layoutConfig = layoutInfo.LayoutConfig; - - 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); - - DrawingHelper.DrawPreviewBackground(previewGraphics, layoutInfo.PreviewBounds, layoutInfo.ScreenBounds); - - var desktopHwnd = HWND.Null; - var desktopHdc = HDC.Null; - var previewHdc = HDC.Null; - 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)); - placeholdersDrawn = true; - } - - MainForm.RefreshPreview(form); - - // we've still got more screens to draw so open the device context again - DrawingHelper.EnsurePreviewDeviceContext(previewGraphics, ref previewHdc); - } - } - } - finally - { - DrawingHelper.FreeDesktopDeviceContext(ref desktopHwnd, ref desktopHdc); - DrawingHelper.FreePreviewDeviceContext(previewGraphics, ref previewHdc); - } - - MainForm.RefreshPreview(form); - stopwatch.Stop(); - } - private void ClearPreview() { if (this.Thumbnail.Image is null) @@ -310,17 +221,40 @@ internal partial class MainForm : Form GC.Collect(); } - private static void RefreshPreview(MainForm form) + /// + /// Resize and position the specified form. + /// + private void PositionForm(RectangleInfo bounds) { - if (!form.Visible) + // 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 void OnPreviewImageUpdated() + { + if (!this.Visible) { // we seem to need to turn off topmost and then re-enable it again - // when we show the form, otherwise it doesn't get shown topmost... - form.TopMost = false; - form.TopMost = true; - form.Show(); + // when we show the form, otherwise it doesn't always get shown topmost... + this.TopMost = false; + this.TopMost = true; + this.Show(); } - form.Thumbnail.Refresh(); + this.Thumbnail.Refresh(); } } diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.resx b/src/modules/MouseUtils/MouseJumpUI/MainForm.resx index e33d2ec3e8..4df95b5495 100644 --- a/src/modules/MouseUtils/MouseJumpUI/MainForm.resx +++ b/src/modules/MouseUtils/MouseJumpUI/MainForm.resx @@ -1,4 +1,64 @@ - + + +