[MouseJump]Refactor code to allow later introduction of customizable appearance (#32838)

* [Mouse Jump] - move code shared with FancyMouse into "Common" folder (#25482)

* [Mouse Jump] - updates to NativeMethods (#25482)

* [Mouse Jump] - added new drawing / layout / style classes (#25482)

* [Mouse Jump] - new style-based preview rendering (actual preview visual style unchanged) (#25482)

* [Mouse Jump] - add words to spell checker (#25482)

* [Mouse Jump] - small tweak to error handling (#25482)

* [Mouse Jump] - fixed failing test (#25482)
This commit is contained in:
Michael Clayton 2024-06-12 16:30:18 +01:00 committed by GitHub
parent 3e07b9b8f4
commit 651f2e4bd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 2968 additions and 954 deletions

View File

@ -124,6 +124,7 @@ bootstrapper
BOOTSTRAPPERINSTALLFOLDER
bostrot
BOTTOMALIGN
boxmodel
BPBF
bpmf
bpp

View File

@ -0,0 +1,152 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Common.Helpers;
using MouseJumpUI.Common.Imaging;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Styles;
using MouseJumpUI.Helpers;
namespace MouseJumpUI.UnitTests.Common.Helpers;
[TestClass]
public static class DrawingHelperTests
{
[TestClass]
public sealed class GetPreviewLayoutTests
{
public sealed class TestCase
{
public TestCase(PreviewStyle previewStyle, List<RectangleInfo> screens, PointInfo activatedLocation, string desktopImageFilename, string expectedImageFilename)
{
this.PreviewStyle = previewStyle;
this.Screens = screens;
this.ActivatedLocation = activatedLocation;
this.DesktopImageFilename = desktopImageFilename;
this.ExpectedImageFilename = expectedImageFilename;
}
public PreviewStyle PreviewStyle { get; }
public List<RectangleInfo> Screens { get; }
public PointInfo ActivatedLocation { get; }
public string DesktopImageFilename { get; }
public string ExpectedImageFilename { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
/* 4-grid */
yield return new object[]
{
new TestCase(
previewStyle: StyleHelper.DefaultPreviewStyle,
screens: new List<RectangleInfo>()
{
new(0, 0, 500, 500),
new(500, 0, 500, 500),
new(500, 500, 500, 500),
new(0, 500, 500, 500),
},
activatedLocation: new(x: 50, y: 50),
desktopImageFilename: "Common/Helpers/_test-4grid-desktop.png",
expectedImageFilename: "Common/Helpers/_test-4grid-expected.png"),
};
/* win 11 */
yield return new object[]
{
new TestCase(
previewStyle: StyleHelper.DefaultPreviewStyle,
screens: new List<RectangleInfo>()
{
new(5120, 349, 1920, 1080),
new(0, 0, 5120, 1440),
},
activatedLocation: new(x: 50, y: 50),
desktopImageFilename: "Common/Helpers/_test-win11-desktop.png",
expectedImageFilename: "Common/Helpers/_test-win11-expected.png"),
};
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
// load the fake desktop image
using var desktopImage = GetPreviewLayoutTests.LoadImageResource(data.DesktopImageFilename);
// draw the preview image
var previewLayout = LayoutHelper.GetPreviewLayout(
previewStyle: data.PreviewStyle,
screens: data.Screens,
activatedLocation: data.ActivatedLocation);
var imageCopyService = new StaticImageRegionCopyService(desktopImage);
using var actual = DrawingHelper.RenderPreview(previewLayout, imageCopyService);
// load the expected image
var expected = GetPreviewLayoutTests.LoadImageResource(data.ExpectedImageFilename);
// compare the images
var screens = System.Windows.Forms.Screen.AllScreens;
AssertImagesEqual(expected, actual);
}
private static Bitmap LoadImageResource(string filename)
{
var assembly = Assembly.GetExecutingAssembly();
var assemblyName = new AssemblyName(assembly.FullName ?? throw new InvalidOperationException());
var resourceName = $"Microsoft.{assemblyName.Name}.{filename.Replace("/", ".")}";
var resourceNames = assembly.GetManifestResourceNames();
if (!resourceNames.Contains(resourceName))
{
throw new InvalidOperationException($"Embedded resource '{resourceName}' does not exist.");
}
var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException();
var image = (Bitmap)Image.FromStream(stream);
return image;
}
/// <summary>
/// Naive / brute force image comparison - we can optimise this later :-)
/// </summary>
private static void AssertImagesEqual(Bitmap expected, Bitmap actual)
{
Assert.AreEqual(
expected.Width,
actual.Width,
$"expected width: {expected.Width}, actual width: {actual.Width}");
Assert.AreEqual(
expected.Height,
actual.Height,
$"expected height: {expected.Height}, actual height: {actual.Height}");
for (var y = 0; y < expected.Height; y++)
{
for (var x = 0; x < expected.Width; x++)
{
var expectedPixel = expected.GetPixel(x, y);
var actualPixel = actual.GetPixel(x, y);
// allow a small tolerance for rounding differences in gdi
Assert.IsTrue(
(Math.Abs(expectedPixel.A - actualPixel.A) <= 1) &&
(Math.Abs(expectedPixel.R - actualPixel.R) <= 1) &&
(Math.Abs(expectedPixel.G - actualPixel.G) <= 1) &&
(Math.Abs(expectedPixel.B - actualPixel.B) <= 1),
$"images differ at pixel ({x}, {y}) - expected: {expectedPixel}, actual: {actualPixel}");
}
}
}
}
}

View File

@ -0,0 +1,452 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Drawing;
using System.Text.Json;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Common.Helpers;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Layout;
using MouseJumpUI.Common.Models.Styles;
namespace MouseJumpUI.UnitTests.Common.Helpers;
[TestClass]
public static class LayoutHelperTests
{
/*
[TestClass]
public sealed class OldLayoutTests
{
public static IEnumerable<object[]> GetTestCases()
{
// check we handle rounding errors in scaling the preview form
// that might make the form *larger* than the current screen -
// e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768
// with a 5px form padding border:
//
// ((decimal)1014 / 7168) * 7168 = 1014.0000000000000000000000002
//
// +----------------+
// | |
// | 1 +-------+
// | | 0 |
// +----------------+-------+
layoutConfig = new LayoutConfig(
virtualScreenBounds: new(0, 0, 7168, 1440),
screens: new List<ScreenInfo>
{
new(HMONITOR.Null, false, new(6144, 0, 1024, 768), new(6144, 0, 1024, 768)),
new(HMONITOR.Null, false, new(0, 0, 6144, 1440), new(0, 0, 6144, 1440)),
},
activatedLocation: new(6656, 384),
activatedScreenIndex: 0,
activatedScreenNumber: 1,
maximumFormSize: new(1600, 1200),
formPadding: new(5, 5, 5, 5),
previewPadding: new(0, 0, 0, 0));
layoutInfo = new LayoutInfo(
layoutConfig: layoutConfig,
formBounds: new(6144, 277.14732M, 1024, 213.70535M),
previewBounds: new(0, 0, 1014, 203.70535M),
screenBounds: new List<RectangleInfo>
{
new(869.14285M, 0, 144.85714M, 108.642857M),
new(0, 0, 869.142857M, 203.705357M),
},
activatedScreenBounds: new(6144, 0, 1024, 768));
yield return new object[] { new TestCase(layoutConfig, layoutInfo) };
// check we handle rounding errors in scaling the preview form
// that might make the form a pixel *smaller* than the current screen -
// e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768
// with a 5px form padding border:
//
// ((decimal)1280 / 7424) * 7424 = 1279.9999999999999999999999999
//
// +----------------+
// | |
// | 1 +-------+
// | | 0 |
// +----------------+-------+
layoutConfig = new LayoutConfig(
virtualScreenBounds: new(0, 0, 7424, 1440),
screens: new List<ScreenInfo>
{
new(HMONITOR.Null, false, new(6144, 0, 1280, 768), new(6144, 0, 1280, 768)),
new(HMONITOR.Null, false, new(0, 0, 6144, 1440), new(0, 0, 6144, 1440)),
},
activatedLocation: new(6784, 384),
activatedScreenIndex: 0,
activatedScreenNumber: 1,
maximumFormSize: new(1600, 1200),
formPadding: new(5, 5, 5, 5),
previewPadding: new(0, 0, 0, 0));
layoutInfo = new LayoutInfo(
layoutConfig: layoutConfig,
formBounds: new(
6144,
255.83189M, // (768 - (((decimal)(1280-10) / 7424 * 1440) + 10)) / 2
1280,
256.33620M // ((decimal)(1280 - 10) / 7424 * 1440) + 10
),
previewBounds: new(0, 0, 1270, 246.33620M),
screenBounds: new List<RectangleInfo>
{
new(1051.03448M, 0, 218.96551M, 131.37931M),
new(0, 0M, 1051.03448M, 246.33620M),
},
activatedScreenBounds: new(6144, 0, 1280, 768));
yield return new object[] { new TestCase(layoutConfig, layoutInfo) };
}
}
*/
[TestClass]
public sealed class GetPreviewLayoutTests
{
public sealed class TestCase
{
public TestCase(PreviewStyle previewStyle, List<RectangleInfo> screens, PointInfo activatedLocation, PreviewLayout expectedResult)
{
this.PreviewStyle = previewStyle;
this.Screens = screens;
this.ActivatedLocation = activatedLocation;
this.ExpectedResult = expectedResult;
}
public PreviewStyle PreviewStyle { get; }
public List<RectangleInfo> Screens { get; }
public PointInfo ActivatedLocation { get; }
public PreviewLayout ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// happy path - single screen with 50% scaling,
// *has* a preview borders but *no* screenshot borders
//
// +----------------+
// | |
// | 0 |
// | |
// +----------------+
var previewStyle = new PreviewStyle(
canvasSize: new(
width: 524,
height: 396
),
canvasStyle: new(
marginStyle: MarginStyle.Empty,
borderStyle: new(
color: SystemColors.Highlight,
all: 5,
depth: 3),
paddingStyle: new(
all: 1),
backgroundStyle: new(
color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue
)
),
screenStyle: BoxStyle.Empty);
var screens = new List<RectangleInfo>
{
new(0, 0, 1024, 768),
};
var activatedLocation = new PointInfo(512, 384);
var previewLayout = new PreviewLayout(
virtualScreen: new(0, 0, 1024, 768),
screens: screens,
activatedScreenIndex: 0,
formBounds: new(250, 186, 524, 396),
previewStyle: previewStyle,
previewBounds: new(
outerBounds: new(0, 0, 524, 396),
marginBounds: new(0, 0, 524, 396),
borderBounds: new(0, 0, 524, 396),
paddingBounds: new(5, 5, 514, 386),
contentBounds: new(6, 6, 512, 384)
),
screenshotBounds: new()
{
new(
outerBounds: new(6, 6, 512, 384),
marginBounds: new(6, 6, 512, 384),
borderBounds: new(6, 6, 512, 384),
paddingBounds: new(6, 6, 512, 384),
contentBounds: new(6, 6, 512, 384)
),
});
yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, previewLayout) };
// happy path - single screen with 50% scaling,
// *no* preview borders but *has* screenshot borders
//
// +----------------+
// | |
// | 0 |
// | |
// +----------------+
previewStyle = new PreviewStyle(
canvasSize: new(
width: 512,
height: 384
),
canvasStyle: BoxStyle.Empty,
screenStyle: new(
marginStyle: new(
all: 1),
borderStyle: new(
color: SystemColors.Highlight,
all: 5,
depth: 3),
paddingStyle: PaddingStyle.Empty,
backgroundStyle: new(
color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue
)
));
screens = new List<RectangleInfo>
{
new(0, 0, 1024, 768),
};
activatedLocation = new PointInfo(512, 384);
previewLayout = new PreviewLayout(
virtualScreen: new(0, 0, 1024, 768),
screens: screens,
activatedScreenIndex: 0,
formBounds: new(256, 192, 512, 384),
previewStyle: previewStyle,
previewBounds: new(
outerBounds: new(0, 0, 512, 384),
marginBounds: new(0, 0, 512, 384),
borderBounds: new(0, 0, 512, 384),
paddingBounds: new(0, 0, 512, 384),
contentBounds: new(0, 0, 512, 384)
),
screenshotBounds: new()
{
new(
outerBounds: new(0, 0, 512, 384),
marginBounds: new(0, 0, 512, 384),
borderBounds: new(1, 1, 510, 382),
paddingBounds: new(6, 6, 500, 372),
contentBounds: new(6, 6, 500, 372)
),
});
yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, previewLayout) };
// primary monitor not topmost / leftmost - if there are screens
// that are further left or higher up than the primary monitor
// they'll have negative coordinates which has caused some
// issues with calculations in the past. this test will make
// sure we handle screens with negative coordinates gracefully
//
// +-------+
// | 0 +----------------+
// +-------+ |
// | 1 |
// | |
// +----------------+
previewStyle = new PreviewStyle(
canvasSize: new(
width: 716,
height: 204
),
canvasStyle: new(
marginStyle: MarginStyle.Empty,
borderStyle: new(
color: SystemColors.Highlight,
all: 5,
depth: 3),
paddingStyle: new(
all: 1),
backgroundStyle: new(
color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue
)
),
screenStyle: new(
marginStyle: new(
all: 1),
borderStyle: new(
color: SystemColors.Highlight,
all: 5,
depth: 3),
paddingStyle: PaddingStyle.Empty,
backgroundStyle: new(
color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue
)
));
screens = new List<RectangleInfo>
{
new(-1920, -480, 1920, 1080),
new(0, 0, 5120, 1440),
};
activatedLocation = new(-960, 60);
previewLayout = new PreviewLayout(
virtualScreen: new(-1920, -480, 7040, 1920),
screens: screens,
activatedScreenIndex: 0,
formBounds: new(-1318, -42, 716, 204),
previewStyle: previewStyle,
previewBounds: new(
outerBounds: new(0, 0, 716, 204),
marginBounds: new(0, 0, 716, 204),
borderBounds: new(0, 0, 716, 204),
paddingBounds: new(5, 5, 706, 194),
contentBounds: new(6, 6, 704, 192)
),
screenshotBounds: new()
{
new(
outerBounds: new(6, 6, 192, 108),
marginBounds: new(6, 6, 192, 108),
borderBounds: new(7, 7, 190, 106),
paddingBounds: new(12, 12, 180, 96),
contentBounds: new(12, 12, 180, 96)
),
new(
outerBounds: new(198, 54, 512, 144),
marginBounds: new(198, 54, 512, 144),
borderBounds: new(199, 55, 510, 142),
paddingBounds: new(204, 60, 500, 132),
contentBounds: new(204, 60, 500, 132)
),
});
yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, previewLayout) };
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
// note - even if values are within 0.0001M of each other they could
// still round to different values - e.g.
// (int)1279.999999999999 -> 1279
// vs
// (int)1280.000000000000 -> 1280
// so we'll compare the raw values, *and* convert to an int-based
// Rectangle to compare rounded values
var actual = LayoutHelper.GetPreviewLayout(data.PreviewStyle, data.Screens, data.ActivatedLocation);
var expected = data.ExpectedResult;
var options = new JsonSerializerOptions
{
WriteIndented = true,
};
Assert.AreEqual(
JsonSerializer.Serialize(expected, options),
JsonSerializer.Serialize(actual, options));
}
}
[TestClass]
public sealed class GetBoxBoundsFromContentBoundsTests
{
public sealed class TestCase
{
public TestCase(RectangleInfo contentBounds, BoxStyle boxStyle, BoxBounds expectedResult)
{
this.ContentBounds = contentBounds;
this.BoxStyle = boxStyle;
this.ExpectedResult = expectedResult;
}
public RectangleInfo ContentBounds { get; set; }
public BoxStyle BoxStyle { get; set; }
public BoxBounds ExpectedResult { get; set; }
}
public static IEnumerable<object[]> GetTestCases()
{
yield return new[]
{
new TestCase(
contentBounds: new(100, 100, 800, 600),
boxStyle: new(
marginStyle: new(3),
borderStyle: new(Color.Red, 5, 0),
paddingStyle: new(7),
backgroundStyle: BackgroundStyle.Empty),
expectedResult: new(
outerBounds: new(85, 85, 830, 630),
marginBounds: new(85, 85, 830, 630),
borderBounds: new(88, 88, 824, 624),
paddingBounds: new(93, 93, 814, 614),
contentBounds: new(100, 100, 800, 600))),
};
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
var actual = LayoutHelper.GetBoxBoundsFromContentBounds(data.ContentBounds, data.BoxStyle);
var expected = data.ExpectedResult;
Assert.AreEqual(
JsonSerializer.Serialize(expected),
JsonSerializer.Serialize(actual));
}
}
[TestClass]
public sealed class GetBoxBoundsFromOuterBoundsTests
{
public sealed class TestCase
{
public TestCase(RectangleInfo outerBounds, BoxStyle boxStyle, BoxBounds expectedResult)
{
this.OuterBounds = outerBounds;
this.BoxStyle = boxStyle;
this.ExpectedResult = expectedResult;
}
public RectangleInfo OuterBounds { get; set; }
public BoxStyle BoxStyle { get; set; }
public BoxBounds ExpectedResult { get; set; }
}
public static IEnumerable<object[]> GetTestCases()
{
yield return new[]
{
new TestCase(
outerBounds: new(85, 85, 830, 630),
boxStyle: new(
marginStyle: new(3),
borderStyle: new(Color.Red, 5, 0),
paddingStyle: new(7),
backgroundStyle: BackgroundStyle.Empty),
expectedResult: new(
outerBounds: new(85, 85, 830, 630),
marginBounds: new(85, 85, 830, 630),
borderBounds: new(88, 88, 824, 624),
paddingBounds: new(93, 93, 814, 614),
contentBounds: new(100, 100, 800, 600))),
};
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
var actual = LayoutHelper.GetBoxBoundsFromOuterBounds(data.OuterBounds, data.BoxStyle);
var expected = data.ExpectedResult;
Assert.AreEqual(
JsonSerializer.Serialize(expected),
JsonSerializer.Serialize(actual));
}
}
}

View File

@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Common.Helpers;
using MouseJumpUI.Common.Models.Drawing;
namespace MouseJumpUI.UnitTests.Common.Helpers;
[TestClass]
public static class MouseHelperTests
{
[TestClass]
public sealed class GetJumpLocationTests
{
public sealed class TestCase
{
public TestCase(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds, PointInfo expectedResult)
{
this.PreviewLocation = previewLocation;
this.PreviewSize = previewSize;
this.DesktopBounds = desktopBounds;
this.ExpectedResult = expectedResult;
}
public PointInfo PreviewLocation { get; }
public SizeInfo PreviewSize { get; }
public RectangleInfo DesktopBounds { get; }
public PointInfo ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// screen corners and midpoint with a zero origin
yield return new object[] { new TestCase(new(0, 0), new(160, 120), new(0, 0, 1600, 1200), new(0, 0)) };
yield return new object[] { new TestCase(new(160, 0), new(160, 120), new(0, 0, 1600, 1200), new(1600, 0)) };
yield return new object[] { new TestCase(new(0, 120), new(160, 120), new(0, 0, 1600, 1200), new(0, 1200)) };
yield return new object[] { new TestCase(new(160, 120), new(160, 120), new(0, 0, 1600, 1200), new(1600, 1200)) };
yield return new object[] { new TestCase(new(80, 60), new(160, 120), new(0, 0, 1600, 1200), new(800, 600)) };
// screen corners and midpoint with a positive origin
yield return new object[] { new TestCase(new(0, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 1000)) };
yield return new object[] { new TestCase(new(160, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 1000)) };
yield return new object[] { new TestCase(new(0, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 2200)) };
yield return new object[] { new TestCase(new(160, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 2200)) };
yield return new object[] { new TestCase(new(80, 60), new(160, 120), new(1000, 1000, 1600, 1200), new(1800, 1600)) };
// screen corners and midpoint with a negative origin
yield return new object[] { new TestCase(new(0, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, -1000)) };
yield return new object[] { new TestCase(new(160, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, -1000)) };
yield return new object[] { new TestCase(new(0, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, 200)) };
yield return new object[] { new TestCase(new(160, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, 200)) };
yield return new object[] { new TestCase(new(80, 60), new(160, 120), new(-1000, -1000, 1600, 1200), new(-200, -400)) };
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
var actual = MouseHelper.GetJumpLocation(
data.PreviewLocation,
data.PreviewSize,
data.DesktopBounds);
var expected = data.ExpectedResult;
Assert.AreEqual(expected.X, actual.X);
Assert.AreEqual(expected.Y, actual.Y);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

View File

@ -4,9 +4,9 @@
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.Common.Models.Drawing;
namespace MouseJumpUI.UnitTests.Models.Drawing;
namespace MouseJumpUI.UnitTests.Common.Models.Drawing;
[TestClass]
public static class RectangleInfoTests
@ -23,30 +23,30 @@ public static class RectangleInfoTests
this.ExpectedResult = expectedResult;
}
public RectangleInfo Rectangle { get; set; }
public RectangleInfo Rectangle { get; }
public PointInfo Point { get; set; }
public PointInfo Point { get; }
public RectangleInfo ExpectedResult { get; set; }
public RectangleInfo ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// zero-sized
yield return new[] { new TestCase(new(0, 0, 0, 0), new(0, 0), new(0, 0, 0, 0)), };
yield return new object[] { new TestCase(new(0, 0, 0, 0), new(0, 0), new(0, 0, 0, 0)), };
// zero-origin
yield return new[] { new TestCase(new(0, 0, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
yield return new[] { new TestCase(new(0, 0, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
yield return new[] { new TestCase(new(0, 0, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
yield return new object[] { new TestCase(new(0, 0, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
yield return new object[] { new TestCase(new(0, 0, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
yield return new object[] { new TestCase(new(0, 0, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
// non-zero origin
yield return new[] { new TestCase(new(1000, 2000, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
yield return new[] { new TestCase(new(1000, 2000, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
yield return new[] { new TestCase(new(1000, 2000, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
yield return new object[] { new TestCase(new(1000, 2000, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
yield return new object[] { new TestCase(new(1000, 2000, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
yield return new object[] { new TestCase(new(1000, 2000, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
// negative result
yield return new[] { new TestCase(new(0, 0, 1000, 1200), new(300, 300), new(-200, -300, 1000, 1200)), };
yield return new object[] { new TestCase(new(0, 0, 1000, 1200), new(300, 300), new(-200, -300, 1000, 1200)), };
}
[TestMethod]
@ -74,53 +74,53 @@ public static class RectangleInfoTests
this.ExpectedResult = expectedResult;
}
public RectangleInfo Inner { get; set; }
public RectangleInfo Inner { get; }
public RectangleInfo Outer { get; set; }
public RectangleInfo Outer { get; }
public RectangleInfo ExpectedResult { get; set; }
public RectangleInfo ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// already inside - obj fills bounds exactly
yield return new[]
yield return new object[]
{
new TestCase(new(0, 0, 100, 100), new(0, 0, 100, 100), new(0, 0, 100, 100)),
};
// already inside - obj exactly in each corner
yield return new[]
yield return new object[]
{
new TestCase(new(0, 0, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)),
};
yield return new[]
yield return new object[]
{
new TestCase(new(100, 0, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)),
};
yield return new[]
yield return new object[]
{
new TestCase(new(0, 100, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)),
};
yield return new[]
yield return new object[]
{
new TestCase(new(100, 100, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)),
};
// move inside - obj outside each corner
yield return new[]
yield return new object[]
{
new TestCase(new(-50, -50, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)),
};
yield return new[]
yield return new object[]
{
new TestCase(new(250, -50, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)),
};
yield return new[]
yield return new object[]
{
new TestCase(new(-50, 250, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)),
};
yield return new[]
yield return new object[]
{
new TestCase(new(150, 150, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)),
};

View File

@ -4,9 +4,9 @@
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.Common.Models.Drawing;
namespace MouseJumpUI.UnitTests.Drawing;
namespace MouseJumpUI.UnitTests.Common.Models.Drawing;
[TestClass]
public static class SizeInfoTests
@ -23,28 +23,28 @@ public static class SizeInfoTests
this.ExpectedResult = expectedResult;
}
public SizeInfo Obj { get; set; }
public SizeInfo Obj { get; }
public SizeInfo Bounds { get; set; }
public SizeInfo Bounds { get; }
public SizeInfo ExpectedResult { get; set; }
public SizeInfo ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// identity tests
yield return new[] { new TestCase(new(512, 384), new(512, 384), new(512, 384)), };
yield return new[] { new TestCase(new(1024, 768), new(1024, 768), new(1024, 768)), };
yield return new object[] { new TestCase(new(512, 384), new(512, 384), new(512, 384)), };
yield return new object[] { new TestCase(new(1024, 768), new(1024, 768), new(1024, 768)), };
// general tests
yield return new[] { new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536)), };
yield return new[] { new TestCase(new(2048, 1536), new(1024, 768), new(1024, 768)), };
yield return new object[] { new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536)), };
yield return new object[] { new TestCase(new(2048, 1536), new(1024, 768), new(1024, 768)), };
// scale to fit width
yield return new[] { new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)), };
yield return new object[] { new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)), };
// scale to fit height
yield return new[] { new TestCase(new(512, 384), new(4096, 1536), new(2048, 1536)), };
yield return new object[] { new TestCase(new(512, 384), new(4096, 1536), new(2048, 1536)), };
}
[TestMethod]
@ -70,28 +70,28 @@ public static class SizeInfoTests
this.ExpectedResult = expectedResult;
}
public SizeInfo Obj { get; set; }
public SizeInfo Obj { get; }
public SizeInfo Bounds { get; set; }
public SizeInfo Bounds { get; }
public decimal ExpectedResult { get; set; }
public decimal ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// identity tests
yield return new[] { new TestCase(new(512, 384), new(512, 384), 1), };
yield return new[] { new TestCase(new(1024, 768), new(1024, 768), 1), };
yield return new object[] { new TestCase(new(512, 384), new(512, 384), 1), };
yield return new object[] { new TestCase(new(1024, 768), new(1024, 768), 1), };
// general tests
yield return new[] { new TestCase(new(512, 384), new(2048, 1536), 4), };
yield return new[] { new TestCase(new(2048, 1536), new(1024, 768), 0.5M), };
yield return new object[] { new TestCase(new(512, 384), new(2048, 1536), 4), };
yield return new object[] { new TestCase(new(2048, 1536), new(1024, 768), 0.5M), };
// scale to fit width
yield return new[] { new TestCase(new(512, 384), new(2048, 3072), 4), };
yield return new object[] { new TestCase(new(512, 384), new(2048, 3072), 4), };
// scale to fit height
yield return new[] { new TestCase(new(512, 384), new(4096, 1536), 4), };
yield return new object[] { new TestCase(new(512, 384), new(4096, 1536), 4), };
}
[TestMethod]

View File

@ -1,229 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Helpers;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.Models.Layout;
using MouseJumpUI.Models.Screen;
using static MouseJumpUI.NativeMethods.Core;
namespace MouseJumpUI.UnitTests.Helpers;
[TestClass]
public static class DrawingHelperTests
{
[TestClass]
public sealed class CalculateLayoutInfoTests
{
public sealed class TestCase
{
public TestCase(LayoutConfig layoutConfig, LayoutInfo expectedResult)
{
this.LayoutConfig = layoutConfig;
this.ExpectedResult = expectedResult;
}
public LayoutConfig LayoutConfig { get; set; }
public LayoutInfo ExpectedResult { get; set; }
}
public static IEnumerable<object[]> GetTestCases()
{
// happy path - check the preview form is shown
// at the correct size and position on a single screen
//
// +----------------+
// | |
// | 0 |
// | |
// +----------------+
var layoutConfig = new LayoutConfig(
virtualScreenBounds: new(0, 0, 5120, 1440),
screens: new List<ScreenInfo>
{
new ScreenInfo(HMONITOR.Null, false, new(0, 0, 5120, 1440), new(0, 0, 5120, 1440)),
},
activatedLocation: new(5120 / 2, 1440 / 2),
activatedScreenIndex: 0,
activatedScreenNumber: 1,
maximumFormSize: new(1600, 1200),
formPadding: new(5, 5, 5, 5),
previewPadding: new(0, 0, 0, 0));
var layoutInfo = new LayoutInfo(
layoutConfig: layoutConfig,
formBounds: new(1760, 491.40625M, 1600, 457.1875M),
previewBounds: new(0, 0, 1590, 447.1875M),
screenBounds: new List<RectangleInfo>
{
new(0, 0, 1590, 447.1875M),
},
activatedScreenBounds: new(0, 0, 5120, 1440));
yield return new[] { new TestCase(layoutConfig, layoutInfo) };
// primary monitor not topmost / leftmost - if there are screens
// that are further left or higher than the primary monitor
// they'll have negative coordinates which has caused some
// issues with calculations in the past. this test will make
// sure we handle negative coordinates gracefully
//
// +-------+
// | 0 +----------------+
// +-------+ |
// | 1 |
// | |
// +----------------+
layoutConfig = new LayoutConfig(
virtualScreenBounds: new(-1920, -472, 7040, 1912),
screens: new List<ScreenInfo>
{
new ScreenInfo(HMONITOR.Null, false, new(-1920, -472, 1920, 1080), new(-1920, -472, 1920, 1080)),
new ScreenInfo(HMONITOR.Null, false, new(0, 0, 5120, 1440), new(0, 0, 5120, 1440)),
},
activatedLocation: new(-960, -236),
activatedScreenIndex: 0,
activatedScreenNumber: 1,
maximumFormSize: new(1600, 1200),
formPadding: new(5, 5, 5, 5),
previewPadding: new(0, 0, 0, 0));
layoutInfo = new LayoutInfo(
layoutConfig: layoutConfig,
formBounds: new(
-1760,
-456.91477M, // -236 - (((decimal)(1600-10) / 7040 * 1912) + 10) / 2
1600,
441.829545M // ((decimal)(1600-10) / 7040 * 1912) + 10
),
previewBounds: new(0, 0, 1590, 431.829545M),
screenBounds: new List<RectangleInfo>
{
new(0, 0, 433.63636M, 243.92045M),
new(433.63636M, 106.602270M, 1156.36363M, 325.22727M),
},
activatedScreenBounds: new(-1920, -472, 1920, 1080));
yield return new[] { new TestCase(layoutConfig, layoutInfo) };
// check we handle rounding errors in scaling the preview form
// that might make the form *larger* than the current screen -
// e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768
// with a 5px form padding border:
//
// ((decimal)1014 / 7168) * 7168 = 1014.0000000000000000000000002
//
// +----------------+
// | |
// | 1 +-------+
// | | 0 |
// +----------------+-------+
layoutConfig = new LayoutConfig(
virtualScreenBounds: new(0, 0, 7168, 1440),
screens: new List<ScreenInfo>
{
new ScreenInfo(HMONITOR.Null, false, new(6144, 0, 1024, 768), new(6144, 0, 1024, 768)),
new ScreenInfo(HMONITOR.Null, false, new(0, 0, 6144, 1440), new(0, 0, 6144, 1440)),
},
activatedLocation: new(6656, 384),
activatedScreenIndex: 0,
activatedScreenNumber: 1,
maximumFormSize: new(1600, 1200),
formPadding: new(5, 5, 5, 5),
previewPadding: new(0, 0, 0, 0));
layoutInfo = new LayoutInfo(
layoutConfig: layoutConfig,
formBounds: new(6144, 277.14732M, 1024, 213.70535M),
previewBounds: new(0, 0, 1014, 203.70535M),
screenBounds: new List<RectangleInfo>
{
new(869.14285M, 0, 144.85714M, 108.642857M),
new(0, 0, 869.142857M, 203.705357M),
},
activatedScreenBounds: new(6144, 0, 1024, 768));
yield return new[] { new TestCase(layoutConfig, layoutInfo) };
// check we handle rounding errors in scaling the preview form
// that might make the form a pixel *smaller* than the current screen -
// e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768
// with a 5px form padding border:
//
// ((decimal)1280 / 7424) * 7424 = 1279.9999999999999999999999999
//
// +----------------+
// | |
// | 1 +-------+
// | | 0 |
// +----------------+-------+
layoutConfig = new LayoutConfig(
virtualScreenBounds: new(0, 0, 7424, 1440),
screens: new List<ScreenInfo>
{
new ScreenInfo(HMONITOR.Null, false, new(6144, 0, 1280, 768), new(6144, 0, 1280, 768)),
new ScreenInfo(HMONITOR.Null, false, new(0, 0, 6144, 1440), new(0, 0, 6144, 1440)),
},
activatedLocation: new(6784, 384),
activatedScreenIndex: 0,
activatedScreenNumber: 1,
maximumFormSize: new(1600, 1200),
formPadding: new(5, 5, 5, 5),
previewPadding: new(0, 0, 0, 0));
layoutInfo = new LayoutInfo(
layoutConfig: layoutConfig,
formBounds: new(
6144,
255.83189M, // (768 - (((decimal)(1280-10) / 7424 * 1440) + 10)) / 2
1280,
256.33620M // ((decimal)(1280 - 10) / 7424 * 1440) + 10
),
previewBounds: new(0, 0, 1270, 246.33620M),
screenBounds: new List<RectangleInfo>
{
new(1051.03448M, 0, 218.96551M, 131.37931M),
new(0, 0M, 1051.03448M, 246.33620M),
},
activatedScreenBounds: new(6144, 0, 1280, 768));
yield return new[] { new TestCase(layoutConfig, layoutInfo) };
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
// note - even if values are within 0.0001M of each other they could
// still round to different values - e.g.
// (int)1279.999999999999 -> 1279
// vs
// (int)1280.000000000000 -> 1280
// so we'll compare the raw values, *and* convert to an int-based
// Rectangle to compare rounded values
var actual = LayoutHelper.CalculateLayoutInfo(data.LayoutConfig);
var expected = data.ExpectedResult;
Assert.AreEqual(expected.FormBounds.X, actual.FormBounds.X, 0.00001M, "FormBounds.X");
Assert.AreEqual(expected.FormBounds.Y, actual.FormBounds.Y, 0.00001M, "FormBounds.Y");
Assert.AreEqual(expected.FormBounds.Width, actual.FormBounds.Width, 0.00001M, "FormBounds.Width");
Assert.AreEqual(expected.FormBounds.Height, actual.FormBounds.Height, 0.00001M, "FormBounds.Height");
Assert.AreEqual(expected.FormBounds.ToRectangle(), actual.FormBounds.ToRectangle(), "FormBounds.ToRectangle");
Assert.AreEqual(expected.PreviewBounds.X, actual.PreviewBounds.X, 0.00001M, "PreviewBounds.X");
Assert.AreEqual(expected.PreviewBounds.Y, actual.PreviewBounds.Y, 0.00001M, "PreviewBounds.Y");
Assert.AreEqual(expected.PreviewBounds.Width, actual.PreviewBounds.Width, 0.00001M, "PreviewBounds.Width");
Assert.AreEqual(expected.PreviewBounds.Height, actual.PreviewBounds.Height, 0.00001M, "PreviewBounds.Height");
Assert.AreEqual(expected.PreviewBounds.ToRectangle(), actual.PreviewBounds.ToRectangle(), "PreviewBounds.ToRectangle");
Assert.AreEqual(expected.ScreenBounds.Count, actual.ScreenBounds.Count, "ScreenBounds.Count");
for (var i = 0; i < expected.ScreenBounds.Count; i++)
{
Assert.AreEqual(expected.ScreenBounds[i].X, actual.ScreenBounds[i].X, 0.00001M, $"ScreenBounds[{i}].X");
Assert.AreEqual(expected.ScreenBounds[i].Y, actual.ScreenBounds[i].Y, 0.00001M, $"ScreenBounds[{i}].Y");
Assert.AreEqual(expected.ScreenBounds[i].Width, actual.ScreenBounds[i].Width, 0.00001M, $"ScreenBounds[{i}].Width");
Assert.AreEqual(expected.ScreenBounds[i].Height, actual.ScreenBounds[i].Height, 0.00001M, $"ScreenBounds[{i}].Height");
Assert.AreEqual(expected.ScreenBounds[i].ToRectangle(), actual.ScreenBounds[i].ToRectangle(), "ActivatedScreen.ToRectangle");
}
Assert.AreEqual(expected.ActivatedScreenBounds.X, actual.ActivatedScreenBounds.X, "ActivatedScreen.X");
Assert.AreEqual(expected.ActivatedScreenBounds.Y, actual.ActivatedScreenBounds.Y, "ActivatedScreen.Y");
Assert.AreEqual(expected.ActivatedScreenBounds.Width, actual.ActivatedScreenBounds.Width, "ActivatedScreen.Width");
Assert.AreEqual(expected.ActivatedScreenBounds.Height, actual.ActivatedScreenBounds.Height, "ActivatedScreen.Height");
Assert.AreEqual(expected.ActivatedScreenBounds.ToRectangle(), actual.ActivatedScreenBounds.ToRectangle(), "ActivatedScreen.ToRectangle");
}
}
}

View File

@ -1,74 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Helpers;
using MouseJumpUI.Models.Drawing;
namespace MouseJumpUI.UnitTests.Helpers;
[TestClass]
public static class MouseHelperTests
{
[TestClass]
public sealed class GetJumpLocationTests
{
public sealed class TestCase
{
public TestCase(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds, PointInfo expectedResult)
{
this.PreviewLocation = previewLocation;
this.PreviewSize = previewSize;
this.DesktopBounds = desktopBounds;
this.ExpectedResult = expectedResult;
}
public PointInfo PreviewLocation { get; set; }
public SizeInfo PreviewSize { get; set; }
public RectangleInfo DesktopBounds { get; set; }
public PointInfo ExpectedResult { get; set; }
}
public static IEnumerable<object[]> GetTestCases()
{
// screen corners and midpoint with a zero origin
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(0, 0, 1600, 1200), new(0, 0)) };
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(0, 0, 1600, 1200), new(1600, 0)) };
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(0, 0, 1600, 1200), new(0, 1200)) };
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(0, 0, 1600, 1200), new(1600, 1200)) };
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(0, 0, 1600, 1200), new(800, 600)) };
// screen corners and midpoint with a positive origin
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 1000)) };
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 1000)) };
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 2200)) };
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 2200)) };
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(1000, 1000, 1600, 1200), new(1800, 1600)) };
// screen corners and midpoint with a negative origin
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, -1000)) };
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, -1000)) };
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, 200)) };
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, 200)) };
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(-1000, -1000, 1600, 1200), new(-200, -400)) };
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
var actual = MouseHelper.GetJumpLocation(
data.PreviewLocation,
data.PreviewSize,
data.DesktopBounds);
var expected = data.ExpectedResult;
Assert.AreEqual(expected.X, actual.X);
Assert.AreEqual(expected.Y, actual.Y);
}
}
}

