diff --git a/.github/actions/spell-check/excludes.txt b/.github/actions/spell-check/excludes.txt index 24045c080aa7..e9fb1cadf815 100644 --- a/.github/actions/spell-check/excludes.txt +++ b/.github/actions/spell-check/excludes.txt @@ -114,7 +114,7 @@ ^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/Text/.*\.cs$ ^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$ ^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/ -^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$ +^src/modules/MouseUtils/MouseJumpUI/UI/MainForm\.resx$ ^src/modules/MouseWithoutBorders/App/.*/NativeMethods\.cs$ ^src/modules/MouseWithoutBorders/App/Form/.*\.Designer\.cs$ ^src/modules/MouseWithoutBorders/App/Form/.*\.resx$ @@ -139,6 +139,7 @@ ^src/modules/ZoomIt/ZoomIt/selfie_segmentation\.onnx$ ^src/modules/ZoomIt/ZoomIt/ZoomIt\.idc$ ^src/Monaco/ +^tools/MouseJump.Kicker/KickerForm\.resx$ ^tools/project_template/ModuleTemplate/resource\.h$ ^tools/Verification scripts/Check preview handler registration\.ps1$ ignore$ diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 27d8cbcf943d..587e2d59de86 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -47,7 +47,6 @@ ALLVIEW ALPHATYPE altkey amr -ANDSCANS animatedvisuals Animnate AOC @@ -194,6 +193,7 @@ CAPTURECHANGED CARETBLINKING carlos Carlseibert +cascadia caseinsensitive caub CBN @@ -313,7 +313,6 @@ CREATESCHEDULEDTASK CREATESTRUCT CREATEWINDOWFAILED creativecommons -CRECT CRH critsec cropandlock @@ -389,7 +388,6 @@ DELA DELD deld DELETEDKEYIMAGE -DELETESCANS DEMOTYPE DENORMAL depersist @@ -457,10 +455,8 @@ dreamsofameaningfullife drivedetectionwarning DROPFILES DSPDLOG -DSTINVERT DString dto -DUMMYUNIONNAME dumpbin dutil DVASPECT @@ -505,6 +501,8 @@ encodedlaunch encryptor ENDSESSION ENSUREVISIBLE +ENTERIDLE +ENTERMENULOOP ENTERSIZEMOVE ENTRYW ENU @@ -538,6 +536,8 @@ EXECUTEDEFAULT executionpolicy exename exf +EXITCOMMAND +EXITMENULOOP EXITSIZEMOVE exlist EXPCMDFLAGS @@ -669,6 +669,7 @@ gwl GWLP GWLSTYLE hangeul +Hanguel Hantai Hanzi Hardlines @@ -812,6 +813,7 @@ imageresizerinput imageresizersettings imagingdevices ime +IMEAceept imgflip inapp inbox @@ -826,6 +828,8 @@ INITCOMMONCONTROLSEX INITDIALOG INITGUID initialfile +INITMENU +INITMENUPOPUP INITTOLOGFONTSTRUCT inkscape INLINEPREFIX @@ -930,6 +934,7 @@ Lclean Ldone Ldr LEFTALIGN +LEFTBUTTON leftclick LEFTSCROLLBAR LEFTTEXT @@ -981,11 +986,9 @@ LPCLSID lpcmi LPCMINVOKECOMMANDINFO LPCREATESTRUCT -LPCRECT LPCTSTR lpdw lpfn -LPINPUT LPITEMIDLIST lpmi LPMINMAXINFO @@ -994,6 +997,7 @@ LPOSVERSIONINFOEXW LPQUERY lprc LPSAFEARRAY +LPSECURITY lpstr lpsz LPTHREAD @@ -1054,8 +1058,7 @@ meme memicmp MENUITEMINFO MENUITEMINFOW -MERGECOPY -MERGEPAINT +MENUSELECT Metacharacter metadatamatters Metadatas @@ -1202,7 +1205,6 @@ NOASYNC NOCHANGEDIR NOCLIP NOCLOSEPROCESS -NOCOALESCE NOCOMM NOCONFIRMMKDIR NOCOPYBITS @@ -1219,10 +1221,10 @@ NOLINKINFO nologo NOMCX NOMINMAX -NOMIRRORBITMAP nomove NONANTIALIASED nonclient +Nonconvert NONCLIENTMETRICSW NONELEVATED nonspace @@ -1251,8 +1253,6 @@ NOTIFYICONDATAW NOTIMPL NOTOPMOST NOTRACK -NOTSRCCOPY -NOTSRCERASE Notupdated notwindows NOTXORPEN @@ -1277,7 +1277,10 @@ nwc ocr Ocrsettings odbccp +Oemcomma OEMCONVERT +Oemplus +Oemtilde officehubintl OFN ofs @@ -1302,7 +1305,6 @@ openxmlformats OPTIMIZEFORINVOKE Optronics ORPHANEDDIALOGTITLE -ORSCANS oss OSVERSIONINFO OSVERSIONINFOEX @@ -1334,10 +1336,7 @@ PARENTRELATIVEPARSING parray parseable PARTIALCONFIRMATIONDIALOGTITLE -PATCOPY PATHMUSTEXIST -PATINVERT -PATPAINT pbc pbi PBP @@ -1567,6 +1566,7 @@ REMAPSUCCESSFUL REMAPUNSUCCESSFUL Remotable remotedesktop +remotehost remoteip Removelnk renamable @@ -1603,7 +1603,6 @@ RIGHTBUTTON RIGHTSCROLLBAR riid rollups -rop ROUNDSMALL rpcrt RRF @@ -1717,6 +1716,7 @@ shortdate SHORTPATH shortsplit showcolorname +SHOWCONTEXTMENU SHOWDEFAULT SHOWELEVATIONPROMPT SHOWMAGNIFIEDCURSOR @@ -1779,12 +1779,8 @@ spsi spsia spsrm spsv -SRCAND SRCCOPY -SRCERASE Srch -SRCINVERT -SRCPAINT SResize srme srre @@ -1948,6 +1944,7 @@ tracerpt trackbar trafficmanager transicc +TRAYICON TRANSIENTWINDOW transitioning TRAYMOUSEMESSAGE @@ -1966,6 +1963,8 @@ TYPEMOUSE TYPESHORTCUT UAC UACUI +UAHINITMENU +UAHMEASUREMENUITEM UAL uap UBR @@ -1990,6 +1989,7 @@ unescaped unforgeable ungroup UNICODETEXT +UNINITMENUPOPUP unins uninsdeletekey Uninstaller @@ -2060,7 +2060,6 @@ vgetq viewmodels virama VIRTKEY -VIRTUALDESK VISEGRADRELAY visiblecolorformats visio diff --git a/PowerToys.slnx b/PowerToys.slnx index f0ac6c974eac..358ebce869e3 100644 --- a/PowerToys.slnx +++ b/PowerToys.slnx @@ -541,11 +541,19 @@ - - + + + + + + + + + + @@ -553,6 +561,14 @@ + + + + + + + + diff --git a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/DrawingHelperTests.cs b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/DrawingHelperTests.cs index 63f372f20bb8..ad9379326eb8 100644 --- a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/DrawingHelperTests.cs +++ b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/DrawingHelperTests.cs @@ -2,29 +2,32 @@ // 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.Globalization; +using System.Drawing; +using System.Drawing.Imaging; using System.Reflection; -using System.Text; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using MouseJump.Common.Helpers; using MouseJump.Common.Imaging; -using MouseJump.Common.Models.Drawing; -using MouseJump.Common.Models.Styles; +using MouseJump.Models.Display; +using MouseJump.Models.Drawing; +using MouseJump.Models.Styles; namespace MouseJump.Common.UnitTests.Helpers; -[TestClass] public static class DrawingHelperTests { [TestClass] - public sealed class RenderPreviewTests + public sealed class GetPreviewLayoutTests { public sealed class TestCase { - public TestCase(PreviewStyle previewStyle, List screens, PointInfo activatedLocation, string desktopImageFilename, string expectedImageFilename) + public TestCase(PreviewStyle previewStyle, DisplayInfo displayInfo, ScreenInfo activatedScreen, PointInfo activatedLocation, string desktopImageFilename, string expectedImageFilename) { this.PreviewStyle = previewStyle; - this.Screens = screens; + this.DisplayInfo = displayInfo; + this.ActivatedScreen = activatedScreen; this.ActivatedLocation = activatedLocation; this.DesktopImageFilename = desktopImageFilename; this.ExpectedImageFilename = expectedImageFilename; @@ -32,7 +35,9 @@ public TestCase(PreviewStyle previewStyle, List screens, PointInf public PreviewStyle PreviewStyle { get; } - public List Screens { get; } + public DisplayInfo DisplayInfo { get; } + + public ScreenInfo ActivatedScreen { get; } public PointInfo ActivatedLocation { get; } @@ -43,32 +48,94 @@ public TestCase(PreviewStyle previewStyle, List screens, PointInf public static IEnumerable GetTestCases() { - /* 4-grid */ + // colors like "SystemColors.Highlight" can change between versions of windows, + // so we'll use hard-coded colors instead to reduce false test failures + var systemHighlight = Color.FromArgb(0, 120, 215); + var previewStyle = StyleHelper.BezelledPreviewStyle; + previewStyle = new( + canvasSize: previewStyle.CanvasSize, + canvasStyle: new( + marginStyle: previewStyle.CanvasStyle.MarginStyle, + borderStyle: previewStyle.CanvasStyle.BorderStyle.WithColor(systemHighlight), + paddingStyle: previewStyle.CanvasStyle.PaddingStyle, + backgroundStyle: previewStyle.CanvasStyle.BackgroundStyle + ), + screenStyle: previewStyle.ScreenStyle, + extraColors: previewStyle.ExtraColors + ); + + // 4-grid + var displayInfo = new DisplayInfo( + devices: new DeviceInfo[] + { + new( + hostname: "localhost", + localhost: true, + screens: new List + { + new( + handle: 0, + primary: true, + displayArea: new(0, 0, 500, 500), + workingArea: new(0, 0, 500, 500)), + new( + handle: 0, + primary: false, + displayArea: new(500, 0, 500, 500), + workingArea: new(500, 0, 500, 500)), + new( + handle: 0, + primary: false, + displayArea: new(500, 500, 500, 500), + workingArea: new(500, 500, 500, 500)), + new( + handle: 0, + primary: false, + displayArea: new(0, 500, 500, 500), + workingArea: new(0, 500, 500, 500)), + } + ), + }); yield return new object[] { new TestCase( - previewStyle: StyleHelper.BezelledPreviewStyle, - screens: new List() - { - new(0, 0, 500, 500), - new(500, 0, 500, 500), - new(500, 500, 500, 500), - new(0, 500, 500, 500), - }, + previewStyle: previewStyle, + displayInfo: displayInfo, + activatedScreen: displayInfo.Devices[0].Screens[0], activatedLocation: new(x: 50, y: 50), desktopImageFilename: "_test-4grid-desktop.png", expectedImageFilename: "_test-4grid-expected.png"), }; - /* win 11 */ + + // win 11 + displayInfo = new( + devices: new DeviceInfo[] + { + new( + hostname: "localhost", + localhost: true, + screens: new List + { + new( + handle: 0, + primary: true, + displayArea: new(5120, 349, 1920, 1080), + workingArea: new(5120, 349, 1920, 1080)), + new( + handle: 0, + primary: false, + displayArea: new(0, 0, 5120, 1440), + workingArea: new(0, 0, 5120, 1440)), + } + ), + } + ); yield return new object[] { new TestCase( - previewStyle: StyleHelper.BezelledPreviewStyle, - screens: new List() - { - new(5120, 349, 1920, 1080), - new(0, 0, 5120, 1440), - }, + previewStyle: previewStyle, + displayInfo: displayInfo, + activatedScreen: displayInfo.Devices[0].Screens[0], activatedLocation: new(x: 50, y: 50), desktopImageFilename: "_test-win11-desktop.png", expectedImageFilename: "_test-win11-expected.png"), @@ -76,22 +143,36 @@ public static IEnumerable GetTestCases() } [TestMethod] - [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] - public void RunTestCases(TestCase data) + [DynamicData(nameof(GetTestCases))] + public async Task RunTestCases(TestCase data) { // load the fake desktop image - using var desktopImage = RenderPreviewTests.LoadImageResource(data.DesktopImageFilename); + using var desktopImage = GetPreviewLayoutTests.LoadImageResource(data.DesktopImageFilename); - // draw the preview image - var previewLayout = LayoutHelper.GetPreviewLayout( + var formLayout = LayoutHelper.GetFormLayout( previewStyle: data.PreviewStyle, - screens: data.Screens, + displayInfo: data.DisplayInfo, + activatedScreen: data.ActivatedScreen, activatedLocation: data.ActivatedLocation); - var imageCopyService = new StaticImageRegionCopyService(desktopImage); - using var actual = DrawingHelper.RenderPreview(previewLayout, imageCopyService); + + var imageCopyServices = formLayout.CanvasLayout.DeviceLayouts + .Select( + deviceLayout => (IImageRegionCopyService)new StaticImageRegionCopyService(desktopImage)) + .ToList(); + + // draw the preview image + using var actual = await DrawingHelper.RenderPreviewAsync(formLayout.CanvasLayout, data.ActivatedScreen, imageCopyServices); + + // save the actual image so we can pick it up as a build artifact + var actualFilename = Path.GetFileNameWithoutExtension(data.ExpectedImageFilename) + "_actual" + Path.GetExtension(data.ExpectedImageFilename); + actual.Save(actualFilename, ImageFormat.Png); // load the expected image - var expected = RenderPreviewTests.LoadImageResource(data.ExpectedImageFilename); + var expected = GetPreviewLayoutTests.LoadImageResource(data.ExpectedImageFilename); + + // save the actual image so we can pick it up as a build artifact + var expectedFilename = Path.GetFileNameWithoutExtension(data.ExpectedImageFilename) + "_expected" + Path.GetExtension(data.ExpectedImageFilename); + expected.Save(expectedFilename, ImageFormat.Png); // compare the images AssertImagesEqual(expected, actual); @@ -99,24 +180,26 @@ public void RunTestCases(TestCase data) private static Bitmap LoadImageResource(string filename) { - var assembly = Assembly.GetExecutingAssembly(); - var assemblyName = new AssemblyName(assembly.FullName ?? throw new InvalidOperationException()); - var resourceName = $"{typeof(DrawingHelperTests).Namespace}.{filename.Replace("/", ".")}"; - var resourceNames = assembly.GetManifestResourceNames(); + var referenceType = typeof(DrawingHelperTests); + var resourceAssembly = referenceType.Assembly; + + var resourcePrefix = referenceType.Namespace; + var resourceName = $"{resourcePrefix}.{filename.Replace("/", ".")}"; + var resourceNames = resourceAssembly.GetManifestResourceNames(); if (!resourceNames.Contains(resourceName)) { - var message = new StringBuilder(); - message.AppendLine(CultureInfo.InvariantCulture, $"Embedded resource '{resourceName}' does not exist."); - message.AppendLine($"Known resources:"); - foreach (var name in resourceNames) - { - message.AppendLine(name); - } - - throw new InvalidOperationException(message.ToString()); + var message = string.Join( + Environment.NewLine, + new List([ + $"Embedded resource '{resourceName}' does not exist.", + string.Empty, + "Valid resource names are:", + string.Empty]) + .Concat(resourceNames)); + throw new InvalidOperationException(message); } - var stream = assembly.GetManifestResourceStream(resourceName) + var stream = resourceAssembly.GetManifestResourceStream(resourceName) ?? throw new InvalidOperationException(); var image = (Bitmap)Image.FromStream(stream); return image; @@ -143,12 +226,11 @@ private static void AssertImagesEqual(Bitmap expected, Bitmap actual) var actualPixel = actual.GetPixel(x, y); // allow a small tolerance for rounding differences in gdi - // using a tolerance of 3 for support of minor differences in Windows Server 2025 CI Assert.IsTrue( - (Math.Abs(expectedPixel.A - actualPixel.A) <= 3) && - (Math.Abs(expectedPixel.R - actualPixel.R) <= 3) && - (Math.Abs(expectedPixel.G - actualPixel.G) <= 3) && - (Math.Abs(expectedPixel.B - actualPixel.B) <= 3), + (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/MouseJump.Common.UnitTests/Helpers/LayoutHelperTests.cs b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/LayoutHelperTests.cs index 95d364b10035..8e7804e20efb 100644 --- a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/LayoutHelperTests.cs +++ b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/LayoutHelperTests.cs @@ -2,17 +2,20 @@ // 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.Diagnostics; +using System.Drawing; using System.Text.Json; using Microsoft.VisualStudio.TestTools.UnitTesting; + using MouseJump.Common.Helpers; -using MouseJump.Common.Models.Drawing; -using MouseJump.Common.Models.Layout; -using MouseJump.Common.Models.Styles; +using MouseJump.Models.Display; +using MouseJump.Models.Drawing; +using MouseJump.Models.Styles; +using MouseJump.Models.ViewModel; namespace MouseJump.Common.UnitTests.Helpers; -[TestClass] public static class LayoutHelperTests { /* @@ -109,26 +112,32 @@ public sealed class GetPreviewLayoutTests { public sealed class TestCase { - public TestCase(PreviewStyle previewStyle, List screens, PointInfo activatedLocation, PreviewLayout expectedResult) + public TestCase(string testName, PreviewStyle previewStyle, DisplayInfo displayInfo, ScreenInfo activatedScreen, PointInfo activatedLocation, FormViewModel expectedResult) { + this.TestName = testName; this.PreviewStyle = previewStyle; - this.Screens = screens; + this.DisplayInfo = displayInfo; this.ActivatedLocation = activatedLocation; + this.ActivatedScreen = activatedScreen; this.ExpectedResult = expectedResult; } + public string TestName { get; } + public PreviewStyle PreviewStyle { get; } - public List Screens { get; } + public DisplayInfo DisplayInfo { get; } + + public ScreenInfo ActivatedScreen { get; } public PointInfo ActivatedLocation { get; } - public PreviewLayout ExpectedResult { get; } + public FormViewModel ExpectedResult { get; } } public static IEnumerable GetTestCases() { - // happy path - single screen with 50% scaling, + // happy path - single device with screen and 50% scaling, // *has* a preview border but *no* screenshot borders // // +----------------+ @@ -136,10 +145,11 @@ public static IEnumerable GetTestCases() // | 0 | // | | // +----------------+ + var testName = "Test 1 - Happy Path"; var previewStyle = new PreviewStyle( canvasSize: new( - width: 524, - height: 396 + width: (1024 / 2) + (5 * 2) + (1 * 2), // half the screen size, plus additional room for canvas border and padding + height: (768 / 2) + (5 * 2) + (1 * 2) // half the screen size, plus additional room for canvas border and padding ), canvasStyle: new( marginStyle: MarginStyle.Empty, @@ -154,38 +164,56 @@ public static IEnumerable GetTestCases() 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 expectedResult = 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() + screenStyle: BoxStyle.Empty, + extraColors: Array.Empty()); + var displayInfo = new DisplayInfo( + devices: new List { 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) + hostname: "localhost", + localhost: true, + screens: new List + { + new( + handle: 0, + primary: true, + displayArea: new(0, 0, 1024, 768), + workingArea: new(0, 0, 1024, 768)), + } ), }); - yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, expectedResult) }; + var activatedScreen = displayInfo.Devices[0].Screens[0]; + var activatedLocation = activatedScreen.DisplayArea.Midpoint; + var expectedResult = new FormViewModel( + formBounds: new(250, 186, 524, 396), + canvasLayout: new( + canvasBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(0, 0, 524, 396), + boxStyle: previewStyle.CanvasStyle), + canvasStyle: previewStyle.CanvasStyle, + deviceLayouts: new List() + { + new( + deviceInfo: displayInfo.Devices[0], + deviceBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(6, 6, 512, 384), + boxStyle: BoxStyle.Empty), + deviceStyle: BoxStyle.Empty, + screenLayouts: new List + { + new( + screenInfo: displayInfo.Devices[0].Screens[0], + screenBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(6, 6, 512, 384), + boxStyle: previewStyle.ScreenStyle), + screenStyle: previewStyle.ScreenStyle), + } + ), + } + )); + yield return new object[] { new TestCase(testName, previewStyle, displayInfo, activatedScreen, activatedLocation, expectedResult) }; - // happy path - single screen with 50% scaling, + // happy path - single device with screen and 50% scaling, // *no* preview borders but *has* screenshot borders // // +----------------+ @@ -193,10 +221,11 @@ public static IEnumerable GetTestCases() // | 0 | // | | // +----------------+ + testName = "Test 2 - Happy Path"; previewStyle = new PreviewStyle( canvasSize: new( - width: 512, - height: 384 + width: 1024 / 2, // half the screen size + height: 768 / 2 // half the screen size ), canvasStyle: BoxStyle.Empty, screenStyle: new( @@ -210,37 +239,54 @@ public static IEnumerable GetTestCases() 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); - expectedResult = 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() + )), + extraColors: Array.Empty()); + displayInfo = new DisplayInfo( + devices: new List { 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) + hostname: "localhost", + localhost: true, + screens: new List + { + new( + handle: 0, + primary: true, + displayArea: new(0, 0, 1024, 768), + workingArea: new(0, 0, 1024, 768)), + } ), }); - yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, expectedResult) }; + activatedScreen = displayInfo.Devices[0].Screens[0]; + activatedLocation = activatedScreen.DisplayArea.Midpoint; + expectedResult = new FormViewModel( + formBounds: new(256, 192, 512, 384), + canvasLayout: new( + canvasBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(0, 0, 512, 384), + boxStyle: previewStyle.CanvasStyle), + canvasStyle: previewStyle.CanvasStyle, + deviceLayouts: new List() + { + new( + deviceInfo: displayInfo.Devices[0], + deviceBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(0, 0, 512, 384), + boxStyle: BoxStyle.Empty), + deviceStyle: BoxStyle.Empty, + screenLayouts: new List + { + new( + screenInfo: displayInfo.Devices[0].Screens[0], + screenBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(0, 0, 512, 384), + boxStyle: previewStyle.ScreenStyle), + screenStyle: previewStyle.ScreenStyle), + } + ), + } + )); + yield return new object[] { new TestCase(testName, previewStyle, displayInfo, activatedScreen, activatedLocation, expectedResult) }; // rounding error check - single screen with 33% scaling, // no borders, check to make sure form scales to exactly @@ -257,42 +303,61 @@ public static IEnumerable GetTestCases() // | 0 | // | | // +----------------+ + testName = "Test 3 - Rounding Error Check"; previewStyle = new PreviewStyle( canvasSize: new( width: 300, height: 200 ), canvasStyle: BoxStyle.Empty, - screenStyle: BoxStyle.Empty); - screens = new List - { - new(0, 0, 900, 200), - }; - activatedLocation = new PointInfo(450, 100); - expectedResult = new PreviewLayout( - virtualScreen: new(0, 0, 900, 200), - screens: screens, - activatedScreenIndex: 0, - formBounds: new(300, 66.5m, 300, 67), - previewStyle: previewStyle, - previewBounds: new( - outerBounds: new(0, 0, 300, 67), - marginBounds: new(0, 0, 300, 67), - borderBounds: new(0, 0, 300, 67), - paddingBounds: new(0, 0, 300, 67), - contentBounds: new(0, 0, 300, 67) - ), - screenshotBounds: new() + screenStyle: BoxStyle.Empty, + extraColors: Array.Empty()); + displayInfo = new DisplayInfo( + devices: new List { new( - outerBounds: new(0, 0, 300, 67), - marginBounds: new(0, 0, 300, 67), - borderBounds: new(0, 0, 300, 67), - paddingBounds: new(0, 0, 300, 67), - contentBounds: new(0, 0, 300, 67) + hostname: "localhost", + localhost: true, + screens: new List + { + new( + handle: 0, + primary: true, + displayArea: new(0, 0, 900, 200), + workingArea: new(0, 0, 900, 200)), + } ), }); - yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, expectedResult) }; + activatedScreen = displayInfo.Devices[0].Screens[0]; + activatedLocation = activatedScreen.DisplayArea.Midpoint; + expectedResult = new FormViewModel( + formBounds: new(300, 66.5m, 300, 67), + canvasLayout: new( + canvasBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(0, 0, 300, 67), + boxStyle: previewStyle.CanvasStyle), + canvasStyle: previewStyle.CanvasStyle, + deviceLayouts: new List() + { + new( + deviceInfo: displayInfo.Devices[0], + deviceBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(0, 0, 300, 67), + boxStyle: BoxStyle.Empty), + deviceStyle: BoxStyle.Empty, + screenLayouts: new List() + { + new( + screenInfo: displayInfo.Devices[0].Screens[0], + screenBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(0, 0, 300, 67), + boxStyle: previewStyle.ScreenStyle), + screenStyle: previewStyle.ScreenStyle), + } + ), + } + )); + yield return new object[] { new TestCase(testName, previewStyle, displayInfo, activatedScreen, activatedLocation, expectedResult) }; // primary monitor not topmost / leftmost - if there are screens // that are further left or higher up than the primary monitor @@ -306,6 +371,7 @@ public static IEnumerable GetTestCases() // | 1 | // | | // +----------------+ + testName = "Test 4 - Negative Coordinates"; previewStyle = new PreviewStyle( canvasSize: new( width: 716, @@ -335,49 +401,164 @@ public static IEnumerable GetTestCases() 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); - expectedResult = new PreviewLayout( - virtualScreen: new(-1920, -480, 7040, 1920), - screens: screens, - activatedScreenIndex: 0, + )), + extraColors: Array.Empty()); + displayInfo = new DisplayInfo( + devices: new List + { + new( + hostname: "localhost", + localhost: true, + screens: new List + { + new( + handle: 0, + primary: true, + displayArea: new(-1920, -480, 1920, 1080), + workingArea: new(-1920, -480, 1920, 1080)), + new( + handle: 0, + primary: false, + displayArea: new(0, 0, 5120, 1440), + workingArea: new(0, 0, 5120, 1440)), + } + ), + }); + activatedScreen = displayInfo.Devices[0].Screens[0]; + activatedLocation = activatedScreen.DisplayArea.Midpoint; + expectedResult = new FormViewModel( 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) + canvasLayout: new( + canvasBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(0, 0, 716, 204), + boxStyle: previewStyle.CanvasStyle), + canvasStyle: previewStyle.CanvasStyle, + deviceLayouts: new List() + { + new( + deviceInfo: displayInfo.Devices[0], + deviceBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(6, 6, 704, 192), + boxStyle: BoxStyle.Empty), + deviceStyle: BoxStyle.Empty, + screenLayouts: new List() + { + new( + screenInfo: displayInfo.Devices[0].Screens[0], + screenBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(6, 6, 192, 108), + boxStyle: previewStyle.ScreenStyle), + screenStyle: previewStyle.ScreenStyle), + new( + screenInfo: displayInfo.Devices[0].Screens[1], + screenBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(198, 54, 512, 144), + boxStyle: previewStyle.ScreenStyle), + screenStyle: previewStyle.ScreenStyle), + } + ), + } + )); + yield return new object[] { new TestCase(testName, previewStyle, displayInfo, activatedScreen, activatedLocation, expectedResult) }; + + // two devices side-by-side with a single screen each + // + // device 1 device 2 + // +----------------+ +----------------+ + // | | | | + // | 0 | | 0 | + // | | | | + // +----------------+ +----------------+ + testName = "Test 5 - Two Devices Side-by-Side"; + previewStyle = new PreviewStyle( + canvasSize: new( + width: 1600, + height: 1200 ), - screenshotBounds: new() + canvasStyle: BoxStyle.Empty, + screenStyle: BoxStyle.Empty, + extraColors: Array.Empty()); + displayInfo = new DisplayInfo( + devices: new List { 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) + hostname: "localhost", + localhost: true, + screens: new List + { + new( + handle: 0, + primary: true, + displayArea: new(0, 0, 5120, 1440), + workingArea: new(0, 0, 5120, 1440)), + } ), 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) + hostname: "remotehost", + localhost: false, + screens: new List + { + new( + handle: 0, + primary: true, + displayArea: new(0, 0, 5120, 1440), + workingArea: new(0, 0, 5120, 1440)), + } ), }); - yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, expectedResult) }; + activatedScreen = displayInfo.Devices[0].Screens[0]; + activatedLocation = activatedScreen.DisplayArea.Midpoint; + expectedResult = new FormViewModel( + formBounds: new(1760, 607.5m, 1600, 225), + canvasLayout: new( + canvasBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(0, 0, 1600, 225), + boxStyle: previewStyle.CanvasStyle), + canvasStyle: previewStyle.CanvasStyle, + deviceLayouts: new List() + { + new( + deviceInfo: displayInfo.Devices[0], + deviceBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(0, 0, 800, 225), + boxStyle: BoxStyle.Empty), + deviceStyle: BoxStyle.Empty, + screenLayouts: new List() + { + new( + screenInfo: displayInfo.Devices[0].Screens[0], + screenBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(0, 0, 800, 225), + boxStyle: previewStyle.ScreenStyle), + screenStyle: previewStyle.ScreenStyle), + } + ), + new( + deviceInfo: displayInfo.Devices[1], + deviceBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(800, 0, 800, 225), + boxStyle: BoxStyle.Empty), + deviceStyle: BoxStyle.Empty, + screenLayouts: new List() + { + new( + screenInfo: displayInfo.Devices[1].Screens[0], + screenBounds: BoxBounds.CreateFromOuterBounds( + outerBounds: new(800, 0, 800, 225), + boxStyle: previewStyle.ScreenStyle), + screenStyle: previewStyle.ScreenStyle), + } + ), + } + )); + yield return new object[] { new TestCase(testName, previewStyle, displayInfo, activatedScreen, activatedLocation, expectedResult) }; + + // TODO: add a test to make sure the form is nudged into the bounds + // of the screen if it's activated near an edge or corner } [TestMethod] - [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + [DynamicData(nameof(GetTestCases))] public void RunTestCases(TestCase data) { // note - even if values are within 0.0001M of each other they could @@ -387,117 +568,101 @@ public void RunTestCases(TestCase data) // (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 actual = LayoutHelper.GetFormLayout(data.PreviewStyle, data.DisplayInfo, data.ActivatedScreen, 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))), - }; + var actualJson = JsonSerializer.Serialize(actual, options); + var expectedJson = JsonSerializer.Serialize(expected, options); + Assert.AreEqual(expectedJson, actualJson); } + /// + /// Basic performance test just to avoid any massive regressions. + /// [TestMethod] - [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] - public void RunTestCases(TestCase data) + [Ignore("Ignore on CI runners - only run locally")] + public void BasicPerformanceTest() { - var actual = LayoutHelper.GetBoxBoundsFromContentBounds(data.ContentBounds, data.BoxStyle); - var expected = data.ExpectedResult; - Assert.AreEqual( - JsonSerializer.Serialize(expected), - JsonSerializer.Serialize(actual)); - } - } + // 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 | + // | | + // +----------------+ + var 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 + )), + extraColors: Array.Empty()); + var displayInfo = new DisplayInfo( + devices: new List + { + new( + hostname: "localhost", + localhost: true, + screens: new List + { + new( + handle: 0, + primary: true, + displayArea: new(-1920, -480, 1920, 1080), + workingArea: new(-1920, -480, 1920, 1080)), + new( + handle: 0, + primary: false, + displayArea: new(0, 0, 5120, 1440), + workingArea: new(0, 0, 5120, 1440)), + } + ), + }); + var activatedScreen = displayInfo.Devices[0].Screens[0]; + var activatedLocation = activatedScreen.DisplayArea.Midpoint; - [TestClass] - public sealed class GetBoxBoundsFromOuterBoundsTests - { - public sealed class TestCase - { - public TestCase(RectangleInfo outerBounds, BoxStyle boxStyle, BoxBounds expectedResult) + var timer = Stopwatch.StartNew(); + for (var i = 0; i < 10_000; i++) { - this.OuterBounds = outerBounds; - this.BoxStyle = boxStyle; - this.ExpectedResult = expectedResult; + var formLayout = LayoutHelper.GetFormLayout(previewStyle, displayInfo, activatedScreen, activatedLocation); } - 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))), - }; - } + timer.Stop(); - [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)); + // runs on my machine in about 180-200ms, so leave a bit of headroom + Assert.IsTrue(timer.ElapsedMilliseconds < 225); } } } diff --git a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/MouseHelperTests.cs b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/MouseHelperTests.cs index 52870e8cc17b..5ee8573ad31c 100644 --- a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/MouseHelperTests.cs +++ b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/MouseHelperTests.cs @@ -3,12 +3,12 @@ // See the LICENSE file in the project root for more information. using Microsoft.VisualStudio.TestTools.UnitTesting; + using MouseJump.Common.Helpers; -using MouseJump.Common.Models.Drawing; +using MouseJump.Models.Drawing; namespace MouseJump.Common.UnitTests.Helpers; -[TestClass] public static class MouseHelperTests { [TestClass] @@ -58,7 +58,7 @@ public static IEnumerable GetTestCases() } [TestMethod] - [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + [DynamicData(nameof(GetTestCases))] public void RunTestCases(TestCase data) { var actual = MouseHelper.GetJumpLocation( diff --git a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/_test-win11-expected.png b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/_test-win11-expected.png index 1816a9125a5b..bb0ab46820d1 100644 Binary files a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/_test-win11-expected.png and b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/_test-win11-expected.png differ diff --git a/src/modules/MouseUtils/MouseJump.Common.UnitTests/MouseJump.Common.UnitTests.csproj b/src/modules/MouseUtils/MouseJump.Common.UnitTests/MouseJump.Common.UnitTests.csproj index aa885b907109..d2fbef25a0e1 100644 --- a/src/modules/MouseUtils/MouseJump.Common.UnitTests/MouseJump.Common.UnitTests.csproj +++ b/src/modules/MouseUtils/MouseJump.Common.UnitTests/MouseJump.Common.UnitTests.csproj @@ -1,44 +1,40 @@ - - - - - - - {D5E42C63-57C5-4EF6-AECE-1E2FCA725B77} - PowerToys.MouseJump.Common.UnitTests - PowerToys.MouseJump.Common.UnitTests - PowerToys MouseJump.Common.UnitTests - Exe - $(RepoRoot)$(Platform)\$(Configuration)\tests\MouseJump.Common.UnitTests\ - false - false - - enable - true - enable - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + {D5E42C63-57C5-4EF6-AECE-1E2FCA725B77} + PowerToys.MouseJump.Common.UnitTests + PowerToys.MouseJump.Common.UnitTests + PowerToys MouseJump.Common.UnitTests + Library + ..\..\..\..\$(Platform)\$(Configuration)\tests\MouseJump.Common.UnitTests\ + false + false + + enable + enable + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/ConfigHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/ColorHelper.cs similarity index 96% rename from src/modules/MouseUtils/MouseJump.Common/Helpers/ConfigHelper.cs rename to src/modules/MouseUtils/MouseJump.Common/Helpers/ColorHelper.cs index bc2e67203dfe..f97dcbeebe7d 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Helpers/ConfigHelper.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/ColorHelper.cs @@ -1,12 +1,13 @@ -// Copyright (c) Microsoft Corporation +// 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 System.Globalization; namespace MouseJump.Common.Helpers; -public static class ConfigHelper +public static class ColorHelper { public static Color? ToUnnamedColor(Color? value) { diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/DeviceHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/DeviceHelper.cs new file mode 100644 index 000000000000..87b7598e7701 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/DeviceHelper.cs @@ -0,0 +1,37 @@ +// 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 MouseJump.Models.Display; +using MouseJump.Models.Drawing; + +namespace MouseJump.Common.Helpers; + +public static class DeviceHelper +{ + public static DisplayInfo GetDisplayInfo() + { + var devices = new List(); + + // add the local devices + devices.Add( + new( + hostname: Environment.MachineName, + localhost: true, + screens: ScreenHelper.GetAllScreens())); + + return new DisplayInfo( + devices: devices); + } + + public static ScreenInfo GetActivatedScreen(DeviceInfo deviceInfo, PointInfo activatedLocation) + { + ArgumentNullException.ThrowIfNull(deviceInfo); + ArgumentNullException.ThrowIfNull(activatedLocation); + + var activatedScreen = deviceInfo.Screens + .Single(screen => screen.DisplayArea.Contains(activatedLocation)); + + return activatedScreen; + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/DpiModeHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/DpiModeHelper.cs new file mode 100644 index 000000000000..1aec6401b82d --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/DpiModeHelper.cs @@ -0,0 +1,55 @@ +// 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.Runtime.Versioning; + +using Windows.Win32; +using Windows.Win32.UI.HiDpi; + +namespace MouseJump.Common.Helpers; + +/// +/// Based on WinForms Application.HighDpiMode +/// see https://github.com/dotnet/winforms/blob/1c324d074280ab5de6342d973069faa687f2c165/src/System.Windows.Forms/System/Windows/Forms/Application.cs#L18 +/// https://github.com/dotnet/winforms/blob/1c324d074280ab5de6342d973069faa687f2c165/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/ScaleHelper.cs#L14 +/// +public static class DpiModeHelper +{ + /// + /// See https://github.com/dotnet/winforms/blob/bd91bfb26ce90ac31e950c01dcb2b6e0776453a7/src/System.Private.Windows.Core/src/System/Private/Windows/OsVersion.cs#L9 + /// + private static bool IsWindows10_1607OrGreater() + => OperatingSystem.IsWindowsVersionAtLeast(major: 10, build: 14393); + + /// + /// Ensures that the application is running in per-monitor v2 DPI awareness mode. + /// + /// + /// See https://github.com/dotnet/winforms/blob/bd91bfb26ce90ac31e950c01dcb2b6e0776453a7/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/ScaleHelper.cs#L368 + /// + [SupportedOSPlatform("windows10.0.14393")] + public static void EnsurePerMonitorV2Enabled() + { + // PowerToys supports the following operating systems: + // + // * Windows 11 or Windows 10 version 2004 (code name 20H1 / build number 19041) or newer. + // + // so we'll do the same(ish) + if (!DpiModeHelper.IsWindows10_1607OrGreater()) + { + throw new PlatformNotSupportedException( + "Windows 10 version1607 or higher is required to use this application."); + } + + // there's a weird problem where AreDpiAwarenessContextsEqual was returning TRUE in debug mode + // but FALSE in release mode and i couldn't work out why, so we can't do it the *right* way. + // we'll just use GetAwarenessFromDpiAwarenessContext instead as a near-enough workaround. + var dpiAwarenessContext = PInvoke.GetThreadDpiAwarenessContext(); + var dpiAwareness = PInvoke.GetAwarenessFromDpiAwarenessContext(dpiAwarenessContext); + if (dpiAwareness != DPI_AWARENESS.DPI_AWARENESS_PER_MONITOR_AWARE) + { + throw new InvalidOperationException($"high dpi mode is not set to {nameof(DPI_AWARENESS.DPI_AWARENESS_PER_MONITOR_AWARE)}"); + } + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/DrawingHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/DrawingHelper.cs index 668e90dd3d67..99eef641ac9e 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Helpers/DrawingHelper.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/DrawingHelper.cs @@ -3,62 +3,101 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using MouseJump.Common.Imaging; -using MouseJump.Common.Models.Drawing; -using MouseJump.Common.Models.Layout; -using MouseJump.Common.Models.Styles; +using MouseJump.Models.Display; +using MouseJump.Models.Drawing; +using MouseJump.Models.Styles; +using MouseJump.Models.ViewModel; namespace MouseJump.Common.Helpers; public static class DrawingHelper { - public static Bitmap RenderPreview( - PreviewLayout previewLayout, - IImageRegionCopyService imageCopyService, - Action? previewImageCreatedCallback = null, - Action? previewImageUpdatedCallback = null) + /// + /// Renders a preview image of the specified canvas layout. + /// + /// + /// The layout of the canvas, including the layout of all devices and screens. + /// + /// + /// The screen that is currently activated (i.e. the one that the user is interacting with). + /// + /// + /// A list of IImageRegionCopyService implementations, one for each device in the canvas layout. + /// + /// + /// A callback that is invoked when the preview image is created. + /// + /// + /// A callback that is invoked when the preview image is updated. + /// + /// + /// A preview image of the canvas layout. + /// + public static async Task RenderPreviewAsync( + CanvasViewModel canvasLayout, + ScreenInfo activatedScreen, + List imageRegionCopyServices, + Func? previewImageCreatedCallback = null, + Func? previewImageUpdatedCallback = null) { var stopwatch = Stopwatch.StartNew(); // initialize the preview image - var previewBounds = previewLayout.PreviewBounds.OuterBounds.ToRectangle(); + var previewBounds = canvasLayout.CanvasBounds.OuterBounds.ToRectangle(); var previewImage = new Bitmap(previewBounds.Width, previewBounds.Height, PixelFormat.Format32bppPArgb); - var previewGraphics = Graphics.FromImage(previewImage); - previewImageCreatedCallback?.Invoke(previewImage); + using var previewGraphics = Graphics.FromImage(previewImage); + if (previewImageCreatedCallback != null) + { + await previewImageCreatedCallback(previewImage); + } - DrawingHelper.DrawRaisedBorder(previewGraphics, previewLayout.PreviewStyle.CanvasStyle, previewLayout.PreviewBounds); + DrawingHelper.DrawRaisedBorder(previewGraphics, canvasLayout.CanvasBounds, canvasLayout.CanvasStyle); DrawingHelper.DrawBackgroundFill( previewGraphics, - previewLayout.PreviewStyle.CanvasStyle, - previewLayout.PreviewBounds, + canvasLayout.CanvasStyle, + canvasLayout.CanvasBounds, []); // 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)) + var screenDrawingOps = canvasLayout.DeviceLayouts + .SelectMany( + (deviceLayout, deviceIndex) => deviceLayout.ScreenLayouts.Select( + screenLayout => new + { + DeviceIndex = deviceIndex, + DeviceLayout = deviceLayout, + ScreenLayout = screenLayout, + CopyService = imageRegionCopyServices[deviceIndex], + })) + .OrderByDescending( + pair => object.ReferenceEquals(pair.ScreenLayout.ScreenInfo, activatedScreen)) .ToList(); // draw all the screenshot bezels - foreach (var screenshotBounds in previewLayout.ScreenshotBounds) + foreach (var screenDrawingOp in screenDrawingOps) { DrawingHelper.DrawRaisedBorder( - previewGraphics, previewLayout.PreviewStyle.ScreenStyle, screenshotBounds); + previewGraphics, screenDrawingOp.ScreenLayout.ScreenBounds, screenDrawingOp.ScreenLayout.ScreenStyle); } var refreshRequired = false; var placeholdersDrawn = false; - for (var i = 0; i < sourceScreens.Count; i++) + for (var i = 0; i < screenDrawingOps.Count; i++) { - imageCopyService.CopyImageRegion(previewGraphics, sourceScreens[i], targetScreens[i].ContentBounds); + var screenDrawingOp = screenDrawingOps[i]; + + screenDrawingOp.CopyService.CopyImageRegion( + targetGraphics: previewGraphics, + sourceBounds: screenDrawingOp.ScreenLayout.ScreenInfo.DisplayArea, + targetBounds: screenDrawingOp.ScreenLayout.ScreenBounds.ContentBounds); refreshRequired = true; // show the placeholder images and show the form if it looks like it might take @@ -70,19 +109,26 @@ public static Bitmap RenderPreview( { DrawingHelper.DrawScreenPlaceholders( previewGraphics, - previewLayout.PreviewStyle.ScreenStyle, - targetScreens.GetRange(i + 1, targetScreens.Count - i - 1)); + screenDrawingOp.ScreenLayout.ScreenStyle, + screenDrawingOps + .Skip(i + 1) + .Select(drawingOp => drawingOp.ScreenLayout.ScreenBounds) + .ToList()); placeholdersDrawn = true; } - previewImageUpdatedCallback?.Invoke(); + if (previewImageUpdatedCallback is not null) + { + await previewImageUpdatedCallback(previewImage); + } + refreshRequired = false; } } - if (refreshRequired) + if (refreshRequired && (previewImageUpdatedCallback is not null)) { - previewImageUpdatedCallback?.Invoke(); + await previewImageUpdatedCallback(previewImage); } stopwatch.Stop(); @@ -94,10 +140,10 @@ public static Bitmap RenderPreview( /// Draws a border shape with an optional raised 3d highlight and shadow effect. /// private static void DrawRaisedBorder( - Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds) + Graphics graphics, BoxBounds boxBounds, BoxStyle boxStyle) { var borderStyle = boxStyle.BorderStyle; - if ((borderStyle.Horizontal == 0) || (borderStyle.Vertical == 0)) + if ((borderStyle.Horizontal < 1) || (borderStyle.Vertical < 1)) { return; } @@ -109,7 +155,7 @@ private static void DrawRaisedBorder( // draw the main box border using var borderBrush = new SolidBrush(borderStyle.Color.Value); - var borderRegion = new Region(boxBounds.BorderBounds.ToRectangle()); + using var borderRegion = new Region(boxBounds.BorderBounds.ToRectangle()); borderRegion.Exclude(boxBounds.PaddingBounds.ToRectangle()); graphics.FillRegion(borderBrush, borderRegion); @@ -195,7 +241,7 @@ private static void DrawBackgroundFill( // 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()); + using var backgroundRegion = new Region(backgroundBounds.ToRectangle()); foreach (var exclude in excludeBounds) { backgroundRegion.Exclude(exclude.ToRectangle()); @@ -208,14 +254,14 @@ private static void DrawBackgroundFill( /// Draws placeholder background images for the specified screens on the preview. /// private static void DrawScreenPlaceholders( - Graphics graphics, BoxStyle screenStyle, IList screenBounds) + Graphics graphics, BoxStyle screenStyle, List screenBounds) { if (screenBounds.Count == 0) { return; } - if (screenStyle?.BackgroundStyle?.Color1 == null) + if (screenStyle.BackgroundStyle.Color1 == null) { return; } diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/LayoutHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/LayoutHelper.cs index 589ff42178a9..88f219ac29b8 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Helpers/LayoutHelper.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/LayoutHelper.cs @@ -2,152 +2,401 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using MouseJump.Common.Models.Drawing; -using MouseJump.Common.Models.Layout; -using MouseJump.Common.Models.Styles; +using System.Diagnostics; +using System.Drawing; + +using MouseJump.Models.Display; +using MouseJump.Models.Drawing; +using MouseJump.Models.Styles; +using MouseJump.Models.ViewModel; namespace MouseJump.Common.Helpers; public static class LayoutHelper { - public static PreviewLayout GetPreviewLayout( - PreviewStyle previewStyle, List screens, PointInfo activatedLocation) + public static RectangleInfo GetCombinedScreenBounds(List screenBounds) + { + return screenBounds.Skip(1).Aggregate( + seed: screenBounds.First(), + (combined, screenBounds) => combined.Union(screenBounds)); + } + + public static FormViewModel GetFormLayout( + PreviewStyle previewStyle, DisplayInfo displayInfo, ScreenInfo activatedScreen, PointInfo activatedLocation) { ArgumentNullException.ThrowIfNull(previewStyle); - ArgumentNullException.ThrowIfNull(screens); + ArgumentNullException.ThrowIfNull(displayInfo); + ArgumentNullException.ThrowIfNull(activatedScreen); + ArgumentNullException.ThrowIfNull(activatedLocation); - if (screens.Count == 0) - { - throw new ArgumentException("Value must contain at least one item.", nameof(screens)); - } + /* + + example layout: + + +-------------------------------[form]------------------------------+ + |▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒[canvas]▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| + |▒▒+----------------------------[grid]----+----------------------+▒▒| + |▒▒| |▓▓▓▓▓▓[device 2]▓▓▓▓▓▓|▒▒| + |▒▒| |▓▓░░░░[screen 1]░░░░▓▓|▒▒| + |▒▒|▓▓▓▓▓▓▓▓▓▓▓▓▓▓[device 1]▓▓▓▓▓▓▓▓▓▓▓▓▓▓|▓▓░░ ░░▓▓|▒▒| + |▒▒|▓▓░░░░[screen 1]░░░░░░[screen 2]░░░░▓▓|▓▓░░ ░░▓▓|▒▒| + |▒▒|▓▓░░ ░░ ░░▓▓|▓▓░░ ░░▓▓|▒▒| + |▒▒|▓▓░░ ░░ ░░▓▓|▓▓░░░░░░░░░░░░░░░░░░▓▓|▒▒| + |▒▒|▓▓░░ ░░ ░░▓▓|▓▓░░░░[screen 2]░░░░▓▓|▒▒| + |▒▒|▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓|▓▓░░ ░░▓▓|▒▒| + |▒▒|▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓|▓▓░░ ░░▓▓|▒▒| + |▒▒| |▓▓░░ ░░▓▓|▒▒| + |▒▒| |▓▓░░░░░░░░░░░░░░░░░░▓▓|▒▒| + |▒▒| |▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓|▒▒| + |▒▒+--------------------------------------+----------------------+▒▒| + |▒▒| ▓▓▓▓▓▓[device 3]▓▓▓▓▓▓ | |▒▒| + |▒▒| ▓▓░░░░[screen 1]░░░░▓▓ | |▒▒| + |▒▒| ▓▓░░ ░░▓▓ | |▒▒| + |▒▒| ▓▓░░ ░░▓▓ | ▓▓▓[device 4]▓▓▓ |▒▒| + |▒▒| ▓▓░░ ░░▓▓ | ▓▓░[screen 1]░▓▓ |▒▒| + |▒▒| ▓▓░░░░░░░░░░░░░░░░░░▓▓ | ▓▓░░ ░░▓▓ |▒▒| + |▒▒| ▓▓░░░░[screen 2]░░░░▓▓ | ▓▓░░ ░░▓▓ |▒▒| + |▒▒| ▓▓░░ ░░▓▓ | ▓▓░░░░░░░░░░░░▓▓ |▒▒| + |▒▒| ▓▓░░ ░░▓▓ | ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ |▒▒| + |▒▒| ▓▓░░ ░░▓▓ | |▒▒| + |▒▒| ▓▓░░░░░░░░░░░░░░░░░░▓▓ | |▒▒| + |▒▒| ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ | |▒▒| + |▒▒+--------------------------------------+----------------------+▒▒| + |▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| + +-------------------------------------------------------------------+ - var builder = new PreviewLayout.Builder(); - builder.Screens = screens.ToList(); + */ - // calculate the bounding rectangle for the virtual screen - builder.VirtualScreen = LayoutHelper.GetCombinedScreenBounds(builder.Screens); + // arrange the form, canvas, devices and screens + var formLayout = LayoutHelper.CreateInitialFormLayout(previewStyle, displayInfo, activatedScreen); + LayoutHelper.ArrangeAndScaleDeviceLayouts(formLayout); + LayoutHelper.ArrangeAndScaleScreenLayouts(formLayout); + LayoutHelper.ArrangeAndResizeCanvasLayout(formLayout); + LayoutHelper.ArrangeAndResizeFormLayout(formLayout, activatedScreen, activatedLocation); + + return formLayout.Build(); + } + + internal static FormViewModel.Builder CreateInitialFormLayout( + PreviewStyle previewStyle, DisplayInfo displayInfo, ScreenInfo activatedScreen) + { + ArgumentNullException.ThrowIfNull(previewStyle); + ArgumentNullException.ThrowIfNull(displayInfo); + + // check we have at least one device + if (displayInfo.Devices.Count == 0) + { + throw new ArgumentException("Value must contain at least one device.", nameof(displayInfo)); + } - // 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); + /* + // check each device has at least one screen. + for (var deviceIndex = 0; deviceIndex < displayInfo.Devices.Count; deviceIndex++) + { + var device = displayInfo.Devices[deviceIndex]; + if (device.Screens.Count == 0) + { + throw new ArgumentException($"{nameof(displayInfo)}.{nameof(displayInfo.Devices)}[{deviceIndex}] must contain at least one screen.", nameof(displayInfo)); + } + } + */ // 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); - - // 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 - .ScaleToFit(maxContentSize, out var scalingRatio) - .Round() - .Clamp(maxContentSize) - .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; + var formMaxBounds = new RectangleInfo( + previewStyle.CanvasSize + .Clamp(activatedScreen.DisplayArea.Size)); - // 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(scalingRatio) - .Offset(builder.PreviewBounds.ContentBounds.Location.ToSize()) - .Round(), - previewStyle.ScreenStyle)) - .ToList(); - - return builder.Build(); + var screenStyles = LayoutHelper.GetDeviceScreenStyles(previewStyle, displayInfo.Devices.Count).ToList(); + + // create an initial form layout. + // this is a nested structure of mutable "builder" objects that can be used to + // build the final immutable layout objects once all the bounds have been calculated + var formLayout = new FormViewModel.Builder + { + FormBounds = RectangleInfo.Empty, + CanvasLayout = new() + { + CanvasBounds = BoxBounds.CreateFromOuterBounds( + outerBounds: formMaxBounds, + boxStyle: previewStyle.CanvasStyle), + CanvasStyle = previewStyle.CanvasStyle, + DeviceLayouts = displayInfo.Devices.Select( + (deviceInfo, deviceIndex) => new DeviceViewModel.Builder + { + DeviceInfo = deviceInfo, + DeviceBounds = BoxBounds.Empty, + DeviceStyle = BoxStyle.Empty, + ScreenLayouts = deviceInfo.Screens.Select( + screenInfo => new ScreenViewModel.Builder + { + ScreenInfo = screenInfo, + ScreenBounds = BoxBounds.Empty, + ScreenStyle = screenStyles[deviceIndex], + }).ToList(), + }).ToList(), + }, + }; + + return formLayout; } - public static RectangleInfo GetCombinedScreenBounds(List screens) + internal static IEnumerable GetDeviceScreenStyles(PreviewStyle previewStyle, int screenCount) { - return screens.Skip(1).Aggregate( - seed: screens.First(), - (bounds, screen) => bounds.Union(screen)); + // work out the colors to use for the screen borders for each device + // (add the default localhost color first and then loop over the extra colors until we've got enough) + var screenBorderColors = new List { previewStyle.ScreenStyle.BorderStyle.Color }; + while (screenBorderColors.Count < screenCount) + { + if (previewStyle.ExtraColors?.Count > 0) + { + screenBorderColors.AddRange(previewStyle.ExtraColors.Cast()); + continue; + } + + screenBorderColors.Add(previewStyle.ScreenStyle.BorderStyle.Color); + } + + // convert the colors into screen styles + // note - only create a new ScreenStyle if the border color is different. + // (this is to preserve a reference to ScreenStyle.Empty - and + // "ScreenStyle.IsEmpty == true" - if the new border color is Transparent) + return screenBorderColors + .Take(screenCount) + .Select( + color => previewStyle.ScreenStyle.BorderStyle.Color == color + ? previewStyle.ScreenStyle + : new BoxStyle( + previewStyle.ScreenStyle.MarginStyle, + previewStyle.ScreenStyle.BorderStyle.WithColor(color), + previewStyle.ScreenStyle.PaddingStyle, + previewStyle.ScreenStyle.BackgroundStyle)); } /// - /// 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. + /// Arranges the device layouts into a non-overlapping grid and scales them to fit inside the specified content bounds. /// - /// 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. - public static BoxBounds GetBoxBoundsFromContentBounds( - RectangleInfo contentBounds, - BoxStyle boxStyle) + internal static void ArrangeAndScaleDeviceLayouts(FormViewModel.Builder formLayout) { - ArgumentNullException.ThrowIfNull(contentBounds); - ArgumentNullException.ThrowIfNull(boxStyle); - if (boxStyle.PaddingStyle == null || boxStyle.BorderStyle == null || boxStyle.MarginStyle == null) + var deviceLayouts = formLayout.CanvasLayout?.DeviceLayouts + ?? throw new InvalidOperationException(); + var contentBounds = formLayout.CanvasLayout.CanvasBounds?.ContentBounds + ?? throw new InvalidOperationException(); + + // build an initial grid of devices at 100% scale. the grid is currently a single row + // of cells high with all the devices arranged left to right for the time being. + // we'll enhance this to allow for multiple rows later to support the + // Mouse Without Borders "square" arrangement. + var gridRowCount = 1; + var gridColumnCount = deviceLayouts.Count; + var deviceGrid = new DeviceViewModel.Builder[gridRowCount, gridColumnCount]; + for (var columnIndex = 0; columnIndex < gridColumnCount; columnIndex++) { - throw new ArgumentException(null, nameof(boxStyle)); + deviceGrid[0, columnIndex] = deviceLayouts[columnIndex]; } - 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); + // if a device has zero screens (or if we failed to connect and read screen layouts) we + // still want to allocate space in the full size grid so that it doesn't disappear into + // a zero width or height cell. + var fullSizeGridCellMinSize = new Size(1024, 768); + + // find the size of the tallest device in each row and the widest device in each column. + // this tells us how tall each row needs to be and how wide each column needs to be. + var fullSizeGridCellBounds = new RectangleInfo[gridRowCount, gridColumnCount]; + var fullSizeRowHeights = new decimal[gridRowCount]; + var fullSizeColumnWidths = new decimal[gridColumnCount]; + for (var rowIndex = 0; rowIndex < gridRowCount; rowIndex++) + { + for (var columnIndex = 0; columnIndex < gridColumnCount; columnIndex++) + { + var deviceInfo = deviceGrid[rowIndex, columnIndex].DeviceInfo ?? throw new InvalidOperationException(); + var deviceBounds = deviceInfo.GetCombinedDisplayArea(); + + // if the device bounds are empty, it probably means there aren't any + // screens available for this device so we'll use the minimum size instead + // for the grid cell size + var fullSizeGridCell = deviceBounds.IsEmpty + ? deviceBounds.Resize( + width: fullSizeGridCellMinSize.Width, + height: fullSizeGridCellMinSize.Height) + : deviceBounds; + + fullSizeGridCellBounds[rowIndex, columnIndex] = fullSizeGridCell; + fullSizeRowHeights[rowIndex] = Math.Max(fullSizeRowHeights[rowIndex], fullSizeGridCell.Height); + fullSizeColumnWidths[columnIndex] = Math.Max(fullSizeColumnWidths[columnIndex], fullSizeGridCell.Width); + } + } + + // use the row heights to calculate the absolute coordinates of each grid row + var fullSizeRowCoordinates = new decimal[gridRowCount]; + fullSizeRowCoordinates[0] = 0; + for (var rowIndex = 1; rowIndex < gridRowCount; rowIndex++) + { + fullSizeRowCoordinates[rowIndex] = fullSizeRowCoordinates[rowIndex - 1] + fullSizeRowHeights[rowIndex - 1]; + } + + // use the column widths to calculate the absolute coordinates of each grid column + var fullSizeColumnCoordinates = new decimal[gridColumnCount]; + fullSizeColumnCoordinates[0] = 0; + for (var columnIndex = 1; columnIndex < gridColumnCount; columnIndex++) + { + fullSizeColumnCoordinates[columnIndex] = fullSizeColumnCoordinates[columnIndex - 1] + fullSizeColumnWidths[columnIndex - 1]; + } + + // calculate the full size bounds for the entire grid + var fullSizeGridBounds = new RectangleInfo( + x: 0, + y: 0, + width: fullSizeColumnCoordinates[gridColumnCount - 1] + fullSizeColumnWidths[gridColumnCount - 1], + height: fullSizeRowCoordinates[gridRowCount - 1] + fullSizeRowHeights[gridRowCount - 1]); + + // work out the scaling factor that will fit the full size grid into the content bounds + var scaledGridSize = fullSizeGridBounds.Size + .ScaleToFit(contentBounds.Size, out var scalingFactor); + + // scale the row and column coordinates by the factor. round coordinates to ensure they + // don't overlap when rendered as pixel dimensions in the image. we'll also add a final + // "fencepost" coordinate to simplify building the end cell of each row and column in the + // scaled grid. + var scaledRowCoordinates = fullSizeRowCoordinates + .Select(fullSizeRowCoordinate => Math.Round(fullSizeRowCoordinate * scalingFactor)) + .Concat([scaledGridSize.Height]) + .Select(scaledRowCoordinate => scaledRowCoordinate + contentBounds.Top) + .ToArray(); + var scaledColumnCoordinates = fullSizeColumnCoordinates + .Select(fullSizeColumnCoordinate => Math.Round(fullSizeColumnCoordinate * scalingFactor)) + .Concat([scaledGridSize.Width]) + .Select(scaledColumnCoordinate => scaledColumnCoordinate + contentBounds.Left) + .ToArray(); + + // arrange the device layouts into their scaled grid positions + for (var rowIndex = 0; rowIndex < gridRowCount; rowIndex++) + { + for (var columnIndex = 0; columnIndex < gridColumnCount; columnIndex++) + { + var fullSizeCellSize = fullSizeGridCellBounds[rowIndex, columnIndex].Size; + if ((fullSizeCellSize.Width == 0) || (fullSizeCellSize.Height == 0)) + { + // if the full size bounds are zero width or height, it probably means there aren't any + // screens available for this device so we can't scale it to fit inside the grid cell + continue; + } + + // work out the scaled coordinates for this grid cell + var scaledGridCellBounds = new RectangleInfo( + x: scaledColumnCoordinates[columnIndex], + y: scaledRowCoordinates[rowIndex], + width: scaledColumnCoordinates[columnIndex + 1] - scaledColumnCoordinates[columnIndex], + height: scaledRowCoordinates[rowIndex + 1] - scaledRowCoordinates[rowIndex]); + + // scale and center the device layout to fit inside the grid cell + var deviceLayout = deviceGrid[rowIndex, columnIndex]; + deviceLayout.DeviceBounds = BoxBounds.CreateFromOuterBounds( + outerBounds: new RectangleInfo( + size: fullSizeCellSize.ScaleToFit(scaledGridCellBounds.Size)) + .Center(scaledGridCellBounds.Midpoint) + .Round() + .Intersect(scaledGridCellBounds.Round()), + boxStyle: deviceLayout.DeviceStyle); + } + } + + // verify that we're pixel-perfect on the positions + var scaledDeviceBounds = RectangleInfo.Union( + deviceLayouts + .Select(deviceLayout => deviceLayout.DeviceBounds.OuterBounds)); + Debug.Assert( + (scaledDeviceBounds.Width == contentBounds.Width) || (scaledDeviceBounds.Height == contentBounds.Height), + string.Join( + "\r\n", + $"{nameof(ArrangeAndScaleDeviceLayouts)} - scaled device layout does not perfectly fill content bounds:", + $"scaled grid = '{scaledDeviceBounds}'", + $"content bounds = '{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. + /// Arranges the screen layouts inside their respective device cells and + /// scales them to fit inside their parent device layouts. /// - /// 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. - public static BoxBounds GetBoxBoundsFromOuterBounds( - RectangleInfo outerBounds, - BoxStyle boxStyle) + internal static void ArrangeAndScaleScreenLayouts(FormViewModel.Builder formLayout) { - ArgumentNullException.ThrowIfNull(outerBounds); - ArgumentNullException.ThrowIfNull(boxStyle); - if (outerBounds == null || boxStyle.MarginStyle == null || boxStyle.BorderStyle == null || boxStyle.PaddingStyle == null) + var deviceLayouts = formLayout.CanvasLayout?.DeviceLayouts + ?? throw new InvalidOperationException(); + + foreach (var deviceLayout in deviceLayouts) { - throw new ArgumentException(null, nameof(boxStyle)); + var deviceInfo = deviceLayout.DeviceInfo ?? throw new InvalidOperationException(); + var screenLayouts = deviceLayout.ScreenLayouts ?? throw new InvalidOperationException(); + if (screenLayouts.Count == 0) + { + // nothing to arrange or scale, and we'll get a divide by zero error + // in ScaleToFitRatio if we don't quit early + continue; + } + + // work out the scaling factor that will fit the screen layouts into the content bounds + var fullSizeDisplayArea = deviceInfo.GetCombinedDisplayArea(); + var contentBounds = deviceLayout.DeviceBounds.ContentBounds; + var scalingFactor = fullSizeDisplayArea.Size.ScaleToFitRatio(contentBounds.Size); + + foreach (var screenLayout in screenLayouts) + { + var screenInfo = screenLayout.ScreenInfo ?? throw new InvalidOperationException(); + screenLayout.ScreenBounds = BoxBounds.CreateFromOuterBounds( + outerBounds: screenInfo.DisplayArea + .Offset(-fullSizeDisplayArea.Left, -fullSizeDisplayArea.Top) + .Scale(scalingFactor) + .Offset(contentBounds.Left, contentBounds.Top) + .Round() + .Intersect(contentBounds), + boxStyle: screenLayout.ScreenStyle); + } + + // verify that we're pixel-perfect on the positions + var scaledScreenBounds = RectangleInfo.Union( + screenLayouts + .Select(screenLayout => screenLayout.ScreenBounds.OuterBounds)); + Debug.Assert( + (scaledScreenBounds.Width == contentBounds.Width) || (scaledScreenBounds.Height == contentBounds.Height), + string.Join( + "\r\n", + $"{nameof(ArrangeAndScaleScreenLayouts)} - scaled screen layouts do not perfectly fill content bounds:", + $"scaled grid = '{scaledScreenBounds}'", + $"content bounds = '{contentBounds}'")); } + } + + internal static void ArrangeAndResizeCanvasLayout(FormViewModel.Builder formLayout) + { + var canvasLayout = formLayout.CanvasLayout ?? throw new InvalidOperationException(); - 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); + // work out how big the canvas needs to be in order to contain the device layouts + var canvasContentBounds = RectangleInfo.Union( + (canvasLayout.DeviceLayouts ?? throw new InvalidOperationException()) + .Select(deviceLayout => deviceLayout.DeviceBounds.OuterBounds)); + var canvasOuterBounds = BoxBounds.CreateFromContentBounds( + contentBounds: canvasContentBounds, + boxStyle: formLayout.CanvasLayout.CanvasStyle); + + // move the canvas into position + var positionedOuterBounds = canvasOuterBounds.OuterBounds.MoveTo( + (canvasLayout.CanvasBounds ?? throw new InvalidOperationException()) + .OuterBounds.Location); + + canvasLayout.CanvasBounds = BoxBounds.CreateFromOuterBounds( + outerBounds: positionedOuterBounds, + boxStyle: formLayout.CanvasLayout.CanvasStyle); + } + + internal static void ArrangeAndResizeFormLayout(FormViewModel.Builder formLayout, ScreenInfo activatedScreen, PointInfo activatedLocation) + { + var canvasOuterBounds = formLayout.CanvasLayout?.CanvasBounds?.OuterBounds + ?? throw new InvalidOperationException(); + + // resize and center the form on the activated location + formLayout.FormBounds = canvasOuterBounds + .Center(activatedLocation) + .MoveInside(activatedScreen.DisplayArea); } } diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/MouseHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/MouseHelper.cs index 364618fb735e..91e7449b02c6 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Helpers/MouseHelper.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/MouseHelper.cs @@ -5,10 +5,12 @@ using System.ComponentModel; using System.Runtime.InteropServices; -using MouseJump.Common.Models.Drawing; -using MouseJump.Common.NativeMethods; -using static MouseJump.Common.NativeMethods.Core; -using static MouseJump.Common.NativeMethods.User32; +using MouseJump.Common.Interop; +using MouseJump.Models.Drawing; + +using Windows.Win32; +using Windows.Win32.UI.Input.KeyboardAndMouse; +using Windows.Win32.UI.WindowsAndMessaging; namespace MouseJump.Common.Helpers; @@ -35,19 +37,9 @@ public static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo prev /// public static PointInfo GetCursorPosition() { - var lpPoint = new LPPOINT(new POINT(0, 0)); - var result = User32.GetCursorPos(lpPoint); - if (!result) - { - throw new Win32Exception( - Marshal.GetLastWin32Error()); - } - - var point = lpPoint.ToStructure(); - lpPoint.Free(); - - return new PointInfo( - point.x, point.y); + var result = PInvoke.GetCursorPos(out var point); + ResultHandler.ThrowIfZero(result: result, getLastError: true, memberName: nameof(PInvoke.GetCursorPos)); + return new(point.X, point.Y); } /// @@ -56,43 +48,52 @@ public static PointInfo GetCursorPosition() /// /// See https://github.com/mikeclayton/FancyMouse/pull/3 /// - public static void SetCursorPosition(PointInfo location) + public static void SetCursorPosition(PointInfo position) + { + MouseHelper.SetCursorPositionInternal(position); + + // temporary workaround for issue #1273 + MouseHelper.SimulateMouseMovementEvent(position); + } + + private static void SetCursorPositionInternal(PointInfo position) { // 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 - // monitors - e.g. when trying to move the cursor from (a) to (b) we can - // *sometimes* - for no clear reason - end up at (c) instead. + // monitors - e.g. when trying to move the cursor from (a) to (b) through + // the dotted area we can *sometimes* - for no clear reason - end up at + // (c) instead. // - // +----------------+ - // |(c) (b) | - // | | - // | | - // | | + // ..........+----------------+ + // ..........|(c) (b) | + // ..........| | + // ..........| | + // ..........| | // +---------+ | // | (a) | | // +---------+----------------+ // - // setting the position again seems to fix this and moves the + // setting the position more than once seems to fix this and moves the // cursor to the expected location (b) - var target = location.ToPoint(); + var targetPosition = position.ToPoint(); for (var i = 0; i < 2; i++) { - var result = User32.SetCursorPos(target.X, target.Y); - if (!result) + // SetCursorPos has been known to return zero (i.e. an error), + // with GetLastError also returning zero to indicate success + var result1 = PInvoke.SetCursorPos(targetPosition.X, targetPosition.Y); + if (result1 == 0) { - throw new Win32Exception( - Marshal.GetLastWin32Error()); + var lastError = Marshal.GetLastPInvokeError(); + ResultHandler.HandleResult(result: result1, success: lastError == 0, lastError: lastError, memberName: nameof(PInvoke.SetCursorPos)); } - var current = MouseHelper.GetCursorPosition(); - if ((current.X == target.X) || (current.Y == target.Y)) + var result2 = PInvoke.GetCursorPos(out var currentPosition); + ResultHandler.ThrowIfZero(result: result2, getLastError: true, memberName: nameof(PInvoke.GetCursorPos)); + if ((currentPosition.X == position.X) && (currentPosition.Y == position.Y)) { break; } } - - // temporary workaround for issue #1273 - MouseHelper.SimulateMouseMovementEvent(location); } /// @@ -104,27 +105,31 @@ public static void SetCursorPosition(PointInfo location) /// private static void SimulateMouseMovementEvent(PointInfo location) { - var inputs = new User32.INPUT[] + var inputs = new INPUT[] { - new( - 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: MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE, - time: 0, - dwExtraInfo: ULONG_PTR.Null))), + new() + { + type = INPUT_TYPE.INPUT_MOUSE, + Anonymous = new() + { + mi = new MOUSEINPUT + { + dx = (int)MouseHelper.CalculateAbsoluteCoordinateX(location.X), + dy = (int)MouseHelper.CalculateAbsoluteCoordinateY(location.Y), + mouseData = 0, + dwFlags = MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE, + time = 0, + dwExtraInfo = default, + }, + }, + }, }; - var result = User32.SendInput( - (UINT)inputs.Length, - new LPINPUT(inputs), - INPUT.Size * inputs.Length); + + var cbSize = Marshal.SizeOf(); + var result = PInvoke.SendInput(inputs, cbSize); if (result != inputs.Length) { - throw new Win32Exception( - Marshal.GetLastWin32Error()); + ResultHandler.HandleFailure(result: result, getLastError: true, memberName: nameof(PInvoke.SendInput)); } } @@ -132,13 +137,17 @@ private static decimal CalculateAbsoluteCoordinateX(decimal x) { // 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(SYSTEM_METRICS_INDEX.SM_CXSCREEN); + var result = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN); + ResultHandler.ThrowIfZero(result: result, getLastError: false, memberName: nameof(PInvoke.GetSystemMetrics)); + return (x * 65535) / result; } 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(SYSTEM_METRICS_INDEX.SM_CYSCREEN); + var result = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN); + ResultHandler.ThrowIfZero(result: result, getLastError: false, memberName: nameof(PInvoke.GetSystemMetrics)); + return (y * 65535) / result; } } diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/ScreenHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/ScreenHelper.cs index a863b45bbea4..3ae3bb0b667d 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Helpers/ScreenHelper.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/ScreenHelper.cs @@ -2,12 +2,16 @@ // 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.ComponentModel; +using System.Runtime.InteropServices; -using MouseJump.Common.Models.Drawing; -using MouseJump.Common.NativeMethods; -using static MouseJump.Common.NativeMethods.Core; -using static MouseJump.Common.NativeMethods.User32; +using MouseJump.Common.Interop; +using MouseJump.Models.Display; +using MouseJump.Models.Drawing; + +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Gdi; +using Windows.Win32.UI.WindowsAndMessaging; namespace MouseJump.Common.Helpers; @@ -20,49 +24,47 @@ public static class ScreenHelper private static RectangleInfo GetVirtualScreen() { return new( - User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_XVIRTUALSCREEN), - User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_YVIRTUALSCREEN), - User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXVIRTUALSCREEN), - User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYVIRTUALSCREEN)); + PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_XVIRTUALSCREEN), + PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_YVIRTUALSCREEN), + PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXVIRTUALSCREEN), + PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYVIRTUALSCREEN)); } public static IEnumerable GetAllScreens() { // enumerate the monitors attached to the system var hMonitors = new List(); - var callback = new User32.MONITORENUMPROC( - (hMonitor, hdcMonitor, lprcMonitor, dwData) => - { - hMonitors.Add(hMonitor); - return true; - }); - var result = User32.EnumDisplayMonitors(HDC.Null, LPCRECT.Null, callback, LPARAM.Null); - if (!result) + unsafe { - throw new Win32Exception( - result.Value, - $"{nameof(User32.EnumDisplayMonitors)} failed with return code {result.Value}"); + var callback = new MONITORENUMPROC( + (hMonitor, hdcMonitor, lprcMonitor, dwData) => + { + hMonitors.Add(hMonitor); + return true; + }); + var result = PInvoke.EnumDisplayMonitors(HDC.Null, null, callback, (LPARAM)0); + if (result == 0) + { + throw new InvalidOperationException("failed to enumerate monitors"); + } + + // prevent callback from being collected during the enumeration + GC.KeepAlive(callback); } // get detailed info about each monitor + var monitorInfo = new MONITORINFO + { + cbSize = (uint)Marshal.SizeOf(), + }; foreach (var hMonitor in hMonitors) { - var monitorInfoPtr = new LPMONITORINFO( - 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}"); - } - - var monitorInfo = monitorInfoPtr.ToStructure(); - monitorInfoPtr.Free(); + var result = PInvoke.GetMonitorInfo(hMonitor, ref monitorInfo); + ResultHandler.ThrowIfZero(result: result, getLastError: true, memberName: nameof(PInvoke.GetMonitorInfo)); yield return new ScreenInfo( handle: hMonitor, - primary: monitorInfo.dwFlags.HasFlag(User32.MONITOR_INFO_FLAGS.MONITORINFOF_PRIMARY), + primary: (monitorInfo.dwFlags & PInvoke.MONITORINFOF_PRIMARY) != 0, displayArea: new RectangleInfo( monitorInfo.rcMonitor.left, monitorInfo.rcMonitor.top, @@ -81,9 +83,9 @@ public static ScreenInfo GetScreenFromPoint( PointInfo pt) { // get the monitor handle from the point - var hMonitor = User32.MonitorFromPoint( + var hMonitor = PInvoke.MonitorFromPoint( new((int)pt.X, (int)pt.Y), - User32.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST); + MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST); if (hMonitor.IsNull) { throw new InvalidOperationException($"no monitor found for point {pt}"); diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/StyleHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/StyleHelper.cs index ae2f97264dcc..5350d06fa603 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Helpers/StyleHelper.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/StyleHelper.cs @@ -2,8 +2,10 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using MouseJump.Common.Models.Drawing; -using MouseJump.Common.Models.Styles; +using System.Drawing; + +using MouseJump.Models.Drawing; +using MouseJump.Models.Styles; namespace MouseJump.Common.Helpers; @@ -46,7 +48,13 @@ public static class StyleHelper color1: Color.MidnightBlue, color2: Color.MidnightBlue ) - ) + ), + extraColors: new[] + { + Color.Red, + Color.Blue, + Color.Green, + } ); /// @@ -86,7 +94,13 @@ public static class StyleHelper color1: Color.MidnightBlue, color2: Color.MidnightBlue ) - ) + ), + extraColors: new[] + { + Color.Red, + Color.Blue, + Color.Green, + } ); public static PreviewStyle WithCanvasSize(this PreviewStyle previewStyle, SizeInfo canvasSize) @@ -96,6 +110,7 @@ public static PreviewStyle WithCanvasSize(this PreviewStyle previewStyle, SizeIn return new PreviewStyle( canvasSize: canvasSize, canvasStyle: previewStyle.CanvasStyle, - screenStyle: previewStyle.ScreenStyle); + screenStyle: previewStyle.ScreenStyle, + extraColors: previewStyle.ExtraColors); } } diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/Win32Helper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/Win32Helper.cs new file mode 100644 index 000000000000..3222341d09cf --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/Win32Helper.cs @@ -0,0 +1,111 @@ +// 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.Diagnostics; +using System.Runtime.InteropServices; + +using MouseJump.Common.Interop; + +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Gdi; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace MouseJump.Common.Helpers; + +public static class Win32Helper +{ + /// + /// Creates a new "message only" window. + /// + /// + /// A message-only window is a window that is not visible and does not have a user interface. + /// It exists solely to receive and process messages. + /// These windows are used for background tasks that need to handle messages without displaying any graphical elements to the user. + /// + public static Win32Window CreateMessageOnlyWindow(string className, string windowName, Win32WindowProc.WindowProcDelegate windowProcDelegate) + { + ArgumentNullException.ThrowIfNullOrEmpty(className); + ArgumentNullException.ThrowIfNullOrEmpty(windowName); + ArgumentNullException.ThrowIfNull(windowProcDelegate); + + var windowClass = Win32Helper.RegisterWindowClass(className, windowProcDelegate); + var window = Win32Helper.CreateWindow(windowClass, windowName); + + return window; + } + + private static Win32WindowClass RegisterWindowClass(string className, Win32WindowProc.WindowProcDelegate windowProcDelegate) + { + ArgumentNullException.ThrowIfNullOrEmpty(className); + ArgumentNullException.ThrowIfNull(windowProcDelegate); + + using var hModule = PInvoke.GetModuleHandle(null); + var hInstance = (HINSTANCE)hModule.DangerousGetHandle(); + + // register the window class + // see https://stackoverflow.com/a/30992796/3156906 + var windowProc = new Win32WindowProc(windowProcDelegate); + var classAtom = default(ushort); + unsafe + { + fixed (char* classNamePtr = className) + { + var wndClass = new WNDCLASSEXW + { + cbSize = (uint)Marshal.SizeOf(), + style = 0, + lpfnWndProc = windowProc.WndProcDelegate, + cbClsExtra = 0, + cbWndExtra = 0, + hInstance = hInstance, + hIcon = HICON.Null, + hCursor = HCURSOR.Null, + hbrBackground = HBRUSH.Null, + lpszMenuName = default, + lpszClassName = new PCWSTR(classNamePtr), + hIconSm = HICON.Null, + }; + classAtom = PInvoke.RegisterClassEx(wndClass); + } + } + + ResultHandler.ThrowIfZero(result: classAtom, getLastError: true, memberName: nameof(PInvoke.RegisterClassEx)); + + var windowClass = new Win32WindowClass(classAtom, className, windowProc); + return windowClass; + } + + private static Win32Window CreateWindow(Win32WindowClass windowClass, string windowName) + { + // create the window + // see https://learn.microsoft.com/en-us/windows/win32/winmsg/window-features#message-only-windows + // https://devblogs.microsoft.com/oldnewthing/20171218-00/?p=97595 + // https://stackoverflow.com/a/30992796/3156906 + var hWnd = default(HWND); + + using var hModule = PInvoke.GetModuleHandle(null); + + unsafe + { + hWnd = PInvoke.CreateWindowEx( + dwExStyle: 0, + lpClassName: windowClass.ClassName, + lpWindowName: windowName, + dwStyle: 0, + X: 0, + Y: 0, + nWidth: 300, + nHeight: 400, + hWndParent: HWND.HWND_MESSAGE, // message-only window + hMenu: null, + hInstance: hModule, + lpParam: null); + } + + ResultHandler.ThrowIfZero(result: hWnd, getLastError: true, memberName: nameof(PInvoke.CreateWindowEx)); + + var window = new Win32Window(windowClass, windowName, hWnd); + return window; + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Imaging/DesktopImageRegionCopyService.cs b/src/modules/MouseUtils/MouseJump.Common/Imaging/DesktopImageRegionCopyService.cs index 07ba85d09c32..de339cf4e606 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Imaging/DesktopImageRegionCopyService.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Imaging/DesktopImageRegionCopyService.cs @@ -3,10 +3,14 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using System.Drawing; -using MouseJump.Common.Models.Drawing; -using MouseJump.Common.NativeMethods; -using static MouseJump.Common.NativeMethods.Core; +using MouseJump.Common.Interop; +using MouseJump.Models.Drawing; + +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Gdi; namespace MouseJump.Common.Imaging; @@ -28,12 +32,12 @@ public void CopyImageRegion( var stopwatch = Stopwatch.StartNew(); var (desktopHwnd, desktopHdc) = DesktopImageRegionCopyService.GetDesktopDeviceContext(); var previewHdc = DesktopImageRegionCopyService.GetGraphicsDeviceContext( - targetGraphics, Gdi32.STRETCH_BLT_MODE.STRETCH_HALFTONE); + targetGraphics, STRETCH_BLT_MODE.STRETCH_HALFTONE); stopwatch.Stop(); var source = sourceBounds.ToRectangle(); var target = targetBounds.ToRectangle(); - var result = Gdi32.StretchBlt( + var result = PInvoke.StretchBlt( previewHdc, target.X, target.Y, @@ -44,15 +48,11 @@ public void CopyImageRegion( source.Y, source.Width, source.Height, - Gdi32.ROP_CODE.SRCCOPY); - if (!result) - { - throw new InvalidOperationException( - $"{nameof(Gdi32.StretchBlt)} returned {result.Value}"); - } + ROP_CODE.SRCCOPY); + ResultHandler.ThrowIfZero(result: result, getLastError: false, memberName: nameof(PInvoke.StretchBlt)); // we need to release the graphics device context handle before anything - // else tries to use the Graphics object otherwise it'll give an error + // 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); @@ -61,14 +61,9 @@ public void CopyImageRegion( 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"); - } - + var desktopHwnd = PInvoke.GetDesktopWindow(); + var desktopHdc = PInvoke.GetWindowDC(desktopHwnd); + ResultHandler.HandleResult(result: desktopHdc, success: !desktopHdc.IsNull, getLastError: false, memberName: nameof(PInvoke.GetWindowDC)); return (desktopHwnd, desktopHdc); } @@ -76,12 +71,8 @@ private static void FreeDesktopDeviceContext(ref HWND desktopHwnd, ref HDC deskt { if (!desktopHwnd.IsNull && !desktopHdc.IsNull) { - var result = User32.ReleaseDC(desktopHwnd, desktopHdc); - if (result == 0) - { - throw new InvalidOperationException( - $"{nameof(User32.ReleaseDC)} returned {result}"); - } + var result = PInvoke.ReleaseDC(desktopHwnd, desktopHdc); + ResultHandler.ThrowIfZero(result: result, getLastError: false, memberName: nameof(PInvoke.ReleaseDC)); } desktopHwnd = HWND.Null; @@ -92,17 +83,11 @@ private static void FreeDesktopDeviceContext(ref HWND desktopHwnd, ref HDC deskt /// 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) + private static HDC GetGraphicsDeviceContext(Graphics graphics, 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}"); - } - + var result = PInvoke.SetStretchBltMode(graphicsHdc, mode); + ResultHandler.ThrowIfZero(result: result, getLastError: false, memberName: nameof(PInvoke.SetStretchBltMode)); return graphicsHdc; } @@ -116,7 +101,7 @@ private static void FreeGraphicsDeviceContext(Graphics graphics, ref HDC graphic return; } - graphics.ReleaseHdc(graphicsHdc.Value); + graphics.ReleaseHdc(graphicsHdc); graphicsHdc = HDC.Null; } } diff --git a/src/modules/MouseUtils/MouseJump.Common/Imaging/IImageRegionCopyService.cs b/src/modules/MouseUtils/MouseJump.Common/Imaging/IImageRegionCopyService.cs index 54f068132832..b397e5457024 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Imaging/IImageRegionCopyService.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Imaging/IImageRegionCopyService.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. -using MouseJump.Common.Models.Drawing; +using System.Drawing; + +using MouseJump.Models.Drawing; namespace MouseJump.Common.Imaging; @@ -10,7 +12,7 @@ public 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. + /// 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 diff --git a/src/modules/MouseUtils/MouseJump.Common/Imaging/SolidColorRegionCopyService.cs b/src/modules/MouseUtils/MouseJump.Common/Imaging/SolidColorRegionCopyService.cs new file mode 100644 index 000000000000..bf43a134ecfb --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Imaging/SolidColorRegionCopyService.cs @@ -0,0 +1,47 @@ +// 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 System.Drawing.Drawing2D; + +using MouseJump.Models.Drawing; + +namespace MouseJump.Common.Imaging; + +/// +/// Implements an IImageRegionCopyService that paints a solid block of the specified color. +/// This is used for testing the DrawingHelper rather than as part of the main application. +/// +public sealed class SolidColorRegionCopyService : IImageRegionCopyService +{ + public SolidColorRegionCopyService(Color color) + { + this.Color = color; + } + + private Color Color + { + get; + } + + /// + /// Draws a region in a solid color. + /// + public void CopyImageRegion( + Graphics targetGraphics, + RectangleInfo sourceBounds, + RectangleInfo targetBounds) + { + using var brush = new SolidBrush(this.Color); + + // prevent the background bleeding through into screen images + // (see https://github.com/mikeclayton/FancyMouse/issues/44) + targetGraphics.PixelOffsetMode = PixelOffsetMode.Half; + targetGraphics.InterpolationMode = InterpolationMode.NearestNeighbor; + + targetGraphics.FillRectangle( + brush: brush, + rect: targetBounds.ToRectangle()); + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Imaging/StaticImageRegionCopyService.cs b/src/modules/MouseUtils/MouseJump.Common/Imaging/StaticImageRegionCopyService.cs index 5f0304e4cc45..2950f6967b29 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Imaging/StaticImageRegionCopyService.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Imaging/StaticImageRegionCopyService.cs @@ -2,9 +2,10 @@ // 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 System.Drawing.Drawing2D; -using MouseJump.Common.Models.Drawing; +using MouseJump.Models.Drawing; namespace MouseJump.Common.Imaging; diff --git a/src/modules/MouseUtils/MouseJump.Common/Interop/ResultHandler_int.cs b/src/modules/MouseUtils/MouseJump.Common/Interop/ResultHandler_int.cs new file mode 100644 index 000000000000..660a43faf381 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Interop/ResultHandler_int.cs @@ -0,0 +1,121 @@ +// 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.ComponentModel; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace MouseJump.Common.Interop; + +public static partial class ResultHandler +{ + /* + helpers for handling results from Win32 API calls that return an int value, + throwing exceptions when errors occur, and reading the last error code when necessary. + */ + + public static void ThrowIfZero( + int result, + bool getLastError = false, + [CallerMemberName] string memberName = "") + { + ResultHandler.HandleResult( + result, + result != 0, + getLastError, + memberName); + } + + public static void ThrowIfNotZero( + int result, + bool getLastError = false, + [CallerMemberName] string memberName = "") + { + ResultHandler.HandleResult( + result, + result == 0, + getLastError, + memberName); + } + + public static void HandleResult( + int result, + bool success, + bool getLastError = false, + [CallerMemberName] string memberName = "") + { + if (success) + { + return; + } + + ResultHandler.HandleFailure(result, getLastError, memberName); + } + + public static void HandleResult( + int result, + bool success, + Func? getLastError, + [CallerMemberName] string memberName = "") + { + if (success) + { + return; + } + + var lastError = getLastError?.Invoke(); + ResultHandler.HandleFailure(result, lastError, memberName); + } + + public static void HandleResult( + int result, + bool success, + int? lastError, + [CallerMemberName] string memberName = "") + { + if (success) + { + return; + } + + ResultHandler.HandleFailure(result, lastError, memberName); + } + + public static void HandleFailure( + int result, + bool getLastError = false, + [CallerMemberName] string memberName = "") + { + var lastError = getLastError ? (int?)Marshal.GetLastPInvokeError() : null; + ResultHandler.HandleFailure(result, lastError, memberName); + } + + public static void HandleFailure( + int result, + Func? getLastError, + [CallerMemberName] string memberName = "") + { + var lastError = getLastError?.Invoke(); + ResultHandler.HandleFailure(result, lastError, memberName); + } + + public static void HandleFailure( + int result, + int? lastError, + [CallerMemberName] string memberName = "") + { + var lines = new List + { + $"{memberName} failed with result {result}.", + }; + + if (lastError is not null) + { + lines.Add($"last error was '{lastError}'"); + } + + var message = string.Join(Environment.NewLine, lines); + throw new Win32Exception(message); + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Interop/ResultHandler_nint.cs b/src/modules/MouseUtils/MouseJump.Common/Interop/ResultHandler_nint.cs new file mode 100644 index 000000000000..2bc1a53b0ca0 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Interop/ResultHandler_nint.cs @@ -0,0 +1,121 @@ +// 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.ComponentModel; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace MouseJump.Common.Interop; + +public static partial class ResultHandler +{ + /* + helpers for handling results from Win32 API calls that return an nint value, + throwing exceptions when errors occur, and reading the last error code when necessary. + */ + + public static void ThrowIfZero( + nint result, + bool getLastError = false, + [CallerMemberName] string memberName = "") + { + ResultHandler.HandleResult( + result, + result.ToInt64() != 0, + getLastError, + memberName); + } + + public static void ThrowIfNotZero( + nint result, + bool getLastError = false, + [CallerMemberName] string memberName = "") + { + ResultHandler.HandleResult( + result, + result.ToInt64() == 0, + getLastError, + memberName); + } + + public static void HandleResult( + nint result, + bool success, + bool getLastError = false, + [CallerMemberName] string memberName = "") + { + if (success) + { + return; + } + + ResultHandler.HandleFailure(result, getLastError, memberName); + } + + public static void HandleResult( + nint result, + bool success, + Func? getLastError, + [CallerMemberName] string memberName = "") + { + if (success) + { + return; + } + + var lastError = getLastError?.Invoke(); + ResultHandler.HandleFailure(result, lastError, memberName); + } + + public static void HandleResult( + nint result, + bool success, + int? lastError, + [CallerMemberName] string memberName = "") + { + if (success) + { + return; + } + + ResultHandler.HandleFailure(result, lastError, memberName); + } + + public static void HandleFailure( + nint result, + bool getLastError = false, + [CallerMemberName] string memberName = "") + { + var lastError = getLastError ? (int?)Marshal.GetLastPInvokeError() : null; + ResultHandler.HandleFailure(result, lastError, memberName); + } + + public static void HandleFailure( + nint result, + Func? getLastError, + [CallerMemberName] string memberName = "") + { + var lastError = getLastError?.Invoke(); + ResultHandler.HandleFailure(result, lastError, memberName); + } + + public static void HandleFailure( + nint result, + int? lastError, + [CallerMemberName] string memberName = "") + { + var lines = new List + { + $"{memberName} failed with result {result}.", + }; + + if (lastError is not null) + { + lines.Add($"last error was '{lastError}'"); + } + + var message = string.Join(Environment.NewLine, lines); + throw new Win32Exception(message); + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Interop/ResultHandler_uint.cs b/src/modules/MouseUtils/MouseJump.Common/Interop/ResultHandler_uint.cs new file mode 100644 index 000000000000..1457818a3696 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Interop/ResultHandler_uint.cs @@ -0,0 +1,54 @@ +// 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.ComponentModel; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace MouseJump.Common.Interop; + +public static partial class ResultHandler +{ + /* + helpers for handling results from Win32 API calls that return a uint value, + throwing exceptions when errors occur, and reading the last error code when necessary. + */ + + public static void HandleFailure( + uint result, + bool getLastError = false, + [CallerMemberName] string memberName = "") + { + var lastError = getLastError ? (int?)Marshal.GetLastPInvokeError() : null; + ResultHandler.HandleFailure(result, lastError, memberName); + } + + public static void HandleFailure( + uint result, + Func? getLastError, + [CallerMemberName] string memberName = "") + { + var lastError = getLastError?.Invoke(); + ResultHandler.HandleFailure(result, lastError, memberName); + } + + public static void HandleFailure( + uint result, + int? lastError, + [CallerMemberName] string memberName = "") + { + var lines = new List + { + $"{memberName} failed with result {result}.", + }; + + if (lastError is not null) + { + lines.Add($"last error was '{lastError}'"); + } + + var message = string.Join(Environment.NewLine, lines); + throw new Win32Exception(message); + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Interop/Win32SafeHandle.cs b/src/modules/MouseUtils/MouseJump.Common/Interop/Win32SafeHandle.cs new file mode 100644 index 000000000000..9fec8c20fdfe --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Interop/Win32SafeHandle.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.Runtime.InteropServices; + +namespace MouseJump.Common.Interop; + +public sealed class Win32SafeHandle : SafeHandle +{ + public Win32SafeHandle(nint handle) + : base(IntPtr.Zero, false) + { + this.SetHandle(handle); + } + + public Win32SafeHandle(nint handle, bool ownsHandle = false) + : base(IntPtr.Zero, ownsHandle) + { + this.SetHandle(handle); + } + + public Win32SafeHandle(nint handle, bool ownsHandle, Func release) + : base(IntPtr.Zero, ownsHandle) + { + this.ReleaseDelegate = release ?? throw new ArgumentNullException(nameof(release)); + this.SetHandle(handle); + } + + private Func? ReleaseDelegate + { + get; + } + + public override bool IsInvalid + => handle == nint.Zero; + + protected override bool ReleaseHandle() + { + return this.ReleaseDelegate?.Invoke(this.handle) ?? true; + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Interop/Win32Window.cs b/src/modules/MouseUtils/MouseJump.Common/Interop/Win32Window.cs new file mode 100644 index 000000000000..054cca1f9c26 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Interop/Win32Window.cs @@ -0,0 +1,32 @@ +// 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 Windows.Win32.Foundation; + +namespace MouseJump.Common.Interop; + +public sealed class Win32Window +{ + internal Win32Window(Win32WindowClass windowClass, string windowName, HWND hwnd) + { + this.WindowClass = windowClass ?? throw new ArgumentNullException(nameof(windowClass)); + this.WindowName = windowName ?? throw new ArgumentNullException(nameof(windowName)); + this.Hwnd = hwnd; + } + + public Win32WindowClass WindowClass + { + get; + } + + public string WindowName + { + get; + } + + public nint Hwnd + { + get; + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Interop/Win32WindowClass.cs b/src/modules/MouseUtils/MouseJump.Common/Interop/Win32WindowClass.cs new file mode 100644 index 000000000000..a9425116f08b --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Interop/Win32WindowClass.cs @@ -0,0 +1,38 @@ +// 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 Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace MouseJump.Common.Interop; + +/// +/// Wrapper around a Windows window class that can be passed to +/// external assemblies to manage WNDPROC delegate lifetime without +/// exposing internals. +/// +public sealed class Win32WindowClass +{ + internal Win32WindowClass(ushort classAtom, string className, Win32WindowProc windowProc) + { + this.ClassAtom = classAtom; + this.ClassName = className ?? throw new ArgumentNullException(nameof(className)); + this.WindowProc = windowProc ?? throw new ArgumentNullException(nameof(windowProc)); + } + + internal ushort ClassAtom + { + get; + } + + public string ClassName + { + get; + } + + internal Win32WindowProc WindowProc + { + get; + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Interop/Win32WindowProc.cs b/src/modules/MouseUtils/MouseJump.Common/Interop/Win32WindowProc.cs new file mode 100644 index 000000000000..4f1d90a3f413 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Interop/Win32WindowProc.cs @@ -0,0 +1,71 @@ +// 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 Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace MouseJump.Common.Interop; + +/// +/// Wrapper around a WNDPROC delegate that can be handed to +/// external assemblies to manage native WNDPROC delegate lifetime +/// without exposing internals. +/// +public sealed class Win32WindowProc +{ + /// + /// Public definition of WNDPROC using CLR types instead of CsWin32 generated types. + /// + public delegate nint WindowProcDelegate(nint hWnd, uint msg, nuint wParam, nint lParam); + + internal Win32WindowProc(WindowProcDelegate windowProc) + { + this.WindowProcInternal = windowProc ?? throw new ArgumentNullException(nameof(windowProc)); + + // tl;dr... + // + // * Native code stores only the unmanaged function pointer. + // * Managed code must keep the delegate alive for native code to call. + // + // long version... + // + // A *method group* (e.g. WndProcInternal) is *not* a delegate (WNDPROC). + // When a method group is passed to anything that receives a delegate, the + // CLR creates a new temporary delegate instance based on the method group. + // + // Windows api methods such as RegisterClassEx don't store a reference to + // the managed delegate object - they only store an unmanaged function + // pointer. Once the delegate instance becomes unreferenced on the managed + // side the GC is allowed to collect it, even though the Win32 api continues + // to store the unmanaged pointer to it. If Win32 subsequently calls the + // (now invalid) unmanaged function pointer it can cause access violations + // or other runtime crashes. + // + // To prevent this, we'll cache a strongly-typed reference to the managed + // *delegate* instance in WndProcDelegate. Keeping this delegate reference + // alive ensures the underlying unmanaged function pointer remains valid + // for the entire lifetime of this Window instance. + this.WndProcDelegate = this.WndProcInternal; + } + + internal ushort ClassAtom + { + get; + } + + private WindowProcDelegate WindowProcInternal + { + get; + } + + internal WNDPROC WndProcDelegate + { + get; + } + + private LRESULT WndProcInternal(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam) + { + return (LRESULT)this.WindowProcInternal(hWnd, msg, wParam, lParam); + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/BoxBounds.cs b/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/BoxBounds.cs deleted file mode 100644 index 0c2d81a19615..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/BoxBounds.cs +++ /dev/null @@ -1,73 +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. - -namespace MouseJump.Common.Models.Drawing; - -public sealed class BoxBounds -{ - /* - - see https://www.w3schools.com/css/css_boxmodel.asp - - +--------------[bounds]---------------+ - |▒▒▒▒▒▒▒▒▒▒▒▒▒▒[margin]▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| - |▒▒▓▓▓▓▓▓▓▓▓▓▓▓[border]▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒| - |▒▒▓▓░░░░░░░░░░[padding]░░░░░░░░░░▓▓▒▒| - |▒▒▓▓░░ ░░▓▓▒▒| - |▒▒▓▓░░ ░░▓▓▒▒| - |▒▒▓▓░░ [content] ░░▓▓▒▒| - |▒▒▓▓░░ ░░▓▓▒▒| - |▒▒▓▓░░ ░░▓▓▒▒| - |▒▒▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▒▒| - |▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒| - |▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| - +-------------------------------------+ - - */ - - public 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/MouseJump.Common/Models/Layout/PreviewLayout.cs b/src/modules/MouseUtils/MouseJump.Common/Models/Layout/PreviewLayout.cs deleted file mode 100644 index 342389d45d76..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Layout/PreviewLayout.cs +++ /dev/null @@ -1,131 +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.ObjectModel; - -using MouseJump.Common.Models.Drawing; -using MouseJump.Common.Models.Styles; - -namespace MouseJump.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/MouseJump.Common/MouseJump.Common.csproj b/src/modules/MouseUtils/MouseJump.Common/MouseJump.Common.csproj index 79d6b7b47ea5..f94b2971d096 100644 --- a/src/modules/MouseUtils/MouseJump.Common/MouseJump.Common.csproj +++ b/src/modules/MouseUtils/MouseJump.Common/MouseJump.Common.csproj @@ -1,24 +1,32 @@ - - - + + + - + + {923DF87C-CA99-4D1C-B1D2-959174E95BFA} + PowerToys.MouseJump.Common + PowerToys.MouseJump.Common + PowerToys MouseJump.Common + Library + $(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps + false + false - {923DF87C-CA99-4D1C-B1D2-959174E95BFA} - PowerToys.MouseJump.Common - PowerToys.MouseJump.Common - PowerToys MouseJump.Common - Library - $(RepoRoot)$(Platform)\$(Configuration) - false - false + PerMonitorV2 + true + enable + enable + - PerMonitorV2 - true - enable - true - enable - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods.txt b/src/modules/MouseUtils/MouseJump.Common/NativeMethods.txt new file mode 100644 index 000000000000..9274e7b27e08 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/NativeMethods.txt @@ -0,0 +1,69 @@ +HBRUSH +HCURSOR +HICON +HINSTANCE + +WM_ACTIVATE +WM_CREATE +WM_ENTERIDLE +WM_ENTERMENULOOP +WM_EXITMENULOOP +WM_KILLFOCUS +WM_GETMINMAXINFO +WM_IME_SETCONTEXT +WM_IME_NOTIFY +WM_INITMENU +WM_INITMENUPOPUP +WM_LBUTTONDOWN +WM_MENUSELECT +WM_NCACTIVATE +WM_NCCALCSIZE +WM_NCCREATE +WM_RBUTTONDOWN +WM_SETCURSOR +WM_SETFOCUS +WM_UNINITMENUPOPUP +WM_USER +WM_WINDOWPOSCHANGED +WM_WINDOWPOSCHANGING +WM_COMMAND + +CreateWindowExW +DefWindowProc +GetWindowLong +RegisterClassEx +SetForegroundWindow +WNDPROC + +MONITORINFOF_PRIMARY +ScreenToClient +EnumDisplayMonitors +GetMonitorInfo +GetSystemMetrics +MonitorFromPoint +SendInput + +GetCursorPos +SetCursorPos + +AppendMenu +CreatePopupMenu +SetMenuInfo +TrackPopupMenuEx +TRACK_POPUP_MENU_FLAGS + +SetStretchBltMode +StretchBlt + +GetDesktopWindow +GetWindowDC +ReleaseDC + +Shell_NotifyIconW + +GetAwarenessFromDpiAwarenessContext +GetThreadDpiAwarenessContext + +HWND_MESSAGE + +GetModuleHandleW \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/CRECT.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/CRECT.cs deleted file mode 100644 index 03e3d8efd1a7..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/CRECT.cs +++ /dev/null @@ -1,54 +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.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Core -{ - /// - /// The CRECT structure defines a rectangle by the coordinates of its upper-left and lower-right corners. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/windef/ns-windef-rect - /// - [SuppressMessage("Naming Rules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Name and value taken from Win32Api")] - internal readonly struct CRECT - { - public static readonly CRECT Empty = new(0, 0, 0, 0); - - public readonly LONG left; - public readonly LONG top; - 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) - { - this.left = left; - this.top = top; - this.right = right; - this.bottom = bottom; - } - - public static int Size => - Marshal.SizeOf(typeof(CRECT)); - - public override string ToString() - { - return $"left={this.left},top={this.top},right={this.right},bottom={this.bottom}"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/HDC.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/HDC.cs deleted file mode 100644 index 038ae8df2bfc..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/HDC.cs +++ /dev/null @@ -1,39 +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. - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Core -{ - /// - /// A handle to a device context (DC). - /// This type is declared in WinDef.h as follows: - /// typedef HANDLE HDC; - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types - /// - internal readonly struct HDC - { - public static readonly HDC Null = new(IntPtr.Zero); - - public readonly IntPtr Value; - - public HDC(IntPtr value) - { - this.Value = value; - } - - public bool IsNull => this.Value == HDC.Null.Value; - - public static implicit operator IntPtr(HDC value) => value.Value; - - public static explicit operator HDC(IntPtr value) => new(value); - - public override string ToString() - { - return $"{this.GetType().Name}({this.Value})"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/HMONITOR.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/HMONITOR.cs deleted file mode 100644 index 9344208770f5..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/HMONITOR.cs +++ /dev/null @@ -1,47 +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. - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Core -{ - /// - /// A handle to a display monitor. - /// This type is declared in WinDef.h as follows: - /// if(WINVER >= 0x0500) typedef HANDLE HMONITOR; - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types - /// - internal readonly struct HMONITOR - { - public static readonly HMONITOR Null = new(IntPtr.Zero); - - public readonly IntPtr Value; - - public HMONITOR(IntPtr value) - { - this.Value = value; - } - - public bool IsNull => this.Value == HMONITOR.Null.Value; - - public static implicit operator int(HMONITOR value) => value.Value.ToInt32(); - - public static explicit operator HMONITOR(int value) => new(value); - - public static implicit operator IntPtr(HMONITOR value) => value.Value; - - public static explicit operator HMONITOR(IntPtr value) => new(value); - - public static implicit operator HANDLE(HMONITOR value) => new(value.Value); - - public static explicit operator HMONITOR(HANDLE value) => new(value.Value); - - public override string ToString() - { - return $"{this.GetType().Name}({this.Value})"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LONG.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LONG.cs deleted file mode 100644 index 3ac457eb4400..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LONG.cs +++ /dev/null @@ -1,35 +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. - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Core -{ - /// - /// A 32-bit signed integer.The range is -2147483648 through 2147483647 decimal. - /// This type is declared in WinNT.h as follows: - /// typedef long LONG; - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types - /// - internal readonly struct LONG - { - public readonly int Value; - - public LONG(int value) - { - this.Value = value; - } - - public static implicit operator int(LONG value) => value.Value; - - public static implicit operator LONG(int value) => new(value); - - public override string ToString() - { - return $"{this.GetType().Name}({this.Value})"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LPCRECT.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LPCRECT.cs deleted file mode 100644 index 5cc6d9f3c79a..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LPCRECT.cs +++ /dev/null @@ -1,50 +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.Runtime.InteropServices; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Core -{ - internal readonly struct LPCRECT - { - public static readonly LPCRECT Null = new(IntPtr.Zero); - - public readonly IntPtr Value; - - public LPCRECT(IntPtr value) - { - this.Value = value; - } - - public LPCRECT(CRECT value) - { - this.Value = LPCRECT.ToPtr(value); - } - - public bool IsNull => this.Value == LPCRECT.Null.Value; - - private static IntPtr ToPtr(CRECT value) - { - var ptr = Marshal.AllocHGlobal(CRECT.Size); - Marshal.StructureToPtr(value, ptr, false); - return ptr; - } - - public void Free() - { - Marshal.FreeHGlobal(this.Value); - } - - public static implicit operator IntPtr(LPCRECT value) => value.Value; - - public static explicit operator LPCRECT(IntPtr value) => new(value); - - public override string ToString() - { - return $"{this.GetType().Name}({this.Value})"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LPPOINT.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LPPOINT.cs deleted file mode 100644 index 725cc154d1b3..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LPPOINT.cs +++ /dev/null @@ -1,55 +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.Runtime.InteropServices; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Core -{ - internal readonly struct LPPOINT - { - public static readonly LPPOINT Null = new(IntPtr.Zero); - - public readonly IntPtr Value; - - public LPPOINT(IntPtr value) - { - this.Value = value; - } - - public LPPOINT(POINT value) - { - this.Value = LPPOINT.ToPtr(value); - } - - public bool IsNull => this.Value == LPPOINT.Null.Value; - - private static IntPtr ToPtr(POINT value) - { - var ptr = Marshal.AllocHGlobal(POINT.Size); - Marshal.StructureToPtr(value, ptr, false); - return ptr; - } - - public POINT ToStructure() - { - return Marshal.PtrToStructure(this.Value); - } - - public void Free() - { - Marshal.FreeHGlobal(this.Value); - } - - public static implicit operator IntPtr(LPPOINT value) => value.Value; - - public static explicit operator LPPOINT(IntPtr value) => new(value); - - public override string ToString() - { - return $"{this.GetType().Name}({this.Value})"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LPRECT.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LPRECT.cs deleted file mode 100644 index 31ba69fd4c29..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LPRECT.cs +++ /dev/null @@ -1,50 +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.Runtime.InteropServices; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Core -{ - internal readonly struct LPRECT - { - public static readonly LPRECT Null = new(IntPtr.Zero); - - public readonly IntPtr Value; - - public LPRECT(IntPtr value) - { - this.Value = value; - } - - public LPRECT(RECT value) - { - this.Value = LPRECT.ToPtr(value); - } - - public bool IsNull => this.Value == LPRECT.Null.Value; - - private static IntPtr ToPtr(RECT value) - { - var ptr = Marshal.AllocHGlobal(RECT.Size); - Marshal.StructureToPtr(value, ptr, false); - return ptr; - } - - public void Free() - { - Marshal.FreeHGlobal(this.Value); - } - - public static implicit operator IntPtr(LPRECT value) => value.Value; - - public static explicit operator LPRECT(IntPtr value) => new(value); - - public override string ToString() - { - return $"{this.GetType().Name}({this.Value})"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/POINT.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/POINT.cs deleted file mode 100644 index a843059b998b..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/POINT.cs +++ /dev/null @@ -1,55 +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.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Core -{ - /// - /// The POINT structure defines the x- and y-coordinates of a point. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/windef/ns-windef-point - /// - [SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Names match Win32 api")] - internal readonly struct POINT - { - /// - /// Specifies the x-coordinate of the point. - /// - public readonly LONG x; - - /// - /// Specifies the y-coordinate of the point. - /// - public readonly LONG y; - - public POINT( - int x, - int y) - { - this.x = x; - this.y = y; - } - - public POINT( - LONG x, - LONG y) - { - this.x = x; - this.y = y; - } - - public static int Size => - Marshal.SizeOf(typeof(POINT)); - - public override string ToString() - { - return $"x={this.x},y={this.y}"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/RECT.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/RECT.cs deleted file mode 100644 index 04df9a98c8ec..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/RECT.cs +++ /dev/null @@ -1,54 +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.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Core -{ - /// - /// The RECT structure defines a rectangle by the coordinates of its upper-left and lower-right corners. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/windef/ns-windef-rect - /// - [SuppressMessage("Naming Rules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Name and value taken from Win32Api")] - internal readonly struct RECT - { - public static readonly RECT Empty = new(0, 0, 0, 0); - - public readonly LONG left; - public readonly LONG top; - 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) - { - this.left = left; - this.top = top; - this.right = right; - this.bottom = bottom; - } - - public static int Size => - Marshal.SizeOf(typeof(RECT)); - - public override string ToString() - { - return $"left={this.left},top={this.top},right={this.right},bottom={this.bottom}"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/UINT.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/UINT.cs deleted file mode 100644 index f178f8f866ec..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/UINT.cs +++ /dev/null @@ -1,39 +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. - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Core -{ - /// - /// An unsigned INT. The range is 0 through 4294967295 decimal. - /// This type is declared in WinDef.h as follows: - /// typedef unsigned int UINT; - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types - /// - internal readonly struct UINT - { - public readonly uint Value; - - public UINT(uint value) - { - this.Value = value; - } - - public static implicit operator uint(UINT value) => value.Value; - - 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/MouseJump.Common/NativeMethods/Core/ULONG_PTR.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/ULONG_PTR.cs deleted file mode 100644 index b8a8764c3a2b..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/ULONG_PTR.cs +++ /dev/null @@ -1,42 +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. - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Core -{ - /// - /// An unsigned LONG_PTR. - /// This type is declared in BaseTsd.h as follows: - /// C++ - /// #if defined(_WIN64) - /// typedef unsigned __int64 ULONG_PTR; - /// #else - /// typedef unsigned long ULONG_PTR; - /// #endif - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types - /// - internal readonly struct ULONG_PTR - { - public static readonly ULONG_PTR Null = new(UIntPtr.Zero); - - public readonly UIntPtr Value; - - public ULONG_PTR(UIntPtr value) - { - this.Value = value; - } - - public static implicit operator UIntPtr(ULONG_PTR value) => value.Value; - - public static explicit operator ULONG_PTR(UIntPtr value) => new(value); - - public override string ToString() - { - return $"{this.GetType().Name}({this.Value})"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/WORD.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/WORD.cs deleted file mode 100644 index 8ef05dd17a9a..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/WORD.cs +++ /dev/null @@ -1,35 +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. - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Core -{ - /// - /// A 16-bit unsigned integer.The range is 0 through 65535 decimal. - /// This type is declared in WinDef.h as follows: - /// typedef unsigned short WORD; - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types - /// - internal readonly struct WORD - { - public readonly ushort Value; - - public WORD(ushort value) - { - this.Value = value; - } - - public static implicit operator ulong(WORD value) => value.Value; - - public static implicit operator WORD(ushort value) => new(value); - - public override string ToString() - { - return $"{this.GetType().Name}({this.Value})"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.ROP_CODE.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.ROP_CODE.cs deleted file mode 100644 index 4f4a9673c400..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.ROP_CODE.cs +++ /dev/null @@ -1,37 +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. - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Gdi32 -{ - /// - /// A raster-operation code. These codes define how the color data for the source - /// rectangle is to be combined with the color data for the destination rectangle - /// to achieve the final color. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-bitblt - /// - internal enum ROP_CODE : uint - { - BLACKNESS = 0x00000042, - CAPTUREBLT = 0x40000000, - DSTINVERT = 0x00550009, - MERGECOPY = 0x00C000CA, - MERGEPAINT = 0x00BB0226, - NOMIRRORBITMAP = 0x80000000, - NOTSRCCOPY = 0x00330008, - NOTSRCERASE = 0x001100A6, - PATCOPY = 0x00F00021, - PATINVERT = 0x005A0049, - PATPAINT = 0x00FB0A09, - SRCAND = 0x008800C6, - SRCCOPY = 0x00CC0020, - SRCERASE = 0x00440328, - SRCINVERT = 0x00660046, - SRCPAINT = 0x00EE0086, - WHITENESS = 0x00FF0062, - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.STRETCH_BLT_MODE.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.STRETCH_BLT_MODE.cs deleted file mode 100644 index 0b25a3d565ff..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.STRETCH_BLT_MODE.cs +++ /dev/null @@ -1,26 +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. - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Gdi32 -{ - /// - /// The stretching mode. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-setstretchbltmode - /// - internal enum STRETCH_BLT_MODE : int - { - BLACKONWHITE = 1, - COLORONCOLOR = 3, - HALFTONE = 4, - WHITEONBLACK = 2, - STRETCH_ANDSCANS = STRETCH_BLT_MODE.BLACKONWHITE, - STRETCH_DELETESCANS = STRETCH_BLT_MODE.COLORONCOLOR, - STRETCH_HALFTONE = STRETCH_BLT_MODE.HALFTONE, - STRETCH_ORSCANS = STRETCH_BLT_MODE.WHITEONBLACK, - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.SetStretchBltMode.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.SetStretchBltMode.cs deleted file mode 100644 index 95c71137a59d..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.SetStretchBltMode.cs +++ /dev/null @@ -1,28 +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.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Gdi32 -{ - /// - /// The SetStretchBltMode function sets the bitmap stretching mode in the specified device context. - /// - /// - /// If the function succeeds, the return value is the previous stretching mode. - /// If the function fails, the return value is zero. - /// This function can return the following value. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-setstretchbltmode - /// - [LibraryImport(Libraries.Gdi32)] - internal static partial int SetStretchBltMode( - HDC hdc, - STRETCH_BLT_MODE mode); -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.StretchBlt.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.StretchBlt.cs deleted file mode 100644 index f6f924381518..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Gdi32/Graphics/Gdi/Gdi32.StretchBlt.cs +++ /dev/null @@ -1,39 +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.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class Gdi32 -{ - /// - /// The StretchBlt function copies a bitmap from a source rectangle into a destination - /// rectangle, stretching or compressing the bitmap to fit the dimensions of the - /// destination rectangle, if necessary. The system stretches or compresses the bitmap - /// according to the stretching mode currently set in the destination device context. - /// - /// - /// If the function succeeds, the return value is nonzero. - /// If the function fails, the return value is zero. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-stretchblt - /// - [LibraryImport(Libraries.Gdi32)] - internal static partial BOOL StretchBlt( - HDC hdcDest, - int xDest, - int yDest, - int wDest, - int hDest, - HDC hdcSrc, - int xSrc, - int ySrc, - int wSrc, - int hSrc, - ROP_CODE rop); -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.EnumDisplayMonitors.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.EnumDisplayMonitors.cs deleted file mode 100644 index 7ebbea2dfa16..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.EnumDisplayMonitors.cs +++ /dev/null @@ -1,34 +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.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// The EnumDisplayMonitors function enumerates display monitors (including invisible - /// pseudo-monitors associated with the mirroring drivers) that intersect a region formed - /// by the intersection of a specified clipping rectangle and the visible region of a - /// device context. EnumDisplayMonitors calls an application-defined MonitorEnumProc - /// callback function once for each monitor that is enumerated. Note that - /// GetSystemMetrics (SM_CMONITORS) counts only the display monitors. - /// - /// - /// If the function succeeds, the return value is nonzero. - /// If the function fails, the return value is zero. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors - /// - [LibraryImport(Libraries.User32)] - internal static partial BOOL EnumDisplayMonitors( - HDC hdc, - LPCRECT lprcClip, - MONITORENUMPROC lpfnEnum, - LPARAM dwData); -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.GetWindowDC.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.GetWindowDC.cs deleted file mode 100644 index 229dedcdf165..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.GetWindowDC.cs +++ /dev/null @@ -1,32 +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.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// The GetWindowDC function retrieves the device context (DC) for the entire window, - /// including title bar, menus, and scroll bars. A window device context permits painting - /// anywhere in a window, because the origin of the device context is the upper-left - /// corner of the window instead of the client area. - /// - /// GetWindowDC assigns default attributes to the window device context each time it - /// retrieves the device context. Previous attributes are lost. - /// - /// - /// If the function succeeds, the return value is a handle to a device context for the specified window. - /// If the function fails, the return value is NULL, indicating an error or an invalid hWnd parameter. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowdc - /// - [LibraryImport(Libraries.User32)] - internal static partial HDC GetWindowDC( - HWND hWnd); -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.LPMONITORINFO.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.LPMONITORINFO.cs deleted file mode 100644 index d70342fa0b08..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.LPMONITORINFO.cs +++ /dev/null @@ -1,52 +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.Runtime.InteropServices; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendinput - /// - internal readonly struct LPMONITORINFO - { - public static readonly LPMONITORINFO Null = new(IntPtr.Zero); - - public readonly IntPtr Value; - - public LPMONITORINFO(IntPtr value) - { - this.Value = value; - } - - public LPMONITORINFO(MONITORINFO value) - { - this.Value = LPMONITORINFO.ToPtr(value); - } - - public MONITORINFO ToStructure() - { - return Marshal.PtrToStructure(this.Value); - } - - private static IntPtr ToPtr(MONITORINFO value) - { - var ptr = Marshal.AllocHGlobal(MONITORINFO.Size); - Marshal.StructureToPtr(value, ptr, false); - return ptr; - } - - public void Free() - { - Marshal.FreeHGlobal(this.Value); - } - - public override string ToString() - { - return $"{this.GetType().Name}({this.Value})"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MONITORENUMPROC .cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MONITORENUMPROC .cs deleted file mode 100644 index 017f8a2d72b2..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MONITORENUMPROC .cs +++ /dev/null @@ -1,22 +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 static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// A MonitorEnumProc function is an application-defined callback function that is called by the EnumDisplayMonitors function. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-monitorenumproc - /// - internal delegate BOOL MONITORENUMPROC( - HMONITOR unnamedParam1, - HDC unnamedParam2, - LPRECT unnamedParam3, - LPARAM unnamedParam4); -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MONITORINFO.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MONITORINFO.cs deleted file mode 100644 index 06c5a07b7e8f..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MONITORINFO.cs +++ /dev/null @@ -1,40 +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.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// Used by SendInput to store information for synthesizing input events such as keystrokes, mouse movement, and mouse clicks. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfo - /// - [SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Parameter name matches Win32 api")] - [StructLayout(LayoutKind.Sequential)] - internal readonly struct MONITORINFO - { - public readonly DWORD cbSize; - public readonly RECT rcMonitor; - public readonly RECT rcWork; - public readonly MONITOR_INFO_FLAGS dwFlags; - - public MONITORINFO(DWORD cbSize, RECT rcMonitor, RECT rcWork, MONITOR_INFO_FLAGS dwFlags) - { - this.cbSize = cbSize; - this.rcMonitor = rcMonitor; - this.rcWork = rcWork; - this.dwFlags = dwFlags; - } - - public static int Size => - Marshal.SizeOf(typeof(MONITORINFO)); - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_FROM_FLAGS.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_FROM_FLAGS.cs deleted file mode 100644 index 61f15c2944a5..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_FROM_FLAGS.cs +++ /dev/null @@ -1,24 +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.Diagnostics.CodeAnalysis; - -namespace MouseJump.Common.NativeMethods; - -[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")] -internal static partial class User32 -{ - /// - /// Determines the function's return value if the point is not contained within any display monitor. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfrompoint - /// - internal enum MONITOR_FROM_FLAGS : uint - { - MONITOR_DEFAULTTONULL = 0x00000000, - MONITOR_DEFAULTTOPRIMARY = 0x00000001, - MONITOR_DEFAULTTONEAREST = 0x00000002, - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_INFO_FLAGS.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_INFO_FLAGS.cs deleted file mode 100644 index c7bbe4033b0d..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MONITOR_INFO_FLAGS.cs +++ /dev/null @@ -1,22 +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.Diagnostics.CodeAnalysis; - -namespace MouseJump.Common.NativeMethods; - -[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")] -internal static partial class User32 -{ - /// - /// A set of flags that represent attributes of the display monitor. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfo - /// - internal enum MONITOR_INFO_FLAGS : uint - { - MONITORINFOF_PRIMARY = 1, - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MonitorFromPoint .cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MonitorFromPoint .cs deleted file mode 100644 index 04eb36a3125e..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.MonitorFromPoint .cs +++ /dev/null @@ -1,27 +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.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// The MonitorFromPoint function retrieves a handle to the display monitor that contains a specified point. - /// - /// - /// If the point is contained by a display monitor, the return value is an HMONITOR handle to that display monitor. - /// If the point is not contained by a display monitor, the return value depends on the value of dwFlags. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfrompoint - /// - [LibraryImport(Libraries.User32)] - internal static partial HMONITOR MonitorFromPoint( - POINT pt, - MONITOR_FROM_FLAGS dwFlags); -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.ReleaseDC.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.ReleaseDC.cs deleted file mode 100644 index 6fa39e023da7..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.ReleaseDC.cs +++ /dev/null @@ -1,29 +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.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// The ReleaseDC function releases a device context (DC), freeing it for use by other - /// applications. The effect of the ReleaseDC function depends on the type of DC. It - /// frees only common and window DCs. It has no effect on class or private DCs. - /// - /// - /// The return value indicates whether the DC was released. If the DC was released, the return value is 1. - /// If the DC was not released, the return value is zero. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-releasedc - /// - [LibraryImport(Libraries.User32)] - internal static partial int ReleaseDC( - HWND hWnd, - HDC hDC); -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.HARDWAREINPUT.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.HARDWAREINPUT.cs deleted file mode 100644 index 84b43227f717..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.HARDWAREINPUT.cs +++ /dev/null @@ -1,39 +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.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// Contains information about a simulated message generated by an input device - /// other than a keyboard or mouse. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-hardwareinput - /// - [SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Parameter name matches Win32 api")] - [StructLayout(LayoutKind.Sequential)] - internal readonly struct HARDWAREINPUT - { - public readonly DWORD uMsg; - public readonly WORD wParamL; - public readonly WORD wParamH; - - public HARDWAREINPUT( - DWORD uMsg, - WORD wParamL, - WORD wParamH) - { - this.uMsg = uMsg; - this.wParamL = wParamL; - this.wParamH = wParamH; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT.cs deleted file mode 100644 index 543e7d8e18be..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT.cs +++ /dev/null @@ -1,50 +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.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// Used by SendInput to store information for synthesizing input events such as keystrokes, mouse movement, and mouse clicks. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-input - /// - [SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Parameter name matches Win32 api")] - [StructLayout(LayoutKind.Sequential)] - internal readonly struct INPUT - { - public readonly INPUT_TYPE type; - public readonly DUMMYUNIONNAME data; - - public INPUT(INPUT_TYPE type, DUMMYUNIONNAME data) - { - this.type = type; - this.data = data; - } - - public static int Size => - Marshal.SizeOf(typeof(INPUT)); - - [StructLayout(LayoutKind.Explicit)] - public readonly struct DUMMYUNIONNAME - { - [FieldOffset(0)] - public readonly MOUSEINPUT mi; - [FieldOffset(0)] - public readonly KEYBDINPUT ki; - [FieldOffset(0)] - public readonly HARDWAREINPUT hi; - - public DUMMYUNIONNAME(MOUSEINPUT mi) - { - this.mi = mi; - } - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT_TYPE.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT_TYPE.cs deleted file mode 100644 index 8fd5a8ad20af..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.INPUT_TYPE.cs +++ /dev/null @@ -1,24 +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.Diagnostics.CodeAnalysis; - -namespace MouseJump.Common.NativeMethods; - -[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")] -internal static partial class User32 -{ - /// - /// The type of the input event. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-input - /// - internal enum INPUT_TYPE : uint - { - INPUT_MOUSE = 0, - INPUT_KEYBOARD = 1, - INPUT_HARDWARE = 2, - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.KEYBDINPUT.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.KEYBDINPUT.cs deleted file mode 100644 index 052836d5ef00..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.KEYBDINPUT.cs +++ /dev/null @@ -1,44 +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.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// Contains information about a simulated keyboard event. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-keybdinput - /// - [SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Parameter name matches Win32 api")] - [StructLayout(LayoutKind.Sequential)] - internal readonly struct KEYBDINPUT - { - public readonly WORD wVk; - public readonly WORD wScan; - public readonly DWORD dwFlags; - public readonly DWORD time; - public readonly ULONG_PTR dwExtraInfo; - - public KEYBDINPUT( - WORD wVk, - WORD wScan, - DWORD dwFlags, - DWORD time, - ULONG_PTR dwExtraInfo) - { - this.wVk = wVk; - this.wScan = wScan; - this.dwFlags = dwFlags; - this.time = time; - this.dwExtraInfo = dwExtraInfo; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.LPINPUT.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.LPINPUT.cs deleted file mode 100644 index 5730ff9dc584..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.LPINPUT.cs +++ /dev/null @@ -1,70 +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.Runtime.InteropServices; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendinput - /// - internal readonly struct LPINPUT - { - public static readonly LPINPUT Null = new(IntPtr.Zero); - - public readonly IntPtr Value; - - public LPINPUT(IntPtr value) - { - this.Value = value; - } - - public LPINPUT(INPUT[] values) - { - this.Value = LPINPUT.ToPtr(values); - } - - public INPUT ToStructure() - { - return Marshal.PtrToStructure(this.Value); - } - - public IEnumerable ToStructure(int count) - { - var ptr = this.Value; - var size = INPUT.Size; - for (var i = 0; i < count; i++) - { - yield return Marshal.PtrToStructure(this.Value); - ptr += size; - } - } - - private static IntPtr ToPtr(INPUT[] values) - { - var mem = Marshal.AllocHGlobal(INPUT.Size * values.Length); - var ptr = mem; - var size = INPUT.Size; - foreach (var value in values) - { - Marshal.StructureToPtr(value, ptr, false); - ptr += size; - } - - return mem; - } - - public void Free() - { - Marshal.FreeHGlobal(this.Value); - } - - public override string ToString() - { - return $"{this.GetType().Name}({this.Value})"; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSEINPUT.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSEINPUT.cs deleted file mode 100644 index be243b778fef..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSEINPUT.cs +++ /dev/null @@ -1,47 +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.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// Contains information about a simulated mouse event. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput - /// - [SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Parameter name matches Win32 api")] - [StructLayout(LayoutKind.Sequential)] - internal readonly struct MOUSEINPUT - { - public readonly int dx; - public readonly int dy; - public readonly DWORD mouseData; - public readonly MOUSE_EVENT_FLAGS dwFlags; - public readonly DWORD time; - public readonly ULONG_PTR dwExtraInfo; - - public MOUSEINPUT( - int dx, - int dy, - DWORD mouseData, - MOUSE_EVENT_FLAGS dwFlags, - DWORD time, - ULONG_PTR dwExtraInfo) - { - this.dx = dx; - this.dy = dy; - this.mouseData = mouseData; - this.dwFlags = dwFlags; - this.time = time; - this.dwExtraInfo = dwExtraInfo; - } - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSE_EVENT_FLAGS.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSE_EVENT_FLAGS.cs deleted file mode 100644 index 08b078032787..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.MOUSE_EVENT_FLAGS.cs +++ /dev/null @@ -1,33 +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.Diagnostics.CodeAnalysis; - -namespace MouseJump.Common.NativeMethods; - -[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")] -internal static partial class User32 -{ - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput - /// - [Flags] - internal enum MOUSE_EVENT_FLAGS : uint - { - MOUSEEVENTF_MOVE = 0x0001, - MOUSEEVENTF_LEFTDOWN = 0x0002, - MOUSEEVENTF_LEFTUP = 0x0004, - MOUSEEVENTF_RIGHTDOWN = 0x0008, - MOUSEEVENTF_RIGHTUP = 0x0010, - MOUSEEVENTF_MIDDLEDOWN = 0x0020, - MOUSEEVENTF_MIDDLEUP = 0x0040, - MOUSEEVENTF_XDOWN = 0x0080, - MOUSEEVENTF_XUP = 0x0100, - MOUSEEVENTF_WHEEL = 0x0800, - MOUSEEVENTF_HWHEEL = 0x1000, - MOUSEEVENTF_MOVE_NOCOALESCE = 0x2000, - MOUSEEVENTF_VIRTUALDESK = 0x4000, - MOUSEEVENTF_ABSOLUTE = 0x8000, - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.SendInput.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.SendInput.cs deleted file mode 100644 index 74eb5ac9fb65..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/Input/KeyboardAndMouse/User32.SendInput.cs +++ /dev/null @@ -1,29 +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.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// Synthesizes keystrokes, mouse motions, and button clicks. - /// - /// - /// The function returns the number of events that it successfully inserted into the keyboard or mouse input stream. - /// If the function returns zero, the input was already blocked by another thread. - /// To get extended error information, call GetLastError. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendinput - /// - [LibraryImport(Libraries.User32, SetLastError = true)] - internal static partial UINT SendInput( - UINT cInputs, - LPINPUT pInputs, - int cbSize); -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetCursorPos.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetCursorPos.cs deleted file mode 100644 index 1d84e17e2a5b..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetCursorPos.cs +++ /dev/null @@ -1,26 +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.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// Retrieves the position of the mouse cursor, in screen coordinates. - /// - /// - /// Returns nonzero if successful or zero otherwise. - /// To get extended error information, call GetLastError. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getcursorpos - /// - [LibraryImport(Libraries.User32, SetLastError = true)] - internal static partial BOOL GetCursorPos( - LPPOINT lpPoint); -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetDesktopWindow.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetDesktopWindow.cs deleted file mode 100644 index 8984e9639cbc..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetDesktopWindow.cs +++ /dev/null @@ -1,25 +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.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// Retrieves a handle to the desktop window. The desktop window covers the entire - /// screen. The desktop window is the area on top of which other windows are painted. - /// - /// - /// The return value is a handle to the desktop window. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdesktopwindow - /// - [LibraryImport(Libraries.User32)] - internal static partial HWND GetDesktopWindow(); -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetSystemMetrics.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetSystemMetrics.cs deleted file mode 100644 index dafa5daadd15..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.GetSystemMetrics.cs +++ /dev/null @@ -1,26 +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.Runtime.InteropServices; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// Retrieves the specified system metric or system configuration setting. - /// - /// Note that all dimensions retrieved by GetSystemMetrics are in pixels. - /// - /// - /// If the function succeeds, the return value is the requested system metric or configuration setting. - /// If the function fails, the return value is 0. GetLastError does not provide extended error information. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics - /// - [LibraryImport(Libraries.User32)] - internal static partial int GetSystemMetrics( - SYSTEM_METRICS_INDEX smIndex); -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SYSTEM_METRICS_INDEX.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SYSTEM_METRICS_INDEX.cs deleted file mode 100644 index 8a714ad052b0..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SYSTEM_METRICS_INDEX.cs +++ /dev/null @@ -1,110 +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.Diagnostics.CodeAnalysis; - -namespace MouseJump.Common.NativeMethods; - -[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")] -internal static partial class User32 -{ - internal enum SYSTEM_METRICS_INDEX : uint - { - SM_ARRANGE = 56, - SM_CLEANBOOT = 67, - SM_CMONITORS = 80, - SM_CMOUSEBUTTONS = 43, - SM_CONVERTIBLESLATEMODE = 0x2003, - SM_CXBORDER = 5, - SM_CXCURSOR = 13, - SM_CXDLGFRAME = 7, - SM_CXDOUBLECLK = 36, - SM_CXDRAG = 68, - SM_CXEDGE = 45, - SM_CXFIXEDFRAME = SM_CXDLGFRAME, - SM_CXFOCUSBORDER = 83, - SM_CXFRAME = 32, - SM_CXFULLSCREEN = 16, - SM_CXHSCROLL = 21, - SM_CXHTHUMB = 10, - SM_CXICON = 11, - SM_CXICONSPACING = 38, - SM_CXMAXIMIZED = 61, - SM_CXMAXTRACK = 59, - SM_CXMENUCHECK = 71, - SM_CXMENUSIZE = 54, - SM_CXMIN = 28, - SM_CXMINIMIZED = 57, - SM_CXMINSPACING = 47, - SM_CXMINTRACK = 34, - SM_CXPADDEDBORDER = 92, - SM_CXSCREEN = 0, - SM_CXSIZE = 30, - SM_CXSIZEFRAME = SM_CXFRAME, - SM_CXSMICON = 49, - SM_CXSMSIZE = 52, - SM_CXVIRTUALSCREEN = 78, - SM_CXVSCROLL = 2, - SM_CYBORDER = 6, - SM_CYCAPTION = 4, - SM_CYCURSOR = 14, - SM_CYDLGFRAME = 8, - SM_CYDOUBLECLK = 37, - SM_CYDRAG = 69, - SM_CYEDGE = 46, - SM_CYFIXEDFRAME = SM_CYDLGFRAME, - SM_CYFOCUSBORDER = 84, - SM_CYFRAME = 33, - SM_CYFULLSCREEN = 17, - SM_CYHSCROLL = 3, - SM_CYICON = 12, - SM_CYICONSPACING = 39, - SM_CYKANJIWINDOW = 18, - SM_CYMAXIMIZED = 62, - SM_CYMAXTRACK = 60, - SM_CYMENU = 15, - SM_CYMENUCHECK = 72, - SM_CYMENUSIZE = 55, - SM_CYMIN = 29, - SM_CYMINIMIZED = 58, - SM_CYMINSPACING = 48, - SM_CYMINTRACK = 35, - SM_CYSCREEN = 1, - SM_CYSIZE = 31, - SM_CYSIZEFRAME = SM_CYFRAME, - SM_CYSMCAPTION = 51, - SM_CYSMICON = 50, - SM_CYSMSIZE = 53, - SM_CYVIRTUALSCREEN = 79, - SM_CYVSCROLL = 20, - SM_CYVTHUMB = 9, - SM_DBCSENABLED = 42, - SM_DEBUG = 22, - SM_DIGITIZER = 94, - SM_IMMENABLED = 82, - SM_MAXIMUMTOUCHES = 95, - SM_MEDIACENTER = 87, - SM_MENUDROPALIGNMENT = 40, - SM_MIDEASTENABLED = 74, - SM_MOUSEPRESENT = 19, - SM_MOUSEHORIZONTALWHEELPRESENT = 91, - SM_MOUSEWHEELPRESENT = 75, - SM_NETWORK = 63, - SM_PENWINDOWS = 41, - SM_REMOTECONTROL = 0x2001, - SM_REMOTESESSION = 0x1000, - SM_SAMEDISPLAYFORMA = 81, - SM_SECURE = 44, - SM_SERVERR2 = 89, - SM_SHOWSOUNDS = 70, - SM_SHUTTINGDOWN = 0x2000, - SM_SLOWMACHINE = 73, - SM_STARTER = 88, - SM_SWAPBUTTON = 23, - SM_SYSTEMDOCKED = 0x2004, - SM_TABLETPC = 86, - SM_XVIRTUALSCREEN = 76, - SM_YVIRTUALSCREEN = 77, - } -} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SetCursorPos.cs b/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SetCursorPos.cs deleted file mode 100644 index b3bb04fef92c..000000000000 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SetCursorPos.cs +++ /dev/null @@ -1,29 +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.Runtime.InteropServices; - -using static MouseJump.Common.NativeMethods.Core; - -namespace MouseJump.Common.NativeMethods; - -internal static partial class User32 -{ - /// - /// Moves the cursor to the specified screen coordinates. If the new coordinates are not within - /// the screen rectangle set by the most recent ClipCursor function call, the system automatically - /// adjusts the coordinates so that the cursor stays within the rectangle. - /// - /// - /// Returns nonzero if successful or zero otherwise. - /// To get extended error information, call GetLastError. - /// - /// - /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getcursorpos - /// - [LibraryImport(Libraries.User32, SetLastError = true)] - internal static partial BOOL SetCursorPos( - int X, - int Y); -} diff --git a/src/modules/MouseUtils/MouseJump.HotKeys.UnitTests/KeystrokeTests.cs b/src/modules/MouseUtils/MouseJump.HotKeys.UnitTests/KeystrokeTests.cs new file mode 100644 index 000000000000..e1b234d92e44 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.HotKeys.UnitTests/KeystrokeTests.cs @@ -0,0 +1,61 @@ +// 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 Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MouseJump.HotKeys.UnitTests; + +public static class KeystrokeTests +{ + [TestClass] + public sealed class ParseTests + { + public sealed class TestCase + { + public TestCase(string value, Keystroke expectedResult) + { + this.Value = value; + this.ExpectedResult = expectedResult; + } + + public string Value { get; } + + public Keystroke ExpectedResult { get; } + } + + public static IEnumerable GetTestCases() + { + // individual modifiers / keys + yield return new object[] { new TestCase("CTRL", new(Keys.None, KeyModifiers.Control)) }; + yield return new object[] { new TestCase("ALT", new(Keys.None, KeyModifiers.Alt)) }; + yield return new object[] { new TestCase("SHIFT", new(Keys.None, KeyModifiers.Shift)) }; + yield return new object[] { new TestCase("WIN", new(Keys.None, KeyModifiers.Windows)) }; + yield return new object[] { new TestCase("F", new(Keys.F, KeyModifiers.None)) }; + + // multiple modifiers + yield return new object[] { new TestCase("CTRL + ALT + SHIFT", new(Keys.None, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) }; + yield return new object[] { new TestCase("SHIFT + ALT + CTRL", new(Keys.None, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) }; + + // modifiers and keys + yield return new object[] { new TestCase("CTRL + ALT + SHIFT + F", new(Keys.F, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) }; + yield return new object[] { new TestCase("CTRL + ALT + SHIFT + WIN + F", new(Keys.F, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift | KeyModifiers.Windows)) }; + } + + [TestMethod] + [DynamicData(nameof(GetTestCases))] + public void RunTestCases(TestCase data) + { + var expected = data.ExpectedResult; + var result = Keystroke.TryParse(data.Value, out var actual); + Assert.IsTrue(result); + if (actual == null) + { + throw new InvalidOperationException(); + } + + Assert.AreEqual(expected.Key, actual.Key); + Assert.AreEqual(expected.Modifiers, actual.Modifiers); + } + } +} diff --git a/src/modules/MouseUtils/MouseJump.HotKeys.UnitTests/MouseJump.HotKeys.UnitTests.csproj b/src/modules/MouseUtils/MouseJump.HotKeys.UnitTests/MouseJump.HotKeys.UnitTests.csproj new file mode 100644 index 000000000000..6282d1d6afc0 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.HotKeys.UnitTests/MouseJump.HotKeys.UnitTests.csproj @@ -0,0 +1,26 @@ + + + + + + {C1434F2F-D34A-4E4E-9471-4041066B8EC4} + PowerToys.MouseJump.HotKeys.UnitTests + PowerToys.MouseJump.HotKeys.UnitTests + PowerToys MouseJump.HotKeys.UnitTests + Library + ..\..\..\..\$(Platform)\$(Configuration)\tests\MouseJump.HotKeys.UnitTests\ + false + false + + enable + enable + + + + + + + + + + diff --git a/src/modules/MouseUtils/MouseJump.HotKeys/KeyModifiers.cs b/src/modules/MouseUtils/MouseJump.HotKeys/KeyModifiers.cs new file mode 100644 index 000000000000..ae43dc89015e --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.HotKeys/KeyModifiers.cs @@ -0,0 +1,18 @@ +// 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 Windows.Win32.UI.Input.KeyboardAndMouse; + +namespace MouseJump.HotKeys; + +[Flags] +public enum KeyModifiers +{ + None = 0, + Alt = (int)HOT_KEY_MODIFIERS.MOD_ALT, + Control = (int)HOT_KEY_MODIFIERS.MOD_CONTROL, + Shift = (int)HOT_KEY_MODIFIERS.MOD_SHIFT, + Windows = (int)HOT_KEY_MODIFIERS.MOD_WIN, + NoRepeat = (int)HOT_KEY_MODIFIERS.MOD_NOREPEAT, +} diff --git a/src/modules/MouseUtils/MouseJump.HotKeys/Keys.cs b/src/modules/MouseUtils/MouseJump.HotKeys/Keys.cs new file mode 100644 index 000000000000..e70944526bde --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.HotKeys/Keys.cs @@ -0,0 +1,1005 @@ +// 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. + +#pragma warning disable SA1512 // SingleLineCommentsMustNotBeFollowedByBlankLine +#pragma warning disable SA1515 // SingleLineCommentMustBePrecededByBlankLine +#pragma warning disable SA1636 // FileHeaderCopyrightTextMustMatch +// copied from https://github.com/dotnet/winforms/blob/main/src/System.Windows.Forms/src/System/Windows/Forms/Keys.cs +// so we don't need a reference to System.Windows.Forms in this project + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma warning disable SA1636 // FileHeaderCopyrightTextMustMatch +#pragma warning restore SA1515 // SingleLineCommentMustBePrecededByBlankLine +#pragma warning disable SA1512 // SingleLineCommentsMustNotBeFollowedByBlankLine + +using System.Diagnostics.CodeAnalysis; + +namespace MouseJump.HotKeys; + +/// +/// Specifies key codes and modifiers. +/// +[Flags] +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Names and values taken from Win32Api")] +[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Names and values taken from Win32Api")] +public enum Keys +{ + /// + /// The bit mask to extract a key code from a key value. + /// + KeyCode = 0x0000FFFF, + + /// + /// The bit mask to extract modifiers from a key value. + /// + Modifiers = unchecked((int)0xFFFF0000), + + /// + /// No key pressed. + /// + None = 0x00, + + /// + /// The left mouse button. + /// + LButton = 0x01, + + /// + /// The right mouse button. + /// + RButton = 0x02, + + /// + /// The CANCEL key. + /// + Cancel = 0x03, + + /// + /// The middle mouse button (three-button mouse). + /// + MButton = 0x04, + + /// + /// The first x mouse button (five-button mouse). + /// + XButton1 = 0x05, + + /// + /// The second x mouse button (five-button mouse). + /// + XButton2 = 0x06, + + /// + /// The BACKSPACE key. + /// + Back = 0x08, + + /// + /// The TAB key. + /// + Tab = 0x09, + + /// + /// The CLEAR key. + /// + LineFeed = 0x0A, + + /// + /// The CLEAR key. + /// + Clear = 0x0C, + + /// + /// The RETURN key. + /// + Return = 0x0D, + + /// + /// The ENTER key. + /// + Enter = Return, + + /// + /// The SHIFT key. + /// + ShiftKey = 0x10, + + /// + /// The CTRL key. + /// + ControlKey = 0x11, + + /// + /// The ALT key. + /// + Menu = 0x12, + + /// + /// The PAUSE key. + /// + Pause = 0x13, + + /// + /// The CAPS LOCK key. + /// + Capital = 0x14, + + /// + /// The CAPS LOCK key. + /// + [SuppressMessage("Naming", "CA1069", Justification = "Name and value taken from Win32Api")] + CapsLock = 0x14, + + /// + /// The IME Kana mode key. + /// + KanaMode = 0x15, + + /// + /// The IME Hanguel mode key. + /// + [SuppressMessage("Naming", "CA1069", Justification = "Name and value taken from Win32Api")] + HanguelMode = 0x15, + + /// + /// The IME Hangul mode key. + /// + [SuppressMessage("Naming", "CA1069", Justification = "Name and value taken from Win32Api")] + HangulMode = 0x15, + + /// + /// The IME Junja mode key. + /// + JunjaMode = 0x17, + + /// + /// The IME Final mode key. + /// + FinalMode = 0x18, + + /// + /// The IME Hanja mode key. + /// + HanjaMode = 0x19, + + /// + /// The IME Kanji mode key. + /// + [SuppressMessage("Naming", "CA1069", Justification = "Name and value taken from Win32Api")] + KanjiMode = 0x19, + + /// + /// The ESC key. + /// + Escape = 0x1B, + + /// + /// The IME Convert key. + /// + IMEConvert = 0x1C, + + /// + /// The IME NonConvert key. + /// + IMENonconvert = 0x1D, + + /// + /// The IME Accept key. + /// + IMEAccept = 0x1E, + + /// + /// The IME Accept key. + /// + IMEAceept = IMEAccept, + + /// + /// The IME Mode change request. + /// + IMEModeChange = 0x1F, + + /// + /// The SPACEBAR key. + /// + Space = 0x20, + + /// + /// The PAGE UP key. + /// + Prior = 0x21, + + /// + /// The PAGE UP key. + /// + PageUp = Prior, + + /// + /// The PAGE DOWN key. + /// + Next = 0x22, + + /// + /// The PAGE DOWN key. + /// + PageDown = Next, + + /// + /// The END key. + /// + End = 0x23, + + /// + /// The HOME key. + /// + Home = 0x24, + + /// + /// The LEFT ARROW key. + /// + Left = 0x25, + + /// + /// The UP ARROW key. + /// + Up = 0x26, + + /// + /// The RIGHT ARROW key. + /// + Right = 0x27, + + /// + /// The DOWN ARROW key. + /// + Down = 0x28, + + /// + /// The SELECT key. + /// + Select = 0x29, + + /// + /// The PRINT key. + /// + Print = 0x2A, + + /// + /// The EXECUTE key. + /// + Execute = 0x2B, + + /// + /// The PRINT SCREEN key. + /// + Snapshot = 0x2C, + + /// + /// The PRINT SCREEN key. + /// + PrintScreen = Snapshot, + + /// + /// The INS key. + /// + Insert = 0x2D, + + /// + /// The DEL key. + /// + Delete = 0x2E, + + /// + /// The HELP key. + /// + Help = 0x2F, + + /// + /// The 0 key. + /// + D0 = 0x30, // 0 + + /// + /// The 1 key. + /// + D1 = 0x31, // 1 + + /// + /// The 2 key. + /// + D2 = 0x32, // 2 + + /// + /// The 3 key. + /// + D3 = 0x33, // 3 + + /// + /// The 4 key. + /// + D4 = 0x34, // 4 + + /// + /// The 5 key. + /// + D5 = 0x35, // 5 + + /// + /// The 6 key. + /// + D6 = 0x36, // 6 + + /// + /// The 7 key. + /// + D7 = 0x37, // 7 + + /// + /// The 8 key. + /// + D8 = 0x38, // 8 + + /// + /// The 9 key. + /// + D9 = 0x39, // 9 + + /// + /// The A key. + /// + A = 0x41, + + /// + /// The B key. + /// + B = 0x42, + + /// + /// The C key. + /// + C = 0x43, + + /// + /// The D key. + /// + D = 0x44, + + /// + /// The E key. + /// + E = 0x45, + + /// + /// The F key. + /// + F = 0x46, + + /// + /// The G key. + /// + G = 0x47, + + /// + /// The H key. + /// + H = 0x48, + + /// + /// The I key. + /// + I = 0x49, + + /// + /// The J key. + /// + J = 0x4A, + + /// + /// The K key. + /// + K = 0x4B, + + /// + /// The L key. + /// + L = 0x4C, + + /// + /// The M key. + /// + M = 0x4D, + + /// + /// The N key. + /// + N = 0x4E, + + /// + /// The O key. + /// + O = 0x4F, + + /// + /// The P key. + /// + P = 0x50, + + /// + /// The Q key. + /// + Q = 0x51, + + /// + /// The R key. + /// + R = 0x52, + + /// + /// The S key. + /// + S = 0x53, + + /// + /// The T key. + /// + T = 0x54, + + /// + /// The U key. + /// + U = 0x55, + + /// + /// The V key. + /// + V = 0x56, + + /// + /// The W key. + /// + W = 0x57, + + /// + /// The X key. + /// + X = 0x58, + + /// + /// The Y key. + /// + Y = 0x59, + + /// + /// The Z key. + /// + Z = 0x5A, + + /// + /// The left Windows logo key (Microsoft Natural Keyboard). + /// + LWin = 0x5B, + + /// + /// The right Windows logo key (Microsoft Natural Keyboard). + /// + RWin = 0x5C, + + /// + /// The Application key (Microsoft Natural Keyboard). + /// + Apps = 0x5D, + + /// + /// The Computer Sleep key. + /// + Sleep = 0x5F, + + /// + /// The 0 key on the numeric keypad. + /// + NumPad0 = 0x60, + + /// + /// The 1 key on the numeric keypad. + /// + NumPad1 = 0x61, + + /// + /// The 2 key on the numeric keypad. + /// + NumPad2 = 0x62, + + /// + /// The 3 key on the numeric keypad. + /// + NumPad3 = 0x63, + + /// + /// The 4 key on the numeric keypad. + /// + NumPad4 = 0x64, + + /// + /// The 5 key on the numeric keypad. + /// + NumPad5 = 0x65, + + /// + /// The 6 key on the numeric keypad. + /// + NumPad6 = 0x66, + + /// + /// The 7 key on the numeric keypad. + /// + NumPad7 = 0x67, + + /// + /// The 8 key on the numeric keypad. + /// + NumPad8 = 0x68, + + /// + /// The 9 key on the numeric keypad. + /// + NumPad9 = 0x69, + + /// + /// The Multiply key. + /// + Multiply = 0x6A, + + /// + /// The Add key. + /// + Add = 0x6B, + + /// + /// The Separator key. + /// + Separator = 0x6C, + + /// + /// The Subtract key. + /// + Subtract = 0x6D, + + /// + /// The Decimal key. + /// + [SuppressMessage("Naming", "CA1720", Justification = "Name and value taken from Win32Api")] + Decimal = 0x6E, + + /// + /// The Divide key. + /// + Divide = 0x6F, + + /// + /// The F1 key. + /// + F1 = 0x70, + + /// + /// The F2 key. + /// + F2 = 0x71, + + /// + /// The F3 key. + /// + F3 = 0x72, + + /// + /// The F4 key. + /// + F4 = 0x73, + + /// + /// The F5 key. + /// + F5 = 0x74, + + /// + /// The F6 key. + /// + F6 = 0x75, + + /// + /// The F7 key. + /// + F7 = 0x76, + + /// + /// The F8 key. + /// + F8 = 0x77, + + /// + /// The F9 key. + /// + F9 = 0x78, + + /// + /// The F10 key. + /// + F10 = 0x79, + + /// + /// The F11 key. + /// + F11 = 0x7A, + + /// + /// The F12 key. + /// + F12 = 0x7B, + + /// + /// The F13 key. + /// + F13 = 0x7C, + + /// + /// The F14 key. + /// + F14 = 0x7D, + + /// + /// The F15 key. + /// + F15 = 0x7E, + + /// + /// The F16 key. + /// + F16 = 0x7F, + + /// + /// The F17 key. + /// + F17 = 0x80, + + /// + /// The F18 key. + /// + F18 = 0x81, + + /// + /// The F19 key. + /// + F19 = 0x82, + + /// + /// The F20 key. + /// + F20 = 0x83, + + /// + /// The F21 key. + /// + F21 = 0x84, + + /// + /// The F22 key. + /// + F22 = 0x85, + + /// + /// The F23 key. + /// + F23 = 0x86, + + /// + /// The F24 key. + /// + F24 = 0x87, + + /// + /// The NUM LOCK key. + /// + NumLock = 0x90, + + /// + /// The SCROLL LOCK key. + /// + Scroll = 0x91, + + /// + /// The left SHIFT key. + /// + LShiftKey = 0xA0, + + /// + /// The right SHIFT key. + /// + RShiftKey = 0xA1, + + /// + /// The left CTRL key. + /// + LControlKey = 0xA2, + + /// + /// The right CTRL key. + /// + RControlKey = 0xA3, + + /// + /// The left ALT key. + /// + LMenu = 0xA4, + + /// + /// The right ALT key. + /// + RMenu = 0xA5, + + /// + /// The Browser Back key. + /// + BrowserBack = 0xA6, + + /// + /// The Browser Forward key. + /// + BrowserForward = 0xA7, + + /// + /// The Browser Refresh key. + /// + BrowserRefresh = 0xA8, + + /// + /// The Browser Stop key. + /// + BrowserStop = 0xA9, + + /// + /// The Browser Search key. + /// + BrowserSearch = 0xAA, + + /// + /// The Browser Favorites key. + /// + BrowserFavorites = 0xAB, + + /// + /// The Browser Home key. + /// + BrowserHome = 0xAC, + + /// + /// The Volume Mute key. + /// + VolumeMute = 0xAD, + + /// + /// The Volume Down key. + /// + VolumeDown = 0xAE, + + /// + /// The Volume Up key. + /// + VolumeUp = 0xAF, + + /// + /// The Media Next Track key. + /// + MediaNextTrack = 0xB0, + + /// + /// The Media Previous Track key. + /// + MediaPreviousTrack = 0xB1, + + /// + /// The Media Stop key. + /// + MediaStop = 0xB2, + + /// + /// The Media Play Pause key. + /// + MediaPlayPause = 0xB3, + + /// + /// The Launch Mail key. + /// + LaunchMail = 0xB4, + + /// + /// The Select Media key. + /// + SelectMedia = 0xB5, + + /// + /// The Launch Application1 key. + /// + LaunchApplication1 = 0xB6, + + /// + /// The Launch Application2 key. + /// + LaunchApplication2 = 0xB7, + + /// + /// The Oem Semicolon key. + /// + OemSemicolon = 0xBA, + + /// + /// The Oem 1 key. + /// + Oem1 = OemSemicolon, + + /// + /// The Oem plus key. + /// + Oemplus = 0xBB, + + /// + /// The Oem comma key. + /// + Oemcomma = 0xBC, + + /// + /// The Oem Minus key. + /// + OemMinus = 0xBD, + + /// + /// The Oem Period key. + /// + OemPeriod = 0xBE, + + /// + /// The Oem Question key. + /// + OemQuestion = 0xBF, + + /// + /// The Oem 2 key. + /// + Oem2 = OemQuestion, + + /// + /// The Oem tilde key. + /// + Oemtilde = 0xC0, + + /// + /// The Oem 3 key. + /// + Oem3 = Oemtilde, + + /// + /// The Oem Open Brackets key. + /// + OemOpenBrackets = 0xDB, + + /// + /// The Oem 4 key. + /// + Oem4 = OemOpenBrackets, + + /// + /// The Oem Pipe key. + /// + OemPipe = 0xDC, + + /// + /// The Oem 5 key. + /// + Oem5 = OemPipe, + + /// + /// The Oem Close Brackets key. + /// + OemCloseBrackets = 0xDD, + + /// + /// The Oem 6 key. + /// + Oem6 = OemCloseBrackets, + + /// + /// The Oem Quotes key. + /// + OemQuotes = 0xDE, + + /// + /// The Oem 7 key. + /// + Oem7 = OemQuotes, + + /// + /// The Oem8 key. + /// + Oem8 = 0xDF, + + /// + /// The Oem Backslash key. + /// + OemBackslash = 0xE2, + + /// + /// The Oem 102 key. + /// + Oem102 = OemBackslash, + + /// + /// The PROCESS KEY key. + /// + ProcessKey = 0xE5, + + /// + /// The Packet KEY key. + /// + Packet = 0xE7, + + /// + /// The ATTN key. + /// + Attn = 0xF6, + + /// + /// The CRSEL key. + /// + Crsel = 0xF7, + + /// + /// The EXSEL key. + /// + Exsel = 0xF8, + + /// + /// The ERASE EOF key. + /// + EraseEof = 0xF9, + + /// + /// The PLAY key. + /// + Play = 0xFA, + + /// + /// The ZOOM key. + /// + Zoom = 0xFB, + + /// + /// A constant reserved for future use. + /// + NoName = 0xFC, + + /// + /// The PA1 key. + /// + Pa1 = 0xFD, + + /// + /// The CLEAR key. + /// + OemClear = 0xFE, + + /// + /// The SHIFT modifier key. + /// + Shift = 0x00010000, + + /// + /// The CTRL modifier key. + /// + Control = 0x00020000, + + /// + /// The ALT modifier key. + /// + Alt = 0x00040000, +} diff --git a/src/modules/MouseUtils/MouseJump.HotKeys/Keystroke.cs b/src/modules/MouseUtils/MouseJump.HotKeys/Keystroke.cs new file mode 100644 index 000000000000..0ac6e20838c9 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.HotKeys/Keystroke.cs @@ -0,0 +1,120 @@ +// 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.Diagnostics.CodeAnalysis; + +namespace MouseJump.HotKeys; + +public sealed class Keystroke +{ + public Keystroke(Keys key, KeyModifiers modifiers) + { + this.Key = key; + this.Modifiers = modifiers; + } + + public Keys Key + { + get; + } + + public KeyModifiers Modifiers + { + get; + } + + public static Keystroke Parse(string s) + { + if (!Keystroke.TryParse(s, out var result)) + { + throw new ArgumentException("Invalid argument format.", nameof(s)); + } + + return result; + } + + public static bool TryParse(string s, [NotNullWhen(true)] out Keystroke? result) + { + // see https://github.com/microsoft/terminal/blob/14919073a12fc0ecb4a9805cc183fdd68d30c4b6/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp#L124 + // for an alternate implementation + + // e.g. "CTRL + ALT + SHIFT + F" + if (string.IsNullOrEmpty(s)) + { + result = null; + return false; + } + + var parts = s + .Replace(" ", string.Empty) + .ToUpperInvariant() + .Split('+'); + + var keystroke = (Keys: Keys.None, Modifiers: KeyModifiers.None); + + foreach (var part in parts) + { + switch (part) + { + case "CTRL": + keystroke.Modifiers |= KeyModifiers.Control; + break; + case "ALT": + keystroke.Modifiers |= KeyModifiers.Alt; + break; + case "SHIFT": + keystroke.Modifiers |= KeyModifiers.Shift; + break; + case "WIN": + keystroke.Modifiers |= KeyModifiers.Windows; + break; + default: + if (!Enum.TryParse(part, out var key)) + { + result = null; + return false; + } + + keystroke.Keys = key; + break; + } + } + + result = new Keystroke( + keystroke.Keys, keystroke.Modifiers); + return true; + } + + public override string ToString() + { + var parts = new List(); + + if (this.Modifiers.HasFlag(KeyModifiers.Control)) + { + parts.Add("CTRL"); + } + + if (this.Modifiers.HasFlag(KeyModifiers.Alt)) + { + parts.Add("ALT"); + } + + if (this.Modifiers.HasFlag(KeyModifiers.Shift)) + { + parts.Add("SHIFT"); + } + + if (this.Modifiers.HasFlag(KeyModifiers.Windows)) + { + parts.Add("WIN"); + } + + if (this.Key != Keys.None) + { + parts.Add(this.Key.ToString()); + } + + return string.Join(" + ", parts); + } +} diff --git a/src/modules/MouseUtils/MouseJump.HotKeys/MouseJump.HotKeys.csproj b/src/modules/MouseUtils/MouseJump.HotKeys/MouseJump.HotKeys.csproj new file mode 100644 index 000000000000..e534d45d8501 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.HotKeys/MouseJump.HotKeys.csproj @@ -0,0 +1,31 @@ + + + + + + {A8D81CE0-7E24-4079-AD45-6BCE6747FE8B} + PowerToys.MouseJump.HotKeys + PowerToys.MouseJump.HotKeys + PowerToys MouseJump.HotKeys + Library + $(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps + false + false + + PerMonitorV2 + true + enable + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/modules/MouseUtils/MouseJump.HotKeys/NativeMethods.txt b/src/modules/MouseUtils/MouseJump.HotKeys/NativeMethods.txt new file mode 100644 index 000000000000..7aa162ee126b --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.HotKeys/NativeMethods.txt @@ -0,0 +1 @@ +HOT_KEY_MODIFIERS diff --git a/src/modules/MouseUtils/MouseJump.Models.UnitTests/Drawing/BoxBoundsTests.cs b/src/modules/MouseUtils/MouseJump.Models.UnitTests/Drawing/BoxBoundsTests.cs new file mode 100644 index 000000000000..6f7b3a9183d9 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models.UnitTests/Drawing/BoxBoundsTests.cs @@ -0,0 +1,116 @@ +// 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 System.Text.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MouseJump.Models.Drawing; +using MouseJump.Models.Styles; + +namespace MouseJump.Models.UnitTests.Drawing; + +public static class BoxBoundsTests +{ + [TestClass] + public sealed class CreateFromContentBounds + { + 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; } + + public BoxStyle BoxStyle { get; } + + public BoxBounds ExpectedResult { get; } + } + + public static IEnumerable GetTestCases() + { + yield return new object[] + { + 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))] + public void RunTestCases(TestCase data) + { + var actual = BoxBounds.CreateFromContentBounds(data.ContentBounds, data.BoxStyle); + var expected = data.ExpectedResult; + Assert.AreEqual( + JsonSerializer.Serialize(expected), + JsonSerializer.Serialize(actual)); + } + } + + [TestClass] + public sealed class CreateFromOuterBounds + { + 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; } + + public BoxStyle BoxStyle { get; } + + public BoxBounds ExpectedResult { get; } + } + + public static IEnumerable GetTestCases() + { + yield return new object[] + { + 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))] + public void RunTestCases(TestCase data) + { + var actual = BoxBounds.CreateFromOuterBounds(data.OuterBounds, data.BoxStyle); + var expected = data.ExpectedResult; + Assert.AreEqual( + JsonSerializer.Serialize(expected), + JsonSerializer.Serialize(actual)); + } + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Models/Drawing/RectangleInfoTests.cs b/src/modules/MouseUtils/MouseJump.Models.UnitTests/Drawing/RectangleInfoTests.cs similarity index 93% rename from src/modules/MouseUtils/MouseJump.Common.UnitTests/Models/Drawing/RectangleInfoTests.cs rename to src/modules/MouseUtils/MouseJump.Models.UnitTests/Drawing/RectangleInfoTests.cs index d2d6aba81ce6..fb0fcb8a2933 100644 --- a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Models/Drawing/RectangleInfoTests.cs +++ b/src/modules/MouseUtils/MouseJump.Models.UnitTests/Drawing/RectangleInfoTests.cs @@ -3,11 +3,10 @@ // See the LICENSE file in the project root for more information. using Microsoft.VisualStudio.TestTools.UnitTesting; -using MouseJump.Common.Models.Drawing; +using MouseJump.Models.Drawing; -namespace MouseJump.Common.UnitTests.Models.Drawing; +namespace MouseJump.Models.UnitTests.Drawing; -[TestClass] public static class RectangleInfoTests { [TestClass] @@ -49,7 +48,7 @@ public static IEnumerable GetTestCases() } [TestMethod] - [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + [DynamicData(nameof(GetTestCases))] public void RunTestCases(TestCase data) { var actual = data.Rectangle.Center(data.Point); @@ -62,7 +61,7 @@ public void RunTestCases(TestCase data) } [TestClass] - public sealed class ClampTests + public sealed class MoveInsideTests { public sealed class TestCase { @@ -126,10 +125,10 @@ public static IEnumerable GetTestCases() } [TestMethod] - [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + [DynamicData(nameof(GetTestCases))] public void RunTestCases(TestCase data) { - var actual = data.Inner.Clamp(data.Outer); + var actual = data.Inner.MoveInside(data.Outer); var expected = data.ExpectedResult; Assert.AreEqual(expected.X, actual.X); Assert.AreEqual(expected.Y, actual.Y); diff --git a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Models/Drawing/SizeInfoTests.cs b/src/modules/MouseUtils/MouseJump.Models.UnitTests/Drawing/SizeInfoTests.cs similarity index 93% rename from src/modules/MouseUtils/MouseJump.Common.UnitTests/Models/Drawing/SizeInfoTests.cs rename to src/modules/MouseUtils/MouseJump.Models.UnitTests/Drawing/SizeInfoTests.cs index 9871d0526cdd..6bf31a2124d7 100644 --- a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Models/Drawing/SizeInfoTests.cs +++ b/src/modules/MouseUtils/MouseJump.Models.UnitTests/Drawing/SizeInfoTests.cs @@ -3,11 +3,10 @@ // See the LICENSE file in the project root for more information. using Microsoft.VisualStudio.TestTools.UnitTesting; -using MouseJump.Common.Models.Drawing; +using MouseJump.Models.Drawing; -namespace MouseJump.Common.UnitTests.Models.Drawing; +namespace MouseJump.Models.UnitTests.Drawing; -[TestClass] public static class SizeInfoTests { [TestClass] @@ -50,7 +49,7 @@ public static IEnumerable GetTestCases() } [TestMethod] - [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + [DynamicData(nameof(GetTestCases))] public void RunTestCases(TestCase data) { var actual = data.Source.ScaleToFit(data.Bounds, out var scalingRatio); @@ -98,7 +97,7 @@ public static IEnumerable GetTestCases() } [TestMethod] - [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + [DynamicData(nameof(GetTestCases))] public void RunTestCases(TestCase data) { var actual = data.Obj.ScaleToFitRatio(data.Bounds); diff --git a/src/modules/MouseUtils/MouseJump.Models.UnitTests/MSTestSettings.cs b/src/modules/MouseUtils/MouseJump.Models.UnitTests/MSTestSettings.cs new file mode 100644 index 000000000000..8b659fd48747 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models.UnitTests/MSTestSettings.cs @@ -0,0 +1,7 @@ +// 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 Microsoft.VisualStudio.TestTools.UnitTesting; + +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/src/modules/MouseUtils/MouseJump.Models.UnitTests/MouseJump.Models.UnitTests.csproj b/src/modules/MouseUtils/MouseJump.Models.UnitTests/MouseJump.Models.UnitTests.csproj new file mode 100644 index 000000000000..637445cf19cc --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models.UnitTests/MouseJump.Models.UnitTests.csproj @@ -0,0 +1,26 @@ + + + + + + {AB6305DB-CA33-4EF4-BA40-B0CFC10E7B9F} + PowerToys.MouseJump.Models.UnitTests + PowerToys.MouseJump.Models.UnitTests + PowerToys MouseJump.Models.UnitTests + Library + ..\..\..\..\$(Platform)\$(Configuration)\tests\MouseJump.Models.UnitTests\ + false + false + + enable + enable + + + + + + + + + + diff --git a/src/modules/MouseUtils/MouseJump.Models/Display/DesktopInfo.cs b/src/modules/MouseUtils/MouseJump.Models/Display/DesktopInfo.cs new file mode 100644 index 000000000000..89d120695718 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models/Display/DesktopInfo.cs @@ -0,0 +1,32 @@ +// 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.ObjectModel; + +using MouseJump.Models.Drawing; + +namespace MouseJump.Models.Display; + +/// +/// Represents the entire desktop or virtual screen for a single logical device. +/// +public sealed record DesktopInfo +{ + public DesktopInfo(IEnumerable screens) + { + this.Screens = (screens ?? throw new ArgumentNullException(nameof(screens))).ToList().AsReadOnly(); + } + + public ReadOnlyCollection Screens + { + get; + } + + public RectangleInfo GetCombinedDisplayArea() + { + return (this.Screens.Count == 0) + ? throw new InvalidOperationException($"{nameof(GetCombinedDisplayArea)} requires one or more screens.") + : RectangleInfo.Union(this.Screens.Select(screen => screen.DisplayArea)); + } +} diff --git a/src/modules/MouseUtils/MouseJump.Models/Display/DeviceInfo.cs b/src/modules/MouseUtils/MouseJump.Models/Display/DeviceInfo.cs new file mode 100644 index 000000000000..0b02ed760573 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models/Display/DeviceInfo.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.Collections.ObjectModel; + +using MouseJump.Models.Drawing; + +namespace MouseJump.Models.Display; + +/// +/// Represents a device whose screens are rendered in the preview image. +/// +public sealed record DeviceInfo +{ + public DeviceInfo(string hostname, bool localhost, IEnumerable screens) + { + this.Hostname = hostname ?? throw new ArgumentNullException(nameof(hostname)); + this.Localhost = localhost; + this.Screens = (screens ?? throw new ArgumentNullException(nameof(screens))).ToList().AsReadOnly(); + } + + public string Hostname + { + get; + } + + public bool Localhost + { + get; + } + + public ReadOnlyCollection Screens + { + get; + } + + public RectangleInfo GetCombinedDisplayArea() + { + return RectangleInfo.Union(this.Screens.Select(screen => screen.DisplayArea)); + } +} diff --git a/src/modules/MouseUtils/MouseJump.Models/Display/DisplayInfo.cs b/src/modules/MouseUtils/MouseJump.Models/Display/DisplayInfo.cs new file mode 100644 index 000000000000..096fcc24280d --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models/Display/DisplayInfo.cs @@ -0,0 +1,20 @@ +// 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.ObjectModel; + +namespace MouseJump.Models.Display; + +public sealed record DisplayInfo +{ + public DisplayInfo(IEnumerable devices) + { + this.Devices = (devices ?? throw new ArgumentNullException(nameof(devices))).ToList().AsReadOnly(); + } + + public ReadOnlyCollection Devices + { + get; + } +} diff --git a/src/modules/MouseUtils/MouseJump.Models/Display/HighDpiMode.cs b/src/modules/MouseUtils/MouseJump.Models/Display/HighDpiMode.cs new file mode 100644 index 000000000000..cef2da52ca2b --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models/Display/HighDpiMode.cs @@ -0,0 +1,17 @@ +// 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 MouseJump.Models.Display; + +/// +/// See https://github.com/dotnet/winforms/blob/main/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/HighDpiMode.cs#L26 +/// +public enum HighDpiMode +{ + DpiUnaware, + SystemAware, + PerMonitor, + PerMonitorV2, + DpiUnawareGdiScaled, +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/ScreenInfo.cs b/src/modules/MouseUtils/MouseJump.Models/Display/ScreenInfo.cs similarity index 61% rename from src/modules/MouseUtils/MouseJump.Common/Models/Drawing/ScreenInfo.cs rename to src/modules/MouseUtils/MouseJump.Models/Display/ScreenInfo.cs index 6a7862236f12..b7235d4c7797 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/ScreenInfo.cs +++ b/src/modules/MouseUtils/MouseJump.Models/Display/ScreenInfo.cs @@ -2,40 +2,49 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MouseJump.Common.Models.Drawing; +using System.Text.Json.Serialization; + +using MouseJump.Models.Drawing; + +namespace MouseJump.Models.Display; /// /// Immutable version of a System.Windows.Forms.Screen object so we don't need to /// take a dependency on WinForms just for screen info. /// -public sealed class ScreenInfo +public sealed record ScreenInfo { - public ScreenInfo(int handle, bool primary, RectangleInfo displayArea, RectangleInfo workingArea) + public ScreenInfo(nint handle, bool primary, RectangleInfo displayArea, RectangleInfo? workingArea) { // this.Handle is a HMONITOR that has been cast to an int because we don't want // to expose the HMONITOR type outside the current assembly. this.Handle = handle; this.Primary = primary; this.DisplayArea = displayArea ?? throw new ArgumentNullException(nameof(displayArea)); - this.WorkingArea = workingArea ?? throw new ArgumentNullException(nameof(workingArea)); + this.WorkingArea = workingArea; } - public int Handle + [JsonPropertyName("handle")] + [JsonConverter(typeof(NintJsonConverter))] + public nint Handle { get; } + [JsonPropertyName("primary")] public bool Primary { get; } + [JsonPropertyName("displayArea")] public RectangleInfo DisplayArea { get; } - public RectangleInfo WorkingArea + [JsonPropertyName("workingArea")] + public RectangleInfo? WorkingArea { get; } diff --git a/src/modules/MouseUtils/MouseJump.Models/Drawing/BoxBounds.cs b/src/modules/MouseUtils/MouseJump.Models/Drawing/BoxBounds.cs new file mode 100644 index 000000000000..407a2e84b90a --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models/Drawing/BoxBounds.cs @@ -0,0 +1,167 @@ +// 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 MouseJump.Models.Styles; + +namespace MouseJump.Models.Drawing; + +/// +/// Represents the bounds of a layout box, including outer, margin, border, padding, +/// and content bounds. This class is used to model the box layout as described +/// in the CSS box model. +/// +public sealed class BoxBounds +{ + /* + + see https://www.w3schools.com/css/css_boxmodel.asp + + +--------------[bounds]---------------+ + |▒▒▒▒▒▒▒▒▒▒▒▒▒▒[margin]▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| + |▒▒▓▓▓▓▓▓▓▓▓▓▓▓[border]▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒| + |▒▒▓▓░░░░░░░░░░[padding]░░░░░░░░░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ [content] ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░ ░░▓▓▒▒| + |▒▒▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▒▒| + |▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒| + |▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒| + +-------------------------------------+ + + */ + + public static readonly BoxBounds Empty = new( + outerBounds: RectangleInfo.Empty, + marginBounds: RectangleInfo.Empty, + borderBounds: RectangleInfo.Empty, + paddingBounds: RectangleInfo.Empty, + contentBounds: RectangleInfo.Empty, + isEmpty: true); + + public BoxBounds( + RectangleInfo outerBounds, + RectangleInfo marginBounds, + RectangleInfo borderBounds, + RectangleInfo paddingBounds, + RectangleInfo contentBounds) + : this( + outerBounds, + marginBounds, + borderBounds, + paddingBounds, + contentBounds, + isEmpty: false) + { + } + + private BoxBounds( + RectangleInfo outerBounds, + RectangleInfo marginBounds, + RectangleInfo borderBounds, + RectangleInfo paddingBounds, + RectangleInfo contentBounds, + bool isEmpty) + { + 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)); + this.IsEmpty = isEmpty; + } + + /// + /// 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; + } + + public bool IsEmpty + { + get; + } + + /// + /// 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. + public static BoxBounds CreateFromContentBounds( + 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. + public static BoxBounds CreateFromOuterBounds( + 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/MouseJump.Common/Models/Drawing/PointInfo.cs b/src/modules/MouseUtils/MouseJump.Models/Drawing/PointInfo.cs similarity index 78% rename from src/modules/MouseUtils/MouseJump.Common/Models/Drawing/PointInfo.cs rename to src/modules/MouseUtils/MouseJump.Models/Drawing/PointInfo.cs index 676fdb92c0a1..36ee36d63f3c 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/PointInfo.cs +++ b/src/modules/MouseUtils/MouseJump.Models/Drawing/PointInfo.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 MouseJump.Common.Models.Drawing; +using System.Drawing; + +namespace MouseJump.Models.Drawing; /// /// Immutable version of a System.Drawing.Point object with some extra utility methods. @@ -40,26 +42,36 @@ public PointInfo Clamp(RectangleInfo outer) 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 PointInfo Offset(SizeInfo amount) => new(this.X + amount.Width, this.Y + amount.Height); - public SizeInfo ToSize() - { - return new((int)this.X, (int)this.Y); - } + public PointInfo Offset(decimal amountX, decimal amountY) => new(this.X + amountX, this.Y + amountY); + + public PointInfo Scale(decimal scalingFactor) => new(this.X * scalingFactor, this.Y * scalingFactor); /// /// 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( + public PointInfo Stretch(RectangleInfo source, RectangleInfo target) => + new( x: ((this.X - source.X) / source.Width * target.Width) + target.X, y: ((this.Y - source.Y) / source.Height * target.Height) + target.Y); + + /// + /// Subtracts the specified point from the current point. + /// + public PointInfo Subtract(PointInfo point) => + new( + x: this.X - point.X, + y: this.Y - point.Y); + + public Point ToPoint() => new((int)this.X, (int)this.Y); + + public SizeInfo ToSize() + { + return new((int)this.X, (int)this.Y); } public PointInfo Truncate() => diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/RectangleInfo.cs b/src/modules/MouseUtils/MouseJump.Models/Drawing/RectangleInfo.cs similarity index 87% rename from src/modules/MouseUtils/MouseJump.Common/Models/Drawing/RectangleInfo.cs rename to src/modules/MouseUtils/MouseJump.Models/Drawing/RectangleInfo.cs index ab5bec86768f..88e730e7abc5 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/RectangleInfo.cs +++ b/src/modules/MouseUtils/MouseJump.Models/Drawing/RectangleInfo.cs @@ -2,12 +2,14 @@ // 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 System.Text.Json.Serialization; -using MouseJump.Common.Models.Styles; -using BorderStyle = MouseJump.Common.Models.Styles.BorderStyle; +using MouseJump.Models.Styles; -namespace MouseJump.Common.Models.Drawing; +using BorderStyle = MouseJump.Models.Styles.BorderStyle; + +namespace MouseJump.Models.Drawing; /// /// Immutable version of a System.Drawing.Rectangle object with some extra utility methods. @@ -16,6 +18,7 @@ public sealed class RectangleInfo { public static readonly RectangleInfo Empty = new(0, 0, 0, 0); + [JsonConstructor] public RectangleInfo(decimal x, decimal y, decimal width, decimal height) { this.X = x; @@ -29,7 +32,7 @@ public RectangleInfo(Rectangle rectangle) { } - public RectangleInfo(Point location, SizeInfo size) + public RectangleInfo(PointInfo location, SizeInfo size) : this(location.X, location.Y, size.Width, size.Height) { } @@ -39,26 +42,34 @@ public RectangleInfo(SizeInfo size) { } + [JsonPropertyName("x")] public decimal X { get; } + [JsonPropertyName("y")] public decimal Y { get; } + [JsonPropertyName("width")] public decimal Width { get; } + [JsonPropertyName("height")] public decimal Height { get; } + [JsonIgnore] + public bool IsEmpty => + object.ReferenceEquals(this, RectangleInfo.Empty); + [JsonIgnore] public decimal Left => this.X; @@ -83,6 +94,9 @@ public decimal Height public PointInfo Location => new(this.X, this.Y); + /// + /// Gets the midpoint of the current rectangle. + /// [JsonIgnore] public PointInfo Midpoint => new( @@ -104,27 +118,6 @@ public RectangleInfo Center(PointInfo point) => 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 @@ -186,6 +179,53 @@ public RectangleInfo Enlarge(PaddingStyle padding) => this.Width + padding.Horizontal, this.Height + padding.Vertical); + public RectangleInfo MoveTo(PointInfo point) => + new( + point.X, + point.Y, + this.Width, + 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 Intersect(RectangleInfo other) + { + var x1 = Math.Max(this.X, other.X); + var x2 = Math.Min(this.X + this.Width, other.X + other.Width); + var y1 = Math.Max(this.Y, other.Y); + var y2 = Math.Min(this.Y + this.Height, other.Y + other.Height); + + if (x2 >= x1 && y2 >= y1) + { + return new RectangleInfo(x1, y1, x2 - x1, y2 - y1); + } + + return RectangleInfo.Empty; + } + + /// + /// 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 MoveInside(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); + } + /// /// Returns a new that is offset by the specified amount. /// @@ -203,6 +243,9 @@ public RectangleInfo Offset(SizeInfo amount) => public RectangleInfo Offset(decimal dx, decimal dy) => new(this.X + dx, this.Y + dy, this.Width, this.Height); + public RectangleInfo Resize(decimal width, decimal height) => + new(this.X, this.Y, width, height); + public RectangleInfo Round() => this.Round(0); @@ -289,6 +332,16 @@ public RectangleInfo Union(RectangleInfo rect) return new RectangleInfo(x1, y1, x2 - x1, y2 - y1); } + public static RectangleInfo Union(IEnumerable rects) + { + var list = rects.ToList(); + return (list.Count == 0) + ? RectangleInfo.Empty + : list.Skip(1).Aggregate( + seed: list.First(), + (aggr, rect) => aggr.Union(rect)); + } + public Rectangle ToRectangle() => new( (int)this.X, diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/SizeInfo.cs b/src/modules/MouseUtils/MouseJump.Models/Drawing/SizeInfo.cs similarity index 86% rename from src/modules/MouseUtils/MouseJump.Common/Models/Drawing/SizeInfo.cs rename to src/modules/MouseUtils/MouseJump.Models/Drawing/SizeInfo.cs index dc605ebb39dd..4534fc6cb8b5 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/SizeInfo.cs +++ b/src/modules/MouseUtils/MouseJump.Models/Drawing/SizeInfo.cs @@ -2,16 +2,23 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using MouseJump.Common.Models.Styles; -using BorderStyle = MouseJump.Common.Models.Styles.BorderStyle; +using System.Drawing; +using System.Text.Json.Serialization; -namespace MouseJump.Common.Models.Drawing; +using MouseJump.Models.Styles; + +using BorderStyle = MouseJump.Models.Styles.BorderStyle; + +namespace MouseJump.Models.Drawing; /// /// Immutable version of a System.Drawing.Size object with some extra utility methods. /// public sealed class SizeInfo { + public static readonly SizeInfo Empty = new(0, 0); + + [JsonConstructor] public SizeInfo(decimal width, decimal height) { this.Width = width; @@ -33,6 +40,9 @@ public decimal Height get; } + public bool IsEmpty => + object.ReferenceEquals(this, SizeInfo.Empty); + public SizeInfo Clamp(SizeInfo max) { return new( @@ -40,13 +50,6 @@ public SizeInfo Clamp(SizeInfo max) height: Math.Clamp(this.Height, 0, max.Height)); } - public SizeInfo Clamp(decimal maxWidth, decimal maxHeight) - { - return new( - width: Math.Clamp(this.Width, 0, maxWidth), - height: Math.Clamp(this.Height, 0, maxHeight)); - } - public SizeInfo Enlarge(BorderStyle border) => new( this.Width + border.Horizontal, @@ -101,7 +104,7 @@ public SizeInfo Invert() => /// 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) => + public RectangleInfo MoveTo(decimal x, decimal y) => new(x, y, this.Width, this.Height); public SizeInfo Round() => @@ -120,21 +123,31 @@ public SizeInfo Round() => /// /// The size to fit this size into. /// A new instance representing the scaled size. - public SizeInfo ScaleToFit(SizeInfo bounds, out decimal scalingRatio) + public SizeInfo ScaleToFit(SizeInfo bounds) + { + return this.ScaleToFit(bounds, out var _); + } + + /// + /// 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, out decimal scalingFactor) { var widthRatio = bounds.Width / this.Width; var heightRatio = bounds.Height / this.Height; switch (widthRatio.CompareTo(heightRatio)) { case < 0: - scalingRatio = widthRatio; + scalingFactor = widthRatio; return new(bounds.Width, this.Height * widthRatio); case 0: // widthRatio and heightRatio are the same, so just pick one - scalingRatio = widthRatio; + scalingFactor = widthRatio; return bounds; case > 0: - scalingRatio = heightRatio; + scalingFactor = heightRatio; return new(this.Width * heightRatio, bounds.Height); } } diff --git a/src/modules/MouseUtils/MouseJump.Models/MouseJump.Models.csproj b/src/modules/MouseUtils/MouseJump.Models/MouseJump.Models.csproj new file mode 100644 index 000000000000..37d9b18e5756 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models/MouseJump.Models.csproj @@ -0,0 +1,20 @@ + + + + + + {478C3576-CF1C-4509-B793-AE10DF394347} + PowerToys.MouseJump.Models + PowerToys.MouseJump.Models + PowerToys MouseJump.Models + Library + $(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps + false + false + + PerMonitorV2 + true + enable + enable + + diff --git a/src/modules/MouseUtils/MouseJump.Models/NintJsonConverter.cs b/src/modules/MouseUtils/MouseJump.Models/NintJsonConverter.cs new file mode 100644 index 000000000000..8060f9b7bb56 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models/NintJsonConverter.cs @@ -0,0 +1,23 @@ +// 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.Text.Json; +using System.Text.Json.Serialization; + +namespace MouseJump.Models; + +public sealed class NintJsonConverter : JsonConverter +{ + public override nint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // nint can be 32 or 64 bit, so convert to Int64 + var value = reader.GetInt64(); + return (nint)value; + } + + public override void Write(Utf8JsonWriter writer, nint value, JsonSerializerOptions options) + { + writer.WriteNumberValue((long)value); + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Settings/PreviewType.cs b/src/modules/MouseUtils/MouseJump.Models/Settings/PreviewType.cs similarity index 85% rename from src/modules/MouseUtils/MouseJump.Common/Models/Settings/PreviewType.cs rename to src/modules/MouseUtils/MouseJump.Models/Settings/PreviewType.cs index 5a3e8076c7ea..b9df4dc37d31 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Settings/PreviewType.cs +++ b/src/modules/MouseUtils/MouseJump.Models/Settings/PreviewType.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 MouseJump.Common.Models.Settings; +namespace MouseJump.Models.Settings; public enum PreviewType { diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/BackgroundStyle.cs b/src/modules/MouseUtils/MouseJump.Models/Styles/BackgroundStyle.cs similarity index 93% rename from src/modules/MouseUtils/MouseJump.Common/Models/Styles/BackgroundStyle.cs rename to src/modules/MouseUtils/MouseJump.Models/Styles/BackgroundStyle.cs index 1d6609a67662..20085301899d 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/BackgroundStyle.cs +++ b/src/modules/MouseUtils/MouseJump.Models/Styles/BackgroundStyle.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 MouseJump.Common.Models.Styles; +using System.Drawing; + +namespace MouseJump.Models.Styles; /// /// Represents the background fill style for a drawing object. diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/BorderStyle.cs b/src/modules/MouseUtils/MouseJump.Models/Styles/BorderStyle.cs similarity index 88% rename from src/modules/MouseUtils/MouseJump.Common/Models/Styles/BorderStyle.cs rename to src/modules/MouseUtils/MouseJump.Models/Styles/BorderStyle.cs index eb0ac423c5bc..53b2ba93ac0e 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/BorderStyle.cs +++ b/src/modules/MouseUtils/MouseJump.Models/Styles/BorderStyle.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 MouseJump.Common.Models.Styles; +using System.Drawing; + +namespace MouseJump.Models.Styles; /// /// Represents the border style for a drawing object. @@ -63,6 +65,11 @@ public decimal Depth public decimal Vertical => this.Top + this.Bottom; + public BorderStyle WithColor(Color? color) + { + return new BorderStyle(color, this.Left, this.Top, this.Right, this.Bottom, this.Depth); + } + public override string ToString() { return "{" + diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/BoxStyle.cs b/src/modules/MouseUtils/MouseJump.Models/Styles/BoxStyle.cs similarity index 82% rename from src/modules/MouseUtils/MouseJump.Common/Models/Styles/BoxStyle.cs rename to src/modules/MouseUtils/MouseJump.Models/Styles/BoxStyle.cs index 55e27c3b9f67..1cb518bd44bd 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/BoxStyle.cs +++ b/src/modules/MouseUtils/MouseJump.Models/Styles/BoxStyle.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 MouseJump.Common.Models.Styles; +namespace MouseJump.Models.Styles; /// /// Represents the styles to apply to a simple box-layout based drawing object. @@ -29,18 +29,34 @@ public sealed class BoxStyle */ - public static readonly BoxStyle Empty = new(MarginStyle.Empty, BorderStyle.Empty, PaddingStyle.Empty, BackgroundStyle.Empty); + public static readonly BoxStyle Empty = new( + MarginStyle.Empty, + BorderStyle.Empty, + PaddingStyle.Empty, + BackgroundStyle.Empty, + isEmpty: true); public BoxStyle( MarginStyle marginStyle, BorderStyle borderStyle, PaddingStyle paddingStyle, BackgroundStyle backgroundStyle) + : this(marginStyle, borderStyle, paddingStyle, backgroundStyle, false) + { + } + + private BoxStyle( + MarginStyle marginStyle, + BorderStyle borderStyle, + PaddingStyle paddingStyle, + BackgroundStyle backgroundStyle, + bool isEmpty) { 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)); + this.IsEmpty = isEmpty; } /// @@ -74,4 +90,9 @@ public BackgroundStyle BackgroundStyle { get; } + + public bool IsEmpty + { + get; + } } diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/MarginStyle.cs b/src/modules/MouseUtils/MouseJump.Models/Styles/MarginStyle.cs similarity index 96% rename from src/modules/MouseUtils/MouseJump.Common/Models/Styles/MarginStyle.cs rename to src/modules/MouseUtils/MouseJump.Models/Styles/MarginStyle.cs index b0a369dfe73b..038502341b36 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/MarginStyle.cs +++ b/src/modules/MouseUtils/MouseJump.Models/Styles/MarginStyle.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 MouseJump.Common.Models.Styles; +namespace MouseJump.Models.Styles; /// /// Represents the margin style for a drawing object. diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/PaddingStyle.cs b/src/modules/MouseUtils/MouseJump.Models/Styles/PaddingStyle.cs similarity index 96% rename from src/modules/MouseUtils/MouseJump.Common/Models/Styles/PaddingStyle.cs rename to src/modules/MouseUtils/MouseJump.Models/Styles/PaddingStyle.cs index d4b0c82b40d8..86373c39131e 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/PaddingStyle.cs +++ b/src/modules/MouseUtils/MouseJump.Models/Styles/PaddingStyle.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 MouseJump.Common.Models.Styles; +namespace MouseJump.Models.Styles; /// /// Represents the margin style for a drawing object. diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/PreviewStyle.cs b/src/modules/MouseUtils/MouseJump.Models/Styles/PreviewStyle.cs similarity index 66% rename from src/modules/MouseUtils/MouseJump.Common/Models/Styles/PreviewStyle.cs rename to src/modules/MouseUtils/MouseJump.Models/Styles/PreviewStyle.cs index 2ca2b1bc2870..ff1e2ad3e438 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/PreviewStyle.cs +++ b/src/modules/MouseUtils/MouseJump.Models/Styles/PreviewStyle.cs @@ -2,20 +2,25 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using MouseJump.Common.Models.Drawing; +using System.Collections.ObjectModel; +using System.Drawing; -namespace MouseJump.Common.Models.Styles; +using MouseJump.Models.Drawing; + +namespace MouseJump.Models.Styles; public sealed class PreviewStyle { public PreviewStyle( SizeInfo canvasSize, BoxStyle canvasStyle, - BoxStyle screenStyle) + BoxStyle screenStyle, + IEnumerable extraColors) { this.CanvasSize = canvasSize ?? throw new ArgumentNullException(nameof(canvasSize)); this.CanvasStyle = canvasStyle ?? throw new ArgumentNullException(nameof(canvasStyle)); this.ScreenStyle = screenStyle ?? throw new ArgumentNullException(nameof(screenStyle)); + this.ExtraColors = (extraColors ?? throw new ArgumentNullException(nameof(extraColors))).ToList().AsReadOnly(); } public SizeInfo CanvasSize @@ -32,4 +37,9 @@ public BoxStyle ScreenStyle { get; } + + public ReadOnlyCollection ExtraColors + { + get; + } } diff --git a/src/modules/MouseUtils/MouseJump.Models/ViewModel/CanvasViewModel.cs b/src/modules/MouseUtils/MouseJump.Models/ViewModel/CanvasViewModel.cs new file mode 100644 index 000000000000..52e05c4cc561 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models/ViewModel/CanvasViewModel.cs @@ -0,0 +1,84 @@ +// 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.ObjectModel; + +using MouseJump.Models.Drawing; +using MouseJump.Models.Styles; + +namespace MouseJump.Models.ViewModel; + +public sealed class CanvasViewModel +{ + public sealed class Builder + { + public Builder() + { + this.DeviceLayouts = new(); + this.CanvasStyle = BoxStyle.Empty; + } + + /// + /// Gets or sets the layout bounds for the canvas. + /// Coordinates are relative to the origin on the containing Form. + /// + public BoxBounds? CanvasBounds + { + get; + set; + } + + public BoxStyle CanvasStyle + { + get; + set; + } + + public List? DeviceLayouts + { + get; + set; + } + + public CanvasViewModel Build() + { + return new CanvasViewModel( + canvasBounds: this.CanvasBounds ?? throw new InvalidOperationException($"{nameof(this.CanvasBounds)} must be initialized before calling {nameof(this.Build)}."), + canvasStyle: this.CanvasStyle ?? throw new InvalidOperationException($"{nameof(this.CanvasStyle)} must be initialized before calling {nameof(this.Build)}."), + deviceLayouts: (this.DeviceLayouts ?? throw new InvalidOperationException($"{nameof(this.DeviceLayouts)} must be initialized before calling {nameof(this.Build)}.")) + .Select(builder => builder.Build())); + } + } + + public CanvasViewModel( + BoxBounds canvasBounds, + BoxStyle canvasStyle, + IEnumerable deviceLayouts) + { + this.CanvasBounds = canvasBounds ?? throw new ArgumentNullException(nameof(canvasBounds)); + this.CanvasStyle = canvasStyle ?? throw new ArgumentNullException(nameof(canvasStyle)); + this.DeviceLayouts = new( + (deviceLayouts ?? throw new ArgumentNullException(nameof(deviceLayouts))) + .ToList()); + } + + /// + /// Gets the layout bounds for the canvas. + /// Coordinates are relative to the origin on the containing Form. + /// + public BoxBounds CanvasBounds + { + get; + } + + public BoxStyle CanvasStyle + { + get; + } + + public ReadOnlyCollection DeviceLayouts + { + get; + } +} diff --git a/src/modules/MouseUtils/MouseJump.Models/ViewModel/DeviceViewModel.cs b/src/modules/MouseUtils/MouseJump.Models/ViewModel/DeviceViewModel.cs new file mode 100644 index 000000000000..ee426f8a4692 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models/ViewModel/DeviceViewModel.cs @@ -0,0 +1,100 @@ +// 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.ObjectModel; + +using MouseJump.Models.Display; +using MouseJump.Models.Drawing; +using MouseJump.Models.Styles; + +namespace MouseJump.Models.ViewModel; + +public sealed class DeviceViewModel +{ + public sealed class Builder + { + public Builder() + { + this.DeviceBounds = BoxBounds.Empty; + this.DeviceStyle = BoxStyle.Empty; + this.ScreenLayouts = new(); + } + + public DeviceInfo? DeviceInfo + { + get; + set; + } + + /// + /// Gets or sets the layout bounds for the device. + /// Coordinates are relative to the origin on the containing Form. + /// + public BoxBounds DeviceBounds + { + get; + set; + } + + public BoxStyle DeviceStyle + { + get; + set; + } + + public List? ScreenLayouts + { + get; + set; + } + + public DeviceViewModel Build() + { + return new DeviceViewModel( + deviceInfo: this.DeviceInfo ?? throw new InvalidOperationException($"{nameof(this.DeviceInfo)} must be initialized before calling {nameof(this.Build)}."), + deviceBounds: this.DeviceBounds ?? throw new InvalidOperationException($"{nameof(this.DeviceBounds)} must be initialized before calling {nameof(this.Build)}."), + deviceStyle: this.DeviceStyle ?? throw new InvalidOperationException($"{nameof(this.DeviceStyle)} must be initialized before calling {nameof(this.Build)}."), + screenLayouts: (this.ScreenLayouts ?? throw new InvalidOperationException($"{nameof(this.ScreenLayouts)} must be initialized before calling {nameof(this.Build)}.")) + .Select(builder => builder.Build())); + } + } + + public DeviceViewModel( + DeviceInfo deviceInfo, + BoxBounds deviceBounds, + BoxStyle deviceStyle, + IEnumerable screenLayouts) + { + this.DeviceInfo = deviceInfo ?? throw new ArgumentNullException(nameof(deviceInfo)); + this.DeviceBounds = deviceBounds ?? throw new ArgumentNullException(nameof(deviceBounds)); + this.DeviceStyle = deviceStyle ?? throw new ArgumentNullException(nameof(deviceStyle)); + this.ScreenLayouts = new( + (screenLayouts ?? throw new ArgumentNullException(nameof(screenLayouts))) + .ToList()); + } + + public DeviceInfo DeviceInfo + { + get; + } + + /// + /// Gets the layout bounds for the device. + /// Coordinates are relative to the origin on the containing Form. + /// + public BoxBounds DeviceBounds + { + get; + } + + public BoxStyle DeviceStyle + { + get; + } + + public ReadOnlyCollection ScreenLayouts + { + get; + } +} diff --git a/src/modules/MouseUtils/MouseJump.Models/ViewModel/FormViewModel.cs b/src/modules/MouseUtils/MouseJump.Models/ViewModel/FormViewModel.cs new file mode 100644 index 000000000000..22a9b864b7f8 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models/ViewModel/FormViewModel.cs @@ -0,0 +1,62 @@ +// 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 MouseJump.Models.Drawing; + +namespace MouseJump.Models.ViewModel; + +/// +/// Defines the preview form size and location. +/// +public sealed class FormViewModel +{ + public sealed class Builder + { + /// + /// Gets or sets the coordinates of the preview form on the host computer's desktop. + /// Coordinates are relative to the actual host desktop. + /// + public RectangleInfo? FormBounds + { + get; + set; + } + + public CanvasViewModel.Builder? CanvasLayout + { + get; + set; + } + + public FormViewModel Build() + { + return new FormViewModel( + formBounds: this.FormBounds ?? throw new InvalidOperationException($"{nameof(this.FormBounds)} must be initialized before calling {nameof(this.Build)}."), + canvasLayout: (this.CanvasLayout ?? throw new InvalidOperationException($"{nameof(this.CanvasLayout)} must be initialized before calling {nameof(this.Build)}.")) + .Build()); + } + } + + public FormViewModel( + RectangleInfo formBounds, + CanvasViewModel canvasLayout) + { + this.FormBounds = formBounds ?? throw new ArgumentNullException(nameof(formBounds)); + this.CanvasLayout = canvasLayout ?? throw new ArgumentNullException(nameof(canvasLayout)); + } + + /// + /// Gets the coordinates of the preview form on the host computer's desktop. + /// Coordinates are relative to the actual host virtual screen / desktop that the preview image will be shown on. + /// + public RectangleInfo FormBounds + { + get; + } + + public CanvasViewModel CanvasLayout + { + get; + } +} diff --git a/src/modules/MouseUtils/MouseJump.Models/ViewModel/ScreenViewModel.cs b/src/modules/MouseUtils/MouseJump.Models/ViewModel/ScreenViewModel.cs new file mode 100644 index 000000000000..f697cb212624 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Models/ViewModel/ScreenViewModel.cs @@ -0,0 +1,85 @@ +// 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 MouseJump.Models.Display; +using MouseJump.Models.Drawing; +using MouseJump.Models.Styles; + +namespace MouseJump.Models.ViewModel; + +public sealed class ScreenViewModel +{ + public sealed class Builder + { + public Builder() + { + this.ScreenBounds = BoxBounds.Empty; + this.ScreenStyle = BoxStyle.Empty; + } + + public ScreenInfo? ScreenInfo + { + get; + set; + } + + /// + /// Gets or sets the layout bounds for the screen. + /// Coordinates are relative to the origin on the containing Form. + /// + public BoxBounds ScreenBounds + { + get; + set; + } + + public BoxStyle ScreenStyle + { + get; + set; + } + + public ScreenViewModel Build() + { + return new ScreenViewModel( + screenInfo: this.ScreenInfo ?? throw new InvalidOperationException($"{nameof(this.ScreenInfo)} must be initialized before calling {nameof(this.Build)}."), + screenBounds: this.ScreenBounds ?? throw new InvalidOperationException($"{nameof(this.ScreenBounds)} must be initialized before calling {nameof(this.Build)}."), + screenStyle: this.ScreenStyle ?? throw new InvalidOperationException($"{nameof(this.ScreenStyle)} must be initialized before calling {nameof(this.Build)}.")); + } + } + + public ScreenViewModel( + ScreenInfo screenInfo, + BoxBounds screenBounds, + BoxStyle screenStyle) + { + this.ScreenInfo = screenInfo ?? throw new ArgumentNullException(nameof(screenInfo)); + this.ScreenBounds = screenBounds ?? throw new ArgumentNullException(nameof(screenBounds)); + this.ScreenStyle = screenStyle ?? throw new ArgumentNullException(nameof(screenStyle)); + } + + public ScreenInfo ScreenInfo + { + get; + } + + /// + /// Gets the layout bounds for the screen. + /// Coordinates are relative to the origin on the containing Form. + /// + public BoxBounds ScreenBounds + { + get; + } + + public BoxStyle ScreenStyle + { + get; + } + + public override string ToString() + { + return $"bounds: {this.ScreenBounds}, style: {this.ScreenStyle}"; + } +} diff --git a/src/modules/MouseUtils/MouseJump.WinUI3/Helpers/MouseJumpEventLoop.cs b/src/modules/MouseUtils/MouseJump.WinUI3/Helpers/MouseJumpEventLoop.cs new file mode 100644 index 000000000000..419fb86040da --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.WinUI3/Helpers/MouseJumpEventLoop.cs @@ -0,0 +1,150 @@ +// 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 ManagedCommon; + +using Microsoft.UI.Dispatching; + +namespace MouseJump.WinUI3.Helpers; + +internal static class MouseJumpEventLoop +{ + /// + /// Based on NativeEventWaiter.WaitForEventLoop. + /// + public static void RunEventHandler(string eventName, Action callback, DispatcherQueue dispatcherQueue, CancellationToken cancel) + { + Logger.LogDebug($"starting event handler for event '{eventName}'"); + var thread = new Thread(() => + { + using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + var waitHandles = new WaitHandle[] { cancel.WaitHandle, eventHandle }; + while (true) + { + Logger.LogDebug($"[{eventName}] - entering event handler loop"); + if (WaitHandle.WaitAny(waitHandles) == 1) + { + try + { + Logger.LogDebug($"[{eventName}] - invoking callback"); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + if (!dispatcherQueue.TryEnqueue(() => + { + try + { + callback(); + tcs.SetResult(); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + })) + { + tcs.SetException(new InvalidOperationException("DispatcherQueue is not accepting work items.")); + } + + tcs.Task.GetAwaiter().GetResult(); + } + catch (Exception ex) + { + // if something went wrong the application could be in an unstable state. + // the main MouseJump module will restart the background process the next + // time the hotkey is invoked, so just rethrow the exception here. + Logger.LogDebug($"[{eventName}] - error occurred"); + Logger.LogError(ex.ToString()); + throw; + } + } + else + { + Logger.LogDebug($"[{eventName}] - exiting event handler loop"); + return; + } + } + }); + + thread.Name = "MouseJumpEventLoopThread"; + thread.IsBackground = true; + + thread.Start(); + } + + /// + /// Based on NativeEventWaiter.WaitForEventLoop, + /// but takes an async callback. + /// + public static void RunAsyncEventHandler(string eventName, Func callback, DispatcherQueue dispatcherQueue, CancellationToken cancel) + { + Logger.LogDebug($"starting event handler for event '{eventName}'"); + var thread = new Thread(() => + { + using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + var waitHandles = new WaitHandle[] { cancel.WaitHandle, eventHandle }; + while (true) + { + Logger.LogDebug($"[{eventName}] - entering event handler loop"); + if (WaitHandle.WaitAny(waitHandles) == 1) + { + try + { + Logger.LogDebug($"[{eventName}] - invoking callback"); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + if (!dispatcherQueue.TryEnqueue(() => + { + try + { + _ = callback().ContinueWith( + t => + { + if (t.IsFaulted) + { + tcs.SetException(t.Exception!.InnerExceptions); + } + else if (t.IsCanceled) + { + tcs.SetCanceled(); + } + else + { + tcs.SetResult(); + } + }, + TaskScheduler.Default); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + })) + { + tcs.SetException(new InvalidOperationException("DispatcherQueue is not accepting work items.")); + } + + tcs.Task.GetAwaiter().GetResult(); // block until callback completes + } + catch (Exception ex) + { + // if something went wrong the application could be in an unstable state. + // the main MouseJump module will restart the background process the next + // time the hotkey is invoked, so just rethrow the exception here. + Logger.LogDebug($"[{eventName}] - error occurred"); + Logger.LogError(ex.ToString()); + throw; + } + } + else + { + Logger.LogDebug($"[{eventName}] - exiting event handler loop"); + return; + } + } + }); + + thread.Name = "MouseJumpEventLoopThread"; + thread.IsBackground = true; + + thread.Start(); + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs b/src/modules/MouseUtils/MouseJump.WinUI3/Helpers/SettingsHelper.cs similarity index 88% rename from src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs rename to src/modules/MouseUtils/MouseJump.WinUI3/Helpers/SettingsHelper.cs index 6e19043547e9..d6904b56a255 100644 --- a/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs +++ b/src/modules/MouseUtils/MouseJump.WinUI3/Helpers/SettingsHelper.cs @@ -8,14 +8,16 @@ using System.Threading; using ManagedCommon; + using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Utilities; + using MouseJump.Common.Helpers; -using MouseJump.Common.Models.Drawing; -using MouseJump.Common.Models.Settings; -using MouseJump.Common.Models.Styles; +using MouseJump.Models.Drawing; +using MouseJump.Models.Settings; +using MouseJump.Models.Styles; -namespace MouseJumpUI.Helpers; +namespace MouseJump.WinUI3.Helpers; internal sealed class SettingsHelper { @@ -24,7 +26,7 @@ public SettingsHelper() this.LockObject = new(); this.CurrentSettings = this.LoadSettings(); - // delay loading settings on change by some time to avoid file in use exception + // delay loading settings on change by some time to avoid "file in use" exception var throttledActionInvoker = new ThrottledActionInvoker(); this.FileSystemWatcher = Helper.GetFileWatcher( moduleName: MouseJumpSettings.ModuleName, @@ -119,7 +121,7 @@ public static PreviewStyle GetActivePreviewStyle(MouseJumpSettings settings) canvasStyle: new( marginStyle: new(0), borderStyle: new( - color: ConfigHelper.DeserializeFromConfigColorString( + color: ColorHelper.DeserializeFromConfigColorString( properties.BorderColor), all: properties.BorderThickness, depth: properties.Border3dDepth @@ -128,9 +130,9 @@ public static PreviewStyle GetActivePreviewStyle(MouseJumpSettings settings) all: properties.BorderPadding ), backgroundStyle: new( - color1: ConfigHelper.DeserializeFromConfigColorString( + color1: ColorHelper.DeserializeFromConfigColorString( properties.BackgroundColor1), - color2: ConfigHelper.DeserializeFromConfigColorString( + color2: ColorHelper.DeserializeFromConfigColorString( properties.BackgroundColor2) ) ), @@ -139,19 +141,20 @@ public static PreviewStyle GetActivePreviewStyle(MouseJumpSettings settings) all: properties.ScreenMargin ), borderStyle: new( - color: ConfigHelper.DeserializeFromConfigColorString( + color: ColorHelper.DeserializeFromConfigColorString( properties.BezelColor), all: properties.BezelThickness, depth: properties.Bezel3dDepth ), paddingStyle: new(0), backgroundStyle: new( - color1: ConfigHelper.DeserializeFromConfigColorString( + color1: ColorHelper.DeserializeFromConfigColorString( properties.ScreenColor1), - color2: ConfigHelper.DeserializeFromConfigColorString( + color2: ColorHelper.DeserializeFromConfigColorString( properties.ScreenColor2) ) - )), + ), + extraColors: []), _ => throw new InvalidOperationException( $"Unhandled {nameof(PreviewType)} '{previewType}'"), }; diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs b/src/modules/MouseUtils/MouseJump.WinUI3/Helpers/ThrottledActionInvoker.cs similarity index 96% rename from src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs rename to src/modules/MouseUtils/MouseJump.WinUI3/Helpers/ThrottledActionInvoker.cs index 5153a7dadef7..0567ec61a16d 100644 --- a/src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs +++ b/src/modules/MouseUtils/MouseJump.WinUI3/Helpers/ThrottledActionInvoker.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Windows.Threading; -namespace MouseJumpUI.Helpers; +namespace MouseJump.WinUI3.Helpers; internal sealed class ThrottledActionInvoker { diff --git a/src/modules/MouseUtils/MouseJump.WinUI3/MouseJump.WinUI3.csproj b/src/modules/MouseUtils/MouseJump.WinUI3/MouseJump.WinUI3.csproj new file mode 100644 index 000000000000..2040903f7ab9 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.WinUI3/MouseJump.WinUI3.csproj @@ -0,0 +1,74 @@ + + + + + + + WinExe + MouseJump.WinUI3 + PowerToys.MouseJump.WinUI3 + app.manifest + true + false + true + None + false + false + true + $(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps + + enable + enable + DISABLE_XAML_GENERATED_MAIN + + + + + + + + + + + + + + + + + + + + + + PowerToys.GPOWrapper + $(OutDir) + false + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/src/modules/MouseUtils/MouseJump.WinUI3/MouseJumpXAML/App.xaml b/src/modules/MouseUtils/MouseJump.WinUI3/MouseJumpXAML/App.xaml new file mode 100644 index 000000000000..b6941ad29993 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.WinUI3/MouseJumpXAML/App.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/src/modules/MouseUtils/MouseJump.WinUI3/MouseJumpXAML/App.xaml.cs b/src/modules/MouseUtils/MouseJump.WinUI3/MouseJumpXAML/App.xaml.cs new file mode 100644 index 000000000000..a4959b218155 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.WinUI3/MouseJumpXAML/App.xaml.cs @@ -0,0 +1,112 @@ +// 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 ManagedCommon; +using Microsoft.PowerToys.Telemetry; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; + +using MouseJump.Common.Helpers; +using MouseJump.WinUI3.Helpers; +using MouseJump.WinUI3.UI; + +using PowerToys.Interop; + +using Application = Microsoft.UI.Xaml.Application; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. +namespace MouseJump.WinUI3; + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : Application, IDisposable +{ + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private readonly DispatcherQueue _dispatcherQueue; + private bool disposedValue; + + /// + /// Initializes a new instance of the class. + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + } + + /// + /// Invoked when the application is launched. + /// + /// Details about the launch request and process. + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + try + { + Logger.LogInfo("entering App.OnLaunched"); + + var etwTrace = new ETWTrace(); + + var settingsHelper = new SettingsHelper(); + var previewWindow = new PreviewWindow(settingsHelper); + + // start the handler that listens for the "show preview" event + Logger.LogInfo("starting 'show preview' event handler"); + MouseJumpEventLoop.RunAsyncEventHandler( + Constants.MouseJumpShowPreviewEvent(), + previewWindow.ShowPreviewAsync, + _dispatcherQueue, + _cancellationTokenSource.Token); + + // start the handler that listens for the "terminate app" event + Logger.LogInfo("starting 'terminate' event loop"); + MouseJumpEventLoop.RunEventHandler( + Constants.TerminateMouseJumpSharedEvent(), + this.TerminateApp, + _dispatcherQueue, + _cancellationTokenSource.Token); + + Logger.LogInfo("leaving App.OnLaunched"); + + etwTrace?.Dispose(); + } + catch (Exception ex) + { + Logger.LogError(ex.ToString()); + throw; + } + } + + internal void TerminateApp() + { + Logger.LogInfo("exiting Mouse Jump."); + _cancellationTokenSource.Cancel(); + _dispatcherQueue.TryEnqueue(this.Exit); + } + + protected virtual void Dispose(bool disposing) + { + if (disposedValue) + { + return; + } + + if (disposing) + { + this._cancellationTokenSource.Cancel(); + } + + disposedValue = true; + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/modules/MouseUtils/MouseJump.WinUI3/MouseJumpXAML/PreviewWindow.xaml b/src/modules/MouseUtils/MouseJump.WinUI3/MouseJumpXAML/PreviewWindow.xaml new file mode 100644 index 000000000000..88d409935870 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.WinUI3/MouseJumpXAML/PreviewWindow.xaml @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/modules/MouseUtils/MouseJump.WinUI3/MouseJumpXAML/PreviewWindow.xaml.cs b/src/modules/MouseUtils/MouseJump.WinUI3/MouseJumpXAML/PreviewWindow.xaml.cs new file mode 100644 index 000000000000..52e98257410d --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.WinUI3/MouseJumpXAML/PreviewWindow.xaml.cs @@ -0,0 +1,537 @@ +// 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.Diagnostics; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using ManagedCommon; +using Microsoft.PowerToys.Telemetry; +using Microsoft.UI.Input; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media.Imaging; + +using MouseJump.Common.Helpers; +using MouseJump.Common.Imaging; +using MouseJump.Common.Interop; +using MouseJump.Models.Display; +using MouseJump.Models.Drawing; +using MouseJump.Models.ViewModel; +using MouseJump.WinUI3.Helpers; + +using Windows.Graphics; +using Windows.System; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. +namespace MouseJump.WinUI3.UI; + +/// +/// An empty window that can be used on its own or navigated to within a Frame. +/// +internal sealed partial class PreviewWindow : Window +{ + internal PreviewWindow(SettingsHelper settingsHelper) + { + this.SettingsHelper = settingsHelper ?? throw new ArgumentNullException(nameof(settingsHelper)); + this.InitializeComponent(); + this.InitializeWindow(); + } + + private FormViewModel? FormLayout + { + get; + set; + } + + private SettingsHelper SettingsHelper + { + get; + } + + /// + /// Initializes some settings on the application window. + /// + private void InitializeWindow() + { + var appWindow = this.AppWindow; + var presenter = appWindow.Presenter as OverlappedPresenter; + if (presenter != null) + { + var hWnd = (HWND)WinRT.Interop.WindowNative.GetWindowHandle(this); + + // get the current window style + PInvoke.SetLastError(0); + var result = PInvoke.GetWindowLong(hWnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE); + if (result == 0) + { + var lastError = Marshal.GetLastPInvokeError(); + ResultHandler.HandleResult(result: result, success: lastError == 0, lastError: lastError, memberName: nameof(PInvoke.GetWindowLong)); + } + + // set the window to be borderless, with no title bar, and hide all of the max / min / close buttons + var style = (WINDOW_STYLE)result; + style &= ~WINDOW_STYLE.WS_OVERLAPPEDWINDOW; + style |= WINDOW_STYLE.WS_POPUP; + PInvoke.SetLastError(0); + result = PInvoke.SetWindowLong(hWnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, (int)style); + if (result == 0) + { + var lastError = Marshal.GetLastPInvokeError(); + ResultHandler.HandleResult(result: result, success: lastError == 0, lastError: lastError, memberName: nameof(PInvoke.SetWindowLong)); + } + + // get the current extended window style + PInvoke.SetLastError(0); + result = PInvoke.GetWindowLong(hWnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + if (result == 0) + { + var lastError = Marshal.GetLastPInvokeError(); + ResultHandler.HandleResult(result: result, success: lastError == 0, lastError: lastError, memberName: nameof(PInvoke.GetWindowLong)); + } + + // set the window to be borderless, with no title bar, and hide all of the max / min / close buttons + var exStyle = (WINDOW_EX_STYLE)result; + exStyle |= WINDOW_EX_STYLE.WS_EX_TOOLWINDOW; // hide the taskbar icon + exStyle |= WINDOW_EX_STYLE.WS_EX_TOPMOST; // make topmost + PInvoke.SetLastError(0); + result = PInvoke.SetWindowLong(hWnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)exStyle); + if (result == 0) + { + var lastError = Marshal.GetLastPInvokeError(); + ResultHandler.HandleResult(result: result, success: lastError == 0, lastError: lastError, memberName: nameof(PInvoke.SetWindowLong)); + } + } + + this.Activated += this.PreviewWindow_Activated; + this.StackPanel.PreviewKeyDown += this.PreviewWindow_PreviewKeyDown; + this.PreviewImage.PreviewKeyDown += this.PreviewWindow_PreviewKeyDown; + this.PreviewImage.PointerPressed += this.PreviewImage_PointerPressed; + } + + private void PreviewWindow_Activated(object sender, WindowActivatedEventArgs e) + { + switch (e.WindowActivationState) + { + case WindowActivationState.CodeActivated: + this.PreviewImage.Focus(FocusState.Programmatic); + break; + case WindowActivationState.Deactivated: + this.HideWindow(); + break; + case WindowActivationState.PointerActivated: + this.PreviewImage.Focus(FocusState.Pointer); + break; + default: + throw new InvalidOperationException(); + } + } + + private void PreviewWindow_PreviewKeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.Escape) + { + this.HideWindow(); + return; + } + + var screens = ScreenHelper.GetAllScreens().ToList(); + if (screens.Count == 0) + { + return; + } + + var currentLocation = MouseHelper.GetCursorPosition(); + var currentScreen = ScreenHelper.GetScreenFromPoint(screens, currentLocation); + var currentScreenIndex = screens.IndexOf(currentScreen); + var targetScreen = default(ScreenInfo?); + + switch (e.Key) + { + case >= VirtualKey.Number1 and <= VirtualKey.Number9: + { + // number keys 1-9 - move to the numbered screen + var screenNumber = e.Key - VirtualKey.Number0; + /* note - screen *numbers* are 1-based, screen *indexes* are 0-based */ + targetScreen = (screenNumber <= screens.Count) + ? targetScreen = screens[screenNumber - 1] + : null; + break; + } + + case >= VirtualKey.NumberPad1 and <= VirtualKey.NumberPad9: + { + // numpad keys 1-9 - move to the numbered screen + var screenNumber = e.Key - VirtualKey.NumberPad0; + /* note - screen *numbers* are 1-based, screen *indexes* are 0-based */ + targetScreen = (screenNumber <= screens.Count) + ? targetScreen = screens[screenNumber - 1] + : null; + break; + } + + case VirtualKey.P: + // "P" - move to the primary screen + targetScreen = screens.Single(screen => screen.Primary); + break; + case VirtualKey.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 VirtualKey.Right: + // move to the next screen, looping round to the start if needed + var nextIndex = (currentScreenIndex + 1) % screens.Count; + targetScreen = screens[nextIndex]; + break; + case VirtualKey.Home: + // move to the first screen + targetScreen = screens.First(); + break; + case VirtualKey.End: + // move to the last screen + targetScreen = screens.Last(); + break; + } + + if (targetScreen is not null) + { + MouseHelper.SetCursorPosition(targetScreen.DisplayArea.Midpoint); + this.HideWindow(); + } + } + + private void PreviewImage_PointerPressed(object sender, PointerRoutedEventArgs e) + { + Logger.LogInfo(string.Join( + '\n', + "-----------", + nameof(PreviewWindow.PreviewImage_PointerPressed), + "-----------")); + + if (!e.Pointer.PointerDeviceType.Equals(PointerDeviceType.Mouse)) + { + // not a mouse click + return; + } + + var pointerPoint = e.GetCurrentPoint((UIElement)sender); + Logger.LogInfo(string.Join( + '\n', + "Reporting mouse event args", + $"\tleft button = {pointerPoint.Properties.IsLeftButtonPressed}", + $"\tright button = {pointerPoint.Properties.IsRightButtonPressed}", + $"\tlocation = {pointerPoint.Position}")); + + if (pointerPoint.Properties.IsLeftButtonPressed) + { + if (this.FormLayout is null) + { + // there's no layout data so we can't work out what screen was clicked + throw new InvalidOperationException(); + } + + // get the *scaled* pointer location + var pointerLocation = new PointInfo((decimal)pointerPoint.Position.X, (decimal)pointerPoint.Position.Y); + + // we need to apply the high-dpi scaling ratio for the current monitor to the pointer location + var highDpiScalingRatio = this.GetHighDpiScalingRatio(); + pointerLocation = pointerLocation.Scale(1 / (decimal)highDpiScalingRatio); + + // work out which screenshot was clicked + var clickedScreen = this.FormLayout.CanvasLayout.DeviceLayouts + .SelectMany(deviceLayout => deviceLayout.ScreenLayouts) + .FirstOrDefault( + screenLayout => screenLayout.ScreenBounds.OuterBounds.Contains(pointerLocation)); + if (clickedScreen is null) + { + return; + } + + // find the device the clicked screenshot belongs to + var clickedDevice = this.FormLayout.CanvasLayout.DeviceLayouts + .FirstOrDefault( + deviceLayout => deviceLayout.ScreenLayouts.Contains(clickedScreen)); + if (clickedDevice 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 clickedDisplayArea = clickedScreen.ScreenInfo.DisplayArea; + var clickedLocation = pointerLocation + .Stretch( + source: clickedScreen.ScreenBounds.ContentBounds, + target: clickedDisplayArea) + .Clamp( + new( + x: clickedDisplayArea.X + 1, + y: clickedDisplayArea.Y + 1, + width: clickedDisplayArea.Width - 1, + height: clickedDisplayArea.Height - 1 + )) + .Truncate(); + + // move mouse pointer + Logger.LogInfo($"clicked location = {clickedLocation}"); + MouseHelper.SetCursorPosition(clickedLocation); + } + + this.HideWindow(); + } + + public async Task ShowPreviewAsync() + { + Logger.LogInfo(string.Join( + '\n', + "-----------", + nameof(PreviewWindow.ShowPreviewAsync), + "-----------")); + + // hide the form while we redraw it... + await this.HideWindowAsync() + .ConfigureAwait(false); + + var stopwatch = Stopwatch.StartNew(); + + // capture this first so we get an accurate mouse location + // (in case the user moves it a few pixels while the form is rendered) + var activatedLocation = MouseHelper.GetCursorPosition(); + + var appSettings = this.SettingsHelper.CurrentSettings ?? throw new InvalidOperationException(); + var displayInfo = DeviceHelper.GetDisplayInfo(); + + var activatedScreen = DeviceHelper.GetActivatedScreen(displayInfo.Devices[0], activatedLocation); + + var previewStyle = SettingsHelper.GetActivePreviewStyle(appSettings); + var formLayout = LayoutHelper.GetFormLayout( + previewStyle, + displayInfo, + activatedScreen: activatedScreen, + activatedLocation: activatedLocation); + + // remember the layout so we can map the mouse clicks back to + // the appropriate device and screen location + this.FormLayout = formLayout; + + await this.PositionWindowAsync(formLayout.FormBounds) + .ConfigureAwait(false); + + var imageCopyServices = displayInfo.Devices + .Select( + deviceInfo => (IImageRegionCopyService)new DesktopImageRegionCopyService()) + .ToList(); + + await DrawingHelper.RenderPreviewAsync( + this.FormLayout.CanvasLayout, + activatedScreen, + imageCopyServices, + this.OnPreviewImageCreatedAsync, + this.OnPreviewImageUpdatedAsync) + .ConfigureAwait(false); + + stopwatch.Stop(); + + PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpShowEvent()); + + await this.ShowWindowAsync() + .ConfigureAwait(false); + } + + private void ClearPreview() + { + if (this.PreviewImage.Source is null) + { + return; + } + + this.PreviewImage.Source = null; + + // force preview image memory to be released - otherwise + // all the disposed images can pile up without being GC'ed + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + /// + /// Calculates the high-dpi scaling ratio based on the current monitor's display settings. + /// + private double GetHighDpiScalingRatio() + { + var hWnd = (HWND)WinRT.Interop.WindowNative.GetWindowHandle(this); + var windowDpi = PInvoke.GetDpiForWindow(hWnd); + ResultHandler.ThrowIfZero(result: (int)windowDpi, getLastError: true, memberName: nameof(PInvoke.GetDpiForWindow)); + var scalingRatio = (double)PInvoke.USER_DEFAULT_SCREEN_DPI / windowDpi; + return scalingRatio; + } + + private async Task InvokeOnUiThreadAsync(Action action) + { + // this might be called from a task that we're awaiting + // so we need to make sure we use the UI thread + var tcs = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + + if (!this.DispatcherQueue.TryEnqueue( + () => + { + try + { + action(); + tcs.SetResult(true); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + })) + { + tcs.SetException( + new InvalidOperationException("DispatcherQueue is not accepting work items.")); + } + + // wait for the task to complete + await tcs.Task.ConfigureAwait(false); + } + + private void HideWindow() + { + this.AppWindow.Hide(); + this.ClearPreview(); + } + + private async Task HideWindowAsync() + { + await this.InvokeOnUiThreadAsync( + () => + { + this.HideWindow(); + }).ConfigureAwait(false); + } + + private async Task ShowWindowAsync() + { + await this.InvokeOnUiThreadAsync( + () => + { + var presenter = this.AppWindow.Presenter as OverlappedPresenter + ?? throw new InvalidOperationException(); + 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 always get shown topmost... + presenter.IsAlwaysOnTop = false; + presenter.IsAlwaysOnTop = true; + } + + var hWnd = (HWND)WinRT.Interop.WindowNative.GetWindowHandle(this); + + // use raw Win32 calls instead of WinUI3's AppWindow.Show()/Activate()/Focus() + // because WinUI3's activation sequence intermittently fails to transfer keyboard + // focus, which causes the Window.Activated "Deactivated" event to never fire + // when the user clicks away, leaving the preview window stuck open. + _ = PInvoke.ShowWindow(hWnd, SHOW_WINDOW_CMD.SW_SHOW); + + var setForegroundResult = PInvoke.SetForegroundWindow(hWnd); + ResultHandler.HandleResult( + result: (int)setForegroundResult, + success: (bool)setForegroundResult, + getLastError: false, + nameof(PInvoke.SetForegroundWindow)); + + PInvoke.SetLastError(0); + var setFocusResult = PInvoke.SetFocus(hWnd); + if (setFocusResult.IsNull) + { + var lastError = Marshal.GetLastPInvokeError(); + ResultHandler.HandleResult( + result: setFocusResult, + success: lastError == 0, + lastError: lastError, + nameof(PInvoke.SetFocus)); + } + + PInvoke.SetLastError(0); + var setActiveResult = PInvoke.SetActiveWindow(hWnd); + if (setActiveResult.IsNull) + { + var lastError = Marshal.GetLastPInvokeError(); + ResultHandler.HandleResult( + result: setActiveResult, + success: lastError == 0, + lastError: lastError, + nameof(PInvoke.SetActiveWindow)); + } + }).ConfigureAwait(false); + } + + /// + /// Resize and position the form. + /// + private async Task PositionWindowAsync(RectangleInfo bounds) + { + await this.InvokeOnUiThreadAsync( + () => + { + // note - do this with two calls to MoveAndResize rather than one as there appears to + // be an issue with dpi scaling even when using PerMonitorV2, where if the window is + // resized *and* moved in one call the resize uses the scaling of the *current* + // monitor before it's moved. + // + // If the move then happens to be to a different monitor, *and* the monitor has a + // different dpi scaling configured, the window size is then wrong for the dpi scaling + // of the new monitor. + // + // the workaround seems to be to call MoveAndResize twice - the first call might + // resize it incorrectly, but it moves the window to the correct monitor, and the + // second call then resizes it correctly. + // + // see https://github.com/mikeclayton/FancyMouse/issues/2 for more details + var windowBounds = new RectInt32((int)bounds.X, (int)bounds.Y, (int)bounds.Width, (int)bounds.Height); + this.AppWindow.MoveAndResize(windowBounds); + this.AppWindow.MoveAndResize(windowBounds); + }).ConfigureAwait(false); + } + + private async Task OnPreviewImageCreatedAsync(Bitmap preview) + { + await this.InvokeOnUiThreadAsync( + () => + { + this.ClearPreview(); + + // we need to apply the high-dpi scaling ratio for the current monitor to the image control size + var highDpiScalingRatio = this.GetHighDpiScalingRatio(); + + this.PreviewImage.Width = preview.Width * highDpiScalingRatio; + this.PreviewImage.Height = preview.Height * highDpiScalingRatio; + }).ConfigureAwait(false); + } + + private async Task OnPreviewImageUpdatedAsync(Bitmap preview) + { + await this.InvokeOnUiThreadAsync( + () => + { + this.ClearPreview(); + + var bitmapImage = new BitmapImage(); + using (var stream = new MemoryStream()) + { + preview.Save(stream, ImageFormat.Png); + stream.Position = 0; + bitmapImage.SetSource(stream.AsRandomAccessStream()); + } + + this.PreviewImage.Source = bitmapImage; + }).ConfigureAwait(false); + } +} diff --git a/src/modules/MouseUtils/MouseJump.WinUI3/NativeMethods.txt b/src/modules/MouseUtils/MouseJump.WinUI3/NativeMethods.txt new file mode 100644 index 000000000000..893bc6b591c9 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.WinUI3/NativeMethods.txt @@ -0,0 +1,14 @@ +WINDOW_EX_STYLE +WINDOW_STYLE +GetWindowLong +SetWindowLong + +USER_DEFAULT_SCREEN_DPI +GetDpiForWindow + +SetLastError + +ShowWindow +SetForegroundWindow +SetFocus +SetActiveWindow diff --git a/src/modules/MouseUtils/MouseJump.WinUI3/Program.cs b/src/modules/MouseUtils/MouseJump.WinUI3/Program.cs new file mode 100644 index 000000000000..faefa6d03293 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.WinUI3/Program.cs @@ -0,0 +1,91 @@ +// 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.Globalization; +using System.Reflection; + +using ManagedCommon; +using Microsoft.PowerToys.Telemetry; +using Microsoft.UI.Dispatching; +using Microsoft.Windows.AppLifecycle; +using MouseJump.Common.Helpers; +using PowerToys.GPOWrapper; + +namespace MouseJump.WinUI3; + +public static class Program +{ + private static App? _app; + + [STAThread] + public static void Main(string[] args) + { + Logger.InitializeLogger("\\MouseJump\\Logs"); + Logger.LogInfo("MouseJump process started"); + + using var etwTrace = new ETWTrace(); + + WinRT.ComWrappersSupport.InitializeComWrappers(); + + // check Group Policy allows this app to run + if (GPOWrapper.GetConfiguredMouseJumpEnabledValue() == GpoRuleConfigured.Disabled) + { + Logger.LogWarning( + "Tried to start with a GPO policy setting the utility to always be disabled. " + + "Please contact your systems administrator."); + return; + } + + // make sure we're in the right high dpi mode otherwise pixel positions and sizes for + // screen captures get distorted and various coordinates aren't calculated correctly. + Logger.LogInfo("checking high dpi mode"); + DpiModeHelper.EnsurePerMonitorV2Enabled(); + Logger.LogInfo("high dpi mode is ok"); + + // validate command line arguments - we're expecting + // a single argument containing the runner pid + if ((args.Length != 1) || !int.TryParse(args[0], out var runnerPid)) + { + var message = string.Join("\r\n", new[] + { + "Invalid command line arguments.", + "Expected usage is:", + string.Empty, + $"{Assembly.GetExecutingAssembly().GetName().Name} ", + }); + Logger.LogInfo(message); + throw new InvalidOperationException(message); + } + + Logger.LogDebug($"Runner PID = {runnerPid}", runnerPid.ToString(CultureInfo.InvariantCulture)); + + // quit the app when the parent process terminates + RunnerHelper.WaitForPowerToysRunner(runnerPid, () => + { + Logger.LogInfo("PowerToys Runner exited."); + _app?.TerminateApp(); + }); + Logger.LogInfo($"Mouse Jump started from the PowerToys Runner. Runner pid={runnerPid}"); + + // prevent multiple instances of the app from running + var instanceKey = AppInstance.FindOrRegisterForKey("MouseJump_Instance"); + if (instanceKey.IsCurrent) + { + Logger.LogWarning("starting application"); + Microsoft.UI.Xaml.Application.Start((p) => + { + var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()); + SynchronizationContext.SetSynchronizationContext(context); + _app = new App(); + }); + Logger.LogWarning("application exited"); + } + else + { + Logger.LogWarning("another instance is running. exiting"); + } + + return; + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpShowEvent.cs b/src/modules/MouseUtils/MouseJump.WinUI3/Telemetry/MouseJumpShowEvent.cs similarity index 54% rename from src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpShowEvent.cs rename to src/modules/MouseUtils/MouseJump.WinUI3/Telemetry/MouseJumpShowEvent.cs index 0d721bbd1ec9..487b28e04925 100644 --- a/src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpShowEvent.cs +++ b/src/modules/MouseUtils/MouseJump.WinUI3/Telemetry/MouseJumpShowEvent.cs @@ -7,12 +7,11 @@ using Microsoft.PowerToys.Telemetry; using Microsoft.PowerToys.Telemetry.Events; -namespace MouseJumpUI.Telemetry +namespace MouseJump.WinUI3.Telemetry; + +[EventData] +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +public sealed class MouseJumpShowEvent : EventBase, IEvent { - [EventData] - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] - public class MouseJumpShowEvent : EventBase, IEvent - { - public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; - } + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; } diff --git a/src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpTeleportCursorEvent.cs b/src/modules/MouseUtils/MouseJump.WinUI3/Telemetry/MouseJumpTeleportCursorEvent.cs similarity index 53% rename from src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpTeleportCursorEvent.cs rename to src/modules/MouseUtils/MouseJump.WinUI3/Telemetry/MouseJumpTeleportCursorEvent.cs index e392ff6d1d49..42064c2b472f 100644 --- a/src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpTeleportCursorEvent.cs +++ b/src/modules/MouseUtils/MouseJump.WinUI3/Telemetry/MouseJumpTeleportCursorEvent.cs @@ -7,12 +7,11 @@ using Microsoft.PowerToys.Telemetry; using Microsoft.PowerToys.Telemetry.Events; -namespace MouseJumpUI.Telemetry +namespace MouseJump.WinUI3.Telemetry; + +[EventData] +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +public sealed class MouseJumpTeleportCursorEvent : EventBase, IEvent { - [EventData] - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] - public class MouseJumpTeleportCursorEvent : EventBase, IEvent - { - public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; - } + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; } diff --git a/src/modules/MouseUtils/MouseJump.WinUI3/app.manifest b/src/modules/MouseUtils/MouseJump.WinUI3/app.manifest new file mode 100644 index 000000000000..c8e96c265e7a --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.WinUI3/app.manifest @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + true + PerMonitorV2 + + + \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj b/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj index c74aac6c94b3..4d2bca18c7d2 100644 --- a/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj +++ b/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj @@ -12,13 +12,13 @@ DynamicLibrary true - + v143 Unicode DynamicLibrary false - + v143 true Unicode @@ -32,7 +32,7 @@ - $(RepoRoot)$(Platform)\$(Configuration)\ + $(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps\ PowerToys.MouseJump diff --git a/src/modules/MouseUtils/MouseJump/dllmain.cpp b/src/modules/MouseUtils/MouseJump/dllmain.cpp index 85894cea1592..b22687175b87 100644 --- a/src/modules/MouseUtils/MouseJump/dllmain.cpp +++ b/src/modules/MouseUtils/MouseJump/dllmain.cpp @@ -44,6 +44,29 @@ namespace const wchar_t JSON_KEY_SHIFT[] = L"shift"; const wchar_t JSON_KEY_CODE[] = L"code"; const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut"; + + struct FindWindowByPidData + { + DWORD pid; + HWND hwnd; + }; + + // a callback function that can be used with EnumWindows to find the first + // top-level window belonging to a given PID. stores the HWND in data->hwnd + // and returns FALSE to stop enumeration when found, or TRUE to continue to + // the next window. works for hidden windows as well as visible ones. + BOOL CALLBACK FindWindowByPidProc(HWND hwnd, LPARAM lParam) + { + auto* data = reinterpret_cast(lParam); + DWORD windowPid = 0; + GetWindowThreadProcessId(hwnd, &windowPid); + if (windowPid == data->pid) + { + data->hwnd = hwnd; + return FALSE; + } + return TRUE; + } } // Implement the PowerToy Module Interface and all the required methods. @@ -55,7 +78,7 @@ class MouseJump : public PowertoyModuleIface // Hotkey to invoke the module - HANDLE m_hProcess; + HANDLE m_hProcess = nullptr; // Time to wait for process to close after sending WM_CLOSE signal static const int MAX_WAIT_MILLISEC = 10000; @@ -118,7 +141,7 @@ class MouseJump : public PowertoyModuleIface SHELLEXECUTEINFOW sei{ sizeof(sei) }; sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI }; - sei.lpFile = L"PowerToys.MouseJumpUI.exe"; + sei.lpFile = L"WinUI3Apps\\PowerToys.MouseJump.WinUI3.exe"; sei.nShow = SW_SHOWNORMAL; sei.lpParameters = executable_args.data(); if (ShellExecuteExW(&sei)) @@ -237,7 +260,10 @@ class MouseJump : public PowertoyModuleIface { Logger::trace("MouseJump::enable()"); ResetEvent(m_hInvokeEvent); + + // pre-launch the ui exe so its window is ready before the first hotkey press launch_process(); + m_enabled = true; Trace::EnableJumpTool(true); } @@ -269,6 +295,34 @@ class MouseJump : public PowertoyModuleIface launch_process(); } + // find the winui3 window by pid. if we don't find one (e.g. the + // process is still starting up), drop this activation — the user + // can press the hotkey again once it's ready + FindWindowByPidData findData{ GetProcessId(m_hProcess), nullptr }; + EnumWindows(FindWindowByPidProc, reinterpret_cast(&findData)); + if (findData.hwnd == nullptr) + { + Logger::warn(L"WinUI3 window not ready yet - dropping activation. Press the hotkey again."); + return true; + } + + // on_hotkey() is called by the "runner" process in response to a + // hotkey activation. the hotkey activation means the runner process + // hook thread has the "last input" status, which in turn means we + // can call SetForegroundWindow on the ui exe window to make it the + // foreground window. this is more reliable than other options like + // AllowSetForegroundWindow (which the runner invalidates by calling + // SendInput straight after it calls action()). + BOOL sfwResult = SetForegroundWindow(findData.hwnd); + if (sfwResult) + { + Logger::trace(L"SetForegroundWindow on WinUI3 window succeeded with result {}", sfwResult); + } + else + { + Logger::warn(L"SetForegroundWindow on WinUI3 window failed with result {}. {}", sfwResult, get_last_error_or_default(GetLastError())); + } + SetEvent(m_hInvokeEvent); return true; } diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.Designer.cs b/src/modules/MouseUtils/MouseJumpUI/MainForm.Designer.cs deleted file mode 100644 index efc431066584..000000000000 --- a/src/modules/MouseUtils/MouseJumpUI/MainForm.Designer.cs +++ /dev/null @@ -1,94 +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.Windows.Forms; - -namespace MouseJumpUI; - -internal sealed partial class MainForm -{ - - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); - panel1 = new Panel(); - Thumbnail = new PictureBox(); - panel1.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)Thumbnail).BeginInit(); - SuspendLayout(); - // - // panel1 - // - panel1.BackColor = System.Drawing.SystemColors.Highlight; - panel1.Controls.Add(Thumbnail); - panel1.Dock = DockStyle.Fill; - panel1.Location = new System.Drawing.Point(0, 0); - panel1.Name = "panel1"; - panel1.Size = new System.Drawing.Size(800, 450); - panel1.TabIndex = 1; - // - // Thumbnail - // - Thumbnail.BackColor = System.Drawing.SystemColors.Control; - Thumbnail.Dock = DockStyle.Fill; - Thumbnail.Location = new System.Drawing.Point(5, 5); - Thumbnail.Name = "Thumbnail"; - Thumbnail.Size = new System.Drawing.Size(800, 450); - Thumbnail.SizeMode = PictureBoxSizeMode.Normal; - Thumbnail.TabIndex = 1; - Thumbnail.TabStop = false; - Thumbnail.Click += Thumbnail_Click; - // - // MainForm - // - AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); - AutoScaleMode = AutoScaleMode.Font; - ClientSize = new System.Drawing.Size(800, 450); - Controls.Add(panel1); - FormBorderStyle = FormBorderStyle.None; - Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon"); - KeyPreview = true; - Name = "MainForm"; - ShowInTaskbar = false; - StartPosition = FormStartPosition.Manual; - Text = "MouseJump"; - TopMost = true; - Deactivate += MainForm_Deactivate; - Load += MainForm_Load; - KeyDown += MainForm_KeyDown; - panel1.ResumeLayout(false); - ((System.ComponentModel.ISupportInitialize)Thumbnail).EndInit(); - ResumeLayout(false); - } - - #endregion - - private Panel panel1; - private PictureBox Thumbnail; - -} diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs deleted file mode 100644 index 6017c427fbdd..000000000000 --- a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs +++ /dev/null @@ -1,261 +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.Diagnostics; -using System.Drawing; -using System.Linq; -using System.Windows.Forms; - -using ManagedCommon; -using MouseJump.Common.Helpers; -using MouseJump.Common.Imaging; -using MouseJump.Common.Models.Drawing; -using MouseJump.Common.Models.Layout; -using MouseJumpUI.Helpers; - -namespace MouseJumpUI; - -internal sealed partial class MainForm : Form -{ - public MainForm(SettingsHelper settingsHelper) - { - this.InitializeComponent(); - this.SettingsHelper = settingsHelper ?? throw new ArgumentNullException(nameof(settingsHelper)); - } - - private PreviewLayout? PreviewLayout - { - get; - set; - } - - public SettingsHelper SettingsHelper - { - get; - } - - private void MainForm_Load(object sender, EventArgs e) - { - } - - private void MainForm_KeyDown(object sender, KeyEventArgs e) - { - if (e.KeyCode == Keys.Escape) - { - this.OnDeactivate(EventArgs.Empty); - return; - } - - var screens = ScreenHelper.GetAllScreens().ToList(); - if (screens.Count == 0) - { - return; - } - - var currentLocation = MouseHelper.GetCursorPosition(); - var currentScreen = ScreenHelper.GetScreenFromPoint(screens, currentLocation); - var currentScreenIndex = screens.IndexOf(currentScreen); - var targetScreen = default(ScreenInfo?); - - switch (e.KeyCode) - { - 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 (targetScreen is not null) - { - MouseHelper.SetCursorPosition(targetScreen.DisplayArea.Midpoint); - this.OnDeactivate(EventArgs.Empty); - } - } - - private void MainForm_Deactivate(object sender, EventArgs e) - { - this.Hide(); - this.ClearPreview(); - } - - private void Thumbnail_Click(object sender, EventArgs e) - { - var mouseEventArgs = (MouseEventArgs)e; - Logger.LogInfo(string.Join( - '\n', - $"Reporting mouse event args", - $"\tbutton = {mouseEventArgs.Button}", - $"\tlocation = {mouseEventArgs.Location}")); - - if (mouseEventArgs.Button == MouseButtons.Left) - { - 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}"); - Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent()); - MouseHelper.SetCursorPosition(clickedLocation); - } - - this.OnDeactivate(EventArgs.Empty); - } - - public void ShowPreview() - { - // hide the form while we redraw it... - this.Visible = false; - - var stopwatch = Stopwatch.StartNew(); - - 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: SettingsHelper.GetActivePreviewStyle(appSettings), - screens: screens, - activatedLocation: activatedLocation); - - this.PositionForm(this.PreviewLayout.FormBounds); - - var imageCopyService = new DesktopImageRegionCopyService(); - DrawingHelper.RenderPreview( - this.PreviewLayout, - imageCopyService, - this.OnPreviewImageCreated, - this.OnPreviewImageUpdated); - - stopwatch.Stop(); - - Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpShowEvent()); - - // we have to activate the form to make sure the deactivate event fires - this.Activate(); - } - - private void ClearPreview() - { - if (this.Thumbnail.Image is null) - { - return; - } - - var tmp = this.Thumbnail.Image; - this.Thumbnail.Image = null; - tmp.Dispose(); - - // force preview image memory to be released; otherwise, - // all the disposed images can pile up without being GC'ed - GC.Collect(); - } - - /// - /// Resize and position the specified form. - /// - private void PositionForm(RectangleInfo bounds) - { - // note - do this in two steps rather than "this.Bounds = formBounds" as there - // appears to be an issue in WinForms with dpi scaling even when using PerMonitorV2, - // where the form scaling uses either the *primary* screen scaling or the *previous* - // screen's scaling when the form is moved to a different screen. i've got no idea - // *why*, but the exact sequence of calls below seems to be a workaround... - // see https://github.com/mikeclayton/FancyMouse/issues/2 - var rect = bounds.ToRectangle(); - this.Location = rect.Location; - _ = this.PointToScreen(Point.Empty); - this.Size = rect.Size; - } - - private void OnPreviewImageCreated(Bitmap preview) - { - this.ClearPreview(); - this.Thumbnail.Image = preview; - } - - private 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 always get shown topmost... - this.TopMost = false; - this.TopMost = true; - this.Show(); - } - - this.Thumbnail.Refresh(); - } -} diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.resx b/src/modules/MouseUtils/MouseJumpUI/MainForm.resx deleted file mode 100644 index 4df95b549542..000000000000 --- a/src/modules/MouseUtils/MouseJumpUI/MainForm.resx +++ /dev/null @@ -1,377 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - - AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAA - AABgAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAD/AAAA/wAAAP8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAP9kdJz/ZHSc/2R0nP9kdJz/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAA/2R0nP9kdJz/ZHSc/2R0nP9kdJz/ZHSc/wAAAP8AAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAA/2R0nP9kdJz/ZHSc/2R0nP9kdJz/ZHSc/wAA - AP8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAAAP9kdJz/ZHSc/2R0 - nP9kdJz/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/xuLj/8bi - 4/8AAAD/AAAA/wAAAP8AAAD/xuLj/8bi4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi - 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/8AAAD/AAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AP/G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi - 4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAA/8bi4//G4uP/xuLj/5Okpf8kISD/k6Sl/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/5Ok - pf8kISD/k6Sl/8bi4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/xuLj/yQhIP8kISD/JCEg/8bi4//G4uP/xuLj/8bi - 4//G4uP/xuLj/yQhIP8kISD/JCEg/8bi4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi4//G4uP/xuLj/5Okpf8kISD/k6Sl/8bi - 4//G4uP/xuLj/8bi4//G4uP/xuLj/5Okpf8kISD/k6Sl/8bi4//G4uP/xuLj/8bi4/8AAAD/AAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi4//G4uP/xuLj/8bi - 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi - 4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAA - AP8AAAD/AAAA/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/wAA - AP8AAAD/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi - 4//G4uP/AAAA/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi - 4//G4uP/AAAA/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAD/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4//G4uP/xuLj/8bi - 4//G4uP/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4/8AAAD/AAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAD/xuLj/wAAAP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi - 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CYtf+AmLX/AAAA/8bi - 4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAAAP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CY - tf8AAAD/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CY - tf+AmLX/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CYtf+AmLX/gJi1/4CY - tf+AmLX/gJi1/wAAAP/G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/AAAA/4CY - tf+AmLX/gJi1/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4/8AAAD/gJi1/4CY - tf+AmLX/gJi1/4CYtf+AmLX/gJi1/wAAAP/G4uP/AAAA/wAAAP/G4uP/xuLj/8bi4//G4uP/AAAA/wAA - AP/G4uP/AAAA/4CYtf+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/wAAAP8AAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi - 4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CYtf+AmLX/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAD/AAAA/wAA - AP8AAAD/AAAAAAAAAP/G4uP/xuLj/wAAAP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/wAA - AP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAA/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4/8AAAD/AAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CY - tf8AAAD/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/wAA - AP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CY - tf+AmLX/gJi1/4CYtf8AAAD/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CYtf+AmLX/gJi1/wAA - AP/G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AP/G4uP/AAAA/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAA - AP+AmLX/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAAAP+AmLX/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAD/xuLj/wAAAP+AmLX/AAAA/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/wAAAP+AmLX/AAAA/8bi4/8AAAD/AAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi4/8AAAD/xuLj/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi4/8AAAD/xuLj/8bi - 4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi - 4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAD/AAAA/wAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAP8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAP///////wAA////////AAD///////8AAP///////wAA////////AAD///////8AAP// - /////wAA////////AAD///////8AAP///////wAA///8P///AAD///gf//8AAP//8A///wAA///gB/// - AAD//8AD//8AAP//gAH//wAA//8AAP//AAD//gAAf/8AAP/8AAA//wAA//wAAD//AAD/+AAAH/8AAP/4 - AAAf/wAA//AAAA//AAD/4AAAB/8AAP/AAAAD/wAA/8AAAAP/AAD/gAAAAf8AAP8AAAAA/wAA/wAAAAD/ - AAD/AAQgAP8AAP8AD/AA/wAA/wAf+AD/AAD/AD/8AP8AAP+AP/wB/wAA/8B//gP/AAD/wH/+A/8AAP/g - //8H/wAA//H//4//AAD///////8AAP///////wAA////////AAD///////8AAP///////wAA//////// - AAD///////8AAP///////wAA////////AAD///////8AACgAAAAgAAAAQAAAAAEAIAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAA/wAAAP8AAAD/AAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/2R0nP9kdJz/ZHSc/2R0nP8AAAD/AAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP9kdJz/ZHSc/2R0nP9kdJz/ZHSc/2R0 - nP8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAA/2R0nP9kdJz/ZHSc/2R0 - nP9kdJz/ZHSc/wAAAP8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/2R0 - nP9kdJz/ZHSc/2R0nP8AAAD/xuLj/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/8bi - 4//G4uP/AAAA/wAAAP8AAAD/AAAA/8bi4//G4uP/xuLj/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi - 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/8AAAD/AAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi - 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi - 4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AP/G4uP/xuLj/8bi4/+TpKX/JCEg/5Okpf/G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/+TpKX/JCEg/5Ok - pf/G4uP/xuLj/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAA/8bi4//G4uP/xuLj/yQhIP8kISD/JCEg/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/yQh - IP8kISD/JCEg/8bi4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAP/G4uP/xuLj/8bi4//G4uP/k6Sl/yQhIP+TpKX/xuLj/8bi4//G4uP/xuLj/8bi - 4//G4uP/k6Sl/yQhIP+TpKX/xuLj/8bi4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi - 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAAAP8AAAD/AAAA/8bi4//G4uP/xuLj/8bi - 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/wAAAP8AAAD/AAAA/8bi4//G4uP/AAAA/wAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/AAAA/8bi - 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/AAAA/8bi - 4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CYtf+AmLX/gJi1/4CY - tf+AmLX/AAAA/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/AAAA/4CYtf+AmLX/gJi1/4CY - tf+AmLX/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/wAAAP+AmLX/gJi1/4CY - tf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/8AAAD/gJi1/4CY - tf+AmLX/gJi1/4CYtf+AmLX/AAAA/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CY - tf+AmLX/gJi1/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/wAA - AP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4/8AAAD/AAAAAAAAAP/G4uP/xuLj/wAA - AP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi - 4//G4uP/xuLj/wAAAP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4/8AAAD/AAAA/8bi - 4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CYtf+AmLX/gJi1/wAAAP/G4uP/AAAA/wAAAP/G4uP/xuLj/8bi - 4//G4uP/AAAA/wAAAP/G4uP/AAAA/4CYtf+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/wAA - AP8AAAD/xuLj/wAAAP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4/8AAAD/AAAAAAAA - AP8AAAD/AAAA/wAAAP8AAAAAAAAA/8bi4//G4uP/AAAA/4CYtf+AmLX/gJi1/4CYtf+AmLX/gJi1/wAA - AP/G4uP/AAAA/wAAAP/G4uP/AAAA/4CYtf+AmLX/gJi1/4CYtf+AmLX/AAAA/8bi4//G4uP/AAAA/wAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CYtf+AmLX/gJi1/4CY - tf+AmLX/AAAA/8bi4/8AAAD/AAAA/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/wAA - AP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CY - tf+AmLX/gJi1/4CYtf8AAAD/xuLj/wAAAP8AAAD/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/AAAA/8bi - 4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi - 4/8AAAD/gJi1/4CYtf+AmLX/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAD/xuLj/8bi4/8AAAD/gJi1/wAA - AP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAD/xuLj/8bi4/8AAAD/gJi1/wAAAP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAD/xuLj/wAA - AP+AmLX/AAAA/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAD/xuLj/wAAAP+AmLX/AAAA/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAA - AP/G4uP/xuLj/wAAAP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAAAP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAP/G4uP/xuLj/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/8bi4/8AAAD/AAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAD/AAAA/wAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////////////D////gf///wD///4Af//8AD//+A - Af//AAD//gAAf/wAAD/8AAA/+AAAH/gAAB/wAAAP4AAAB8AAAAPAAAADgAAAAQAAAAAAAAAAAAQgAAAP - 8AAAH/gAAD/8AIA//AHAf/4DwH/+A+D//wfx//+P//////////8oAAAAEAAAACAAAAABACAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAyOk7/MjpO/wAA - AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAyOk7/ZHSc/2R0 - nP8yOk7/AAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECVqqr/laqq/zI6 - Tv8yOk7/laqq/5Wqqv8AAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECVqqr/xuLj/8bi - 4//G4uP/xuLj/8bi4//G4uP/laqq/wAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABjcXL/xuLj/0BC - Qf+RoqP/xuLj/8bi4/+RoqP/QEJB/8bi4/9jcXL/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAxuLj/8bi - 4/+RoqP/udPU/8bi4//G4uP/udPU/5Gio//G4uP/xuLj/wAAAIAAAAAAAAAAAAAAAAAAAABAlaqq/1Jf - Zv9ATFv/laqq/8bi4//G4uP/xuLj/8bi4/+Vqqr/QExb/1JfZv+Vqqr/AAAAQAAAAAAAAAAAY3Fy/1Jf - Zv+AmLX/gJi1/0BMW//G4uP/xuLj/8bi4//G4uP/QExb/4CYtf+AmLX/Ul9m/2Nxcv8AAAAAQktMv5Wq - qv9gcoj/gJi1/4CYtf9SX2b/xuLj/8bi4//G4uP/xuLj/1JfZv+AmLX/gJi1/2ByiP+Vqqr/QktMv2Nx - cv9ATFv/gJi1/4CYtf9gcoj/laqq/wAAAL9jcXL/Y3Fy/wAAAL+Vqqr/YHKI/4CYtf+AmLX/QExb/2Nx - cv9jcXL/QExb/4CYtf9gcoj/laqq/0JLTL8AAAAAAAAAAAAAAAAAAAAAQktMv5Wqqv9gcoj/gJi1/0BM - W/9jcXL/QktMv5Wqqv9gcoj/Ul9m/2Nxcv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABjcXL/Ul9m/2By - iP+Vqqr/QktMvwAAAABjcXL/Ul9m/5Wqqv8AAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgJWq - qv9SX2b/Y3Fy/wAAAAAAAAAAAAAAQGNxcv9CS0y/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AABCS0y/Y3Fy/wAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//rEH8P6xB+B+sQfAPrEHgB6xB4AesQcADrEGAAaxBgAGsQQAA - rEEAAKxBA8CsQQfgrEGH4axBj/GsQf//rEE= - - - \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj b/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj deleted file mode 100644 index 53f949f4c302..000000000000 --- a/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - PowerToys.MouseJumpUI - PowerToys.MouseJumpUI - PowerToys MouseJumpUI - WinExe - $(RepoRoot)$(Platform)\$(Configuration) - false - false - - PerMonitorV2 - true - enable - true - disable - - - - - - PowerToys.GPOWrapper - $(OutDir) - false - - - - - - - - - - - - \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseJumpUI/Program.cs b/src/modules/MouseUtils/MouseJumpUI/Program.cs deleted file mode 100644 index d327e8030475..000000000000 --- a/src/modules/MouseUtils/MouseJumpUI/Program.cs +++ /dev/null @@ -1,98 +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.Reflection; -using System.Threading; -using System.Windows.Forms; -using System.Windows.Threading; - -using Common.UI; -using ManagedCommon; -using Microsoft.PowerToys.Settings.UI.Library; -using Microsoft.PowerToys.Telemetry; -using MouseJumpUI.Helpers; -using PowerToys.Interop; - -namespace MouseJumpUI; - -internal static class Program -{ - private static CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - - /// - /// The main entry point for the application. - /// - [STAThread] - private static void Main(string[] args) - { - Logger.InitializeLogger("\\MouseJump\\Logs"); - ETWTrace etwTrace = new ETWTrace(); - - // To customize application configuration such as set high DPI settings or default font, - // see https://aka.ms/applicationconfiguration. - ApplicationConfiguration.Initialize(); - - if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredMouseJumpEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled) - { - // TODO : Log message - Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator."); - return; - } - - if (Application.HighDpiMode != HighDpiMode.PerMonitorV2) - { - Logger.LogError("High dpi mode is not set to PerMonitorV2."); - return; - } - - // validate command line arguments - we're expecting - // a single argument containing the runner pid - if ((args.Length != 1) || !int.TryParse(args[0], out var runnerPid)) - { - var message = string.Join("\r\n", new[] - { - "Invalid command line arguments.", - "Expected usage is:", - string.Empty, - $"{Assembly.GetExecutingAssembly().GetName().Name} ", - }); - Logger.LogInfo(message); - throw new InvalidOperationException(message); - } - - Logger.LogInfo($"Mouse Jump started from the PowerToys Runner. Runner pid={runnerPid}"); - - RunnerHelper.WaitForPowerToysRunner(runnerPid, () => - { - Logger.LogInfo("PowerToys Runner exited."); - TerminateApp(); - }); - - var settingsHelper = new SettingsHelper(); - var mainForm = new MainForm(settingsHelper); - - NativeEventWaiter.WaitForEventLoop( - Constants.MouseJumpShowPreviewEvent(), - mainForm.ShowPreview, - Dispatcher.CurrentDispatcher, - cancellationTokenSource.Token); - - NativeEventWaiter.WaitForEventLoop( - Constants.TerminateMouseJumpSharedEvent(), - TerminateApp, - Dispatcher.CurrentDispatcher, - cancellationTokenSource.Token); - - Application.Run(); - etwTrace?.Dispose(); - } - - private static void TerminateApp() - { - Logger.LogInfo("Exiting Mouse Jump."); - cancellationTokenSource.Cancel(); - Application.Exit(); - } -} diff --git a/src/modules/MouseUtils/MouseUtils.UITests/MouseUtils.UITests.csproj b/src/modules/MouseUtils/MouseUtils.UITests/MouseUtils.UITests.csproj index 33d722718f18..dd9612d203b0 100644 --- a/src/modules/MouseUtils/MouseUtils.UITests/MouseUtils.UITests.csproj +++ b/src/modules/MouseUtils/MouseUtils.UITests/MouseUtils.UITests.csproj @@ -1,25 +1,26 @@ - - + + - - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1} - PowerToys.MouseUtils.UITests - PowerToys.MouseUtils.UITests - false - true - enable - Library + + {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1} + PowerToys.MouseUtils.UITests + PowerToys.MouseUtils.UITests + false + true + enable + Library - - false + + false + - $(SolutionDir)$(Platform)\$(Configuration)\tests\MouseUtils.UITests\ + $(SolutionDir)$(Platform)\$(Configuration)\tests\MouseUtils.UITests\ - - + + diff --git a/src/runner/main.cpp b/src/runner/main.cpp index dd7a48083572..7dd7c403eed1 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -265,7 +265,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow L"PowerToys.AwakeModuleInterface.dll", L"PowerToys.FindMyMouse.dll", L"PowerToys.MouseHighlighter.dll", - L"PowerToys.MouseJump.dll", + L"WinUI3Apps/PowerToys.MouseJump.dll", L"PowerToys.AlwaysOnTopModuleInterface.dll", L"PowerToys.MousePointerCrosshairs.dll", L"PowerToys.CursorWrap.dll", diff --git a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs index 91944368a132..e18b00c4268e 100644 --- a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs +++ b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs @@ -6,11 +6,13 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; + using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + using MouseJump.Common.Helpers; -using MouseJump.Common.Models.Settings; +using MouseJump.Models.Settings; namespace Microsoft.PowerToys.Settings.UI.Library { @@ -108,24 +110,24 @@ public bool UpgradeSettingsConfiguration() // set default values for custom preview style var previewStyle = StyleHelper.BezelledPreviewStyle; this.Properties.PreviewType = PreviewType.Bezelled.ToString(); - this.Properties.BackgroundColor1 = ConfigHelper.SerializeToConfigColorString( - ConfigHelper.ToUnnamedColor(previewStyle.CanvasStyle.BackgroundStyle.Color1)); - this.Properties.BackgroundColor2 = ConfigHelper.SerializeToConfigColorString( - ConfigHelper.ToUnnamedColor(previewStyle.CanvasStyle.BackgroundStyle.Color2)); + this.Properties.BackgroundColor1 = ColorHelper.SerializeToConfigColorString( + ColorHelper.ToUnnamedColor(previewStyle.CanvasStyle.BackgroundStyle.Color1)); + this.Properties.BackgroundColor2 = ColorHelper.SerializeToConfigColorString( + ColorHelper.ToUnnamedColor(previewStyle.CanvasStyle.BackgroundStyle.Color2)); this.Properties.BorderThickness = (int)previewStyle.CanvasStyle.BorderStyle.Top; - this.Properties.BorderColor = ConfigHelper.SerializeToConfigColorString( - ConfigHelper.ToUnnamedColor(previewStyle.CanvasStyle.BorderStyle.Color)); + this.Properties.BorderColor = ColorHelper.SerializeToConfigColorString( + ColorHelper.ToUnnamedColor(previewStyle.CanvasStyle.BorderStyle.Color)); this.Properties.Border3dDepth = (int)previewStyle.CanvasStyle.BorderStyle.Depth; this.Properties.BorderPadding = (int)previewStyle.CanvasStyle.PaddingStyle.Top; this.Properties.BezelThickness = (int)previewStyle.ScreenStyle.BorderStyle.Top; - this.Properties.BezelColor = ConfigHelper.SerializeToConfigColorString( - ConfigHelper.ToUnnamedColor(previewStyle.ScreenStyle.BorderStyle.Color)); + this.Properties.BezelColor = ColorHelper.SerializeToConfigColorString( + ColorHelper.ToUnnamedColor(previewStyle.ScreenStyle.BorderStyle.Color)); this.Properties.Bezel3dDepth = (int)previewStyle.ScreenStyle.BorderStyle.Depth; this.Properties.ScreenMargin = (int)previewStyle.ScreenStyle.MarginStyle.Top; - this.Properties.ScreenColor1 = ConfigHelper.SerializeToConfigColorString( - ConfigHelper.ToUnnamedColor(previewStyle.ScreenStyle.BackgroundStyle.Color1)); - this.Properties.ScreenColor2 = ConfigHelper.SerializeToConfigColorString( - ConfigHelper.ToUnnamedColor(previewStyle.ScreenStyle.BackgroundStyle.Color2)); + this.Properties.ScreenColor1 = ColorHelper.SerializeToConfigColorString( + ColorHelper.ToUnnamedColor(previewStyle.ScreenStyle.BackgroundStyle.Color1)); + this.Properties.ScreenColor2 = ColorHelper.SerializeToConfigColorString( + ColorHelper.ToUnnamedColor(previewStyle.ScreenStyle.BackgroundStyle.Color2)); } // we still need to flag the settings as "upgraded" so that the new version gets written diff --git a/src/settings-ui/Settings.UI/Converters/MouseJumpPreviewTypeConverter.cs b/src/settings-ui/Settings.UI/Converters/MouseJumpPreviewTypeConverter.cs index cfc980a8cea1..fd4c96d41486 100644 --- a/src/settings-ui/Settings.UI/Converters/MouseJumpPreviewTypeConverter.cs +++ b/src/settings-ui/Settings.UI/Converters/MouseJumpPreviewTypeConverter.cs @@ -4,7 +4,7 @@ using System; using Microsoft.UI.Xaml.Data; -using MouseJump.Common.Models.Settings; +using MouseJump.Models.Settings; namespace Microsoft.PowerToys.Settings.UI.Converters { diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj index 87bcc9ec7b7f..addcb04ceee1 100644 --- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj +++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj @@ -115,12 +115,13 @@ + + - diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml.cs index de5eabb84421..101efdaa25c1 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml.cs @@ -11,7 +11,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using MouseJump.Common.Helpers; -using MouseJump.Common.Models.Settings; +using MouseJump.Models.Settings; namespace Microsoft.PowerToys.Settings.UI.Panels { @@ -128,24 +128,24 @@ private void MouseUtils_MouseJump_CopyToCustomStyle_MessageBox_PrimaryButtonComm // note that we have to replace Named and System colors with their ARGB equivalents // so that serialization returns an ARGB string rather than the Named or System color *name*. this.ViewModel.MouseJumpPreviewType = selectedPreviewType.ToString(); - this.ViewModel.MouseJumpBackgroundColor1 = ConfigHelper.SerializeToConfigColorString( - ConfigHelper.ToUnnamedColor(selectedPreviewStyle.CanvasStyle.BackgroundStyle.Color1)); - this.ViewModel.MouseJumpBackgroundColor2 = ConfigHelper.SerializeToConfigColorString( - ConfigHelper.ToUnnamedColor(selectedPreviewStyle.CanvasStyle.BackgroundStyle.Color2)); + this.ViewModel.MouseJumpBackgroundColor1 = ColorHelper.SerializeToConfigColorString( + ColorHelper.ToUnnamedColor(selectedPreviewStyle.CanvasStyle.BackgroundStyle.Color1)); + this.ViewModel.MouseJumpBackgroundColor2 = ColorHelper.SerializeToConfigColorString( + ColorHelper.ToUnnamedColor(selectedPreviewStyle.CanvasStyle.BackgroundStyle.Color2)); this.ViewModel.MouseJumpBorderThickness = (int)selectedPreviewStyle.CanvasStyle.BorderStyle.Top; - this.ViewModel.MouseJumpBorderColor = ConfigHelper.SerializeToConfigColorString( - ConfigHelper.ToUnnamedColor(selectedPreviewStyle.CanvasStyle.BorderStyle.Color)); + this.ViewModel.MouseJumpBorderColor = ColorHelper.SerializeToConfigColorString( + ColorHelper.ToUnnamedColor(selectedPreviewStyle.CanvasStyle.BorderStyle.Color)); this.ViewModel.MouseJumpBorder3dDepth = (int)selectedPreviewStyle.CanvasStyle.BorderStyle.Depth; this.ViewModel.MouseJumpBorderPadding = (int)selectedPreviewStyle.CanvasStyle.PaddingStyle.Top; this.ViewModel.MouseJumpBezelThickness = (int)selectedPreviewStyle.ScreenStyle.BorderStyle.Top; - this.ViewModel.MouseJumpBezelColor = ConfigHelper.SerializeToConfigColorString( - ConfigHelper.ToUnnamedColor(selectedPreviewStyle.ScreenStyle.BorderStyle.Color)); + this.ViewModel.MouseJumpBezelColor = ColorHelper.SerializeToConfigColorString( + ColorHelper.ToUnnamedColor(selectedPreviewStyle.ScreenStyle.BorderStyle.Color)); this.ViewModel.MouseJumpBezel3dDepth = (int)selectedPreviewStyle.ScreenStyle.BorderStyle.Depth; this.ViewModel.MouseJumpScreenMargin = (int)selectedPreviewStyle.ScreenStyle.MarginStyle.Top; - this.ViewModel.MouseJumpScreenColor1 = ConfigHelper.SerializeToConfigColorString( - ConfigHelper.ToUnnamedColor(selectedPreviewStyle.ScreenStyle.BackgroundStyle.Color1)); - this.ViewModel.MouseJumpScreenColor2 = ConfigHelper.SerializeToConfigColorString( - ConfigHelper.ToUnnamedColor(selectedPreviewStyle.ScreenStyle.BackgroundStyle.Color2)); + this.ViewModel.MouseJumpScreenColor1 = ColorHelper.SerializeToConfigColorString( + ColorHelper.ToUnnamedColor(selectedPreviewStyle.ScreenStyle.BackgroundStyle.Color1)); + this.ViewModel.MouseJumpScreenColor2 = ColorHelper.SerializeToConfigColorString( + ColorHelper.ToUnnamedColor(selectedPreviewStyle.ScreenStyle.BackgroundStyle.Color2)); } private PreviewType GetSelectedPreviewType() diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel_MouseJump.cs b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel_MouseJump.cs index e954976f2e63..106e91b13179 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel_MouseJump.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel_MouseJump.cs @@ -11,17 +11,21 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; + using global::PowerToys.GPOWrapper; + using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; + using MouseJump.Common.Helpers; using MouseJump.Common.Imaging; -using MouseJump.Common.Models.Drawing; -using MouseJump.Common.Models.Settings; -using MouseJump.Common.Models.Styles; +using MouseJump.Models.Display; +using MouseJump.Models.Drawing; +using MouseJump.Models.Settings; +using MouseJump.Models.Styles; namespace Microsoft.PowerToys.Settings.UI.ViewModels { @@ -154,19 +158,38 @@ public ImageSource MouseJumpPreviewImage { get { - // keep these in sync with the layout of "Images\MouseJump-Desktop.png" - var screens = new List() - { - /* - these magic numbers are the pixel dimensions of the individual screens on the - fake desktop image - "Images\MouseJump-Desktop.png" - used to generate the - preview image in the Settings UI properties page for Mouse Jump. if you update - the fake desktop image be sure to update these values as well. - */ - new(635, 172, 272, 168), - new(0, 0, 635, 339), - }; - var desktopSize = LayoutHelper.GetCombinedScreenBounds(screens).Size; + // build the display info used to generate the preview image in the settings dialog + // (keep the values in sync with the layout of "Images\MouseJump-Desktop.png") + var displayInfo = new DisplayInfo([ + new DeviceInfo( + hostname: "FakeDisplay1", + localhost: true, + screens: [ + /* + these magic numbers are the pixel dimensions of the individual screens on the + fake desktop image - "Images\MouseJump-Desktop.png" - used to generate the + preview image in the Settings UI properties page for Mouse Jump. if you update + the fake desktop image be sure to update these values as well. + */ + new( + handle: IntPtr.Zero, + primary: false, + displayArea: new RectangleInfo(635, 172, 272, 168), + workingArea: new RectangleInfo(635, 172, 272, 168)), + new( + handle: IntPtr.Zero, + primary: true, + displayArea: new RectangleInfo(0, 0, 635, 339), + workingArea: new RectangleInfo(0, 0, 635, 339)), + ]), + ]); + + var desktopSize = LayoutHelper.GetCombinedScreenBounds( + displayInfo.Devices[0].Screens + .Select(screen => screen.DisplayArea) + .ToList()) + .Size; + /* magic number 283 is the content height left in the settings card after removing the top and bottom chrome: @@ -174,7 +197,15 @@ the fake desktop image be sure to update these values as well. this ensures we get a preview image scaled at 100% so borders, etc., are shown at exact pixel sizes in the preview */ - var canvasSize = new SizeInfo(desktopSize.Width, 283).Clamp(desktopSize); + const int settingsCardHeight = 300; + const int settingsCardTopBorder = 1; + const int settingsCardTopMargin = 7; + const int settingsCardBottomMargin = 8; + const int settingsCardBottomBorder = 1; + const int settingsCardContentHeight = // 283 + settingsCardHeight - settingsCardTopBorder - settingsCardTopMargin - settingsCardBottomBorder - settingsCardBottomMargin; + + var canvasSize = new SizeInfo(desktopSize.Width, settingsCardContentHeight).Clamp(desktopSize); var previewType = Enum.TryParse(this.MouseJumpPreviewType, true, out var previewTypeResult) ? previewTypeResult @@ -188,7 +219,7 @@ the fake desktop image be sure to update these values as well. canvasStyle: new( marginStyle: new(0), borderStyle: new( - color: ConfigHelper.DeserializeFromConfigColorString( + color: ColorHelper.DeserializeFromConfigColorString( this.MouseJumpBorderColor), all: this.MouseJumpBorderThickness, depth: this.MouseJumpBorder3dDepth @@ -197,9 +228,9 @@ the fake desktop image be sure to update these values as well. all: this.MouseJumpBorderPadding ), backgroundStyle: new( - color1: ConfigHelper.DeserializeFromConfigColorString( + color1: ColorHelper.DeserializeFromConfigColorString( this.MouseJumpBackgroundColor1), - color2: ConfigHelper.DeserializeFromConfigColorString( + color2: ColorHelper.DeserializeFromConfigColorString( this.MouseJumpBackgroundColor2) ) ), @@ -208,33 +239,39 @@ the fake desktop image be sure to update these values as well. all: this.MouseJumpScreenMargin ), borderStyle: new( - color: ConfigHelper.DeserializeFromConfigColorString( + color: ColorHelper.DeserializeFromConfigColorString( this.MouseJumpBezelColor), all: this.MouseJumpBezelThickness, depth: this.MouseJumpBezel3dDepth ), paddingStyle: new(0), backgroundStyle: new( - color1: ConfigHelper.DeserializeFromConfigColorString( + color1: ColorHelper.DeserializeFromConfigColorString( this.MouseJumpScreenColor1), - color2: ConfigHelper.DeserializeFromConfigColorString( + color2: ColorHelper.DeserializeFromConfigColorString( this.MouseJumpScreenColor2) ) - )), + ), + extraColors: []), _ => throw new InvalidOperationException( $"Unhandled {nameof(MouseJumpPreviewType)} '{previewType}'"), }; - var previewLayout = LayoutHelper.GetPreviewLayout( + var activatedScreen = displayInfo.Devices[0].Screens[0]; + + var previewLayout = LayoutHelper.GetFormLayout( previewStyle: previewStyle, - screens: screens, + displayInfo: displayInfo, + activatedScreen: activatedScreen, activatedLocation: new(0, 0)); var desktopImage = MouseUtilsViewModel.MouseJumpDesktopImage.Value; var imageCopyService = new StaticImageRegionCopyService(desktopImage); - using var previewImage = DrawingHelper.RenderPreview( - previewLayout, - imageCopyService); + var previewImageTask = DrawingHelper.RenderPreviewAsync( + previewLayout.CanvasLayout, + activatedScreen, + new() { imageCopyService }); + using var previewImage = previewImageTask.GetAwaiter().GetResult(); // save the image to a memory stream using var stream = new MemoryStream(); diff --git a/tools/MouseJump.Kicker/Assets/Icons/MouseJump 16x16.ico b/tools/MouseJump.Kicker/Assets/Icons/MouseJump 16x16.ico new file mode 100644 index 000000000000..8b189f4e112d Binary files /dev/null and b/tools/MouseJump.Kicker/Assets/Icons/MouseJump 16x16.ico differ diff --git a/tools/MouseJump.Kicker/KickerForm.Designer.cs b/tools/MouseJump.Kicker/KickerForm.Designer.cs new file mode 100644 index 000000000000..fa07d2d67eec --- /dev/null +++ b/tools/MouseJump.Kicker/KickerForm.Designer.cs @@ -0,0 +1,96 @@ +// 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 System.Windows.Forms; + +namespace MouseJump.Kicker +{ + partial class KickerForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(KickerForm)); + ActivationHotkey = new Button(); + CloseMouseJump = new Button(); + StartWinUI3 = new Button(); + SuspendLayout(); + // + // ActivationHotkey + // + ActivationHotkey.Location = new Point(12, 89); + ActivationHotkey.Name = "ActivationHotkey"; + ActivationHotkey.Size = new Size(260, 70); + ActivationHotkey.TabIndex = 1; + ActivationHotkey.Text = "Activate Hotkey"; + ActivationHotkey.UseVisualStyleBackColor = true; + ActivationHotkey.Click += ActivationHotkey_Click; + // + // CloseMouseJump + // + CloseMouseJump.Location = new Point(12, 165); + CloseMouseJump.Name = "CloseMouseJump"; + CloseMouseJump.Size = new Size(260, 70); + CloseMouseJump.TabIndex = 2; + CloseMouseJump.Text = "Close MouseJump"; + CloseMouseJump.UseVisualStyleBackColor = true; + CloseMouseJump.Click += CloseMouseJump_Click; + // + // StartWinUI3 + // + StartWinUI3.Location = new Point(12, 12); + StartWinUI3.Name = "StartWinUI3"; + StartWinUI3.Size = new Size(260, 70); + StartWinUI3.TabIndex = 3; + StartWinUI3.Text = "Start MouseJump (WinUI3)"; + StartWinUI3.UseVisualStyleBackColor = true; + StartWinUI3.Click += StartWinUI3_Click; + // + // KickerForm + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(281, 242); + Controls.Add(StartWinUI3); + Controls.Add(CloseMouseJump); + Controls.Add(ActivationHotkey); + FormBorderStyle = FormBorderStyle.FixedSingle; + Icon = (Icon)resources.GetObject("$this.Icon"); + MaximizeBox = false; + MinimizeBox = false; + Name = "KickerForm"; + Text = "MouseJump Kicker"; + ResumeLayout(false); + } + + #endregion + private Button ActivationHotkey; + private Button CloseMouseJump; + private Button StartWinUI3; + } +} diff --git a/tools/MouseJump.Kicker/KickerForm.cs b/tools/MouseJump.Kicker/KickerForm.cs new file mode 100644 index 000000000000..8b93faa13288 --- /dev/null +++ b/tools/MouseJump.Kicker/KickerForm.cs @@ -0,0 +1,131 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +using MouseJump.Kicker.NativeMethods; + +using static MouseJump.Kicker.NativeMethods.Core; +using static MouseJump.Kicker.NativeMethods.Kernel32; +using static MouseJump.Kicker.NativeMethods.User32; + +namespace MouseJump.Kicker; + +public partial class KickerForm : Form +{ + public KickerForm() + { + InitializeComponent(); + } + + private Process? Process + { + get; + set; + } + + private void StartWinUI3_Click(object sender, EventArgs e) + { + // this project builds into "{RepoRoot}\tools\MouseJump.Kicker\bin\x64\Debug" + var kickerDir = AppContext.BaseDirectory; + + // step up to the repo root + var repoRoot = Path.Combine(kickerDir, "..", "..", "..", "..", ".."); + + // and back down to the mouse jump exe + var mouseJumpExe = Path.Combine(repoRoot, "x64", "Debug", "WinUI3Apps", "PowerToys.MouseJump.WinUI3.exe"); + + this.Process = this.StartMouseJump(mouseJumpExe); + } + + private Process StartMouseJump(string mouseJumpExe) + { + if (!File.Exists(mouseJumpExe)) + { + throw new FileNotFoundException($"Could not find {mouseJumpExe}"); + } + + var args = new List + { + Environment.ProcessId.ToString(CultureInfo.InvariantCulture), + }; + return Process.Start(mouseJumpExe, args); + } + + private void ActivationHotkey_Click(object sender, EventArgs e) + { + // find the winui3 window and call SetForegroundWindow on it before signalling + // the event. this mirrors the approach used in on_hotkey() in + // src/modules/MouseUtils/MouseJump/dllmain.cpp - see the comment there for a + // full explanation. + // + // in brief: the kicker is the foreground process right now (the user just + // clicked the button), so it has permission to call SetForegroundWindow and + // transfer foreground status directly to the winui3 window. doing this before + // signalling the event means that by the time ShowWindowAsync() calls + // SetForegroundWindow on its own window, the winui3 process already owns the + // foreground and the call succeeds. + // + // using AllowSetForegroundWindow here instead would be unreliable — the + // permission it grants is revoked as soon as the user generates any input + // (e.g. moving the mouse), which is almost certain to happen while the + // preview is rendering. + if (this.Process is not null) + { + var targetPid = (uint)this.Process.Id; + var winUI3Hwnd = HWND.Null; + EnumWindows( + (hwnd, _) => + { + GetWindowThreadProcessId(hwnd, out var windowPid); + if (windowPid == targetPid) + { + winUI3Hwnd = hwnd; + return false; // stop enumeration + } + + return true; // continue enumeration + }, + LPARAM.Zero); + if (!winUI3Hwnd.IsNull) + { + SetForegroundWindow(winUI3Hwnd); + } + } + + const string MOUSE_JUMP_SHOW_PREVIEW_EVENT = "Local\\MouseJumpEvent-aa0be051-3396-4976-b7ba-1a9cc7d236a5"; + var attributes = new LPSECURITY_ATTRIBUTES( + new SECURITY_ATTRIBUTES( + (uint)Marshal.SizeOf(), + LPVOID.Null, + false)); + var hEvent = Kernel32.CreateEventW( + attributes, + false, + false, + MOUSE_JUMP_SHOW_PREVIEW_EVENT); + if (hEvent.IsNull) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var result = Kernel32.SetEvent(hEvent); + if (!result) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + private void CloseMouseJump_Click(object sender, EventArgs e) + { + this.Process?.Kill(); + } +} diff --git a/tools/MouseJump.Kicker/KickerForm.resx b/tools/MouseJump.Kicker/KickerForm.resx new file mode 100644 index 000000000000..4f7675806ddb --- /dev/null +++ b/tools/MouseJump.Kicker/KickerForm.resx @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAMd2C0LGdQpWxHMJVMRyEFS6ZABUsE4AVcBsA1S+awhUvWoGVLxpBlS8aQZUvGkGVLxp + BlS8aQZUvGkGVrxpBkLKegz/yXgL/8h3C//GcwD/xIta/8aljP+7aAD/wnAI/8BuCP+/bQf/vmsH/71q + B/+9agb/vGkG/7xpBv+8aQb/0IAO/85/Df/Nfg3/ynUA/9SqhP/w8O7/yKGD/8JsAP/HdQ3/xXMK/8Ny + Cf/CcQn/wW8I/8BtCP++bAf/vmsH/9WHEP/UhhD/04QP/9F9AP/Vpnj/8/X1/+zp5v/EjVv/yXYA/8t6 + Dv/JeAv/yHcL/8Z1Cv/FdAr/xHMK/8NxCf/bjhL/2owS/9mLE//TgAD/0KV5//Pz8//3+fr/5t3U/8V/ + HP/QfgD/zn8O/81+Df/MfAz/y3sM/8p5DP/IeAv/35QV/9+TFv/ZiwD/ypZb/+Pc1f/19fb/9vb2//r7 + /P/fzb3/y34A/9SFCv/ThBD/0oMG/9GAAP/QgAz/zn4O/+KZGP/hlgD/16dv/+nn5P/09/n/9ff4//f5 + +v/6/P3/+/7//9y4kv/YhwD/14kA/8qAAP/Hhzj/zH4A/9SFD//knhz/4poA/+S3e//s2L//69O0/+zT + s//s0K3/7tGu/+7Pq//lsXT/3o8A/9WTOv/n2s7/9PHs/9zCqv/ThQD/5qMg/+WiIP/loAr/45wA/+Ob + AP/jmgD/4pkA/+GXAP/hlQD/4ZcA/+CTAP/kuYf////////////59Oz/25k7/+epJP/npyL/56Yi/+al + If/mpCD/5aMf/+WhHv/koB3/5J8d/+SfHP/jmwD/5a1f//z27v//////9OPP/+CYBP/qrif/6a0m/+ms + Jf/oqyX/6Kkk/+ioJP/npyP/56Yi/+alIf/mpCD/5aMf/+SfCP/osV//7MGH/+SkPP/jnA7/67EoQuqv + KFbqryhU6q4nVOqtJ1TprCZU6KolVOipJFTnqCNU56ciVOamIlTmpSFU5JwAVOKVAFTkngBW5KAhQgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA//+sQf//rEEAAKxBAACsQQAArEEAAKxBAACsQQAArEEAAKxBAACsQQAArEEAAKxBAACsQQAA + rEH//6xB//+sQQ== + + + \ No newline at end of file diff --git a/tools/MouseJump.Kicker/MouseJump.Kicker.csproj b/tools/MouseJump.Kicker/MouseJump.Kicker.csproj new file mode 100644 index 000000000000..7bb8230623fa --- /dev/null +++ b/tools/MouseJump.Kicker/MouseJump.Kicker.csproj @@ -0,0 +1,20 @@ + + + + + + + PowerToys.MouseJump.Kicker + PowerToys.MouseJump.Kicker + PowerToys MouseJump.Kicker + WinExe + false + false + + PerMonitorV2 + true + enable + true + disable + + diff --git a/tools/MouseJump.Kicker/MouseJump.Kicker.slnx b/tools/MouseJump.Kicker/MouseJump.Kicker.slnx new file mode 100644 index 000000000000..e13efe43adf2 --- /dev/null +++ b/tools/MouseJump.Kicker/MouseJump.Kicker.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/BOOL.cs b/tools/MouseJump.Kicker/NativeMethods/Core/BOOL.cs similarity index 96% rename from src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/BOOL.cs rename to tools/MouseJump.Kicker/NativeMethods/Core/BOOL.cs index 293ae0a22603..0fc04e0d17e9 100644 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/BOOL.cs +++ b/tools/MouseJump.Kicker/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 MouseJump.Common.NativeMethods; +namespace MouseJump.Kicker.NativeMethods; internal static partial class Core { diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/DWORD.cs b/tools/MouseJump.Kicker/NativeMethods/Core/DWORD.cs similarity index 81% rename from src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/DWORD.cs rename to tools/MouseJump.Kicker/NativeMethods/Core/DWORD.cs index 5a7ae72697ca..aab1403afc5e 100644 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/DWORD.cs +++ b/tools/MouseJump.Kicker/NativeMethods/Core/DWORD.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; -namespace MouseJump.Common.NativeMethods; +namespace MouseJump.Kicker.NativeMethods; internal static partial class Core { @@ -26,16 +26,12 @@ public DWORD(uint value) } public static int Size => - Marshal.SizeOf(typeof(DWORD)); + Marshal.SizeOf(); 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/MouseJump.Common/NativeMethods/Core/HANDLE.cs b/tools/MouseJump.Kicker/NativeMethods/Core/HANDLE.cs similarity index 94% rename from src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/HANDLE.cs rename to tools/MouseJump.Kicker/NativeMethods/Core/HANDLE.cs index d32f6f8e3ccd..d78a7b4216ad 100644 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/HANDLE.cs +++ b/tools/MouseJump.Kicker/NativeMethods/Core/HANDLE.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 MouseJump.Common.NativeMethods; +using System; + +namespace MouseJump.Kicker.NativeMethods; internal static partial class Core { diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/HWND.cs b/tools/MouseJump.Kicker/NativeMethods/Core/HWND.cs similarity index 71% rename from src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/HWND.cs rename to tools/MouseJump.Kicker/NativeMethods/Core/HWND.cs index 715f922b66e5..f3861bee1728 100644 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/HWND.cs +++ b/tools/MouseJump.Kicker/NativeMethods/Core/HWND.cs @@ -1,10 +1,10 @@ -// Copyright (c) Microsoft Corporation +// 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.Diagnostics.CodeAnalysis; +using System; -namespace MouseJump.Common.NativeMethods; +namespace MouseJump.Kicker.NativeMethods; internal static partial class Core { @@ -20,9 +20,6 @@ internal readonly struct HWND { 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) @@ -32,6 +29,10 @@ public HWND(IntPtr value) public bool IsNull => this.Value == HWND.Null.Value; + public static implicit operator IntPtr(HWND value) => value.Value; + + public static explicit operator HWND(IntPtr value) => new(value); + public override string ToString() { return $"{this.GetType().Name}({this.Value})"; diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LPARAM.cs b/tools/MouseJump.Kicker/NativeMethods/Core/LPARAM.cs similarity index 58% rename from src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LPARAM.cs rename to tools/MouseJump.Kicker/NativeMethods/Core/LPARAM.cs index f93e089b1252..53cae9bec848 100644 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Core/LPARAM.cs +++ b/tools/MouseJump.Kicker/NativeMethods/Core/LPARAM.cs @@ -1,13 +1,13 @@ -// Copyright (c) Microsoft Corporation +// 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 MouseJump.Common.NativeMethods; +namespace MouseJump.Kicker.NativeMethods; internal static partial class Core { /// - /// A message parameter. + /// A message parameter. This type contains information about a message. /// This type is declared in WinDef.h as follows: /// typedef LONG_PTR LPARAM; /// @@ -16,20 +16,18 @@ internal static partial class Core /// internal readonly struct LPARAM { - public static readonly LPARAM Null = new(IntPtr.Zero); + public static readonly LPARAM Zero = new(0); - public readonly IntPtr Value; + public readonly nint Value; - public LPARAM(IntPtr value) + public LPARAM(nint value) { this.Value = value; } - public bool IsNull => this.Value == LPARAM.Null.Value; + public static implicit operator nint(LPARAM value) => value.Value; - public static implicit operator IntPtr(LPARAM value) => value.Value; - - public static explicit operator LPARAM(IntPtr value) => new(value); + public static explicit operator LPARAM(nint value) => new(value); public override string ToString() { diff --git a/tools/MouseJump.Kicker/NativeMethods/Core/LPCWSTR.cs b/tools/MouseJump.Kicker/NativeMethods/Core/LPCWSTR.cs new file mode 100644 index 000000000000..8bc8eb5518a8 --- /dev/null +++ b/tools/MouseJump.Kicker/NativeMethods/Core/LPCWSTR.cs @@ -0,0 +1,56 @@ +// 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.Runtime.InteropServices; + +namespace MouseJump.Kicker.NativeMethods; + +internal static partial class Core +{ + /// + /// A pointer to a constant null-terminated string of 16-bit Unicode characters.For more information, see Character Sets Used By Fonts. + /// This type is declared in WinNT.h as follows: + /// typedef CONST WCHAR* LPCWSTR; + /// + /// + /// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types + /// + internal readonly struct LPCWSTR + { + public static readonly LPCWSTR Null = new(IntPtr.Zero); + + public readonly IntPtr Value; + + public LPCWSTR(IntPtr value) + { + this.Value = value; + } + + public LPCWSTR(string value) + { + this.Value = Marshal.StringToHGlobalAuto(value); + } + + public bool IsNull => this.Value == LPCWSTR.Null.Value; + + public static implicit operator IntPtr(LPCWSTR value) => value.Value; + + public static explicit operator LPCWSTR(IntPtr value) => new(value); + + public static implicit operator string?(LPCWSTR value) => Marshal.PtrToStringUni(value.Value); + + public static implicit operator LPCWSTR(string value) => new(value); + + public void Free() + { + Marshal.FreeHGlobal(this.Value); + } + + public override string ToString() + { + return $"{this.GetType().Name}({this.Value})"; + } + } +} diff --git a/tools/MouseJump.Kicker/NativeMethods/Core/LPVOID.cs b/tools/MouseJump.Kicker/NativeMethods/Core/LPVOID.cs new file mode 100644 index 000000000000..4bdb41b72cef --- /dev/null +++ b/tools/MouseJump.Kicker/NativeMethods/Core/LPVOID.cs @@ -0,0 +1,56 @@ +// 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.Runtime.InteropServices; + +namespace MouseJump.Kicker.NativeMethods; + +internal static partial class Core +{ + /// + /// A pointer to any type. + /// This type is declared in WinDef.h as follows: + /// typedef void* LPVOID; + /// + /// + /// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types + /// + internal readonly struct LPVOID + { + public static readonly LPVOID Null = new(IntPtr.Zero); + + public readonly IntPtr Value; + + public LPVOID(IntPtr value) + { + this.Value = value; + } + + public static implicit operator IntPtr(LPVOID value) => value.Value; + + public static explicit operator LPVOID(IntPtr value) => new(value); + + public static LPVOID Allocate(int length) + { + var ptr = Marshal.AllocHGlobal(length); + return new LPVOID(ptr); + } + + public string? PtrToStringUni() + { + return Marshal.PtrToStringUni(this.Value); + } + + public void Free() + { + Marshal.FreeHGlobal(this.Value); + } + + public override string ToString() + { + return $"{this.GetType().Name}({this.Value})"; + } + } +} diff --git a/tools/MouseJump.Kicker/NativeMethods/Kernel32/Security/Kernel32.LPSECURITY_ATTRIBUTES.cs b/tools/MouseJump.Kicker/NativeMethods/Kernel32/Security/Kernel32.LPSECURITY_ATTRIBUTES.cs new file mode 100644 index 000000000000..4a7306bed3b9 --- /dev/null +++ b/tools/MouseJump.Kicker/NativeMethods/Kernel32/Security/Kernel32.LPSECURITY_ATTRIBUTES.cs @@ -0,0 +1,56 @@ +// 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.Runtime.InteropServices; + +namespace MouseJump.Kicker.NativeMethods; + +internal static partial class Kernel32 +{ + internal readonly struct LPSECURITY_ATTRIBUTES + { + public static readonly LPSECURITY_ATTRIBUTES Null = new(IntPtr.Zero); + + public readonly IntPtr Value; + + public LPSECURITY_ATTRIBUTES(IntPtr value) + { + this.Value = value; + } + + public LPSECURITY_ATTRIBUTES(SECURITY_ATTRIBUTES value) + { + this.Value = LPSECURITY_ATTRIBUTES.ToPtr(value); + } + + public bool IsNull => this.Value == LPSECURITY_ATTRIBUTES.Null.Value; + + private static IntPtr ToPtr(SECURITY_ATTRIBUTES value) + { + var ptr = Marshal.AllocHGlobal(SECURITY_ATTRIBUTES.Size); + Marshal.StructureToPtr(value, ptr, false); + return ptr; + } + + public SECURITY_ATTRIBUTES ToStructure() + { + return Marshal.PtrToStructure(this.Value); + } + + public void Free() + { + Marshal.FreeHGlobal(this.Value); + } + + public static implicit operator IntPtr(LPSECURITY_ATTRIBUTES value) => value.Value; + + public static implicit operator LPSECURITY_ATTRIBUTES(IntPtr value) => new(value); + + public override string ToString() + { + return $"{this.GetType().Name}({this.Value})"; + } + } +} diff --git a/tools/MouseJump.Kicker/NativeMethods/Kernel32/Security/Kernel32.SECURITY_ATTRIBUTES.cs b/tools/MouseJump.Kicker/NativeMethods/Kernel32/Security/Kernel32.SECURITY_ATTRIBUTES.cs new file mode 100644 index 000000000000..fb27ea7b42fd --- /dev/null +++ b/tools/MouseJump.Kicker/NativeMethods/Kernel32/Security/Kernel32.SECURITY_ATTRIBUTES.cs @@ -0,0 +1,34 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +using static MouseJump.Kicker.NativeMethods.Core; + +namespace MouseJump.Kicker.NativeMethods; + +internal static partial class Kernel32 +{ + [SuppressMessage("SA1304", "SA1304:NonPrivateReadonlyFieldsMustBeginWithUpperCaseLetter", Justification = "Names match Win32 api")] + [SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Names match Win32 api")] + [StructLayout(LayoutKind.Sequential)] + internal readonly struct SECURITY_ATTRIBUTES + { + internal readonly DWORD nLength; + internal readonly LPVOID lpSecurityDescriptor; + internal readonly BOOL bInheritHandle; + + internal SECURITY_ATTRIBUTES( + DWORD nLength, LPVOID lpSecurityDescriptor, BOOL bInheritHandle) + { + this.nLength = nLength; + this.lpSecurityDescriptor = lpSecurityDescriptor; + this.bInheritHandle = bInheritHandle; + } + + public static int Size => + Marshal.SizeOf(); + } +} diff --git a/tools/MouseJump.Kicker/NativeMethods/Kernel32/SystemServices/Kernel32.CreateEventW.cs b/tools/MouseJump.Kicker/NativeMethods/Kernel32/SystemServices/Kernel32.CreateEventW.cs new file mode 100644 index 000000000000..40489c684308 --- /dev/null +++ b/tools/MouseJump.Kicker/NativeMethods/Kernel32/SystemServices/Kernel32.CreateEventW.cs @@ -0,0 +1,32 @@ +// 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.Runtime.InteropServices; +using static MouseJump.Kicker.NativeMethods.Core; + +namespace MouseJump.Kicker.NativeMethods; + +internal static partial class Kernel32 +{ + /// + /// Creates or opens a named or unnamed event object. + /// To specify an access mask for the object, use the CreateEventEx function. + /// + /// + /// If the function succeeds, the return value is a handle to the event object. + /// If the named event object existed before the function call, the function returns + /// a handle to the existing object and GetLastError returns ERROR_ALREADY_EXISTS. + /// If the function fails, the return value is NULL. + /// To get extended error information, call GetLastError. + /// + /// + /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors + /// + [LibraryImport(Libraries.Kernel32, StringMarshalling = StringMarshalling.Utf16, SetLastError = true)] + internal static partial HANDLE CreateEventW( + LPSECURITY_ATTRIBUTES lpEventAttributes, + BOOL bManualReset, + BOOL bInitialState, + LPCWSTR lpName); +} diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.GetMonitorInfoW.cs b/tools/MouseJump.Kicker/NativeMethods/Kernel32/SystemServices/Kernel32.SetEvent.cs similarity index 59% rename from src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.GetMonitorInfoW.cs rename to tools/MouseJump.Kicker/NativeMethods/Kernel32/SystemServices/Kernel32.SetEvent.cs index cd328f76ccea..f03f4cc712ef 100644 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/Graphics/Gdi/User32.GetMonitorInfoW.cs +++ b/tools/MouseJump.Kicker/NativeMethods/Kernel32/SystemServices/Kernel32.SetEvent.cs @@ -4,24 +4,24 @@ using System.Runtime.InteropServices; -using static MouseJump.Common.NativeMethods.Core; +using static MouseJump.Kicker.NativeMethods.Core; -namespace MouseJump.Common.NativeMethods; +namespace MouseJump.Kicker.NativeMethods; -internal static partial class User32 +internal static partial class Kernel32 { /// - /// The GetMonitorInfo function retrieves information about a display monitor. + /// Sets the specified event object to the signaled state. /// /// /// If the function succeeds, the return value is nonzero. /// If the function fails, the return value is zero. + /// To get extended error information, call GetLastError. /// /// /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors /// - [LibraryImport(Libraries.User32)] - internal static partial BOOL GetMonitorInfoW( - HMONITOR hMonitor, - LPMONITORINFO lpmi); + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + internal static partial Core.BOOL SetEvent( + HANDLE hEvent); } diff --git a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Libraries.cs b/tools/MouseJump.Kicker/NativeMethods/Libraries.cs similarity index 69% rename from src/modules/MouseUtils/MouseJump.Common/NativeMethods/Libraries.cs rename to tools/MouseJump.Kicker/NativeMethods/Libraries.cs index 970183cab071..aadb6c596da7 100644 --- a/src/modules/MouseUtils/MouseJump.Common/NativeMethods/Libraries.cs +++ b/tools/MouseJump.Kicker/NativeMethods/Libraries.cs @@ -2,10 +2,12 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MouseJump.Common.NativeMethods; +namespace MouseJump.Kicker.NativeMethods; internal static class Libraries { public const string Gdi32 = "gdi32"; + public const string Kernel32 = "kernel32"; + public const string Shell32 = "shell32"; public const string User32 = "user32"; } diff --git a/tools/MouseJump.Kicker/NativeMethods/User32/WindowsAndMessaging/User32.EnumWindows.cs b/tools/MouseJump.Kicker/NativeMethods/User32/WindowsAndMessaging/User32.EnumWindows.cs new file mode 100644 index 000000000000..ab15ce2e6b53 --- /dev/null +++ b/tools/MouseJump.Kicker/NativeMethods/User32/WindowsAndMessaging/User32.EnumWindows.cs @@ -0,0 +1,38 @@ +// 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.Runtime.InteropServices; + +using static MouseJump.Kicker.NativeMethods.Core; + +namespace MouseJump.Kicker.NativeMethods; + +internal static partial class User32 +{ + /// + /// An application-defined callback function for use with EnumWindows. + /// Return TRUE to continue enumeration, FALSE to stop. + /// + /// + /// See https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms633498(v=vs.85) + /// + internal delegate BOOL EnumWindowsProc(HWND hwnd, LPARAM lParam); + + /// + /// Enumerates all top-level windows on the screen, including hidden windows, + /// by passing the handle to each window in turn to an application-defined callback function. + /// + /// + /// If the function succeeds, the return value is nonzero. + /// If the function fails, the return value is zero. + /// To get extended error information, call GetLastError. + /// + /// + /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumwindows + /// + [DllImport(Libraries.User32, SetLastError = true)] + internal static extern BOOL EnumWindows( + EnumWindowsProc lpEnumFunc, + LPARAM lParam); +} diff --git a/tools/MouseJump.Kicker/NativeMethods/User32/WindowsAndMessaging/User32.GetWindowThreadProcessId.cs b/tools/MouseJump.Kicker/NativeMethods/User32/WindowsAndMessaging/User32.GetWindowThreadProcessId.cs new file mode 100644 index 000000000000..7f431b6efd5c --- /dev/null +++ b/tools/MouseJump.Kicker/NativeMethods/User32/WindowsAndMessaging/User32.GetWindowThreadProcessId.cs @@ -0,0 +1,27 @@ +// 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.Runtime.InteropServices; + +using static MouseJump.Kicker.NativeMethods.Core; + +namespace MouseJump.Kicker.NativeMethods; + +internal static partial class User32 +{ + /// + /// Retrieves the identifier of the thread that created the specified window and, + /// optionally, the identifier of the process that created the window. + /// + /// + /// The return value is the identifier of the thread that created the window. + /// + /// + /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowthreadprocessid + /// + [LibraryImport(Libraries.User32, SetLastError = true)] + internal static partial uint GetWindowThreadProcessId( + HWND hWnd, + out uint lpdwProcessId); +} diff --git a/tools/MouseJump.Kicker/NativeMethods/User32/WindowsAndMessaging/User32.SetForegroundWindow.cs b/tools/MouseJump.Kicker/NativeMethods/User32/WindowsAndMessaging/User32.SetForegroundWindow.cs new file mode 100644 index 000000000000..b39e46426956 --- /dev/null +++ b/tools/MouseJump.Kicker/NativeMethods/User32/WindowsAndMessaging/User32.SetForegroundWindow.cs @@ -0,0 +1,27 @@ +// 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.Runtime.InteropServices; + +using static MouseJump.Kicker.NativeMethods.Core; + +namespace MouseJump.Kicker.NativeMethods; + +internal static partial class User32 +{ + /// + /// Brings the thread that created the specified window into the foreground and + /// activates the window. + /// + /// + /// If the window was brought to the foreground, the return value is nonzero. + /// If the window was not brought to the foreground, the return value is zero. + /// + /// + /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setforegroundwindow + /// + [LibraryImport(Libraries.User32, SetLastError = true)] + internal static partial BOOL SetForegroundWindow( + HWND hWnd); +} diff --git a/tools/MouseJump.Kicker/Program.cs b/tools/MouseJump.Kicker/Program.cs new file mode 100644 index 000000000000..9ba2279dc32d --- /dev/null +++ b/tools/MouseJump.Kicker/Program.cs @@ -0,0 +1,23 @@ +// 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.Windows.Forms; + +namespace MouseJump.Kicker; + +internal static class Program +{ + /// + /// The main entry point for the application. + /// + [STAThread] + public static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new KickerForm()); + } +} diff --git a/tools/MouseJump.Kicker/readme.md b/tools/MouseJump.Kicker/readme.md new file mode 100644 index 000000000000..323133d6333b --- /dev/null +++ b/tools/MouseJump.Kicker/readme.md @@ -0,0 +1,11 @@ +## MouseJump.Kicker + +This is a simple "kicker" app that can be used to launch the Mouse Jump background process +and activate the hotkey without needing to build and run the entire "runner" app. + +It's intended to be used for testing and development purposes, and is not meant to be +included in the main PowerToys release footprint. + +When launching Mouse Jump via the kicker, it will look in the build output folder and +launch the last successful build. Make sure to build the Mouse Jump project before +launching the kicker - otherwise it won't run the latest version of the code. \ No newline at end of file