mirror of
https://github.com/microsoft/PowerToys.git
synced 2024-11-24 04:12:32 +08:00
[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:
parent
3e07b9b8f4
commit
651f2e4bd8
1
.github/actions/spell-check/expect.txt
vendored
1
.github/actions/spell-check/expect.txt
vendored
@ -124,6 +124,7 @@ bootstrapper
|
|||||||
BOOTSTRAPPERINSTALLFOLDER
|
BOOTSTRAPPERINSTALLFOLDER
|
||||||
bostrot
|
bostrot
|
||||||
BOTTOMALIGN
|
BOTTOMALIGN
|
||||||
|
boxmodel
|
||||||
BPBF
|
BPBF
|
||||||
bpmf
|
bpmf
|
||||||
bpp
|
bpp
|
||||||
|
@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 |
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
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]
|
[TestClass]
|
||||||
public static class RectangleInfoTests
|
public static class RectangleInfoTests
|
||||||
@ -23,30 +23,30 @@ public static class RectangleInfoTests
|
|||||||
this.ExpectedResult = expectedResult;
|
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()
|
public static IEnumerable<object[]> GetTestCases()
|
||||||
{
|
{
|
||||||
// zero-sized
|
// 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
|
// zero-origin
|
||||||
yield return new[] { 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(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 object[] { 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, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
|
||||||
|
|
||||||
// non-zero origin
|
// non-zero origin
|
||||||
yield return new[] { 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(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 object[] { 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, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
|
||||||
|
|
||||||
// negative result
|
// 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]
|
[TestMethod]
|
||||||
@ -74,53 +74,53 @@ public static class RectangleInfoTests
|
|||||||
this.ExpectedResult = expectedResult;
|
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()
|
public static IEnumerable<object[]> GetTestCases()
|
||||||
{
|
{
|
||||||
// already inside - obj fills bounds exactly
|
// 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)),
|
new TestCase(new(0, 0, 100, 100), new(0, 0, 100, 100), new(0, 0, 100, 100)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// already inside - obj exactly in each corner
|
// 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)),
|
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)),
|
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)),
|
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)),
|
new TestCase(new(100, 100, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// move inside - obj outside each corner
|
// 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)),
|
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)),
|
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)),
|
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)),
|
new TestCase(new(150, 150, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)),
|
||||||
};
|
};
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
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]
|
[TestClass]
|
||||||
public static class SizeInfoTests
|
public static class SizeInfoTests
|
||||||
@ -23,28 +23,28 @@ public static class SizeInfoTests
|
|||||||
this.ExpectedResult = expectedResult;
|
this.ExpectedResult = expectedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SizeInfo Obj { get; set; }
|
public SizeInfo Obj { get; }
|
||||||
|
|
||||||
public SizeInfo Bounds { get; set; }
|
public SizeInfo Bounds { get; }
|
||||||
|
|
||||||
public SizeInfo ExpectedResult { get; set; }
|
public SizeInfo ExpectedResult { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<object[]> GetTestCases()
|
public static IEnumerable<object[]> GetTestCases()
|
||||||
{
|
{
|
||||||
// identity tests
|
// identity tests
|
||||||
yield return new[] { new TestCase(new(512, 384), new(512, 384), new(512, 384)), };
|
yield return new object[] { 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(1024, 768), new(1024, 768), new(1024, 768)), };
|
||||||
|
|
||||||
// general tests
|
// general tests
|
||||||
yield return new[] { new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536)), };
|
yield return new object[] { 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(2048, 1536), new(1024, 768), new(1024, 768)), };
|
||||||
|
|
||||||
// scale to fit width
|
// 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
|
// 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]
|
[TestMethod]
|
||||||
@ -70,28 +70,28 @@ public static class SizeInfoTests
|
|||||||
this.ExpectedResult = expectedResult;
|
this.ExpectedResult = expectedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SizeInfo Obj { get; set; }
|
public SizeInfo Obj { get; }
|
||||||
|
|
||||||
public SizeInfo Bounds { get; set; }
|
public SizeInfo Bounds { get; }
|
||||||
|
|
||||||
public decimal ExpectedResult { get; set; }
|
public decimal ExpectedResult { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<object[]> GetTestCases()
|
public static IEnumerable<object[]> GetTestCases()
|
||||||
{
|
{
|
||||||
// identity tests
|
// identity tests
|
||||||
yield return new[] { new TestCase(new(512, 384), new(512, 384), 1), };
|
yield return new object[] { 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(1024, 768), new(1024, 768), 1), };
|
||||||
|
|
||||||
// general tests
|
// general tests
|
||||||
yield return new[] { new TestCase(new(512, 384), new(2048, 1536), 4), };
|
yield return new object[] { 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(2048, 1536), new(1024, 768), 0.5M), };
|
||||||
|
|
||||||
// scale to fit width
|
// 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
|
// 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]
|
[TestMethod]
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -28,6 +28,13 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</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>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MouseJumpUI\MouseJumpUI.csproj" />
|
<ProjectReference Include="..\MouseJumpUI\MouseJumpUI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Windows.Forms;
|
using MouseJumpUI.Common.Models.Drawing;
|
||||||
using MouseJumpUI.Models.Drawing;
|
using MouseJumpUI.Common.NativeMethods;
|
||||||
using MouseJumpUI.NativeMethods;
|
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||||
using static MouseJumpUI.NativeMethods.Core;
|
using static MouseJumpUI.Common.NativeMethods.User32;
|
||||||
|
|
||||||
namespace MouseJumpUI.Helpers;
|
namespace MouseJumpUI.Common.Helpers;
|
||||||
|
|
||||||
internal static class MouseHelper
|
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
|
/// 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.
|
/// entire desktop rectangle, so results may contain negative coordinates.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds)
|
internal static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds)
|
||||||
{
|
{
|
||||||
return previewLocation
|
return previewLocation
|
||||||
.Scale(previewSize.ScaleToFitRatio(desktopBounds.Size))
|
.Scale(previewSize.ScaleToFitRatio(desktopBounds.Size))
|
||||||
@ -32,7 +32,7 @@ internal static class MouseHelper
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the current position of the cursor.
|
/// Get the current position of the cursor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static PointInfo GetCursorPosition()
|
internal static PointInfo GetCursorPosition()
|
||||||
{
|
{
|
||||||
var lpPoint = new LPPOINT(new POINT(0, 0));
|
var lpPoint = new LPPOINT(new POINT(0, 0));
|
||||||
var result = User32.GetCursorPos(lpPoint);
|
var result = User32.GetCursorPos(lpPoint);
|
||||||
@ -55,7 +55,7 @@ internal static class MouseHelper
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// See https://github.com/mikeclayton/FancyMouse/pull/3
|
/// See https://github.com/mikeclayton/FancyMouse/pull/3
|
||||||
/// </remarks>
|
/// </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
|
// 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
|
// 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
|
// setting the position a second time seems to fix this and moves the
|
||||||
// cursor to the expected location (b)
|
// cursor to the expected location (b)
|
||||||
var point = location.ToPoint();
|
var target = location.ToPoint();
|
||||||
for (var i = 0; i < 2; i++)
|
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)
|
if (!result)
|
||||||
{
|
{
|
||||||
throw new Win32Exception(
|
throw new Win32Exception(
|
||||||
Marshal.GetLastWin32Error());
|
Marshal.GetLastWin32Error());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var current = MouseHelper.GetCursorPosition();
|
||||||
|
if ((current.X == target.X) || (current.Y == target.Y))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// temporary workaround for issue #1273
|
// temporary workaround for issue #1273
|
||||||
@ -95,25 +101,25 @@ internal static class MouseHelper
|
|||||||
/// See https://github.com/microsoft/PowerToys/issues/24523
|
/// See https://github.com/microsoft/PowerToys/issues/24523
|
||||||
/// https://github.com/microsoft/PowerToys/pull/24527
|
/// https://github.com/microsoft/PowerToys/pull/24527
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static void SimulateMouseMovementEvent(PointInfo location)
|
internal static void SimulateMouseMovementEvent(PointInfo location)
|
||||||
{
|
{
|
||||||
var inputs = new User32.INPUT[]
|
var inputs = new User32.INPUT[]
|
||||||
{
|
{
|
||||||
new(
|
new(
|
||||||
type: User32.INPUT_TYPE.INPUT_MOUSE,
|
type: INPUT_TYPE.INPUT_MOUSE,
|
||||||
data: new User32.INPUT.DUMMYUNIONNAME(
|
data: new INPUT.DUMMYUNIONNAME(
|
||||||
mi: new User32.MOUSEINPUT(
|
mi: new MOUSEINPUT(
|
||||||
dx: (int)MouseHelper.CalculateAbsoluteCoordinateX(location.X),
|
dx: (int)MouseHelper.CalculateAbsoluteCoordinateX(location.X),
|
||||||
dy: (int)MouseHelper.CalculateAbsoluteCoordinateY(location.Y),
|
dy: (int)MouseHelper.CalculateAbsoluteCoordinateY(location.Y),
|
||||||
mouseData: 0,
|
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,
|
time: 0,
|
||||||
dwExtraInfo: ULONG_PTR.Null))),
|
dwExtraInfo: ULONG_PTR.Null))),
|
||||||
};
|
};
|
||||||
var result = User32.SendInput(
|
var result = User32.SendInput(
|
||||||
(uint)inputs.Length,
|
(UINT)inputs.Length,
|
||||||
new User32.LPINPUT(inputs),
|
new LPINPUT(inputs),
|
||||||
User32.INPUT.Size * inputs.Length);
|
INPUT.Size * inputs.Length);
|
||||||
if (result != inputs.Length)
|
if (result != inputs.Length)
|
||||||
{
|
{
|
||||||
throw new Win32Exception(
|
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.
|
// 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
|
// 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.
|
// 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
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,13 +5,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using MouseJumpUI.Models.Drawing;
|
using System.Linq;
|
||||||
using MouseJumpUI.Models.Screen;
|
using MouseJumpUI.Common.Models.Drawing;
|
||||||
using MouseJumpUI.NativeMethods;
|
using MouseJumpUI.Common.NativeMethods;
|
||||||
using static MouseJumpUI.NativeMethods.Core;
|
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||||
using static MouseJumpUI.NativeMethods.User32;
|
using static MouseJumpUI.Common.NativeMethods.User32;
|
||||||
|
|
||||||
namespace MouseJumpUI.Helpers;
|
namespace MouseJumpUI.Common.Helpers;
|
||||||
|
|
||||||
internal static class ScreenHelper
|
internal static class ScreenHelper
|
||||||
{
|
{
|
||||||
@ -28,22 +28,21 @@ internal static class ScreenHelper
|
|||||||
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYVIRTUALSCREEN));
|
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYVIRTUALSCREEN));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<ScreenInfo> GetAllScreens()
|
internal static IEnumerable<ScreenInfo> GetAllScreens()
|
||||||
{
|
{
|
||||||
// enumerate the monitors attached to the system
|
// enumerate the monitors attached to the system
|
||||||
var hMonitors = new List<HMONITOR>();
|
var hMonitors = new List<HMONITOR>();
|
||||||
var result = User32.EnumDisplayMonitors(
|
var callback = new User32.MONITORENUMPROC(
|
||||||
HDC.Null,
|
(hMonitor, hdcMonitor, lprcMonitor, dwData) =>
|
||||||
LPCRECT.Null,
|
|
||||||
(unnamedParam1, unnamedParam2, unnamedParam3, unnamedParam4) =>
|
|
||||||
{
|
{
|
||||||
hMonitors.Add(unnamedParam1);
|
hMonitors.Add(hMonitor);
|
||||||
return true;
|
return true;
|
||||||
},
|
});
|
||||||
LPARAM.Null);
|
var result = User32.EnumDisplayMonitors(HDC.Null, LPCRECT.Null, callback, LPARAM.Null);
|
||||||
if (!result)
|
if (!result)
|
||||||
{
|
{
|
||||||
throw new Win32Exception(
|
throw new Win32Exception(
|
||||||
|
result.Value,
|
||||||
$"{nameof(User32.EnumDisplayMonitors)} failed with return code {result.Value}");
|
$"{nameof(User32.EnumDisplayMonitors)} failed with return code {result.Value}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,11 +50,12 @@ internal static class ScreenHelper
|
|||||||
foreach (var hMonitor in hMonitors)
|
foreach (var hMonitor in hMonitors)
|
||||||
{
|
{
|
||||||
var monitorInfoPtr = new LPMONITORINFO(
|
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);
|
result = User32.GetMonitorInfoW(hMonitor, monitorInfoPtr);
|
||||||
if (!result)
|
if (!result)
|
||||||
{
|
{
|
||||||
throw new Win32Exception(
|
throw new Win32Exception(
|
||||||
|
result.Value,
|
||||||
$"{nameof(User32.GetMonitorInfoW)} failed with return code {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)
|
PointInfo pt)
|
||||||
{
|
{
|
||||||
|
// get the monitor handle from the point
|
||||||
var hMonitor = User32.MonitorFromPoint(
|
var hMonitor = User32.MonitorFromPoint(
|
||||||
new((int)pt.X, (int)pt.Y),
|
new((int)pt.X, (int)pt.Y),
|
||||||
User32.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
|
User32.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
|
||||||
@ -89,6 +91,9 @@ internal static class ScreenHelper
|
|||||||
throw new InvalidOperationException($"no monitor found for point {pt}");
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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}" +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
}
|
@ -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}" +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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}" +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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}" +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
}
|
@ -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}" +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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}" +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
}
|
@ -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}" +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
internal static partial class Core
|
||||||
{
|
{
|
@ -5,7 +5,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
internal static partial class Core
|
||||||
{
|
{
|
||||||
@ -25,6 +25,15 @@ internal static partial class Core
|
|||||||
public readonly LONG right;
|
public readonly LONG right;
|
||||||
public readonly LONG bottom;
|
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(
|
public CRECT(
|
||||||
LONG left, LONG top, LONG right, LONG bottom)
|
LONG left, LONG top, LONG right, LONG bottom)
|
||||||
{
|
{
|
@ -2,7 +2,9 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// 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
|
internal static partial class Core
|
||||||
{
|
{
|
||||||
@ -23,10 +25,17 @@ internal static partial class Core
|
|||||||
this.Value = value;
|
this.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int Size =>
|
||||||
|
Marshal.SizeOf(typeof(DWORD));
|
||||||
|
|
||||||
public static implicit operator uint(DWORD value) => value.Value;
|
public static implicit operator uint(DWORD value) => value.Value;
|
||||||
|
|
||||||
public static implicit operator DWORD(uint value) => new(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()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{this.GetType().Name}({this.Value})";
|
return $"{this.GetType().Name}({this.Value})";
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
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 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()
|
public override string ToString()
|
||||||
{
|
{
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
internal static partial class Core
|
||||||
{
|
{
|
||||||
@ -29,6 +29,10 @@ internal static partial class Core
|
|||||||
|
|
||||||
public bool IsNull => this.Value == HDC.Null.Value;
|
public bool IsNull => this.Value == HDC.Null.Value;
|
||||||
|
|
||||||
|
public static implicit operator IntPtr(HDC value) => value.Value;
|
||||||
|
|
||||||
|
public static explicit operator HDC(IntPtr value) => new(value);
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{this.GetType().Name}({this.Value})";
|
return $"{this.GetType().Name}({this.Value})";
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
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 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 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 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()
|
public override string ToString()
|
||||||
{
|
{
|
@ -3,8 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
internal static partial class Core
|
||||||
{
|
{
|
||||||
@ -20,6 +21,9 @@ internal static partial class Core
|
|||||||
{
|
{
|
||||||
public static readonly HWND Null = new(IntPtr.Zero);
|
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 readonly IntPtr Value;
|
||||||
|
|
||||||
public HWND(IntPtr value)
|
public HWND(IntPtr value)
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
internal static partial class Core
|
||||||
{
|
{
|
@ -3,7 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
internal static partial class Core
|
||||||
{
|
{
|
||||||
@ -26,9 +26,11 @@ internal static partial class Core
|
|||||||
this.Value = value;
|
this.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsNull => this.Value == LPARAM.Null.Value;
|
||||||
|
|
||||||
public static implicit operator IntPtr(LPARAM value) => value.Value;
|
public static implicit operator 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()
|
public override string ToString()
|
||||||
{
|
{
|
@ -5,7 +5,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
internal static partial class Core
|
||||||
{
|
{
|
||||||
@ -25,6 +25,8 @@ internal static partial class Core
|
|||||||
this.Value = LPCRECT.ToPtr(value);
|
this.Value = LPCRECT.ToPtr(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsNull => this.Value == LPCRECT.Null.Value;
|
||||||
|
|
||||||
private static IntPtr ToPtr(CRECT value)
|
private static IntPtr ToPtr(CRECT value)
|
||||||
{
|
{
|
||||||
var ptr = Marshal.AllocHGlobal(CRECT.Size);
|
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 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()
|
public override string ToString()
|
||||||
{
|
{
|
@ -5,7 +5,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
internal static partial class Core
|
||||||
{
|
{
|
||||||
@ -25,6 +25,8 @@ internal static partial class Core
|
|||||||
this.Value = LPPOINT.ToPtr(value);
|
this.Value = LPPOINT.ToPtr(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsNull => this.Value == LPPOINT.Null.Value;
|
||||||
|
|
||||||
private static IntPtr ToPtr(POINT value)
|
private static IntPtr ToPtr(POINT value)
|
||||||
{
|
{
|
||||||
var ptr = Marshal.AllocHGlobal(POINT.Size);
|
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 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()
|
public override string ToString()
|
||||||
{
|
{
|
@ -4,7 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
internal static partial class Core
|
||||||
{
|
{
|
||||||
@ -24,6 +24,8 @@ internal static partial class Core
|
|||||||
this.Value = LPRECT.ToPtr(value);
|
this.Value = LPRECT.ToPtr(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsNull => this.Value == LPRECT.Null.Value;
|
||||||
|
|
||||||
private static IntPtr ToPtr(RECT value)
|
private static IntPtr ToPtr(RECT value)
|
||||||
{
|
{
|
||||||
var ptr = Marshal.AllocHGlobal(RECT.Size);
|
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 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()
|
public override string ToString()
|
||||||
{
|
{
|
@ -5,7 +5,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
internal static partial class Core
|
||||||
{
|
{
|
||||||
@ -28,6 +28,14 @@ internal static partial class Core
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly LONG y;
|
public readonly LONG y;
|
||||||
|
|
||||||
|
public POINT(
|
||||||
|
int x,
|
||||||
|
int y)
|
||||||
|
{
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
public POINT(
|
public POINT(
|
||||||
LONG x,
|
LONG x,
|
||||||
LONG y)
|
LONG y)
|
@ -5,7 +5,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
internal static partial class Core
|
||||||
{
|
{
|
||||||
@ -25,6 +25,15 @@ internal static partial class Core
|
|||||||
public readonly LONG right;
|
public readonly LONG right;
|
||||||
public readonly LONG bottom;
|
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(
|
public RECT(
|
||||||
LONG left, LONG top, LONG right, LONG bottom)
|
LONG left, LONG top, LONG right, LONG bottom)
|
||||||
{
|
{
|
@ -1,7 +1,8 @@
|
|||||||
// Copyright (c) Microsoft Corporation
|
// Copyright (c) Microsoft Corporation
|
||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
namespace MouseJumpUI.NativeMethods;
|
|
||||||
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
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 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()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{this.GetType().Name}({this.Value})";
|
return $"{this.GetType().Name}({this.Value})";
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
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 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()
|
public override string ToString()
|
||||||
{
|
{
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Core
|
internal static partial class Core
|
||||||
{
|
{
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Gdi32
|
internal static partial class Gdi32
|
||||||
{
|
{
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class Gdi32
|
internal static partial class Gdi32
|
||||||
{
|
{
|
@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class Gdi32
|
||||||
{
|
{
|
@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class Gdi32
|
||||||
{
|
{
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static class Libraries
|
internal static class Libraries
|
||||||
{
|
{
|
@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -5,7 +5,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class User32
|
internal static partial class User32
|
||||||
{
|
{
|
@ -2,9 +2,9 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// 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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
||||||
@ -34,6 +34,6 @@ internal static partial class User32
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static int Size =>
|
public static int Size =>
|
||||||
Marshal.SizeOf(typeof(INPUT));
|
Marshal.SizeOf(typeof(MONITORINFO));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
||||||
internal static partial class User32
|
internal static partial class User32
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
||||||
internal static partial class User32
|
internal static partial class User32
|
@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
||||||
internal static partial class User32
|
internal static partial class User32
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -6,7 +6,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class User32
|
internal static partial class User32
|
||||||
{
|
{
|
||||||
@ -52,7 +52,7 @@ internal static partial class User32
|
|||||||
var size = INPUT.Size;
|
var size = INPUT.Size;
|
||||||
foreach (var value in values)
|
foreach (var value in values)
|
||||||
{
|
{
|
||||||
Marshal.StructureToPtr(value, ptr, true);
|
Marshal.StructureToPtr(value, ptr, false);
|
||||||
ptr += size;
|
ptr += size;
|
||||||
}
|
}
|
||||||
|
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -5,7 +5,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
||||||
internal static partial class User32
|
internal static partial class User32
|
@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
internal static partial class User32
|
internal static partial class User32
|
||||||
{
|
{
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace MouseJumpUI.NativeMethods;
|
namespace MouseJumpUI.Common.NativeMethods;
|
||||||
|
|
||||||
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
||||||
internal static partial class User32
|
internal static partial class User32
|
@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
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
|
internal static partial class User32
|
||||||
{
|
{
|
@ -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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
103
src/modules/MouseUtils/MouseJumpUI/Helpers/StyleHelper.cs
Normal file
103
src/modules/MouseUtils/MouseJumpUI/Helpers/StyleHelper.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -49,7 +49,6 @@ partial class MainForm
|
|||||||
panel1.Dock = DockStyle.Fill;
|
panel1.Dock = DockStyle.Fill;
|
||||||
panel1.Location = new System.Drawing.Point(0, 0);
|
panel1.Location = new System.Drawing.Point(0, 0);
|
||||||
panel1.Name = "panel1";
|
panel1.Name = "panel1";
|
||||||
panel1.Padding = new Padding(5);
|
|
||||||
panel1.Size = new System.Drawing.Size(800, 450);
|
panel1.Size = new System.Drawing.Size(800, 450);
|
||||||
panel1.TabIndex = 1;
|
panel1.TabIndex = 1;
|
||||||
//
|
//
|
||||||
@ -59,7 +58,7 @@ partial class MainForm
|
|||||||
Thumbnail.Dock = DockStyle.Fill;
|
Thumbnail.Dock = DockStyle.Fill;
|
||||||
Thumbnail.Location = new System.Drawing.Point(5, 5);
|
Thumbnail.Location = new System.Drawing.Point(5, 5);
|
||||||
Thumbnail.Name = "Thumbnail";
|
Thumbnail.Name = "Thumbnail";
|
||||||
Thumbnail.Size = new System.Drawing.Size(790, 440);
|
Thumbnail.Size = new System.Drawing.Size(800, 450);
|
||||||
Thumbnail.SizeMode = PictureBoxSizeMode.Normal;
|
Thumbnail.SizeMode = PictureBoxSizeMode.Normal;
|
||||||
Thumbnail.TabIndex = 1;
|
Thumbnail.TabIndex = 1;
|
||||||
Thumbnail.TabStop = false;
|
Thumbnail.TabStop = false;
|
||||||
|
@ -5,15 +5,14 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Drawing.Imaging;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using ManagedCommon;
|
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.Helpers;
|
||||||
using MouseJumpUI.Models.Drawing;
|
|
||||||
using MouseJumpUI.Models.Layout;
|
|
||||||
using static MouseJumpUI.NativeMethods.Core;
|
|
||||||
|
|
||||||
namespace MouseJumpUI;
|
namespace MouseJumpUI;
|
||||||
|
|
||||||
@ -25,6 +24,12 @@ internal partial class MainForm : Form
|
|||||||
this.SettingsHelper = settingsHelper ?? throw new ArgumentNullException(nameof(settingsHelper));
|
this.SettingsHelper = settingsHelper ?? throw new ArgumentNullException(nameof(settingsHelper));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PreviewLayout? PreviewLayout
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
public SettingsHelper SettingsHelper
|
public SettingsHelper SettingsHelper
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
@ -42,61 +47,68 @@ internal partial class MainForm : Form
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// map screens to their screen number in "System > Display"
|
var screens = ScreenHelper.GetAllScreens().ToList();
|
||||||
var screens = ScreenHelper.GetAllScreens()
|
if (screens.Count == 0)
|
||||||
.Select((screen, index) => new { Screen = screen, Index = index, Number = index + 1 })
|
{
|
||||||
.ToList();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var currentLocation = MouseHelper.GetCursorPosition();
|
var currentLocation = MouseHelper.GetCursorPosition();
|
||||||
var currentScreenHandle = ScreenHelper.MonitorFromPoint(currentLocation);
|
var currentScreen = ScreenHelper.GetScreenFromPoint(screens, currentLocation);
|
||||||
var currentScreen = screens
|
var currentScreenIndex = screens.IndexOf(currentScreen);
|
||||||
.Single(item => item.Screen.Handle == currentScreenHandle.Value);
|
var targetScreen = default(ScreenInfo?);
|
||||||
var targetScreenNumber = default(int?);
|
|
||||||
|
|
||||||
if (((e.KeyCode >= Keys.D1) && (e.KeyCode <= Keys.D9))
|
switch (e.KeyCode)
|
||||||
|| ((e.KeyCode >= Keys.NumPad1) && (e.KeyCode <= Keys.NumPad9)))
|
|
||||||
{
|
{
|
||||||
// number keys 1-9 or numpad keys 1-9 - move to the numbered screen
|
case >= Keys.D1 and <= Keys.D9:
|
||||||
var screenNumber = e.KeyCode - Keys.D0;
|
{
|
||||||
if (screenNumber <= screens.Count)
|
// number keys 1-9 - move to the numbered screen
|
||||||
{
|
var screenNumber = e.KeyCode - Keys.D0;
|
||||||
targetScreenNumber = screenNumber;
|
/* note - screen *numbers* are 1-based, screen *indexes* are 0-based */
|
||||||
}
|
targetScreen = (screenNumber <= screens.Count)
|
||||||
}
|
? targetScreen = screens[screenNumber - 1]
|
||||||
else if (e.KeyCode == Keys.P)
|
: null;
|
||||||
{
|
break;
|
||||||
// "P" - move to the primary screen
|
}
|
||||||
targetScreenNumber = screens.Single(item => item.Screen.Primary).Number;
|
|
||||||
}
|
case >= Keys.NumPad1 and <= Keys.NumPad9:
|
||||||
else if (e.KeyCode == Keys.Left)
|
{
|
||||||
{
|
// numpad keys 1-9 - move to the numbered screen
|
||||||
// move to the previous screen
|
var screenNumber = e.KeyCode - Keys.NumPad0;
|
||||||
targetScreenNumber = currentScreen.Number == 1
|
/* note - screen *numbers* are 1-based, screen *indexes* are 0-based */
|
||||||
? screens.Count
|
targetScreen = (screenNumber <= screens.Count)
|
||||||
: currentScreen.Number - 1;
|
? targetScreen = screens[screenNumber - 1]
|
||||||
}
|
: null;
|
||||||
else if (e.KeyCode == Keys.Right)
|
break;
|
||||||
{
|
}
|
||||||
// move to the next screen
|
|
||||||
targetScreenNumber = currentScreen.Number == screens.Count
|
case Keys.P:
|
||||||
? 1
|
// "P" - move to the primary screen
|
||||||
: currentScreen.Number + 1;
|
targetScreen = screens.Single(screen => screen.Primary);
|
||||||
}
|
break;
|
||||||
else if (e.KeyCode == Keys.Home)
|
case Keys.Left:
|
||||||
{
|
// move to the previous screen, looping back to the end if needed
|
||||||
// move to the first screen
|
var prevIndex = (currentScreenIndex - 1 + screens.Count) % screens.Count;
|
||||||
targetScreenNumber = 1;
|
targetScreen = screens[prevIndex];
|
||||||
}
|
break;
|
||||||
else if (e.KeyCode == Keys.End)
|
case Keys.Right:
|
||||||
{
|
// move to the next screen, looping round to the start if needed
|
||||||
// move to the last screen
|
var nextIndex = (currentScreenIndex + 1) % screens.Count;
|
||||||
targetScreenNumber = 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(
|
MouseHelper.SetCursorPosition(targetScreen.DisplayArea.Midpoint);
|
||||||
screens[targetScreenNumber.Value - 1].Screen.Bounds.Midpoint);
|
|
||||||
this.OnDeactivate(EventArgs.Empty);
|
this.OnDeactivate(EventArgs.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,15 +130,42 @@ internal partial class MainForm : Form
|
|||||||
|
|
||||||
if (mouseEventArgs.Button == MouseButtons.Left)
|
if (mouseEventArgs.Button == MouseButtons.Left)
|
||||||
{
|
{
|
||||||
// plain click - move mouse pointer
|
if (this.PreviewLayout is null)
|
||||||
var virtualScreen = ScreenHelper.GetVirtualScreen();
|
{
|
||||||
var scaledLocation = MouseHelper.GetJumpLocation(
|
// there's no layout data so we can't work out what screen was clicked
|
||||||
new PointInfo(mouseEventArgs.X, mouseEventArgs.Y),
|
return;
|
||||||
new SizeInfo(this.Thumbnail.Size),
|
}
|
||||||
virtualScreen);
|
|
||||||
Logger.LogInfo($"scaled location = {scaledLocation}");
|
// work out which screenshot was clicked
|
||||||
MouseHelper.SetCursorPosition(scaledLocation);
|
var clickedScreenshot = this.PreviewLayout.ScreenshotBounds
|
||||||
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent());
|
.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);
|
this.OnDeactivate(EventArgs.Empty);
|
||||||
@ -138,162 +177,34 @@ internal partial class MainForm : Form
|
|||||||
this.Visible = false;
|
this.Visible = false;
|
||||||
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
var layoutInfo = MainForm.GetLayoutInfo(this);
|
|
||||||
LayoutHelper.PositionForm(this, layoutInfo.FormBounds);
|
var appSettings = this.SettingsHelper.CurrentSettings ?? throw new InvalidOperationException();
|
||||||
MainForm.RenderPreview(this, layoutInfo);
|
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();
|
stopwatch.Stop();
|
||||||
|
|
||||||
// we have to activate the form to make sure the deactivate event fires
|
// we have to activate the form to make sure the deactivate event fires
|
||||||
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpShowEvent());
|
|
||||||
this.Activate();
|
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()
|
private void ClearPreview()
|
||||||
{
|
{
|
||||||
if (this.Thumbnail.Image is null)
|
if (this.Thumbnail.Image is null)
|
||||||
@ -310,17 +221,40 @@ internal partial class MainForm : Form
|
|||||||
GC.Collect();
|
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
|
// 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...
|
// when we show the form, otherwise it doesn't always get shown topmost...
|
||||||
form.TopMost = false;
|
this.TopMost = false;
|
||||||
form.TopMost = true;
|
this.TopMost = true;
|
||||||
form.Show();
|
this.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
form.Thumbnail.Refresh();
|
this.Thumbnail.Refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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: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:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
Loading…
Reference in New Issue
Block a user