View File

@ -28,6 +28,13 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Common\Helpers\_test-4grid-desktop.png" />
<EmbeddedResource Include="Common\Helpers\_test-4grid-expected.png" />
<EmbeddedResource Include="Common\Helpers\_test-win11-desktop.png" />
<EmbeddedResource Include="Common\Helpers\_test-win11-expected.png" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MouseJumpUI\MouseJumpUI.csproj" />
</ItemGroup>

View File

@ -0,0 +1,248 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Linq;
using MouseJumpUI.Common.Imaging;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Layout;
using MouseJumpUI.Common.Models.Styles;
namespace MouseJumpUI.Common.Helpers;
internal static class DrawingHelper
{
public static Bitmap RenderPreview(
PreviewLayout previewLayout,
IImageRegionCopyService imageCopyService,
Action<Bitmap>? previewImageCreatedCallback = null,
Action? previewImageUpdatedCallback = null)
{
var stopwatch = Stopwatch.StartNew();
// initialize the preview image
var previewBounds = previewLayout.PreviewBounds.OuterBounds.ToRectangle();
var previewImage = new Bitmap(previewBounds.Width, previewBounds.Height, PixelFormat.Format32bppPArgb);
var previewGraphics = Graphics.FromImage(previewImage);
previewImageCreatedCallback?.Invoke(previewImage);
DrawingHelper.DrawRaisedBorder(previewGraphics, previewLayout.PreviewStyle.CanvasStyle, previewLayout.PreviewBounds);
DrawingHelper.DrawBackgroundFill(
previewGraphics,
previewLayout.PreviewStyle.CanvasStyle,
previewLayout.PreviewBounds,
[]);
// sort the source and target screen areas into the order we want to
// draw them, putting the activated screen first (we need to capture
// and draw the activated screen before we show the form because
// otherwise we'll capture the form as part of the screenshot!)
var sourceScreens = new List<RectangleInfo> { previewLayout.Screens[previewLayout.ActivatedScreenIndex] }
.Concat(previewLayout.Screens.Where((_, idx) => idx != previewLayout.ActivatedScreenIndex))
.ToList();
var targetScreens = new List<BoxBounds> { previewLayout.ScreenshotBounds[previewLayout.ActivatedScreenIndex] }
.Concat(previewLayout.ScreenshotBounds.Where((_, idx) => idx != previewLayout.ActivatedScreenIndex))
.ToList();
// draw all the screenshot bezels
foreach (var screenshotBounds in previewLayout.ScreenshotBounds)
{
DrawingHelper.DrawRaisedBorder(
previewGraphics, previewLayout.PreviewStyle.ScreenStyle, screenshotBounds);
}
var refreshRequired = false;
var placeholdersDrawn = false;
for (var i = 0; i < sourceScreens.Count; i++)
{
imageCopyService.CopyImageRegion(previewGraphics, sourceScreens[i], targetScreens[i].ContentBounds);
refreshRequired = true;
// show the placeholder images and show the form if it looks like it might take
// a while to capture the remaining screenshot images (but only if there are any)
if (stopwatch.ElapsedMilliseconds > 250)
{
// draw placeholder backgrounds for any undrawn screens
if (!placeholdersDrawn)
{
DrawingHelper.DrawScreenPlaceholders(
previewGraphics,
previewLayout.PreviewStyle.ScreenStyle,
targetScreens.GetRange(i + 1, targetScreens.Count - i - 1));
placeholdersDrawn = true;
}
previewImageUpdatedCallback?.Invoke();
refreshRequired = false;
}
}
if (refreshRequired)
{
previewImageUpdatedCallback?.Invoke();
}
stopwatch.Stop();
return previewImage;
}
/// <summary>
/// Draws a border shape with an optional raised 3d highlight and shadow effect.
/// </summary>
private static void DrawRaisedBorder(
Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds)
{
var borderStyle = boxStyle.BorderStyle;
if ((borderStyle.Horizontal == 0) || (borderStyle.Vertical == 0))
{
return;
}
// draw the main box border
using var borderBrush = new SolidBrush(borderStyle.Color);
var borderRegion = new Region(boxBounds.BorderBounds.ToRectangle());
borderRegion.Exclude(boxBounds.PaddingBounds.ToRectangle());
graphics.FillRegion(borderBrush, borderRegion);
// draw the highlight and shadow
var bounds = boxBounds.BorderBounds.ToRectangle();
using var highlight = new Pen(Color.FromArgb(0x44, 0xFF, 0xFF, 0xFF));
using var shadow = new Pen(Color.FromArgb(0x44, 0x00, 0x00, 0x00));
var outer = (
Left: bounds.Left,
Top: bounds.Top,
Right: bounds.Right - 1,
Bottom: bounds.Bottom - 1
);
var inner = (
Left: bounds.Left + (int)borderStyle.Left - 1,
Top: bounds.Top + (int)borderStyle.Top - 1,
Right: bounds.Right - (int)borderStyle.Right,
Bottom: bounds.Bottom - (int)borderStyle.Bottom
);
for (var i = 0; i < borderStyle.Depth; i++)
{
// left edge
if (borderStyle.Left >= i * 2)
{
graphics.DrawLine(highlight, outer.Left, outer.Top, outer.Left, outer.Bottom);
graphics.DrawLine(shadow, inner.Left, inner.Top, inner.Left, inner.Bottom);
}
// top edge
if (borderStyle.Top >= i * 2)
{
graphics.DrawLine(highlight, outer.Left, outer.Top, outer.Right, outer.Top);
graphics.DrawLine(shadow, inner.Left, inner.Top, inner.Right, inner.Top);
}
// right edge
if (borderStyle.Right >= i * 2)
{
graphics.DrawLine(highlight, inner.Right, inner.Top, inner.Right, inner.Bottom);
graphics.DrawLine(shadow, outer.Right, outer.Top, outer.Right, outer.Bottom);
}
// bottom edge
if (borderStyle.Bottom >= i * 2)
{
graphics.DrawLine(highlight, inner.Left, inner.Bottom, inner.Right, inner.Bottom);
graphics.DrawLine(shadow, outer.Left, outer.Bottom, outer.Right, outer.Bottom);
}
// shrink the outer border for the next iteration
outer = (
outer.Left + 1,
outer.Top + 1,
outer.Right - 1,
outer.Bottom - 1
);
// enlarge the inner border for the next iteration
inner = (
inner.Left - 1,
inner.Top - 1,
inner.Right + 1,
inner.Bottom + 1
);
}
}
/// <summary>
/// Draws a gradient-filled background shape.
/// </summary>
private static void DrawBackgroundFill(
Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds, IEnumerable<RectangleInfo> excludeBounds)
{
var backgroundBounds = boxBounds.PaddingBounds;
using var backgroundBrush = DrawingHelper.GetBackgroundStyleBrush(boxStyle.BackgroundStyle, backgroundBounds);
if (backgroundBrush == null)
{
return;
}
// it's faster to build a region with the screen areas excluded
// and fill that than it is to fill the entire bounding rectangle
var backgroundRegion = new Region(backgroundBounds.ToRectangle());
foreach (var exclude in excludeBounds)
{
backgroundRegion.Exclude(exclude.ToRectangle());
}
graphics.FillRegion(backgroundBrush, backgroundRegion);
}
/// <summary>
/// Draws placeholder background images for the specified screens on the preview.
/// </summary>
private static void DrawScreenPlaceholders(
Graphics graphics, BoxStyle screenStyle, IList<BoxBounds> screenBounds)
{
if (screenBounds.Count == 0)
{
return;
}
if (screenStyle?.BackgroundStyle?.Color1 == null)
{
return;
}
using var brush = new SolidBrush(screenStyle.BackgroundStyle.Color1.Value);
graphics.FillRectangles(brush, screenBounds.Select(bounds => bounds.PaddingBounds.ToRectangle()).ToArray());
}
private static Brush? GetBackgroundStyleBrush(BackgroundStyle backgroundStyle, RectangleInfo backgroundBounds)
{
var backgroundBrush = backgroundStyle switch
{
{ Color1: not null, Color2: not null } =>
/* draw a gradient fill if both colors are specified */
new LinearGradientBrush(
backgroundBounds.ToRectangle(),
backgroundStyle.Color1.Value,
backgroundStyle.Color2.Value,
LinearGradientMode.ForwardDiagonal),
{ Color1: not null } =>
/* draw a solid fill if only one color is specified */
new SolidBrush(
backgroundStyle.Color1.Value),
{ Color2: not null } =>
/* draw a solid fill if only one color is specified */
new SolidBrush(
backgroundStyle.Color2.Value),
_ => (Brush?)null,
};
return backgroundBrush;
}
}

