mirror of
https://github.com/microsoft/PowerToys.git
synced 2024-11-27 14:59:16 +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
|
||||
bostrot
|
||||
BOTTOMALIGN
|
||||
boxmodel
|
||||
BPBF
|
||||
bpmf
|
||||
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 Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MouseJumpUI.Models.Drawing;
|
||||
using MouseJumpUI.Common.Models.Drawing;
|
||||
|
||||
namespace MouseJumpUI.UnitTests.Models.Drawing;
|
||||
namespace MouseJumpUI.UnitTests.Common.Models.Drawing;
|
||||
|
||||
[TestClass]
|
||||
public static class RectangleInfoTests
|
||||
@ -23,30 +23,30 @@ public static class RectangleInfoTests
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public RectangleInfo Rectangle { get; set; }
|
||||
public RectangleInfo Rectangle { get; }
|
||||
|
||||
public PointInfo Point { get; set; }
|
||||
public PointInfo Point { get; }
|
||||
|
||||
public RectangleInfo ExpectedResult { get; set; }
|
||||
public RectangleInfo ExpectedResult { get; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// zero-sized
|
||||
yield return new[] { new TestCase(new(0, 0, 0, 0), new(0, 0), new(0, 0, 0, 0)), };
|
||||
yield return new object[] { new TestCase(new(0, 0, 0, 0), new(0, 0), new(0, 0, 0, 0)), };
|
||||
|
||||
// zero-origin
|
||||
yield return new[] { new TestCase(new(0, 0, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
|
||||
yield return new[] { new TestCase(new(0, 0, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
|
||||
yield return new[] { new TestCase(new(0, 0, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
|
||||
yield return new object[] { new TestCase(new(0, 0, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
|
||||
yield return new object[] { new TestCase(new(0, 0, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
|
||||
yield return new object[] { new TestCase(new(0, 0, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
|
||||
|
||||
// non-zero origin
|
||||
yield return new[] { new TestCase(new(1000, 2000, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
|
||||
yield return new[] { new TestCase(new(1000, 2000, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
|
||||
yield return new[] { new TestCase(new(1000, 2000, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
|
||||
yield return new object[] { new TestCase(new(1000, 2000, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
|
||||
yield return new object[] { new TestCase(new(1000, 2000, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
|
||||
yield return new object[] { new TestCase(new(1000, 2000, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
|
||||
|
||||
// negative result
|
||||
yield return new[] { new TestCase(new(0, 0, 1000, 1200), new(300, 300), new(-200, -300, 1000, 1200)), };
|
||||
yield return new object[] { new TestCase(new(0, 0, 1000, 1200), new(300, 300), new(-200, -300, 1000, 1200)), };
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -74,53 +74,53 @@ public static class RectangleInfoTests
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public RectangleInfo Inner { get; set; }
|
||||
public RectangleInfo Inner { get; }
|
||||
|
||||
public RectangleInfo Outer { get; set; }
|
||||
public RectangleInfo Outer { get; }
|
||||
|
||||
public RectangleInfo ExpectedResult { get; set; }
|
||||
public RectangleInfo ExpectedResult { get; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// already inside - obj fills bounds exactly
|
||||
yield return new[]
|
||||
yield return new object[]
|
||||
{
|
||||
new TestCase(new(0, 0, 100, 100), new(0, 0, 100, 100), new(0, 0, 100, 100)),
|
||||
};
|
||||
|
||||
// already inside - obj exactly in each corner
|
||||
yield return new[]
|
||||
yield return new object[]
|
||||
{
|
||||
new TestCase(new(0, 0, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
yield return new object[]
|
||||
{
|
||||
new TestCase(new(100, 0, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
yield return new object[]
|
||||
{
|
||||
new TestCase(new(0, 100, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
yield return new object[]
|
||||
{
|
||||
new TestCase(new(100, 100, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)),
|
||||
};
|
||||
|
||||
// move inside - obj outside each corner
|
||||
yield return new[]
|
||||
yield return new object[]
|
||||
{
|
||||
new TestCase(new(-50, -50, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
yield return new object[]
|
||||
{
|
||||
new TestCase(new(250, -50, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
yield return new object[]
|
||||
{
|
||||
new TestCase(new(-50, 250, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
yield return new object[]
|
||||
{
|
||||
new TestCase(new(150, 150, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)),
|
||||
};
|
@ -4,9 +4,9 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MouseJumpUI.Models.Drawing;
|
||||
using MouseJumpUI.Common.Models.Drawing;
|
||||
|
||||
namespace MouseJumpUI.UnitTests.Drawing;
|
||||
namespace MouseJumpUI.UnitTests.Common.Models.Drawing;
|
||||
|
||||
[TestClass]
|
||||
public static class SizeInfoTests
|
||||
@ -23,28 +23,28 @@ public static class SizeInfoTests
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public SizeInfo Obj { get; set; }
|
||||
public SizeInfo Obj { get; }
|
||||
|
||||
public SizeInfo Bounds { get; set; }
|
||||
public SizeInfo Bounds { get; }
|
||||
|
||||
public SizeInfo ExpectedResult { get; set; }
|
||||
public SizeInfo ExpectedResult { get; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// identity tests
|
||||
yield return new[] { new TestCase(new(512, 384), new(512, 384), new(512, 384)), };
|
||||
yield return new[] { new TestCase(new(1024, 768), new(1024, 768), new(1024, 768)), };
|
||||
yield return new object[] { new TestCase(new(512, 384), new(512, 384), new(512, 384)), };
|
||||
yield return new object[] { new TestCase(new(1024, 768), new(1024, 768), new(1024, 768)), };
|
||||
|
||||
// general tests
|
||||
yield return new[] { new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536)), };
|
||||
yield return new[] { new TestCase(new(2048, 1536), new(1024, 768), new(1024, 768)), };
|
||||
yield return new object[] { new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536)), };
|
||||
yield return new object[] { new TestCase(new(2048, 1536), new(1024, 768), new(1024, 768)), };
|
||||
|
||||
// scale to fit width
|
||||
yield return new[] { new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)), };
|
||||
yield return new object[] { new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)), };
|
||||
|
||||
// scale to fit height
|
||||
yield return new[] { new TestCase(new(512, 384), new(4096, 1536), new(2048, 1536)), };
|
||||
yield return new object[] { new TestCase(new(512, 384), new(4096, 1536), new(2048, 1536)), };
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -70,28 +70,28 @@ public static class SizeInfoTests
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public SizeInfo Obj { get; set; }
|
||||
public SizeInfo Obj { get; }
|
||||
|
||||
public SizeInfo Bounds { get; set; }
|
||||
public SizeInfo Bounds { get; }
|
||||
|
||||
public decimal ExpectedResult { get; set; }
|
||||
public decimal ExpectedResult { get; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// identity tests
|
||||
yield return new[] { new TestCase(new(512, 384), new(512, 384), 1), };
|
||||
yield return new[] { new TestCase(new(1024, 768), new(1024, 768), 1), };
|
||||
yield return new object[] { new TestCase(new(512, 384), new(512, 384), 1), };
|
||||
yield return new object[] { new TestCase(new(1024, 768), new(1024, 768), 1), };
|
||||
|
||||
// general tests
|
||||
yield return new[] { new TestCase(new(512, 384), new(2048, 1536), 4), };
|
||||
yield return new[] { new TestCase(new(2048, 1536), new(1024, 768), 0.5M), };
|
||||
yield return new object[] { new TestCase(new(512, 384), new(2048, 1536), 4), };
|
||||
yield return new object[] { new TestCase(new(2048, 1536), new(1024, 768), 0.5M), };
|
||||
|
||||
// scale to fit width
|
||||
yield return new[] { new TestCase(new(512, 384), new(2048, 3072), 4), };
|
||||
yield return new object[] { new TestCase(new(512, 384), new(2048, 3072), 4), };
|
||||
|
||||
// scale to fit height
|
||||
yield return new[] { new TestCase(new(512, 384), new(4096, 1536), 4), };
|
||||
yield return new object[] { new TestCase(new(512, 384), new(4096, 1536), 4), };
|
||||
}
|
||||
|
||||
[TestMethod]
|
@ -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>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Common\Helpers\_test-4grid-desktop.png" />
|
||||
<EmbeddedResource Include="Common\Helpers\_test-4grid-expected.png" />
|
||||
<EmbeddedResource Include="Common\Helpers\_test-win11-desktop.png" />
|
||||
<EmbeddedResource Include="Common\Helpers\_test-win11-expected.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MouseJumpUI\MouseJumpUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
@ -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.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
using MouseJumpUI.Models.Drawing;
|
||||
using MouseJumpUI.NativeMethods;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using MouseJumpUI.Common.Models.Drawing;
|
||||
using MouseJumpUI.Common.NativeMethods;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.User32;
|
||||
|
||||
namespace MouseJumpUI.Helpers;
|
||||
namespace MouseJumpUI.Common.Helpers;
|
||||
|
||||
internal static class MouseHelper
|
||||
{
|
||||
@ -22,7 +22,7 @@ internal static class MouseHelper
|
||||
/// or even negative if the primary monitor is not the at the top-left of the
|
||||
/// entire desktop rectangle, so results may contain negative coordinates.
|
||||
/// </remarks>
|
||||
public static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds)
|
||||
internal static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds)
|
||||
{
|
||||
return previewLocation
|
||||
.Scale(previewSize.ScaleToFitRatio(desktopBounds.Size))
|
||||
@ -32,7 +32,7 @@ internal static class MouseHelper
|
||||
/// <summary>
|
||||
/// Get the current position of the cursor.
|
||||
/// </summary>
|
||||
public static PointInfo GetCursorPosition()
|
||||
internal static PointInfo GetCursorPosition()
|
||||
{
|
||||
var lpPoint = new LPPOINT(new POINT(0, 0));
|
||||
var result = User32.GetCursorPos(lpPoint);
|
||||
@ -55,7 +55,7 @@ internal static class MouseHelper
|
||||
/// <remarks>
|
||||
/// See https://github.com/mikeclayton/FancyMouse/pull/3
|
||||
/// </remarks>
|
||||
public static void SetCursorPosition(PointInfo location)
|
||||
internal static void SetCursorPosition(PointInfo location)
|
||||
{
|
||||
// set the new cursor position *twice* - the cursor sometimes end up in
|
||||
// the wrong place if we try to cross the dead space between non-aligned
|
||||
@ -73,15 +73,21 @@ internal static class MouseHelper
|
||||
//
|
||||
// setting the position a second time seems to fix this and moves the
|
||||
// cursor to the expected location (b)
|
||||
var point = location.ToPoint();
|
||||
var target = location.ToPoint();
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result = User32.SetCursorPos(point.X, point.Y);
|
||||
var result = User32.SetCursorPos(target.X, target.Y);
|
||||
if (!result)
|
||||
{
|
||||
throw new Win32Exception(
|
||||
Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
var current = MouseHelper.GetCursorPosition();
|
||||
if ((current.X == target.X) || (current.Y == target.Y))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// temporary workaround for issue #1273
|
||||
@ -95,25 +101,25 @@ internal static class MouseHelper
|
||||
/// See https://github.com/microsoft/PowerToys/issues/24523
|
||||
/// https://github.com/microsoft/PowerToys/pull/24527
|
||||
/// </remarks>
|
||||
public static void SimulateMouseMovementEvent(PointInfo location)
|
||||
internal static void SimulateMouseMovementEvent(PointInfo location)
|
||||
{
|
||||
var inputs = new User32.INPUT[]
|
||||
{
|
||||
new(
|
||||
type: User32.INPUT_TYPE.INPUT_MOUSE,
|
||||
data: new User32.INPUT.DUMMYUNIONNAME(
|
||||
mi: new User32.MOUSEINPUT(
|
||||
type: INPUT_TYPE.INPUT_MOUSE,
|
||||
data: new INPUT.DUMMYUNIONNAME(
|
||||
mi: new MOUSEINPUT(
|
||||
dx: (int)MouseHelper.CalculateAbsoluteCoordinateX(location.X),
|
||||
dy: (int)MouseHelper.CalculateAbsoluteCoordinateY(location.Y),
|
||||
mouseData: 0,
|
||||
dwFlags: User32.MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | User32.MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE,
|
||||
dwFlags: MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE,
|
||||
time: 0,
|
||||
dwExtraInfo: ULONG_PTR.Null))),
|
||||
};
|
||||
var result = User32.SendInput(
|
||||
(uint)inputs.Length,
|
||||
new User32.LPINPUT(inputs),
|
||||
User32.INPUT.Size * inputs.Length);
|
||||
(UINT)inputs.Length,
|
||||
new LPINPUT(inputs),
|
||||
INPUT.Size * inputs.Length);
|
||||
if (result != inputs.Length)
|
||||
{
|
||||
throw new Win32Exception(
|
||||
@ -125,13 +131,13 @@ internal static class MouseHelper
|
||||
{
|
||||
// If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535.
|
||||
// see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
|
||||
return (x * 65535) / User32.GetSystemMetrics(User32.SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
return (x * 65535) / User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
}
|
||||
|
||||
internal static decimal CalculateAbsoluteCoordinateY(decimal y)
|
||||
private static decimal CalculateAbsoluteCoordinateY(decimal y)
|
||||
{
|
||||
// If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535.
|
||||
// see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
|
||||
return (y * 65535) / User32.GetSystemMetrics(User32.SYSTEM_METRICS_INDEX.SM_CYSCREEN);
|
||||
return (y * 65535) / User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
|
||||
}
|
||||
}
|
@ -5,13 +5,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using MouseJumpUI.Models.Drawing;
|
||||
using MouseJumpUI.Models.Screen;
|
||||
using MouseJumpUI.NativeMethods;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.NativeMethods.User32;
|
||||
using System.Linq;
|
||||
using MouseJumpUI.Common.Models.Drawing;
|
||||
using MouseJumpUI.Common.NativeMethods;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.User32;
|
||||
|
||||
namespace MouseJumpUI.Helpers;
|
||||
namespace MouseJumpUI.Common.Helpers;
|
||||
|
||||
internal static class ScreenHelper
|
||||
{
|
||||
@ -28,22 +28,21 @@ internal static class ScreenHelper
|
||||
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYVIRTUALSCREEN));
|
||||
}
|
||||
|
||||
public static IEnumerable<ScreenInfo> GetAllScreens()
|
||||
internal static IEnumerable<ScreenInfo> GetAllScreens()
|
||||
{
|
||||
// enumerate the monitors attached to the system
|
||||
var hMonitors = new List<HMONITOR>();
|
||||
var result = User32.EnumDisplayMonitors(
|
||||
HDC.Null,
|
||||
LPCRECT.Null,
|
||||
(unnamedParam1, unnamedParam2, unnamedParam3, unnamedParam4) =>
|
||||
var callback = new User32.MONITORENUMPROC(
|
||||
(hMonitor, hdcMonitor, lprcMonitor, dwData) =>
|
||||
{
|
||||
hMonitors.Add(unnamedParam1);
|
||||
hMonitors.Add(hMonitor);
|
||||
return true;
|
||||
},
|
||||
LPARAM.Null);
|
||||
});
|
||||
var result = User32.EnumDisplayMonitors(HDC.Null, LPCRECT.Null, callback, LPARAM.Null);
|
||||
if (!result)
|
||||
{
|
||||
throw new Win32Exception(
|
||||
result.Value,
|
||||
$"{nameof(User32.EnumDisplayMonitors)} failed with return code {result.Value}");
|
||||
}
|
||||
|
||||
@ -51,11 +50,12 @@ internal static class ScreenHelper
|
||||
foreach (var hMonitor in hMonitors)
|
||||
{
|
||||
var monitorInfoPtr = new LPMONITORINFO(
|
||||
new MONITORINFO((uint)MONITORINFO.Size, RECT.Empty, RECT.Empty, 0));
|
||||
new MONITORINFO((DWORD)MONITORINFO.Size, RECT.Empty, RECT.Empty, 0));
|
||||
result = User32.GetMonitorInfoW(hMonitor, monitorInfoPtr);
|
||||
if (!result)
|
||||
{
|
||||
throw new Win32Exception(
|
||||
result.Value,
|
||||
$"{nameof(User32.GetMonitorInfoW)} failed with return code {result.Value}");
|
||||
}
|
||||
|
||||
@ -78,9 +78,11 @@ internal static class ScreenHelper
|
||||
}
|
||||
}
|
||||
|
||||
public static HMONITOR MonitorFromPoint(
|
||||
internal static ScreenInfo GetScreenFromPoint(
|
||||
List<ScreenInfo> screens,
|
||||
PointInfo pt)
|
||||
{
|
||||
// get the monitor handle from the point
|
||||
var hMonitor = User32.MonitorFromPoint(
|
||||
new((int)pt.X, (int)pt.Y),
|
||||
User32.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
|
||||
@ -89,6 +91,9 @@ internal static class ScreenHelper
|
||||
throw new InvalidOperationException($"no monitor found for point {pt}");
|
||||
}
|
||||
|
||||
return hMonitor;
|
||||
// find the screen with the given monitor handle
|
||||
var screen = screens
|
||||
.Single(item => item.Handle == hMonitor);
|
||||
return screen;
|
||||
}
|
||||
}
|
@ -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.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
@ -5,7 +5,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -25,6 +25,15 @@ internal static partial class Core
|
||||
public readonly LONG right;
|
||||
public readonly LONG bottom;
|
||||
|
||||
public CRECT(
|
||||
int left, int top, int right, int bottom)
|
||||
{
|
||||
this.left = left;
|
||||
this.top = top;
|
||||
this.right = right;
|
||||
this.bottom = bottom;
|
||||
}
|
||||
|
||||
public CRECT(
|
||||
LONG left, LONG top, LONG right, LONG bottom)
|
||||
{
|
@ -2,7 +2,9 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -23,10 +25,17 @@ internal static partial class Core
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
public static int Size =>
|
||||
Marshal.SizeOf(typeof(DWORD));
|
||||
|
||||
public static implicit operator uint(DWORD value) => value.Value;
|
||||
|
||||
public static implicit operator DWORD(uint value) => new(value);
|
||||
|
||||
public static explicit operator int(DWORD value) => (int)value.Value;
|
||||
|
||||
public static explicit operator DWORD(int value) => new((uint)value);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.GetType().Name}({this.Value})";
|
@ -4,7 +4,7 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -31,7 +31,7 @@ internal static partial class Core
|
||||
|
||||
public static implicit operator IntPtr(HANDLE value) => value.Value;
|
||||
|
||||
public static implicit operator HANDLE(IntPtr value) => new(value);
|
||||
public static explicit operator HANDLE(IntPtr value) => new(value);
|
||||
|
||||
public override string ToString()
|
||||
{
|
@ -4,7 +4,7 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -29,6 +29,10 @@ internal static partial class Core
|
||||
|
||||
public bool IsNull => this.Value == HDC.Null.Value;
|
||||
|
||||
public static implicit operator IntPtr(HDC value) => value.Value;
|
||||
|
||||
public static explicit operator HDC(IntPtr value) => new(value);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.GetType().Name}({this.Value})";
|
@ -4,7 +4,7 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -31,15 +31,15 @@ internal static partial class Core
|
||||
|
||||
public static implicit operator int(HMONITOR value) => value.Value.ToInt32();
|
||||
|
||||
public static implicit operator HMONITOR(int value) => new(value);
|
||||
public static explicit operator HMONITOR(int value) => new(value);
|
||||
|
||||
public static implicit operator IntPtr(HMONITOR value) => value.Value;
|
||||
|
||||
public static implicit operator HMONITOR(IntPtr value) => new(value);
|
||||
public static explicit operator HMONITOR(IntPtr value) => new(value);
|
||||
|
||||
public static implicit operator HANDLE(HMONITOR value) => new(value.Value);
|
||||
|
||||
public static implicit operator HMONITOR(HANDLE value) => new(value.Value);
|
||||
public static explicit operator HMONITOR(HANDLE value) => new(value.Value);
|
||||
|
||||
public override string ToString()
|
||||
{
|
@ -3,8 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -20,6 +21,9 @@ internal static partial class Core
|
||||
{
|
||||
public static readonly HWND Null = new(IntPtr.Zero);
|
||||
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Name and value taken from Win32Api")]
|
||||
public static readonly HWND HWND_MESSAGE = new(-3);
|
||||
|
||||
public readonly IntPtr Value;
|
||||
|
||||
public HWND(IntPtr value)
|
@ -2,7 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
@ -3,7 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
using System;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -26,9 +26,11 @@ internal static partial class Core
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
public bool IsNull => this.Value == LPARAM.Null.Value;
|
||||
|
||||
public static implicit operator IntPtr(LPARAM value) => value.Value;
|
||||
|
||||
public static implicit operator LPARAM(IntPtr value) => new(value);
|
||||
public static explicit operator LPARAM(IntPtr value) => new(value);
|
||||
|
||||
public override string ToString()
|
||||
{
|
@ -5,7 +5,7 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -25,6 +25,8 @@ internal static partial class Core
|
||||
this.Value = LPCRECT.ToPtr(value);
|
||||
}
|
||||
|
||||
public bool IsNull => this.Value == LPCRECT.Null.Value;
|
||||
|
||||
private static IntPtr ToPtr(CRECT value)
|
||||
{
|
||||
var ptr = Marshal.AllocHGlobal(CRECT.Size);
|
||||
@ -39,7 +41,7 @@ internal static partial class Core
|
||||
|
||||
public static implicit operator IntPtr(LPCRECT value) => value.Value;
|
||||
|
||||
public static implicit operator LPCRECT(IntPtr value) => new(value);
|
||||
public static explicit operator LPCRECT(IntPtr value) => new(value);
|
||||
|
||||
public override string ToString()
|
||||
{
|
@ -5,7 +5,7 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -25,6 +25,8 @@ internal static partial class Core
|
||||
this.Value = LPPOINT.ToPtr(value);
|
||||
}
|
||||
|
||||
public bool IsNull => this.Value == LPPOINT.Null.Value;
|
||||
|
||||
private static IntPtr ToPtr(POINT value)
|
||||
{
|
||||
var ptr = Marshal.AllocHGlobal(POINT.Size);
|
||||
@ -44,7 +46,7 @@ internal static partial class Core
|
||||
|
||||
public static implicit operator IntPtr(LPPOINT value) => value.Value;
|
||||
|
||||
public static implicit operator LPPOINT(IntPtr value) => new(value);
|
||||
public static explicit operator LPPOINT(IntPtr value) => new(value);
|
||||
|
||||
public override string ToString()
|
||||
{
|
@ -4,7 +4,7 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -24,6 +24,8 @@ internal static partial class Core
|
||||
this.Value = LPRECT.ToPtr(value);
|
||||
}
|
||||
|
||||
public bool IsNull => this.Value == LPRECT.Null.Value;
|
||||
|
||||
private static IntPtr ToPtr(RECT value)
|
||||
{
|
||||
var ptr = Marshal.AllocHGlobal(RECT.Size);
|
||||
@ -38,7 +40,7 @@ internal static partial class Core
|
||||
|
||||
public static implicit operator IntPtr(LPRECT value) => value.Value;
|
||||
|
||||
public static implicit operator LPRECT(IntPtr value) => new(value);
|
||||
public static explicit operator LPRECT(IntPtr value) => new(value);
|
||||
|
||||
public override string ToString()
|
||||
{
|
@ -5,7 +5,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -28,6 +28,14 @@ internal static partial class Core
|
||||
/// </summary>
|
||||
public readonly LONG y;
|
||||
|
||||
public POINT(
|
||||
int x,
|
||||
int y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public POINT(
|
||||
LONG x,
|
||||
LONG y)
|
@ -5,7 +5,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -25,6 +25,15 @@ internal static partial class Core
|
||||
public readonly LONG right;
|
||||
public readonly LONG bottom;
|
||||
|
||||
public RECT(
|
||||
int left, int top, int right, int bottom)
|
||||
{
|
||||
this.left = left;
|
||||
this.top = top;
|
||||
this.right = right;
|
||||
this.bottom = bottom;
|
||||
}
|
||||
|
||||
public RECT(
|
||||
LONG left, LONG top, LONG right, LONG bottom)
|
||||
{
|
@ -1,7 +1,8 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -26,6 +27,10 @@ internal static partial class Core
|
||||
|
||||
public static implicit operator UINT(uint value) => new(value);
|
||||
|
||||
public static explicit operator int(UINT value) => (int)value.Value;
|
||||
|
||||
public static explicit operator UINT(int value) => new((uint)value);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.GetType().Name}({this.Value})";
|
@ -4,7 +4,7 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
||||
@ -34,7 +34,7 @@ internal static partial class Core
|
||||
|
||||
public static implicit operator UIntPtr(ULONG_PTR value) => value.Value;
|
||||
|
||||
public static implicit operator ULONG_PTR(UIntPtr value) => new(value);
|
||||
public static explicit operator ULONG_PTR(UIntPtr value) => new(value);
|
||||
|
||||
public override string ToString()
|
||||
{
|
@ -2,7 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Core
|
||||
{
|
@ -2,7 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Gdi32
|
||||
{
|
@ -2,7 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Gdi32
|
||||
{
|
@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Gdi32
|
||||
{
|
@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class Gdi32
|
||||
{
|
@ -2,7 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static class Libraries
|
||||
{
|
@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -5,7 +5,7 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -2,9 +2,9 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -4,9 +4,9 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
||||
@ -34,6 +34,6 @@ internal static partial class User32
|
||||
}
|
||||
|
||||
public static int Size =>
|
||||
Marshal.SizeOf(typeof(INPUT));
|
||||
Marshal.SizeOf(typeof(MONITORINFO));
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
||||
internal static partial class User32
|
@ -4,7 +4,7 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
||||
internal static partial class User32
|
@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -4,9 +4,9 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -4,9 +4,9 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -4,7 +4,7 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
||||
internal static partial class User32
|
@ -4,9 +4,9 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -6,7 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
||||
@ -52,7 +52,7 @@ internal static partial class User32
|
||||
var size = INPUT.Size;
|
||||
foreach (var value in values)
|
||||
{
|
||||
Marshal.StructureToPtr(value, ptr, true);
|
||||
Marshal.StructureToPtr(value, ptr, false);
|
||||
ptr += size;
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -5,7 +5,7 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
||||
internal static partial class User32
|
@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -4,7 +4,7 @@
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -4,7 +4,7 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
|
||||
internal static partial class User32
|
@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
using static MouseJumpUI.Common.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
namespace MouseJumpUI.Common.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
@ -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.Location = new System.Drawing.Point(0, 0);
|
||||
panel1.Name = "panel1";
|
||||
panel1.Padding = new Padding(5);
|
||||
panel1.Size = new System.Drawing.Size(800, 450);
|
||||
panel1.TabIndex = 1;
|
||||
//
|
||||
@ -59,7 +58,7 @@ partial class MainForm
|
||||
Thumbnail.Dock = DockStyle.Fill;
|
||||
Thumbnail.Location = new System.Drawing.Point(5, 5);
|
||||
Thumbnail.Name = "Thumbnail";
|
||||
Thumbnail.Size = new System.Drawing.Size(790, 440);
|
||||
Thumbnail.Size = new System.Drawing.Size(800, 450);
|
||||
Thumbnail.SizeMode = PictureBoxSizeMode.Normal;
|
||||
Thumbnail.TabIndex = 1;
|
||||
Thumbnail.TabStop = false;
|
||||
|
@ -5,15 +5,14 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using MouseJumpUI.Common.Helpers;
|
||||
using MouseJumpUI.Common.Imaging;
|
||||
using MouseJumpUI.Common.Models.Drawing;
|
||||
using MouseJumpUI.Common.Models.Layout;
|
||||
using MouseJumpUI.Helpers;
|
||||
using MouseJumpUI.Models.Drawing;
|
||||
using MouseJumpUI.Models.Layout;
|
||||
using static MouseJumpUI.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI;
|
||||
|
||||
@ -25,6 +24,12 @@ internal partial class MainForm : Form
|
||||
this.SettingsHelper = settingsHelper ?? throw new ArgumentNullException(nameof(settingsHelper));
|
||||
}
|
||||
|
||||
private PreviewLayout? PreviewLayout
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public SettingsHelper SettingsHelper
|
||||
{
|
||||
get;
|
||||
@ -42,61 +47,68 @@ internal partial class MainForm : Form
|
||||
return;
|
||||
}
|
||||
|
||||
// map screens to their screen number in "System > Display"
|
||||
var screens = ScreenHelper.GetAllScreens()
|
||||
.Select((screen, index) => new { Screen = screen, Index = index, Number = index + 1 })
|
||||
.ToList();
|
||||
var screens = ScreenHelper.GetAllScreens().ToList();
|
||||
if (screens.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentLocation = MouseHelper.GetCursorPosition();
|
||||
var currentScreenHandle = ScreenHelper.MonitorFromPoint(currentLocation);
|
||||
var currentScreen = screens
|
||||
.Single(item => item.Screen.Handle == currentScreenHandle.Value);
|
||||
var targetScreenNumber = default(int?);
|
||||
var currentScreen = ScreenHelper.GetScreenFromPoint(screens, currentLocation);
|
||||
var currentScreenIndex = screens.IndexOf(currentScreen);
|
||||
var targetScreen = default(ScreenInfo?);
|
||||
|
||||
if (((e.KeyCode >= Keys.D1) && (e.KeyCode <= Keys.D9))
|
||||
|| ((e.KeyCode >= Keys.NumPad1) && (e.KeyCode <= Keys.NumPad9)))
|
||||
switch (e.KeyCode)
|
||||
{
|
||||
// number keys 1-9 or numpad keys 1-9 - move to the numbered screen
|
||||
case >= Keys.D1 and <= Keys.D9:
|
||||
{
|
||||
// number keys 1-9 - move to the numbered screen
|
||||
var screenNumber = e.KeyCode - Keys.D0;
|
||||
if (screenNumber <= screens.Count)
|
||||
{
|
||||
targetScreenNumber = screenNumber;
|
||||
}
|
||||
}
|
||||
else if (e.KeyCode == Keys.P)
|
||||
{
|
||||
// "P" - move to the primary screen
|
||||
targetScreenNumber = screens.Single(item => item.Screen.Primary).Number;
|
||||
}
|
||||
else if (e.KeyCode == Keys.Left)
|
||||
{
|
||||
// move to the previous screen
|
||||
targetScreenNumber = currentScreen.Number == 1
|
||||
? screens.Count
|
||||
: currentScreen.Number - 1;
|
||||
}
|
||||
else if (e.KeyCode == Keys.Right)
|
||||
{
|
||||
// move to the next screen
|
||||
targetScreenNumber = currentScreen.Number == screens.Count
|
||||
? 1
|
||||
: currentScreen.Number + 1;
|
||||
}
|
||||
else if (e.KeyCode == Keys.Home)
|
||||
{
|
||||
// move to the first screen
|
||||
targetScreenNumber = 1;
|
||||
}
|
||||
else if (e.KeyCode == Keys.End)
|
||||
{
|
||||
// move to the last screen
|
||||
targetScreenNumber = screens.Count;
|
||||
/* note - screen *numbers* are 1-based, screen *indexes* are 0-based */
|
||||
targetScreen = (screenNumber <= screens.Count)
|
||||
? targetScreen = screens[screenNumber - 1]
|
||||
: null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetScreenNumber.HasValue)
|
||||
case >= Keys.NumPad1 and <= Keys.NumPad9:
|
||||
{
|
||||
MouseHelper.SetCursorPosition(
|
||||
screens[targetScreenNumber.Value - 1].Screen.Bounds.Midpoint);
|
||||
// numpad keys 1-9 - move to the numbered screen
|
||||
var screenNumber = e.KeyCode - Keys.NumPad0;
|
||||
/* note - screen *numbers* are 1-based, screen *indexes* are 0-based */
|
||||
targetScreen = (screenNumber <= screens.Count)
|
||||
? targetScreen = screens[screenNumber - 1]
|
||||
: null;
|
||||
break;
|
||||
}
|
||||
|
||||
case Keys.P:
|
||||
// "P" - move to the primary screen
|
||||
targetScreen = screens.Single(screen => screen.Primary);
|
||||
break;
|
||||
case Keys.Left:
|
||||
// move to the previous screen, looping back to the end if needed
|
||||
var prevIndex = (currentScreenIndex - 1 + screens.Count) % screens.Count;
|
||||
targetScreen = screens[prevIndex];
|
||||
break;
|
||||
case Keys.Right:
|
||||
// move to the next screen, looping round to the start if needed
|
||||
var nextIndex = (currentScreenIndex + 1) % screens.Count;
|
||||
targetScreen = screens[nextIndex];
|
||||
break;
|
||||
case Keys.Home:
|
||||
// move to the first screen
|
||||
targetScreen = screens.First();
|
||||
break;
|
||||
case Keys.End:
|
||||
// move to the last screen
|
||||
targetScreen = screens.Last();
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetScreen is not null)
|
||||
{
|
||||
MouseHelper.SetCursorPosition(targetScreen.DisplayArea.Midpoint);
|
||||
this.OnDeactivate(EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
@ -118,15 +130,42 @@ internal partial class MainForm : Form
|
||||
|
||||
if (mouseEventArgs.Button == MouseButtons.Left)
|
||||
{
|
||||
// plain click - move mouse pointer
|
||||
var virtualScreen = ScreenHelper.GetVirtualScreen();
|
||||
var scaledLocation = MouseHelper.GetJumpLocation(
|
||||
new PointInfo(mouseEventArgs.X, mouseEventArgs.Y),
|
||||
new SizeInfo(this.Thumbnail.Size),
|
||||
virtualScreen);
|
||||
Logger.LogInfo($"scaled location = {scaledLocation}");
|
||||
MouseHelper.SetCursorPosition(scaledLocation);
|
||||
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent());
|
||||
if (this.PreviewLayout is null)
|
||||
{
|
||||
// there's no layout data so we can't work out what screen was clicked
|
||||
return;
|
||||
}
|
||||
|
||||
// work out which screenshot was clicked
|
||||
var clickedScreenshot = this.PreviewLayout.ScreenshotBounds
|
||||
.FirstOrDefault(
|
||||
box => box.BorderBounds.Contains(mouseEventArgs.X, mouseEventArgs.Y));
|
||||
if (clickedScreenshot is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// scale up the click onto the physical screen - the aspect ratio of the screenshot
|
||||
// might be distorted compared to the physical screen due to the borders around the
|
||||
// screenshot, so we need to work out the target location on the physical screen first
|
||||
var clickedScreen =
|
||||
this.PreviewLayout.Screens[this.PreviewLayout.ScreenshotBounds.IndexOf(clickedScreenshot)];
|
||||
var clickedLocation = new PointInfo(mouseEventArgs.Location)
|
||||
.Stretch(
|
||||
source: clickedScreenshot.ContentBounds,
|
||||
target: clickedScreen)
|
||||
.Clamp(
|
||||
new(
|
||||
x: clickedScreen.X + 1,
|
||||
y: clickedScreen.Y + 1,
|
||||
width: clickedScreen.Width - 1,
|
||||
height: clickedScreen.Height - 1
|
||||
))
|
||||
.Truncate();
|
||||
|
||||
// move mouse pointer
|
||||
Logger.LogInfo($"clicked location = {clickedLocation}");
|
||||
MouseHelper.SetCursorPosition(clickedLocation);
|
||||
}
|
||||
|
||||
this.OnDeactivate(EventArgs.Empty);
|
||||
@ -138,162 +177,34 @@ internal partial class MainForm : Form
|
||||
this.Visible = false;
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var layoutInfo = MainForm.GetLayoutInfo(this);
|
||||
LayoutHelper.PositionForm(this, layoutInfo.FormBounds);
|
||||
MainForm.RenderPreview(this, layoutInfo);
|
||||
|
||||
var appSettings = this.SettingsHelper.CurrentSettings ?? throw new InvalidOperationException();
|
||||
var screens = ScreenHelper.GetAllScreens().Select(screen => screen.DisplayArea).ToList();
|
||||
var activatedLocation = MouseHelper.GetCursorPosition();
|
||||
this.PreviewLayout = LayoutHelper.GetPreviewLayout(
|
||||
previewStyle: StyleHelper.LegacyPreviewStyle.WithCanvasSize(
|
||||
new(
|
||||
appSettings.Properties.ThumbnailSize.Width,
|
||||
appSettings.Properties.ThumbnailSize.Height
|
||||
)),
|
||||
screens: screens,
|
||||
activatedLocation: activatedLocation);
|
||||
|
||||
this.PositionForm(this.PreviewLayout.FormBounds);
|
||||
|
||||
var imageCopyService = new DesktopImageRegionCopyService();
|
||||
DrawingHelper.RenderPreview(
|
||||
this.PreviewLayout,
|
||||
imageCopyService,
|
||||
this.OnPreviewImageCreated,
|
||||
this.OnPreviewImageUpdated);
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
// we have to activate the form to make sure the deactivate event fires
|
||||
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpShowEvent());
|
||||
this.Activate();
|
||||
}
|
||||
|
||||
private static LayoutInfo GetLayoutInfo(MainForm form)
|
||||
{
|
||||
// map screens to their screen number in "System > Display"
|
||||
var screens = ScreenHelper.GetAllScreens()
|
||||
.Select((screen, index) => new { Screen = screen, Index = index, Number = index + 1 })
|
||||
.ToList();
|
||||
foreach (var screen in screens)
|
||||
{
|
||||
Logger.LogInfo(string.Join(
|
||||
'\n',
|
||||
$"screen[{screen.Number}]",
|
||||
$"\tprimary = {screen.Screen.Primary}",
|
||||
$"\tdisplay area = {screen.Screen.DisplayArea}",
|
||||
$"\tworking area = {screen.Screen.WorkingArea}"));
|
||||
}
|
||||
|
||||
// collect together some values that we need for calculating layout
|
||||
var activatedLocation = MouseHelper.GetCursorPosition();
|
||||
var activatedScreenHandle = ScreenHelper.MonitorFromPoint(activatedLocation);
|
||||
var activatedScreenIndex = screens
|
||||
.Single(item => item.Screen.Handle == activatedScreenHandle.Value)
|
||||
.Index;
|
||||
|
||||
// avoid a race condition - cache the current settings in case they change
|
||||
var currentSettings = form.SettingsHelper.CurrentSettings;
|
||||
|
||||
var layoutConfig = new LayoutConfig(
|
||||
virtualScreenBounds: ScreenHelper.GetVirtualScreen(),
|
||||
screens: screens.Select(item => item.Screen).ToList(),
|
||||
activatedLocation: activatedLocation,
|
||||
activatedScreenIndex: activatedScreenIndex,
|
||||
activatedScreenNumber: activatedScreenIndex + 1,
|
||||
maximumFormSize: new(
|
||||
currentSettings.Properties.ThumbnailSize.Width,
|
||||
currentSettings.Properties.ThumbnailSize.Height),
|
||||
/*
|
||||
don't read the panel padding values because they are affected by dpi scaling
|
||||
and can give wrong values when moving between monitors with different dpi scaling
|
||||
*/
|
||||
formPadding: new(5, 5, 5, 5),
|
||||
previewPadding: new(0));
|
||||
Logger.LogInfo(string.Join(
|
||||
'\n',
|
||||
$"Layout config",
|
||||
$"-------------",
|
||||
$"virtual screen = {layoutConfig.VirtualScreenBounds}",
|
||||
$"activated location = {layoutConfig.ActivatedLocation}",
|
||||
$"activated screen index = {layoutConfig.ActivatedScreenIndex}",
|
||||
$"activated screen number = {layoutConfig.ActivatedScreenNumber}",
|
||||
$"maximum form size = {layoutConfig.MaximumFormSize}",
|
||||
$"form padding = {layoutConfig.FormPadding}",
|
||||
$"preview padding = {layoutConfig.PreviewPadding}"));
|
||||
|
||||
// calculate the layout coordinates for everything
|
||||
var layoutInfo = LayoutHelper.CalculateLayoutInfo(layoutConfig);
|
||||
Logger.LogInfo(string.Join(
|
||||
'\n',
|
||||
$"Layout info",
|
||||
$"-----------",
|
||||
$"form bounds = {layoutInfo.FormBounds}",
|
||||
$"preview bounds = {layoutInfo.PreviewBounds}",
|
||||
$"activated screen = {layoutInfo.ActivatedScreenBounds}"));
|
||||
|
||||
return layoutInfo;
|
||||
}
|
||||
|
||||
private static void RenderPreview(
|
||||
MainForm form, LayoutInfo layoutInfo)
|
||||
{
|
||||
var layoutConfig = layoutInfo.LayoutConfig;
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
// initialize the preview image
|
||||
var preview = new Bitmap(
|
||||
(int)layoutInfo.PreviewBounds.Width,
|
||||
(int)layoutInfo.PreviewBounds.Height,
|
||||
PixelFormat.Format32bppArgb);
|
||||
form.Thumbnail.Image = preview;
|
||||
|
||||
using var previewGraphics = Graphics.FromImage(preview);
|
||||
|
||||
DrawingHelper.DrawPreviewBackground(previewGraphics, layoutInfo.PreviewBounds, layoutInfo.ScreenBounds);
|
||||
|
||||
var desktopHwnd = HWND.Null;
|
||||
var desktopHdc = HDC.Null;
|
||||
var previewHdc = HDC.Null;
|
||||
try
|
||||
{
|
||||
// sort the source and target screen areas, putting the activated screen first
|
||||
// (we need to capture and draw the activated screen before we show the form
|
||||
// because otherwise we'll capture the form as part of the screenshot!)
|
||||
var sourceScreens = layoutConfig.Screens
|
||||
.Where((_, idx) => idx == layoutConfig.ActivatedScreenIndex)
|
||||
.Union(layoutConfig.Screens.Where((_, idx) => idx != layoutConfig.ActivatedScreenIndex))
|
||||
.Select(screen => screen.Bounds)
|
||||
.ToList();
|
||||
var targetScreens = layoutInfo.ScreenBounds
|
||||
.Where((_, idx) => idx == layoutConfig.ActivatedScreenIndex)
|
||||
.Union(layoutInfo.ScreenBounds.Where((_, idx) => idx != layoutConfig.ActivatedScreenIndex))
|
||||
.ToList();
|
||||
|
||||
DrawingHelper.EnsureDesktopDeviceContext(ref desktopHwnd, ref desktopHdc);
|
||||
DrawingHelper.EnsurePreviewDeviceContext(previewGraphics, ref previewHdc);
|
||||
|
||||
var placeholdersDrawn = false;
|
||||
for (var i = 0; i < sourceScreens.Count; i++)
|
||||
{
|
||||
DrawingHelper.DrawPreviewScreen(
|
||||
desktopHdc, previewHdc, sourceScreens[i], targetScreens[i]);
|
||||
|
||||
// show the placeholder images and show the form if it looks like it might take
|
||||
// a while to capture the remaining screenshot images (but only if there are any)
|
||||
if ((i < (sourceScreens.Count - 1)) && (stopwatch.ElapsedMilliseconds > 250))
|
||||
{
|
||||
// we need to release the device context handle before we draw the placeholders
|
||||
// using the Graphics object otherwise we'll get an error from GDI saying
|
||||
// "Object is currently in use elsewhere"
|
||||
DrawingHelper.FreePreviewDeviceContext(previewGraphics, ref previewHdc);
|
||||
|
||||
if (!placeholdersDrawn)
|
||||
{
|
||||
// draw placeholders for any undrawn screens
|
||||
DrawingHelper.DrawPreviewScreenPlaceholders(
|
||||
previewGraphics,
|
||||
targetScreens.Where((_, idx) => idx > i));
|
||||
placeholdersDrawn = true;
|
||||
}
|
||||
|
||||
MainForm.RefreshPreview(form);
|
||||
|
||||
// we've still got more screens to draw so open the device context again
|
||||
DrawingHelper.EnsurePreviewDeviceContext(previewGraphics, ref previewHdc);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DrawingHelper.FreeDesktopDeviceContext(ref desktopHwnd, ref desktopHdc);
|
||||
DrawingHelper.FreePreviewDeviceContext(previewGraphics, ref previewHdc);
|
||||
}
|
||||
|
||||
MainForm.RefreshPreview(form);
|
||||
stopwatch.Stop();
|
||||
}
|
||||
|
||||
private void ClearPreview()
|
||||
{
|
||||
if (this.Thumbnail.Image is null)
|
||||
@ -310,17 +221,40 @@ internal partial class MainForm : Form
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
private static void RefreshPreview(MainForm form)
|
||||
/// <summary>
|
||||
/// Resize and position the specified form.
|
||||
/// </summary>
|
||||
private void PositionForm(RectangleInfo bounds)
|
||||
{
|
||||
if (!form.Visible)
|
||||
{
|
||||
// we seem to need to turn off topmost and then re-enable it again
|
||||
// when we show the form, otherwise it doesn't get shown topmost...
|
||||
form.TopMost = false;
|
||||
form.TopMost = true;
|
||||
form.Show();
|
||||
// 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;
|
||||
}
|
||||
|
||||
form.Thumbnail.Refresh();
|
||||
private void OnPreviewImageCreated(Bitmap preview)
|
||||
{
|
||||
this.ClearPreview();
|
||||
this.Thumbnail.Image = preview;
|
||||
}
|
||||
|
||||
private void OnPreviewImageUpdated()
|
||||
{
|
||||
if (!this.Visible)
|
||||
{
|
||||
// we seem to need to turn off topmost and then re-enable it again
|
||||
// when we show the form, otherwise it doesn't always get shown topmost...
|
||||
this.TopMost = false;
|
||||
this.TopMost = true;
|
||||
this.Show();
|
||||
}
|
||||
|
||||
this.Thumbnail.Refresh();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,64 @@
|
||||
<root>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
Loading…
Reference in New Issue
Block a user