View File

@ -0,0 +1,159 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Layout;
using MouseJumpUI.Common.Models.Styles;
namespace MouseJumpUI.Common.Helpers;
internal static class LayoutHelper
{
public static PreviewLayout GetPreviewLayout(
PreviewStyle previewStyle, List<RectangleInfo> screens, PointInfo activatedLocation)
{
ArgumentNullException.ThrowIfNull(previewStyle);
ArgumentNullException.ThrowIfNull(screens);
if (screens.Count == 0)
{
throw new ArgumentException("Value must contain at least one item.", nameof(screens));
}
var builder = new PreviewLayout.Builder();
builder.Screens = screens.ToList();
// calculate the bounding rectangle for the virtual screen
builder.VirtualScreen = LayoutHelper.GetCombinedScreenBounds(builder.Screens);
// find the screen that contains the activated location - this is the
// one we'll show the preview form on
var activatedScreen = builder.Screens.Single(
screen => screen.Contains(activatedLocation));
builder.ActivatedScreenIndex = builder.Screens.IndexOf(activatedScreen);
// work out the maximum allowed size of the preview form:
// * can't be bigger than the activated screen
// * can't be bigger than the configured canvas size
var maxPreviewSize = activatedScreen.Size
.Intersect(previewStyle.CanvasSize);
// the "content area" (i.e. drawing area) for screenshots is inside the
// preview border and inside the preview padding (if any)
var maxContentSize = maxPreviewSize
.Shrink(previewStyle.CanvasStyle.MarginStyle)
.Shrink(previewStyle.CanvasStyle.BorderStyle)
.Shrink(previewStyle.CanvasStyle.PaddingStyle);
// scale the virtual screen to fit inside the content area
var screenScalingRatio = builder.VirtualScreen.Size
.ScaleToFitRatio(maxContentSize);
// work out the actual size of the "content area" by scaling the virtual screen
// to fit inside the maximum content area while maintaining its aspect ration.
// we'll also offset it to allow for any margins, borders and padding
var contentBounds = builder.VirtualScreen.Size
.Scale(screenScalingRatio)
.Floor()
.PlaceAt(0, 0)
.Offset(previewStyle.CanvasStyle.MarginStyle.Left, previewStyle.CanvasStyle.MarginStyle.Top)
.Offset(previewStyle.CanvasStyle.BorderStyle.Left, previewStyle.CanvasStyle.BorderStyle.Top)
.Offset(previewStyle.CanvasStyle.PaddingStyle.Left, previewStyle.CanvasStyle.PaddingStyle.Top);
// now we know the actual size of the content area we can work outwards to
// get the size of the background bounds including margins, borders and padding
builder.PreviewStyle = previewStyle;
builder.PreviewBounds = LayoutHelper.GetBoxBoundsFromContentBounds(
contentBounds,
previewStyle.CanvasStyle);
// ... and then the size and position of the preview form on the activated screen
// * center the form to the activated position, but nudge it back
// inside the visible area of the activated screen if it falls outside
var formBounds = builder.PreviewBounds.OuterBounds
.Center(activatedLocation)
.Clamp(activatedScreen);
builder.FormBounds = formBounds;
// now calculate the positions of each of the screenshot images on the preview
builder.ScreenshotBounds = builder.Screens
.Select(
screen => LayoutHelper.GetBoxBoundsFromOuterBounds(
screen
.Offset(builder.VirtualScreen.Location.ToSize().Invert())
.Scale(screenScalingRatio)
.Offset(builder.PreviewBounds.ContentBounds.Location.ToSize())
.Truncate(),
previewStyle.ScreenStyle))
.ToList();
return builder.Build();
}
internal static RectangleInfo GetCombinedScreenBounds(List<RectangleInfo> screens)
{
return screens.Skip(1).Aggregate(
seed: screens.First(),
(bounds, screen) => bounds.Union(screen));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="contentBounds">The content bounds of the box.</param>
/// <param name="boxStyle">The style of the box, which includes the sizes of the margin, border, and padding areas.</param>
/// <returns>A <see cref="BoxBounds"/> object that represents the bounds of the different areas of the box.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="contentBounds"/> or <paramref name="boxStyle"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any of the styles in <paramref name="boxStyle"/> is null.</exception>
internal static BoxBounds GetBoxBoundsFromContentBounds(
RectangleInfo contentBounds,
BoxStyle boxStyle)
{
ArgumentNullException.ThrowIfNull(contentBounds);
ArgumentNullException.ThrowIfNull(boxStyle);
if (boxStyle.PaddingStyle == null || boxStyle.BorderStyle == null || boxStyle.MarginStyle == null)
{
throw new ArgumentException(null, nameof(boxStyle));
}
var paddingBounds = contentBounds.Enlarge(boxStyle.PaddingStyle);
var borderBounds = paddingBounds.Enlarge(boxStyle.BorderStyle);
var marginBounds = borderBounds.Enlarge(boxStyle.MarginStyle);
var outerBounds = marginBounds;
return new(
outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="outerBounds">The outer bounds of the box.</param>
/// <param name="boxStyle">The style of the box, which includes the sizes of the margin, border, and padding areas.</param>
/// <returns>A <see cref="BoxBounds"/> object that represents the bounds of the different areas of the box.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="outerBounds"/> or <paramref name="boxStyle"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any of the styles in <paramref name="boxStyle"/> is null.</exception>
internal static BoxBounds GetBoxBoundsFromOuterBounds(
RectangleInfo outerBounds,
BoxStyle boxStyle)
{
ArgumentNullException.ThrowIfNull(outerBounds);
ArgumentNullException.ThrowIfNull(boxStyle);
if (outerBounds == null || boxStyle.MarginStyle == null || boxStyle.BorderStyle == null || boxStyle.PaddingStyle == null)
{
throw new ArgumentException(null, nameof(boxStyle));
}
var marginBounds = outerBounds;
var borderBounds = marginBounds.Shrink(boxStyle.MarginStyle);
var paddingBounds = borderBounds.Shrink(boxStyle.BorderStyle);
var contentBounds = paddingBounds.Shrink(boxStyle.PaddingStyle);
return new(
outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds);
}
}

View File

@ -4,12 +4,12 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.NativeMethods;
using static MouseJumpUI.NativeMethods.Core;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.NativeMethods;
using static MouseJumpUI.Common.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.User32;
namespace MouseJumpUI.Helpers;
namespace MouseJumpUI.Common.Helpers;
internal static class MouseHelper
{
@ -22,7 +22,7 @@ internal static class MouseHelper
/// or even negative if the primary monitor is not the at the top-left of the
/// entire desktop rectangle, so results may contain negative coordinates.
/// </remarks>
public static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds)
internal static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds)
{
return previewLocation
.Scale(previewSize.ScaleToFitRatio(desktopBounds.Size))
@ -32,7 +32,7 @@ internal static class MouseHelper
/// <summary>
/// Get the current position of the cursor.
/// </summary>
public static PointInfo GetCursorPosition()
internal static PointInfo GetCursorPosition()
{
var lpPoint = new LPPOINT(new POINT(0, 0));
var result = User32.GetCursorPos(lpPoint);
@ -55,7 +55,7 @@ internal static class MouseHelper
/// <remarks>
/// See https://github.com/mikeclayton/FancyMouse/pull/3
/// </remarks>
public static void SetCursorPosition(PointInfo location)
internal static void SetCursorPosition(PointInfo location)
{
// set the new cursor position *twice* - the cursor sometimes end up in
// the wrong place if we try to cross the dead space between non-aligned
@ -73,15 +73,21 @@ internal static class MouseHelper
//
// setting the position a second time seems to fix this and moves the
// cursor to the expected location (b)
var point = location.ToPoint();
var target = location.ToPoint();
for (var i = 0; i < 2; i++)
{
var result = User32.SetCursorPos(point.X, point.Y);
var result = User32.SetCursorPos(target.X, target.Y);
if (!result)
{
throw new Win32Exception(
Marshal.GetLastWin32Error());
}
var current = MouseHelper.GetCursorPosition();
if ((current.X == target.X) || (current.Y == target.Y))
{
break;
}
}
// temporary workaround for issue #1273
@ -95,25 +101,25 @@ internal static class MouseHelper
/// See https://github.com/microsoft/PowerToys/issues/24523
/// https://github.com/microsoft/PowerToys/pull/24527
/// </remarks>
public static void SimulateMouseMovementEvent(PointInfo location)
internal static void SimulateMouseMovementEvent(PointInfo location)
{
var inputs = new User32.INPUT[]
{
new(
type: User32.INPUT_TYPE.INPUT_MOUSE,
data: new User32.INPUT.DUMMYUNIONNAME(
mi: new User32.MOUSEINPUT(
type: INPUT_TYPE.INPUT_MOUSE,
data: new INPUT.DUMMYUNIONNAME(
mi: new MOUSEINPUT(
dx: (int)MouseHelper.CalculateAbsoluteCoordinateX(location.X),
dy: (int)MouseHelper.CalculateAbsoluteCoordinateY(location.Y),
mouseData: 0,
dwFlags: User32.MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | User32.MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE,
dwFlags: MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE,
time: 0,
dwExtraInfo: ULONG_PTR.Null))),
};
var result = User32.SendInput(
(uint)inputs.Length,
new User32.LPINPUT(inputs),
User32.INPUT.Size * inputs.Length);
(UINT)inputs.Length,
new LPINPUT(inputs),
INPUT.Size * inputs.Length);
if (result != inputs.Length)
{
throw new Win32Exception(
@ -125,13 +131,13 @@ internal static class MouseHelper
{
// If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535.
// see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
return (x * 65535) / User32.GetSystemMetrics(User32.SYSTEM_METRICS_INDEX.SM_CXSCREEN);
return (x * 65535) / User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
}
internal static decimal CalculateAbsoluteCoordinateY(decimal y)
private static decimal CalculateAbsoluteCoordinateY(decimal y)
{
// If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535.
// see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
return (y * 65535) / User32.GetSystemMetrics(User32.SYSTEM_METRICS_INDEX.SM_CYSCREEN);
return (y * 65535) / User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
}
}

View File

@ -5,13 +5,13 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.Models.Screen;
using MouseJumpUI.NativeMethods;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.NativeMethods.User32;
using System.Linq;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.NativeMethods;
using static MouseJumpUI.Common.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.User32;
namespace MouseJumpUI.Helpers;
namespace MouseJumpUI.Common.Helpers;
internal static class ScreenHelper
{
@ -28,22 +28,21 @@ internal static class ScreenHelper
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYVIRTUALSCREEN));
}
public static IEnumerable<ScreenInfo> GetAllScreens()
internal static IEnumerable<ScreenInfo> GetAllScreens()
{
// enumerate the monitors attached to the system
var hMonitors = new List<HMONITOR>();
var result = User32.EnumDisplayMonitors(
HDC.Null,
LPCRECT.Null,
(unnamedParam1, unnamedParam2, unnamedParam3, unnamedParam4) =>
var callback = new User32.MONITORENUMPROC(
(hMonitor, hdcMonitor, lprcMonitor, dwData) =>
{
hMonitors.Add(unnamedParam1);
hMonitors.Add(hMonitor);
return true;
},
LPARAM.Null);
});
var result = User32.EnumDisplayMonitors(HDC.Null, LPCRECT.Null, callback, LPARAM.Null);
if (!result)
{
throw new Win32Exception(
result.Value,
$"{nameof(User32.EnumDisplayMonitors)} failed with return code {result.Value}");
}
@ -51,11 +50,12 @@ internal static class ScreenHelper
foreach (var hMonitor in hMonitors)
{
var monitorInfoPtr = new LPMONITORINFO(
new MONITORINFO((uint)MONITORINFO.Size, RECT.Empty, RECT.Empty, 0));
new MONITORINFO((DWORD)MONITORINFO.Size, RECT.Empty, RECT.Empty, 0));
result = User32.GetMonitorInfoW(hMonitor, monitorInfoPtr);
if (!result)
{
throw new Win32Exception(
result.Value,
$"{nameof(User32.GetMonitorInfoW)} failed with return code {result.Value}");
}
@ -78,9 +78,11 @@ internal static class ScreenHelper
}
}
public static HMONITOR MonitorFromPoint(
internal static ScreenInfo GetScreenFromPoint(
List<ScreenInfo> screens,
PointInfo pt)
{
// get the monitor handle from the point
var hMonitor = User32.MonitorFromPoint(
new((int)pt.X, (int)pt.Y),
User32.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
@ -89,6 +91,9 @@ internal static class ScreenHelper
throw new InvalidOperationException($"no monitor found for point {pt}");
}
return hMonitor;
// find the screen with the given monitor handle
var screen = screens
.Single(item => item.Handle == hMonitor);
return screen;
}
}

View File

@ -0,0 +1,123 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Drawing;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.NativeMethods;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.Common.Imaging;
/// <summary>
/// Implements an IImageRegionCopyService that uses the current desktop window as the copy source.
/// This is used during the main application runtime to generate preview images of the desktop.
/// </summary>
internal sealed class DesktopImageRegionCopyService : IImageRegionCopyService
{
/// <summary>
/// Copies the source region from the current desktop window
/// to the target region on the specified Graphics object.
/// </summary>
public void CopyImageRegion(
Graphics targetGraphics,
RectangleInfo sourceBounds,
RectangleInfo targetBounds)
{
var stopwatch = Stopwatch.StartNew();
var (desktopHwnd, desktopHdc) = DesktopImageRegionCopyService.GetDesktopDeviceContext();
var previewHdc = DesktopImageRegionCopyService.GetGraphicsDeviceContext(
targetGraphics, Gdi32.STRETCH_BLT_MODE.STRETCH_HALFTONE);
stopwatch.Stop();
var source = sourceBounds.ToRectangle();
var target = targetBounds.ToRectangle();
var result = Gdi32.StretchBlt(
previewHdc,
target.X,
target.Y,
target.Width,
target.Height,
desktopHdc,
source.X,
source.Y,
source.Width,
source.Height,
Gdi32.ROP_CODE.SRCCOPY);
if (!result)
{
throw new InvalidOperationException(
$"{nameof(Gdi32.StretchBlt)} returned {result.Value}");
}
// we need to release the graphics device context handle before anything
// else tries to use the Graphics object otherwise it'll give an error
// from GDI saying "Object is currently in use elsewhere"
DesktopImageRegionCopyService.FreeGraphicsDeviceContext(targetGraphics, ref previewHdc);
DesktopImageRegionCopyService.FreeDesktopDeviceContext(ref desktopHwnd, ref desktopHdc);
}
private static (HWND DesktopHwnd, HDC DesktopHdc) GetDesktopDeviceContext()
{
var desktopHwnd = User32.GetDesktopWindow();
var desktopHdc = User32.GetWindowDC(desktopHwnd);
if (desktopHdc.IsNull)
{
throw new InvalidOperationException(
$"{nameof(User32.GetWindowDC)} returned null");
}
return (desktopHwnd, desktopHdc);
}
private static void FreeDesktopDeviceContext(ref HWND desktopHwnd, ref HDC desktopHdc)
{
if (!desktopHwnd.IsNull && !desktopHdc.IsNull)
{
var result = User32.ReleaseDC(desktopHwnd, desktopHdc);
if (result == 0)
{
throw new InvalidOperationException(
$"{nameof(User32.ReleaseDC)} returned {result}");
}
}
desktopHwnd = HWND.Null;
desktopHdc = HDC.Null;
}
/// <summary>
/// Checks if the target device context handle exists, and creates a new one from the
/// specified Graphics object if not.
/// </summary>
private static HDC GetGraphicsDeviceContext(Graphics graphics, Gdi32.STRETCH_BLT_MODE mode)
{
var graphicsHdc = (HDC)graphics.GetHdc();
var result = Gdi32.SetStretchBltMode(graphicsHdc, mode);
if (result == 0)
{
throw new InvalidOperationException(
$"{nameof(Gdi32.SetStretchBltMode)} returned {result}");
}
return graphicsHdc;
}
/// <summary>
/// Free the specified device context handle if it exists.
/// </summary>
private static void FreeGraphicsDeviceContext(Graphics graphics, ref HDC graphicsHdc)
{
if (graphicsHdc.IsNull)
{
return;
}
graphics.ReleaseHdc(graphicsHdc.Value);
graphicsHdc = HDC.Null;
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Drawing;
using MouseJumpUI.Common.Models.Drawing;
namespace MouseJumpUI.Common.Imaging;
internal interface IImageRegionCopyService
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Implementations of this interface are used to capture regions of the interactive desktop
/// during runtime, or to capture regions of a static reference image during unit tests.
/// </remarks>
void CopyImageRegion(
Graphics targetGraphics,
RectangleInfo sourceBounds,
RectangleInfo targetBounds);
}

View File

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
using MouseJumpUI.Common.Models.Drawing;
namespace MouseJumpUI.Common.Imaging;
/// <summary>
/// Implements an IImageRegionCopyService that uses the specified image as the copy source.
/// This is used for testing the DrawingHelper rather than as part of the main application.
/// </summary>
internal sealed class StaticImageRegionCopyService : IImageRegionCopyService
{
public StaticImageRegionCopyService(Image sourceImage)
{
this.SourceImage = sourceImage ?? throw new ArgumentNullException(nameof(sourceImage));
}
private Image SourceImage
{
get;
}
/// <summary>
/// Copies the source region from the static source image
/// to the target region on the specified Graphics object.
/// </summary>
public void CopyImageRegion(
Graphics targetGraphics,
RectangleInfo sourceBounds,
RectangleInfo targetBounds)
{
targetGraphics.DrawImage(
image: this.SourceImage,
destRect: targetBounds.ToRectangle(),
srcRect: sourceBounds.ToRectangle(),
srcUnit: GraphicsUnit.Pixel);
}
}

View File

@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace MouseJumpUI.Common.Models.Drawing;
public sealed class BoxBounds
{
/*
see https://www.w3schools.com/css/css_boxmodel.asp
+--------------[bounds]---------------+
|[margin]|
|[border]|
|[padding]|
| |
| |
| [content] |
| |
| |
||
||
||
+-------------------------------------+
*/
internal BoxBounds(
RectangleInfo outerBounds,
RectangleInfo marginBounds,
RectangleInfo borderBounds,
RectangleInfo paddingBounds,
RectangleInfo contentBounds)
{
this.OuterBounds = outerBounds ?? throw new ArgumentNullException(nameof(outerBounds));
this.MarginBounds = marginBounds ?? throw new ArgumentNullException(nameof(marginBounds));
this.BorderBounds = borderBounds ?? throw new ArgumentNullException(nameof(borderBounds));
this.PaddingBounds = paddingBounds ?? throw new ArgumentNullException(nameof(paddingBounds));
this.ContentBounds = contentBounds ?? throw new ArgumentNullException(nameof(contentBounds));
}
/// <summary>
/// Gets the outer bounds of this layout box.
/// </summary>
public RectangleInfo OuterBounds
{
get;
}
public RectangleInfo MarginBounds
{
get;
}
public RectangleInfo BorderBounds
{
get;
}
public RectangleInfo PaddingBounds
{
get;
}
/// <summary>
/// Gets the bounds of the content area for this layout box.
/// </summary>
public RectangleInfo ContentBounds
{
get;
}
}

View File

@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
namespace MouseJumpUI.Common.Models.Drawing;
/// <summary>
/// Immutable version of a System.Drawing.Point object with some extra utility methods.
/// </summary>
public sealed class PointInfo
{
public PointInfo(decimal x, decimal y)
{
this.X = x;
this.Y = y;
}
public PointInfo(Point point)
: this(point.X, point.Y)
{
}
public decimal X
{
get;
}
public decimal Y
{
get;
}
/// <summary>
/// Moves this PointInfo inside the specified RectangleInfo.
/// </summary>
public PointInfo Clamp(RectangleInfo outer)
{
return new(
x: Math.Clamp(this.X, outer.X, outer.Right),
y: Math.Clamp(this.Y, outer.Y, outer.Bottom));
}
public PointInfo Scale(decimal scalingFactor) => new(this.X * scalingFactor, this.Y * scalingFactor);
public PointInfo Offset(PointInfo amount) => new(this.X + amount.X, this.Y + amount.Y);
public Point ToPoint() => new((int)this.X, (int)this.Y);
public SizeInfo ToSize()
{
return new((int)this.X, (int)this.Y);
}
/// <summary>
/// Stretches the point to the same proportional position in targetBounds as
/// it currently is in sourceBounds
/// </summary>
public PointInfo Stretch(RectangleInfo source, RectangleInfo target)
{
return new PointInfo(
x: ((this.X - source.X) / source.Width * target.Width) + target.X,
y: ((this.Y - source.Y) / source.Height * target.Height) + target.Y);
}
public PointInfo Truncate() =>
new(
(int)this.X,
(int)this.Y);
public override string ToString()
{
return "{" +
$"{nameof(this.X)}={this.X}," +
$"{nameof(this.Y)}={this.Y}" +
"}";
}
}

View File

@ -0,0 +1,300 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
using System.Text.Json.Serialization;
using MouseJumpUI.Common.Models.Styles;
using BorderStyle = MouseJumpUI.Common.Models.Styles.BorderStyle;
namespace MouseJumpUI.Common.Models.Drawing;
/// <summary>
/// Immutable version of a System.Drawing.Rectangle object with some extra utility methods.
/// </summary>
public sealed class RectangleInfo
{
public static readonly RectangleInfo Empty = new(0, 0, 0, 0);
public RectangleInfo(decimal x, decimal y, decimal width, decimal height)
{
this.X = x;
this.Y = y;
this.Width = width;
this.Height = height;
}
public RectangleInfo(Rectangle rectangle)
: this(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height)
{
}
public RectangleInfo(Point location, SizeInfo size)
: this(location.X, location.Y, size.Width, size.Height)
{
}
public RectangleInfo(SizeInfo size)
: this(0, 0, size.Width, size.Height)
{
}
public decimal X
{
get;
}
public decimal Y
{
get;
}
public decimal Width
{
get;
}
public decimal Height
{
get;
}
[JsonIgnore]
public decimal Left =>
this.X;
[JsonIgnore]
public decimal Top =>
this.Y;
[JsonIgnore]
public decimal Right =>
this.X + this.Width;
[JsonIgnore]
public decimal Bottom =>
this.Y + this.Height;
[JsonIgnore]
public decimal Area =>
this.Width * this.Height;
[JsonIgnore]
public PointInfo Location =>
new(this.X, this.Y);
[JsonIgnore]
public PointInfo Midpoint =>
new(
x: this.X + (this.Width / 2),
y: this.Y + (this.Height / 2));
[JsonIgnore]
public SizeInfo Size => new(this.Width, this.Height);
/// <summary>
/// Centers the rectangle around a specified point.
/// </summary>
/// <param name="point">The <see cref="PointInfo"/> around which the rectangle will be centered.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is centered around the specified point.</returns>
public RectangleInfo Center(PointInfo point) =>
new(
x: point.X - (this.Width / 2),
y: point.Y - (this.Height / 2),
width: this.Width,
height: this.Height);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> 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.
/// </summary>
/// <param name="outer">The outer <see cref="RectangleInfo"/> within which to confine this rectangle.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is the result of moving this rectangle within the bounds of the outer rectangle.</returns>
/// <exception cref="ArgumentException">Thrown when the current rectangle is larger than the outer rectangle.</exception>
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);
}
/// <remarks>
/// 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
/// </remarks>
public bool Contains(decimal x, decimal y) =>
this.X <= x && x < this.X + this.Width && this.Y <= y && y < this.Y + this.Height;
/// <remarks>
/// 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
/// </remarks>
public bool Contains(PointInfo pt) =>
this.Contains(pt.X, pt.Y);
/// <remarks>
/// 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
/// </remarks>
public bool Contains(RectangleInfo rect) =>
(this.X <= rect.X) && (rect.X + rect.Width <= this.X + this.Width) &&
(this.Y <= rect.Y) && (rect.Y + rect.Height <= this.Y + this.Height);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is larger than the current rectangle.
/// The dimensions of the new rectangle are calculated by enlarging the current rectangle's dimensions by the size of the border.
/// </summary>
/// <param name="border">The <see cref="BorderStyle"/> that specifies the amount to enlarge the rectangle on each side.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is larger than the current rectangle by the specified border amounts.</returns>
public RectangleInfo Enlarge(BorderStyle border) =>
new(
this.X - border.Left,
this.Y - border.Top,
this.Width + border.Horizontal,
this.Height + border.Vertical);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is larger than the current rectangle.
/// The dimensions of the new rectangle are calculated by enlarging the current rectangle's dimensions by the size of the margin.
/// </summary>
/// <param name="margin">The <see cref="MarginStyle"/> that specifies the amount to enlarge the rectangle on each side.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is larger than the current rectangle by the specified margin amounts.</returns>
public RectangleInfo Enlarge(MarginStyle margin) =>
new(
this.X - margin.Left,
this.Y - margin.Top,
this.Width + margin.Horizontal,
this.Height + margin.Vertical);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is larger than the current rectangle.
/// The dimensions of the new rectangle are calculated by enlarging the current rectangle's dimensions by the size of the padding.
/// </summary>
/// <param name="padding">The <see cref="PaddingStyle"/> that specifies the amount to enlarge the rectangle on each side.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is larger than the current rectangle by the specified padding amounts.</returns>
public RectangleInfo Enlarge(PaddingStyle padding) =>
new(
this.X - padding.Left,
this.Y - padding.Top,
this.Width + padding.Horizontal,
this.Height + padding.Vertical);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is offset by the specified amount.
/// </summary>
/// <param name="amount">The <see cref="SizeInfo"/> representing the amount to offset in both the X and Y directions.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is offset by the specified amount.</returns>
public RectangleInfo Offset(SizeInfo amount) =>
this.Offset(amount.Width, amount.Height);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is offset by the specified X and Y distances.
/// </summary>
/// <param name="dx">The distance to offset the rectangle along the X-axis.</param>
/// <param name="dy">The distance to offset the rectangle along the Y-axis.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is offset by the specified X and Y distances.</returns>
public RectangleInfo Offset(decimal dx, decimal dy) =>
new(this.X + dx, this.Y + dy, this.Width, this.Height);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is a scaled version of the current rectangle.
/// The dimensions of the new rectangle are calculated by multiplying the current rectangle's dimensions by the scaling factor.
/// </summary>
/// <param name="scalingFactor">The factor by which to scale the rectangle's dimensions.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is a scaled version of the current rectangle.</returns>
public RectangleInfo Scale(decimal scalingFactor) =>
new(
this.X * scalingFactor,
this.Y * scalingFactor,
this.Width * scalingFactor,
this.Height * scalingFactor);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is smaller than the current rectangle.
/// The dimensions of the new rectangle are calculated by shrinking the current rectangle's dimensions by the size of the border.
/// </summary>
/// <param name="border">The <see cref="BorderStyle"/> that specifies the amount to shrink the rectangle on each side.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is smaller than the current rectangle by the specified border amounts.</returns>
public RectangleInfo Shrink(BorderStyle border) =>
new(
this.X + border.Left,
this.Y + border.Top,
this.Width - border.Horizontal,
this.Height - border.Vertical);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is smaller than the current rectangle.
/// The dimensions of the new rectangle are calculated by shrinking the current rectangle's dimensions by the size of the margin.
/// </summary>
/// <param name="margin">The <see cref="MarginStyle"/> that specifies the amount to shrink the rectangle on each side.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is smaller than the current rectangle by the specified margin amounts.</returns>
public RectangleInfo Shrink(MarginStyle margin) =>
new(
this.X + margin.Left,
this.Y + margin.Top,
this.Width - margin.Horizontal,
this.Height - margin.Vertical);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is smaller than the current rectangle.
/// The dimensions of the new rectangle are calculated by shrinking the current rectangle's dimensions by the size of the padding.
/// </summary>
/// <param name="padding">The <see cref="PaddingStyle"/> that specifies the amount to shrink the rectangle on each side.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is smaller than the current rectangle by the specified padding amounts.</returns>
public RectangleInfo Shrink(PaddingStyle padding) =>
new(
this.X + padding.Left,
this.Y + padding.Top,
this.Width - padding.Horizontal,
this.Height - padding.Vertical);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> where the X, Y, Width, and Height properties of the current rectangle are truncated to integers.
/// </summary>
/// <returns>A new <see cref="RectangleInfo"/> with the X, Y, Width, and Height properties of the current rectangle truncated to integers.</returns>
public RectangleInfo Truncate() =>
new(
(int)this.X,
(int)this.Y,
(int)this.Width,
(int)this.Height);
/// <remarks>
/// 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
/// </remarks>
public RectangleInfo Union(RectangleInfo rect)
{
var x1 = Math.Min(this.X, rect.X);
var x2 = Math.Max(this.X + this.Width, rect.X + rect.Width);
var y1 = Math.Min(this.Y, rect.Y);
var y2 = Math.Max(this.Y + this.Height, rect.Y + rect.Height);
return new RectangleInfo(x1, y1, x2 - x1, y2 - y1);
}
public Rectangle ToRectangle() =>
new(
(int)this.X,
(int)this.Y,
(int)this.Width,
(int)this.Height);
public override string ToString()
{
return "{" +
$"{nameof(this.Left)}={this.Left}," +
$"{nameof(this.Top)}={this.Top}," +
$"{nameof(this.Width)}={this.Width}," +
$"{nameof(this.Height)}={this.Height}" +
"}";
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.Common.Models.Drawing;
/// <summary>
/// Immutable version of a System.Windows.Forms.Screen object so we don't need to
/// take a dependency on WinForms just for screen info.
/// </summary>
internal sealed class ScreenInfo
{
internal ScreenInfo(HMONITOR handle, bool primary, RectangleInfo displayArea, RectangleInfo workingArea)
{
this.Handle = handle;
this.Primary = primary;
this.DisplayArea = displayArea ?? throw new ArgumentNullException(nameof(displayArea));
this.WorkingArea = workingArea ?? throw new ArgumentNullException(nameof(workingArea));
}
public int Handle
{
get;
}
public bool Primary
{
get;
}
public RectangleInfo DisplayArea
{
get;
}
public RectangleInfo WorkingArea
{
get;
}
}

View File

@ -0,0 +1,156 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
using MouseJumpUI.Common.Models.Styles;
using BorderStyle = MouseJumpUI.Common.Models.Styles.BorderStyle;
namespace MouseJumpUI.Common.Models.Drawing;
/// <summary>
/// Immutable version of a System.Drawing.Size object with some extra utility methods.
/// </summary>
public sealed class SizeInfo
{
public SizeInfo(decimal width, decimal height)
{
this.Width = width;
this.Height = height;
}
public SizeInfo(Size size)
: this(size.Width, size.Height)
{
}
public decimal Width
{
get;
}
public decimal Height
{
get;
}
public SizeInfo Enlarge(BorderStyle border) =>
new(
this.Width + border.Horizontal,
this.Height + border.Vertical);
public SizeInfo Enlarge(PaddingStyle padding) =>
new(
this.Width + padding.Horizontal,
this.Height + padding.Vertical);
/// <summary>
/// Calculates the intersection of this size with another size, resulting in a size that represents
/// the overlapping dimensions. Both sizes must be non-negative.
/// </summary>
/// <param name="size">The size to intersect with this instance.</param>
/// <returns>A new <see cref="SizeInfo"/> instance representing the intersection of the two sizes.</returns>
/// <exception cref="ArgumentException">Thrown when either this size or the specified size has negative dimensions.</exception>
public SizeInfo Intersect(SizeInfo size)
{
if ((this.Width < 0) || (this.Height < 0) || (size.Width < 0) || (size.Height < 0))
{
throw new ArgumentException("Sizes must be non-negative");
}
return new(
Math.Min(this.Width, size.Width),
Math.Min(this.Height, size.Height));
}
/// <summary>
/// Creates a new <see cref="SizeInfo"/> instance with the width and height negated, effectively inverting its dimensions.
/// </summary>
/// <returns>A new <see cref="SizeInfo"/> instance with inverted dimensions.</returns>
public SizeInfo Invert() =>
new(-this.Width, -this.Height);
public SizeInfo Scale(decimal scalingFactor) => new(
this.Width * scalingFactor,
this.Height * scalingFactor);
public SizeInfo Shrink(BorderStyle border) =>
new(this.Width - border.Horizontal, this.Height - border.Vertical);
public SizeInfo Shrink(MarginStyle margin) =>
new(this.Width - margin.Horizontal, this.Height - margin.Vertical);
public SizeInfo Shrink(PaddingStyle padding) =>
new(this.Width - padding.Horizontal, this.Height - padding.Vertical);
/// <summary>
/// Creates a new <see cref="RectangleInfo"/> instance representing a rectangle with this size,
/// positioned at the specified coordinates.
/// </summary>
/// <param name="x">The x-coordinate of the upper-left corner of the rectangle.</param>
/// <param name="y">The y-coordinate of the upper-left corner of the rectangle.</param>
/// <returns>A new <see cref="RectangleInfo"/> instance representing the positioned rectangle.</returns>
public RectangleInfo PlaceAt(decimal x, decimal y) =>
new(x, y, this.Width, this.Height);
/// <summary>
/// Scales this size to fit within the bounds of another size, while maintaining the aspect ratio.
/// </summary>
/// <param name="bounds">The size to fit this size into.</param>
/// <returns>A new <see cref="SizeInfo"/> instance representing the scaled size.</returns>
public SizeInfo ScaleToFit(SizeInfo bounds)
{
var widthRatio = bounds.Width / this.Width;
var heightRatio = bounds.Height / this.Height;
return widthRatio.CompareTo(heightRatio) switch
{
< 0 => new(bounds.Width, this.Height * widthRatio),
0 => bounds,
> 0 => new(this.Width * heightRatio, bounds.Height),
};
}
/// <summary>
/// Rounds down the width and height of this size to the nearest whole number.
/// </summary>
/// <returns>A new <see cref="SizeInfo"/> instance with floored dimensions.</returns>
public SizeInfo Floor()
{
return new SizeInfo(
Math.Floor(this.Width),
Math.Floor(this.Height));
}
/// <summary>
/// Calculates the scaling ratio needed to fit this size within the bounds of another size without distorting the aspect ratio.
/// </summary>
/// <param name="bounds">The size to fit this size into.</param>
/// <returns>The scaling ratio as a decimal.</returns>
/// <exception cref="ArgumentException">Thrown if the width or height of the bounds is zero.</exception>
public decimal ScaleToFitRatio(SizeInfo bounds)
{
if (bounds.Width == 0 || bounds.Height == 0)
{
throw new ArgumentException($"{nameof(bounds.Width)} or {nameof(bounds.Height)} cannot be zero", nameof(bounds));
}
var widthRatio = bounds.Width / this.Width;
var heightRatio = bounds.Height / this.Height;
var scalingRatio = Math.Min(widthRatio, heightRatio);
return scalingRatio;
}
public Size ToSize() => new((int)this.Width, (int)this.Height);
public Point ToPoint() => new((int)this.Width, (int)this.Height);
public override string ToString()
{
return "{" +
$"{nameof(this.Width)}={this.Width}," +
$"{nameof(this.Height)}={this.Height}" +
"}";
}
}

View File

@ -0,0 +1,133 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Styles;
namespace MouseJumpUI.Common.Models.Layout;
public sealed class PreviewLayout
{
public sealed class Builder
{
public Builder()
{
this.Screens = new();
this.ScreenshotBounds = new();
}
public PreviewStyle? PreviewStyle
{
get;
set;
}
public RectangleInfo? VirtualScreen
{
get;
set;
}
public List<RectangleInfo> Screens
{
get;
set;
}
public int ActivatedScreenIndex
{
get;
set;
}
public RectangleInfo? FormBounds
{
get;
set;
}
public BoxBounds? PreviewBounds
{
get;
set;
}
public List<BoxBounds> 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<RectangleInfo> screens,
int activatedScreenIndex,
RectangleInfo formBounds,
BoxBounds previewBounds,
List<BoxBounds> 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<RectangleInfo> Screens
{
get;
}
public int ActivatedScreenIndex
{
get;
}
public RectangleInfo FormBounds
{
get;
}
public BoxBounds PreviewBounds
{
get;
}
public ReadOnlyCollection<BoxBounds> ScreenshotBounds
{
get;
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Drawing;
namespace MouseJumpUI.Common.Models.Styles;
/// <summary>
/// Represents the background fill style for a drawing object.
/// </summary>
public sealed class BackgroundStyle
{
public static readonly BackgroundStyle Empty = new(
Color.Transparent,
Color.Transparent
);
public BackgroundStyle(
Color? color1,
Color? color2)
{
this.Color1 = color1;
this.Color2 = color2;
}
public Color? Color1
{
get;
}
public Color? Color2
{
get;
}
public override string ToString()
{
return "{" +
$"{nameof(this.Color1)}={this.Color1}," +
$"{nameof(this.Color2)}={this.Color2}" +
"}";
}
}

View File

@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Drawing;
namespace MouseJumpUI.Common.Models.Styles;
/// <summary>
/// Represents the border style for a drawing object.
/// </summary>
public sealed class BorderStyle
{
public static readonly BorderStyle Empty = new(Color.Transparent, 0, 0);
public BorderStyle(Color color, decimal all, decimal depth)
: this(color, all, all, all, all, depth)
{
}
public BorderStyle(Color color, decimal left, decimal top, decimal right, decimal bottom, decimal depth)
{
this.Color = color;
this.Left = left;
this.Top = top;
this.Right = right;
this.Bottom = bottom;
this.Depth = depth;
}
public Color Color
{
get;
}
public decimal Left
{
get;
}
public decimal Top
{
get;
}
public decimal Right
{
get;
}
public decimal Bottom
{
get;
}
/// <summary>
/// Gets the "depth" of the 3d highlight and shadow effect on the border.
/// </summary>
public decimal Depth
{
get;
}
public decimal Horizontal => this.Left + this.Right;
public decimal Vertical => this.Top + this.Bottom;
public override string ToString()
{
return "{" +
$"{nameof(this.Color)}={this.Color}," +
$"{nameof(this.Left)}={this.Left}," +
$"{nameof(this.Top)}={this.Top}," +
$"{nameof(this.Right)}={this.Right}," +
$"{nameof(this.Bottom)}={this.Bottom}," +
$"{nameof(this.Depth)}={this.Depth}" +
"}";
}
}

View File

@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace MouseJumpUI.Common.Models.Styles;
/// <summary>
/// Represents the styles to apply to a simple box-layout based drawing object.
/// </summary>
public sealed class BoxStyle
{
/*
see https://www.w3schools.com/css/css_boxmodel.asp
+--------------[bounds]---------------+
|[margin]|
|[border]|
|[padding]|
| |
| |
| [content] |
| |
| |
||
||
||
+-------------------------------------+
*/
public static readonly BoxStyle Empty = new(MarginStyle.Empty, BorderStyle.Empty, PaddingStyle.Empty, BackgroundStyle.Empty);
public BoxStyle(
MarginStyle marginStyle,
BorderStyle borderStyle,
PaddingStyle paddingStyle,
BackgroundStyle backgroundStyle)
{
this.MarginStyle = marginStyle ?? throw new ArgumentNullException(nameof(marginStyle));
this.BorderStyle = borderStyle ?? throw new ArgumentNullException(nameof(borderStyle));
this.PaddingStyle = paddingStyle ?? throw new ArgumentNullException(nameof(paddingStyle));
this.BackgroundStyle = backgroundStyle ?? throw new ArgumentNullException(nameof(backgroundStyle));
}
/// <summary>
/// Gets the margin style for this layout box.
/// </summary>
public MarginStyle MarginStyle
{
get;
}
/// <summary>
/// Gets the border style for this layout box.
/// </summary>
public BorderStyle BorderStyle
{
get;
}
/// <summary>
/// Gets the padding style for this layout box.
/// </summary>
public PaddingStyle PaddingStyle
{
get;
}
/// <summary>
/// Gets the background fill style for the content area of this layout box.
/// </summary>
public BackgroundStyle BackgroundStyle
{
get;
}
}

View File

@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.Common.Models.Styles;
/// <summary>
/// Represents the margin style for a drawing object.
/// </summary>
public sealed class MarginStyle
{
public static readonly MarginStyle Empty = new(0);
public MarginStyle(decimal all)
: this(all, all, all, all)
{
}
public MarginStyle(decimal left, decimal top, decimal right, decimal bottom)
{
this.Left = left;
this.Top = top;
this.Right = right;
this.Bottom = bottom;
}
public decimal Left
{
get;
}
public decimal Top
{
get;
}
public decimal Right
{
get;
}
public decimal Bottom
{
get;
}
public decimal Horizontal => this.Left + this.Right;
public decimal Vertical => this.Top + this.Bottom;
public override string ToString()
{
return "{" +
$"{nameof(this.Left)}={this.Left}," +
$"{nameof(this.Top)}={this.Top}," +
$"{nameof(this.Right)}={this.Right}," +
$"{nameof(this.Bottom)}={this.Bottom}" +
"}";
}
}

View File

@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.Common.Models.Styles;
/// <summary>
/// Represents the margin style for a drawing object.
/// </summary>
public sealed class PaddingStyle
{
public static readonly PaddingStyle Empty = new(0);
public PaddingStyle(decimal all)
: this(all, all, all, all)
{
}
public PaddingStyle(decimal left, decimal top, decimal right, decimal bottom)
{
this.Left = left;
this.Top = top;
this.Right = right;
this.Bottom = bottom;
}
public decimal Left
{
get;
}
public decimal Top
{
get;
}
public decimal Right
{
get;
}
public decimal Bottom
{
get;
}
public decimal Horizontal => this.Left + this.Right;
public decimal Vertical => this.Top + this.Bottom;
public override string ToString()
{
return "{" +
$"{nameof(this.Left)}={this.Left}," +
$"{nameof(this.Top)}={this.Top}," +
$"{nameof(this.Right)}={this.Right}," +
$"{nameof(this.Bottom)}={this.Bottom}" +
"}";
}
}

View File

@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using MouseJumpUI.Common.Models.Drawing;
namespace MouseJumpUI.Common.Models.Styles;
public sealed class PreviewStyle
{
public PreviewStyle(
SizeInfo canvasSize,
BoxStyle canvasStyle,
BoxStyle screenStyle)
{
this.CanvasSize = canvasSize ?? throw new ArgumentNullException(nameof(canvasSize));
this.CanvasStyle = canvasStyle ?? throw new ArgumentNullException(nameof(canvasStyle));
this.ScreenStyle = screenStyle ?? throw new ArgumentNullException(nameof(screenStyle));
}
public SizeInfo CanvasSize
{
get;
}
public BoxStyle CanvasStyle
{
get;
}
public BoxStyle ScreenStyle
{
get;
}
}

View File

@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{

View File

@ -5,7 +5,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -25,6 +25,15 @@ internal static partial class Core
public readonly LONG right;
public readonly LONG bottom;
public CRECT(
int left, int top, int right, int bottom)
{
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public CRECT(
LONG left, LONG top, LONG right, LONG bottom)
{

View File

@ -2,7 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
using System.Runtime.InteropServices;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -23,10 +25,17 @@ internal static partial class Core
this.Value = value;
}
public static int Size =>
Marshal.SizeOf(typeof(DWORD));
public static implicit operator uint(DWORD value) => value.Value;
public static implicit operator DWORD(uint value) => new(value);
public static explicit operator int(DWORD value) => (int)value.Value;
public static explicit operator DWORD(int value) => new((uint)value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";

View File

@ -4,7 +4,7 @@
using System;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -31,7 +31,7 @@ internal static partial class Core
public static implicit operator IntPtr(HANDLE value) => value.Value;
public static implicit operator HANDLE(IntPtr value) => new(value);
public static explicit operator HANDLE(IntPtr value) => new(value);
public override string ToString()
{

View File

@ -4,7 +4,7 @@
using System;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -29,6 +29,10 @@ internal static partial class Core
public bool IsNull => this.Value == HDC.Null.Value;
public static implicit operator IntPtr(HDC value) => value.Value;
public static explicit operator HDC(IntPtr value) => new(value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";

View File

@ -4,7 +4,7 @@
using System;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -31,15 +31,15 @@ internal static partial class Core
public static implicit operator int(HMONITOR value) => value.Value.ToInt32();
public static implicit operator HMONITOR(int value) => new(value);
public static explicit operator HMONITOR(int value) => new(value);
public static implicit operator IntPtr(HMONITOR value) => value.Value;
public static implicit operator HMONITOR(IntPtr value) => new(value);
public static explicit operator HMONITOR(IntPtr value) => new(value);
public static implicit operator HANDLE(HMONITOR value) => new(value.Value);
public static implicit operator HMONITOR(HANDLE value) => new(value.Value);
public static explicit operator HMONITOR(HANDLE value) => new(value.Value);
public override string ToString()
{

View File

@ -3,8 +3,9 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics.CodeAnalysis;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -20,6 +21,9 @@ internal static partial class Core
{
public static readonly HWND Null = new(IntPtr.Zero);
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Name and value taken from Win32Api")]
public static readonly HWND HWND_MESSAGE = new(-3);
public readonly IntPtr Value;
public HWND(IntPtr value)

View File

@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{

View File

@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -26,9 +26,11 @@ internal static partial class Core
this.Value = value;
}
public bool IsNull => this.Value == LPARAM.Null.Value;
public static implicit operator IntPtr(LPARAM value) => value.Value;
public static implicit operator LPARAM(IntPtr value) => new(value);
public static explicit operator LPARAM(IntPtr value) => new(value);
public override string ToString()
{

View File

@ -5,7 +5,7 @@
using System;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -25,6 +25,8 @@ internal static partial class Core
this.Value = LPCRECT.ToPtr(value);
}
public bool IsNull => this.Value == LPCRECT.Null.Value;
private static IntPtr ToPtr(CRECT value)
{
var ptr = Marshal.AllocHGlobal(CRECT.Size);
@ -39,7 +41,7 @@ internal static partial class Core
public static implicit operator IntPtr(LPCRECT value) => value.Value;
public static implicit operator LPCRECT(IntPtr value) => new(value);
public static explicit operator LPCRECT(IntPtr value) => new(value);
public override string ToString()
{

View File

@ -5,7 +5,7 @@
using System;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -25,6 +25,8 @@ internal static partial class Core
this.Value = LPPOINT.ToPtr(value);
}
public bool IsNull => this.Value == LPPOINT.Null.Value;
private static IntPtr ToPtr(POINT value)
{
var ptr = Marshal.AllocHGlobal(POINT.Size);
@ -44,7 +46,7 @@ internal static partial class Core
public static implicit operator IntPtr(LPPOINT value) => value.Value;
public static implicit operator LPPOINT(IntPtr value) => new(value);
public static explicit operator LPPOINT(IntPtr value) => new(value);
public override string ToString()
{

View File

@ -4,7 +4,7 @@
using System;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -24,6 +24,8 @@ internal static partial class Core
this.Value = LPRECT.ToPtr(value);
}
public bool IsNull => this.Value == LPRECT.Null.Value;
private static IntPtr ToPtr(RECT value)
{
var ptr = Marshal.AllocHGlobal(RECT.Size);
@ -38,7 +40,7 @@ internal static partial class Core
public static implicit operator IntPtr(LPRECT value) => value.Value;
public static implicit operator LPRECT(IntPtr value) => new(value);
public static explicit operator LPRECT(IntPtr value) => new(value);
public override string ToString()
{

View File

@ -5,7 +5,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -28,6 +28,14 @@ internal static partial class Core
/// </summary>
public readonly LONG y;
public POINT(
int x,
int y)
{
this.x = x;
this.y = y;
}
public POINT(
LONG x,
LONG y)

View File

@ -5,7 +5,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -25,6 +25,15 @@ internal static partial class Core
public readonly LONG right;
public readonly LONG bottom;
public RECT(
int left, int top, int right, int bottom)
{
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public RECT(
LONG left, LONG top, LONG right, LONG bottom)
{

View File

@ -1,7 +1,8 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -26,6 +27,10 @@ internal static partial class Core
public static implicit operator UINT(uint value) => new(value);
public static explicit operator int(UINT value) => (int)value.Value;
public static explicit operator UINT(int value) => new((uint)value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";

View File

@ -4,7 +4,7 @@
using System;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@ -34,7 +34,7 @@ internal static partial class Core
public static implicit operator UIntPtr(ULONG_PTR value) => value.Value;
public static implicit operator ULONG_PTR(UIntPtr value) => new(value);
public static explicit operator ULONG_PTR(UIntPtr value) => new(value);
public override string ToString()
{

View File

@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{

View File

@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Gdi32
{

View File

@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Gdi32
{

View File

@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Gdi32
{

View File

@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Gdi32
{

View File

@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static class Libraries
{

View File

@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -5,7 +5,7 @@
using System;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -2,9 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -4,9 +4,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{
@ -34,6 +34,6 @@ internal static partial class User32
}
public static int Size =>
Marshal.SizeOf(typeof(INPUT));
Marshal.SizeOf(typeof(MONITORINFO));
}
}

View File

@ -4,7 +4,7 @@
using System.Diagnostics.CodeAnalysis;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
internal static partial class User32

View File

@ -4,7 +4,7 @@
using System.Diagnostics.CodeAnalysis;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
internal static partial class User32

View File

@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -4,9 +4,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -4,9 +4,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -4,7 +4,7 @@
using System.Diagnostics.CodeAnalysis;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
internal static partial class User32

View File

@ -4,9 +4,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -6,7 +6,7 @@ using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{
@ -52,7 +52,7 @@ internal static partial class User32
var size = INPUT.Size;
foreach (var value in values)
{
Marshal.StructureToPtr(value, ptr, true);
Marshal.StructureToPtr(value, ptr, false);
ptr += size;
}

View File

@ -4,9 +4,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -5,7 +5,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
internal static partial class User32

View File

@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -4,7 +4,7 @@
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -4,7 +4,7 @@
using System.Diagnostics.CodeAnalysis;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
internal static partial class User32

View File

@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@ -1,184 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.NativeMethods;
using static MouseJumpUI.NativeMethods.Core;
namespace MouseJumpUI.Helpers;
internal static class DrawingHelper
{
/// <summary>
/// Draw the gradient-filled preview background.
/// </summary>
public static void DrawPreviewBackground(
Graphics previewGraphics, RectangleInfo previewBounds, IEnumerable<RectangleInfo> screenBounds)
{
using var backgroundBrush = new LinearGradientBrush(
previewBounds.Location.ToPoint(),
previewBounds.Size.ToPoint(),
Color.FromArgb(13, 87, 210), // light blue
Color.FromArgb(3, 68, 192)); // darker blue
// it's faster to build a region with the screen areas excluded
// and fill that than it is to fill the entire bounding rectangle
var backgroundRegion = new Region(previewBounds.ToRectangle());
foreach (var screen in screenBounds)
{
backgroundRegion.Exclude(screen.ToRectangle());
}
previewGraphics.FillRegion(backgroundBrush, backgroundRegion);
}
public static void EnsureDesktopDeviceContext(ref HWND desktopHwnd, ref HDC desktopHdc)
{
if (desktopHwnd.IsNull)
{
desktopHwnd = User32.GetDesktopWindow();
}
if (desktopHdc.IsNull)
{
desktopHdc = User32.GetWindowDC(desktopHwnd);
if (desktopHdc.IsNull)
{
throw new InvalidOperationException(
$"{nameof(User32.GetWindowDC)} returned null");
}
}
}
public static void FreeDesktopDeviceContext(ref HWND desktopHwnd, ref HDC desktopHdc)
{
if (!desktopHwnd.IsNull && !desktopHdc.IsNull)
{
var result = User32.ReleaseDC(desktopHwnd, desktopHdc);
if (result == 0)
{
throw new InvalidOperationException(
$"{nameof(User32.ReleaseDC)} returned {result}");
}
}
desktopHwnd = HWND.Null;
desktopHdc = HDC.Null;
}
/// <summary>
/// Checks if the device context handle exists, and creates a new one from the
/// specified Graphics object if not.
/// </summary>
public static void EnsurePreviewDeviceContext(Graphics previewGraphics, ref HDC previewHdc)
{
if (previewHdc.IsNull)
{
previewHdc = new HDC(previewGraphics.GetHdc());
var result = Gdi32.SetStretchBltMode(previewHdc, Gdi32.STRETCH_BLT_MODE.STRETCH_HALFTONE);
if (result == 0)
{
throw new InvalidOperationException(
$"{nameof(Gdi32.SetStretchBltMode)} returned {result}");
}
}
}
/// <summary>
/// Free the specified device context handle if it exists.
/// </summary>
public static void FreePreviewDeviceContext(Graphics previewGraphics, ref HDC previewHdc)
{
if ((previewGraphics is not null) && !previewHdc.IsNull)
{
previewGraphics.ReleaseHdc(previewHdc.Value);
previewHdc = HDC.Null;
}
}
/// <summary>
/// Draw placeholder images for any non-activated screens on the preview.
/// Will release the specified device context handle if it needs to draw anything.
/// </summary>
public static void DrawPreviewScreenPlaceholders(
Graphics previewGraphics, IEnumerable<RectangleInfo> screenBounds)
{
// we can exclude the activated screen because we've already draw
// the screen capture image for that one on the preview
if (screenBounds.Any())
{
var brush = Brushes.Black;
previewGraphics.FillRectangles(brush, screenBounds.Select(screen => screen.ToRectangle()).ToArray());
}
}
/// <summary>
/// Draws a screen capture from the specified desktop handle onto the target device context.
/// </summary>
public static void DrawPreviewScreen(
HDC sourceHdc,
HDC targetHdc,
RectangleInfo sourceBounds,
RectangleInfo targetBounds)
{
var source = sourceBounds.ToRectangle();
var target = targetBounds.ToRectangle();
var result = Gdi32.StretchBlt(
targetHdc,
target.X,
target.Y,
target.Width,
target.Height,
sourceHdc,
source.X,
source.Y,
source.Width,
source.Height,
Gdi32.ROP_CODE.SRCCOPY);
if (!result)
{
throw new InvalidOperationException(
$"{nameof(Gdi32.StretchBlt)} returned {result.Value}");
}
}
/// <summary>
/// Draws screen captures from the specified desktop handle onto the target device context.
/// </summary>
public static void DrawPreviewScreens(
HDC sourceHdc,
HDC targetHdc,
IList<RectangleInfo> sourceBounds,
IList<RectangleInfo> targetBounds)
{
for (var i = 0; i < sourceBounds.Count; i++)
{
var source = sourceBounds[i].ToRectangle();
var target = targetBounds[i].ToRectangle();
var result = Gdi32.StretchBlt(
targetHdc,
target.X,
target.Y,
target.Width,
target.Height,
sourceHdc,
source.X,
source.Y,
source.Width,
source.Height,
Gdi32.ROP_CODE.SRCCOPY);
if (!result)
{
throw new InvalidOperationException(
$"{nameof(Gdi32.StretchBlt)} returned {result.Value}");
}
}
}
}

View File

@ -1,89 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.Models.Layout;
namespace MouseJumpUI.Helpers;
internal static class LayoutHelper
{
public static LayoutInfo CalculateLayoutInfo(
LayoutConfig layoutConfig)
{
ArgumentNullException.ThrowIfNull(layoutConfig);
var builder = new LayoutInfo.Builder
{
LayoutConfig = layoutConfig,
};
builder.ActivatedScreenBounds = layoutConfig.Screens[layoutConfig.ActivatedScreenIndex].Bounds;
// work out the maximum *constrained* form size
// * can't be bigger than the activated screen
// * can't be bigger than the max form size
var maxFormSize = builder.ActivatedScreenBounds.Size
.Intersect(layoutConfig.MaximumFormSize);
// the drawing area for screen images is inside the
// form border and inside the preview border
var maxDrawingSize = maxFormSize
.Shrink(layoutConfig.FormPadding)
.Shrink(layoutConfig.PreviewPadding);
// scale the virtual screen to fit inside the drawing bounds
var scalingRatio = layoutConfig.VirtualScreenBounds.Size
.ScaleToFitRatio(maxDrawingSize);
// position the drawing bounds inside the preview border
var drawingBounds = layoutConfig.VirtualScreenBounds.Size
.ScaleToFit(maxDrawingSize)
.PlaceAt(layoutConfig.PreviewPadding.Left, layoutConfig.PreviewPadding.Top);
// now we know the size of the drawing area we can work out the preview size
builder.PreviewBounds = drawingBounds.Enlarge(layoutConfig.PreviewPadding);
// ... and the form size
// * center the form to the activated position, but nudge it back
// inside the visible area of the activated screen if it falls outside
builder.FormBounds = builder.PreviewBounds
.Enlarge(layoutConfig.FormPadding)
.Center(layoutConfig.ActivatedLocation)
.Clamp(builder.ActivatedScreenBounds);
// now calculate the positions of each of the screen images on the preview
builder.ScreenBounds = layoutConfig.Screens
.Select(
screen => screen.Bounds
.Offset(layoutConfig.VirtualScreenBounds.Location.ToSize().Negate())
.Scale(scalingRatio)
.Offset(layoutConfig.PreviewPadding.Left, layoutConfig.PreviewPadding.Top))
.ToList();
return builder.Build();
}
/// <summary>
/// Resize and position the specified form.
/// </summary>
public static void PositionForm(
Form form, RectangleInfo formBounds)
{
// note - do this in two steps rather than "this.Bounds = formBounds" as there
// appears to be an issue in WinForms with dpi scaling even when using PerMonitorV2,
// where the form scaling uses either the *primary* screen scaling or the *previous*
// screen's scaling when the form is moved to a different screen. i've got no idea
// *why*, but the exact sequence of calls below seems to be a workaround...
// see https://github.com/mikeclayton/FancyMouse/issues/2
var bounds = formBounds.ToRectangle();
form.Location = bounds.Location;
_ = form.PointToScreen(Point.Empty);
form.Size = bounds.Size;
}
}

View File

@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Styles;
namespace MouseJumpUI.Helpers;
internal static class StyleHelper
{
/// <summary>
/// Default v2 preview style
/// </summary>
public static readonly PreviewStyle DefaultPreviewStyle = new(
canvasSize: new(
width: 1600,
height: 1200
),
canvasStyle: new(
marginStyle: MarginStyle.Empty,
borderStyle: new(
color: SystemColors.Highlight,
all: 6,
depth: 0
),
paddingStyle: new(
all: 4
),
backgroundStyle: new(
color1: Color.FromArgb(0xFF, 0x0D, 0x57, 0xD2),
color2: Color.FromArgb(0xFF, 0x03, 0x44, 0xC0)
)
),
screenStyle: new(
marginStyle: new(
all: 4
),
borderStyle: new(
color: Color.FromArgb(0xFF, 0x22, 0x22, 0x22),
all: 12,
depth: 4
),
paddingStyle: PaddingStyle.Empty,
backgroundStyle: new(
color1: Color.MidnightBlue,
color2: Color.MidnightBlue
)
)
);
/// <summary>
/// Legacy preview style
/// </summary>
public static readonly PreviewStyle LegacyPreviewStyle = new(
canvasSize: new(
width: 1600,
height: 1200
),
canvasStyle: new(
marginStyle: MarginStyle.Empty,
borderStyle: new(
color: SystemColors.Highlight,
all: 6,
depth: 0
),
paddingStyle: new(
all: 0
),
backgroundStyle: new(
color1: Color.FromArgb(0xFF, 0x0D, 0x57, 0xD2),
color2: Color.FromArgb(0xFF, 0x03, 0x44, 0xC0)
)
),
screenStyle: new(
marginStyle: new(
all: 0
),
borderStyle: new(
color: Color.FromArgb(0xFF, 0x22, 0x22, 0x22),
all: 0,
depth: 0
),
paddingStyle: PaddingStyle.Empty,
backgroundStyle: new(
color1: Color.MidnightBlue,
color2: Color.MidnightBlue
)
)
);
public static PreviewStyle WithCanvasSize(this PreviewStyle previewStyle, SizeInfo canvasSize)
{
ArgumentNullException.ThrowIfNull(previewStyle);
ArgumentNullException.ThrowIfNull(canvasSize);
return new PreviewStyle(
canvasSize: canvasSize,
canvasStyle: previewStyle.CanvasStyle,
screenStyle: previewStyle.ScreenStyle);
}
}

View File

@ -49,7 +49,6 @@ partial class MainForm
panel1.Dock = DockStyle.Fill;
panel1.Location = new System.Drawing.Point(0, 0);
panel1.Name = "panel1";
panel1.Padding = new Padding(5);
panel1.Size = new System.Drawing.Size(800, 450);
panel1.TabIndex = 1;
//
@ -59,7 +58,7 @@ partial class MainForm
Thumbnail.Dock = DockStyle.Fill;
Thumbnail.Location = new System.Drawing.Point(5, 5);
Thumbnail.Name = "Thumbnail";
Thumbnail.Size = new System.Drawing.Size(790, 440);
Thumbnail.Size = new System.Drawing.Size(800, 450);
Thumbnail.SizeMode = PictureBoxSizeMode.Normal;
Thumbnail.TabIndex = 1;
Thumbnail.TabStop = false;

View File

@ -5,15 +5,14 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Windows.Forms;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using MouseJumpUI.Common.Helpers;
using MouseJumpUI.Common.Imaging;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Layout;
using MouseJumpUI.Helpers;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.Models.Layout;
using static MouseJumpUI.NativeMethods.Core;
namespace MouseJumpUI;
@ -25,6 +24,12 @@ internal partial class MainForm : Form
this.SettingsHelper = settingsHelper ?? throw new ArgumentNullException(nameof(settingsHelper));
}
private PreviewLayout? PreviewLayout
{
get;
set;
}
public SettingsHelper SettingsHelper
{
get;
@ -42,61 +47,68 @@ internal partial class MainForm : Form
return;
}
// map screens to their screen number in "System > Display"
var screens = ScreenHelper.GetAllScreens()
.Select((screen, index) => new { Screen = screen, Index = index, Number = index + 1 })
.ToList();
var screens = ScreenHelper.GetAllScreens().ToList();
if (screens.Count == 0)
{
return;
}
var currentLocation = MouseHelper.GetCursorPosition();
var currentScreenHandle = ScreenHelper.MonitorFromPoint(currentLocation);
var currentScreen = screens
.Single(item => item.Screen.Handle == currentScreenHandle.Value);
var targetScreenNumber = default(int?);
var currentScreen = ScreenHelper.GetScreenFromPoint(screens, currentLocation);
var currentScreenIndex = screens.IndexOf(currentScreen);
var targetScreen = default(ScreenInfo?);
if (((e.KeyCode >= Keys.D1) && (e.KeyCode <= Keys.D9))
|| ((e.KeyCode >= Keys.NumPad1) && (e.KeyCode <= Keys.NumPad9)))
switch (e.KeyCode)
{
// number keys 1-9 or numpad keys 1-9 - move to the numbered screen
var screenNumber = e.KeyCode - Keys.D0;
if (screenNumber <= screens.Count)
{
targetScreenNumber = screenNumber;
}
}
else if (e.KeyCode == Keys.P)
{
// "P" - move to the primary screen
targetScreenNumber = screens.Single(item => item.Screen.Primary).Number;
}
else if (e.KeyCode == Keys.Left)
{
// move to the previous screen
targetScreenNumber = currentScreen.Number == 1
? screens.Count
: currentScreen.Number - 1;
}
else if (e.KeyCode == Keys.Right)
{
// move to the next screen
targetScreenNumber = currentScreen.Number == screens.Count
? 1
: currentScreen.Number + 1;
}
else if (e.KeyCode == Keys.Home)
{
// move to the first screen
targetScreenNumber = 1;
}
else if (e.KeyCode == Keys.End)
{
// move to the last screen
targetScreenNumber = screens.Count;
case >= Keys.D1 and <= Keys.D9:
{
// number keys 1-9 - move to the numbered screen
var screenNumber = e.KeyCode - Keys.D0;
/* note - screen *numbers* are 1-based, screen *indexes* are 0-based */
targetScreen = (screenNumber <= screens.Count)
? targetScreen = screens[screenNumber - 1]
: null;
break;
}
case >= Keys.NumPad1 and <= Keys.NumPad9:
{
// numpad keys 1-9 - move to the numbered screen
var screenNumber = e.KeyCode - Keys.NumPad0;
/* note - screen *numbers* are 1-based, screen *indexes* are 0-based */
targetScreen = (screenNumber <= screens.Count)
? targetScreen = screens[screenNumber - 1]
: null;
break;
}
case Keys.P:
// "P" - move to the primary screen
targetScreen = screens.Single(screen => screen.Primary);
break;
case Keys.Left:
// move to the previous screen, looping back to the end if needed
var prevIndex = (currentScreenIndex - 1 + screens.Count) % screens.Count;
targetScreen = screens[prevIndex];
break;
case Keys.Right:
// move to the next screen, looping round to the start if needed
var nextIndex = (currentScreenIndex + 1) % screens.Count;
targetScreen = screens[nextIndex];
break;
case Keys.Home:
// move to the first screen
targetScreen = screens.First();
break;
case Keys.End:
// move to the last screen
targetScreen = screens.Last();
break;
}
if (targetScreenNumber.HasValue)
if (targetScreen is not null)
{
MouseHelper.SetCursorPosition(
screens[targetScreenNumber.Value - 1].Screen.Bounds.Midpoint);
MouseHelper.SetCursorPosition(targetScreen.DisplayArea.Midpoint);
this.OnDeactivate(EventArgs.Empty);
}
}
@ -118,15 +130,42 @@ internal partial class MainForm : Form
if (mouseEventArgs.Button == MouseButtons.Left)
{
// plain click - move mouse pointer
var virtualScreen = ScreenHelper.GetVirtualScreen();
var scaledLocation = MouseHelper.GetJumpLocation(
new PointInfo(mouseEventArgs.X, mouseEventArgs.Y),
new SizeInfo(this.Thumbnail.Size),
virtualScreen);
Logger.LogInfo($"scaled location = {scaledLocation}");
MouseHelper.SetCursorPosition(scaledLocation);
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent());
if (this.PreviewLayout is null)
{
// there's no layout data so we can't work out what screen was clicked
return;
}
// work out which screenshot was clicked
var clickedScreenshot = this.PreviewLayout.ScreenshotBounds
.FirstOrDefault(
box => box.BorderBounds.Contains(mouseEventArgs.X, mouseEventArgs.Y));
if (clickedScreenshot is null)
{
return;
}
// scale up the click onto the physical screen - the aspect ratio of the screenshot
// might be distorted compared to the physical screen due to the borders around the
// screenshot, so we need to work out the target location on the physical screen first
var clickedScreen =
this.PreviewLayout.Screens[this.PreviewLayout.ScreenshotBounds.IndexOf(clickedScreenshot)];
var clickedLocation = new PointInfo(mouseEventArgs.Location)
.Stretch(
source: clickedScreenshot.ContentBounds,
target: clickedScreen)
.Clamp(
new(
x: clickedScreen.X + 1,
y: clickedScreen.Y + 1,
width: clickedScreen.Width - 1,
height: clickedScreen.Height - 1
))
.Truncate();
// move mouse pointer
Logger.LogInfo($"clicked location = {clickedLocation}");
MouseHelper.SetCursorPosition(clickedLocation);
}
this.OnDeactivate(EventArgs.Empty);
@ -138,162 +177,34 @@ internal partial class MainForm : Form
this.Visible = false;
var stopwatch = Stopwatch.StartNew();
var layoutInfo = MainForm.GetLayoutInfo(this);
LayoutHelper.PositionForm(this, layoutInfo.FormBounds);
MainForm.RenderPreview(this, layoutInfo);
var appSettings = this.SettingsHelper.CurrentSettings ?? throw new InvalidOperationException();
var screens = ScreenHelper.GetAllScreens().Select(screen => screen.DisplayArea).ToList();
var activatedLocation = MouseHelper.GetCursorPosition();
this.PreviewLayout = LayoutHelper.GetPreviewLayout(
previewStyle: StyleHelper.LegacyPreviewStyle.WithCanvasSize(
new(
appSettings.Properties.ThumbnailSize.Width,
appSettings.Properties.ThumbnailSize.Height
)),
screens: screens,
activatedLocation: activatedLocation);
this.PositionForm(this.PreviewLayout.FormBounds);
var imageCopyService = new DesktopImageRegionCopyService();
DrawingHelper.RenderPreview(
this.PreviewLayout,
imageCopyService,
this.OnPreviewImageCreated,
this.OnPreviewImageUpdated);
stopwatch.Stop();
// we have to activate the form to make sure the deactivate event fires
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpShowEvent());
this.Activate();
}
private static LayoutInfo GetLayoutInfo(MainForm form)
{
// map screens to their screen number in "System > Display"
var screens = ScreenHelper.GetAllScreens()
.Select((screen, index) => new { Screen = screen, Index = index, Number = index + 1 })
.ToList();
foreach (var screen in screens)
{
Logger.LogInfo(string.Join(
'\n',
$"screen[{screen.Number}]",
$"\tprimary = {screen.Screen.Primary}",
$"\tdisplay area = {screen.Screen.DisplayArea}",
$"\tworking area = {screen.Screen.WorkingArea}"));
}
// collect together some values that we need for calculating layout
var activatedLocation = MouseHelper.GetCursorPosition();
var activatedScreenHandle = ScreenHelper.MonitorFromPoint(activatedLocation);
var activatedScreenIndex = screens
.Single(item => item.Screen.Handle == activatedScreenHandle.Value)
.Index;
// avoid a race condition - cache the current settings in case they change
var currentSettings = form.SettingsHelper.CurrentSettings;
var layoutConfig = new LayoutConfig(
virtualScreenBounds: ScreenHelper.GetVirtualScreen(),
screens: screens.Select(item => item.Screen).ToList(),
activatedLocation: activatedLocation,
activatedScreenIndex: activatedScreenIndex,
activatedScreenNumber: activatedScreenIndex + 1,
maximumFormSize: new(
currentSettings.Properties.ThumbnailSize.Width,
currentSettings.Properties.ThumbnailSize.Height),
/*
don't read the panel padding values because they are affected by dpi scaling
and can give wrong values when moving between monitors with different dpi scaling
*/
formPadding: new(5, 5, 5, 5),
previewPadding: new(0));
Logger.LogInfo(string.Join(
'\n',
$"Layout config",
$"-------------",
$"virtual screen = {layoutConfig.VirtualScreenBounds}",
$"activated location = {layoutConfig.ActivatedLocation}",
$"activated screen index = {layoutConfig.ActivatedScreenIndex}",
$"activated screen number = {layoutConfig.ActivatedScreenNumber}",
$"maximum form size = {layoutConfig.MaximumFormSize}",
$"form padding = {layoutConfig.FormPadding}",
$"preview padding = {layoutConfig.PreviewPadding}"));
// calculate the layout coordinates for everything
var layoutInfo = LayoutHelper.CalculateLayoutInfo(layoutConfig);
Logger.LogInfo(string.Join(
'\n',
$"Layout info",
$"-----------",
$"form bounds = {layoutInfo.FormBounds}",
$"preview bounds = {layoutInfo.PreviewBounds}",
$"activated screen = {layoutInfo.ActivatedScreenBounds}"));
return layoutInfo;
}
private static void RenderPreview(
MainForm form, LayoutInfo layoutInfo)
{
var layoutConfig = layoutInfo.LayoutConfig;
var stopwatch = Stopwatch.StartNew();
// initialize the preview image
var preview = new Bitmap(
(int)layoutInfo.PreviewBounds.Width,
(int)layoutInfo.PreviewBounds.Height,
PixelFormat.Format32bppArgb);
form.Thumbnail.Image = preview;
using var previewGraphics = Graphics.FromImage(preview);
DrawingHelper.DrawPreviewBackground(previewGraphics, layoutInfo.PreviewBounds, layoutInfo.ScreenBounds);
var desktopHwnd = HWND.Null;
var desktopHdc = HDC.Null;
var previewHdc = HDC.Null;
try
{
// sort the source and target screen areas, putting the activated screen first
// (we need to capture and draw the activated screen before we show the form
// because otherwise we'll capture the form as part of the screenshot!)
var sourceScreens = layoutConfig.Screens
.Where((_, idx) => idx == layoutConfig.ActivatedScreenIndex)
.Union(layoutConfig.Screens.Where((_, idx) => idx != layoutConfig.ActivatedScreenIndex))
.Select(screen => screen.Bounds)
.ToList();
var targetScreens = layoutInfo.ScreenBounds
.Where((_, idx) => idx == layoutConfig.ActivatedScreenIndex)
.Union(layoutInfo.ScreenBounds.Where((_, idx) => idx != layoutConfig.ActivatedScreenIndex))
.ToList();
DrawingHelper.EnsureDesktopDeviceContext(ref desktopHwnd, ref desktopHdc);
DrawingHelper.EnsurePreviewDeviceContext(previewGraphics, ref previewHdc);
var placeholdersDrawn = false;
for (var i = 0; i < sourceScreens.Count; i++)
{
DrawingHelper.DrawPreviewScreen(
desktopHdc, previewHdc, sourceScreens[i], targetScreens[i]);
// show the placeholder images and show the form if it looks like it might take
// a while to capture the remaining screenshot images (but only if there are any)
if ((i < (sourceScreens.Count - 1)) && (stopwatch.ElapsedMilliseconds > 250))
{
// we need to release the device context handle before we draw the placeholders
// using the Graphics object otherwise we'll get an error from GDI saying
// "Object is currently in use elsewhere"
DrawingHelper.FreePreviewDeviceContext(previewGraphics, ref previewHdc);
if (!placeholdersDrawn)
{
// draw placeholders for any undrawn screens
DrawingHelper.DrawPreviewScreenPlaceholders(
previewGraphics,
targetScreens.Where((_, idx) => idx > i));
placeholdersDrawn = true;
}
MainForm.RefreshPreview(form);
// we've still got more screens to draw so open the device context again
DrawingHelper.EnsurePreviewDeviceContext(previewGraphics, ref previewHdc);
}
}
}
finally
{
DrawingHelper.FreeDesktopDeviceContext(ref desktopHwnd, ref desktopHdc);
DrawingHelper.FreePreviewDeviceContext(previewGraphics, ref previewHdc);
}
MainForm.RefreshPreview(form);
stopwatch.Stop();
}
private void ClearPreview()
{
if (this.Thumbnail.Image is null)
@ -310,17 +221,40 @@ internal partial class MainForm : Form
GC.Collect();
}
private static void RefreshPreview(MainForm form)
/// <summary>
/// Resize and position the specified form.
/// </summary>
private void PositionForm(RectangleInfo bounds)
{
if (!form.Visible)
// note - do this in two steps rather than "this.Bounds = formBounds" as there
// appears to be an issue in WinForms with dpi scaling even when using PerMonitorV2,
// where the form scaling uses either the *primary* screen scaling or the *previous*
// screen's scaling when the form is moved to a different screen. i've got no idea
// *why*, but the exact sequence of calls below seems to be a workaround...
// see https://github.com/mikeclayton/FancyMouse/issues/2
var rect = bounds.ToRectangle();
this.Location = rect.Location;
_ = this.PointToScreen(Point.Empty);
this.Size = rect.Size;
}
private void OnPreviewImageCreated(Bitmap preview)
{
this.ClearPreview();
this.Thumbnail.Image = preview;
}
private void OnPreviewImageUpdated()
{
if (!this.Visible)
{
// we seem to need to turn off topmost and then re-enable it again
// when we show the form, otherwise it doesn't get shown topmost...
form.TopMost = false;
form.TopMost = true;
form.Show();
// when we show the form, otherwise it doesn't always get shown topmost...
this.TopMost = false;
this.TopMost = true;
this.Show();
}
form.Thumbnail.Refresh();
this.Thumbnail.Refresh();
}
}

View File

@ -1,4 +1,64 @@
<root>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">