From 1cfb30b0cc9227b6bf660bd7b7ceb7af55075a93 Mon Sep 17 00:00:00 2001 From: mikeclayton Date: Mon, 21 Oct 2024 21:07:46 +0100 Subject: [PATCH] [MouseJump] added Mouse Jump style controls to Settings UI (#27511) --- .../Helpers/DrawingHelperTests.cs | 19 +- .../Helpers/LayoutHelperTests.cs | 66 ++- .../Models/Drawing/SizeInfoTests.cs | 24 +- .../MouseJump.Common/Helpers/ConfigHelper.cs | 94 +++++ .../MouseJump.Common/Helpers/DrawingHelper.cs | 7 +- .../MouseJump.Common/Helpers/LayoutHelper.cs | 19 +- .../MouseJump.Common/Helpers/MouseHelper.cs | 2 +- .../MouseJump.Common/Helpers/StyleHelper.cs | 24 +- .../Imaging/StaticImageRegionCopyService.cs | 7 + .../Models/Drawing/BoxBounds.cs | 2 +- .../Models/Drawing/RectangleInfo.cs | 9 + .../Models/Drawing/ScreenInfo.cs | 2 + .../Models/Drawing/SizeInfo.cs | 88 ++-- .../Models/Settings/PreviewType.cs | 12 + .../Models/Styles/BorderStyle.cs | 8 +- .../MouseUtils/MouseJumpUI/MainForm.cs | 2 +- src/modules/MouseUtils/MouseJumpUI/Program.cs | 25 -- .../MouseJumpProperties.cs | 95 +++++ .../MouseJumpPreviewTypeConverter.cs | 69 ++++ .../Settings.UI/Images/MouseJump-Desktop.png | Bin 0 -> 26840 bytes .../Settings.UI/PowerToys.Settings.csproj | 9 + .../SettingsXAML/Panels/MouseJumpPanel.xaml | 139 ++++++- .../Panels/MouseJumpPanel.xaml.cs | 140 +++++++ .../Settings.UI/Strings/en-us/Resources.resw | 148 +++++-- .../MouseUtilsViewModel_MouseJump.cs | 387 ++++++++++++++++++ 25 files changed, 1256 insertions(+), 141 deletions(-) create mode 100644 src/modules/MouseUtils/MouseJump.Common/Helpers/ConfigHelper.cs create mode 100644 src/modules/MouseUtils/MouseJump.Common/Models/Settings/PreviewType.cs create mode 100644 src/settings-ui/Settings.UI/Converters/MouseJumpPreviewTypeConverter.cs create mode 100644 src/settings-ui/Settings.UI/Images/MouseJump-Desktop.png diff --git a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/DrawingHelperTests.cs b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/DrawingHelperTests.cs index 5ba0a0eef8ea..10ccf8bf20c5 100644 --- a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/DrawingHelperTests.cs +++ b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/DrawingHelperTests.cs @@ -46,7 +46,7 @@ public static IEnumerable GetTestCases() yield return new object[] { new TestCase( - previewStyle: StyleHelper.DefaultPreviewStyle, + previewStyle: StyleHelper.BezelledPreviewStyle, screens: new List() { new(0, 0, 500, 500), @@ -62,7 +62,7 @@ public static IEnumerable GetTestCases() yield return new object[] { new TestCase( - previewStyle: StyleHelper.DefaultPreviewStyle, + previewStyle: StyleHelper.BezelledPreviewStyle, screens: new List() { new(5120, 349, 1920, 1080), @@ -93,25 +93,18 @@ public void RunTestCases(TestCase data) 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) { - // assume embedded resources are in the same source folder as this - // class, and the namespace hierarchy matches the folder structure. - // that way we can build resource names from the current namespace - var resourcePrefix = typeof(DrawingHelperTests).Namespace; - var resourceName = $"{resourcePrefix}.{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)) { - var message = $"Embedded resource '{resourceName}' does not exist. " + - "Valid resource names are: \r\n" + string.Join("\r\n", resourceNames); - throw new InvalidOperationException(message); + throw new InvalidOperationException($"Embedded resource '{resourceName}' does not exist."); } var stream = assembly.GetManifestResourceStream(resourceName) @@ -121,7 +114,7 @@ private static Bitmap LoadImageResource(string filename) } /// - /// Naive / brute force image comparison - we can optimise this later :-) + /// Naive / brute force image comparison - we can optimize this later :-) /// private static void AssertImagesEqual(Bitmap expected, Bitmap actual) { diff --git a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/LayoutHelperTests.cs b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/LayoutHelperTests.cs index 5bdd4af443d0..64b7d0cef2e1 100644 --- a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/LayoutHelperTests.cs +++ b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/LayoutHelperTests.cs @@ -129,7 +129,7 @@ public TestCase(PreviewStyle previewStyle, List screens, PointInf public static IEnumerable GetTestCases() { // happy path - single screen with 50% scaling, - // *has* a preview borders but *no* screenshot borders + // *has* a preview border but *no* screenshot borders // // +----------------+ // | | @@ -160,7 +160,7 @@ public static IEnumerable GetTestCases() new(0, 0, 1024, 768), }; var activatedLocation = new PointInfo(512, 384); - var previewLayout = new PreviewLayout( + var expectedResult = new PreviewLayout( virtualScreen: new(0, 0, 1024, 768), screens: screens, activatedScreenIndex: 0, @@ -183,7 +183,7 @@ public static IEnumerable GetTestCases() contentBounds: new(6, 6, 512, 384) ), }); - yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, previewLayout) }; + yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, expectedResult) }; // happy path - single screen with 50% scaling, // *no* preview borders but *has* screenshot borders @@ -217,7 +217,7 @@ public static IEnumerable GetTestCases() new(0, 0, 1024, 768), }; activatedLocation = new PointInfo(512, 384); - previewLayout = new PreviewLayout( + expectedResult = new PreviewLayout( virtualScreen: new(0, 0, 1024, 768), screens: screens, activatedScreenIndex: 0, @@ -240,7 +240,59 @@ public static IEnumerable GetTestCases() contentBounds: new(6, 6, 500, 372) ), }); - yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, previewLayout) }; + yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, expectedResult) }; + + // rounding error check - single screen with 33% scaling, + // no borders, check to make sure form scales to exactly + // fill the canvas size with no rounding errors. + // + // in this test the preview width is 300 and the desktop is + // 900, so the scaling factor is 1/3, but this gets rounded + // to 0.3333333333333333333333333333, and 900 times this value + // is 299.99999999999999999999999997. if we don't scale correctly + // the resulting form width might only be 299 pixels instead of 300 + // + // +----------------+ + // | | + // | 0 | + // | | + // +----------------+ + previewStyle = new PreviewStyle( + canvasSize: new( + width: 300, + height: 200 + ), + canvasStyle: BoxStyle.Empty, + screenStyle: BoxStyle.Empty); + screens = new List + { + new(0, 0, 900, 200), + }; + activatedLocation = new PointInfo(450, 100); + expectedResult = new PreviewLayout( + virtualScreen: new(0, 0, 900, 200), + screens: screens, + activatedScreenIndex: 0, + formBounds: new(300, 66.5m, 300, 67), + previewStyle: previewStyle, + previewBounds: new( + outerBounds: new(0, 0, 300, 67), + marginBounds: new(0, 0, 300, 67), + borderBounds: new(0, 0, 300, 67), + paddingBounds: new(0, 0, 300, 67), + contentBounds: new(0, 0, 300, 67) + ), + screenshotBounds: new() + { + new( + outerBounds: new(0, 0, 300, 67), + marginBounds: new(0, 0, 300, 67), + borderBounds: new(0, 0, 300, 67), + paddingBounds: new(0, 0, 300, 67), + contentBounds: new(0, 0, 300, 67) + ), + }); + yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, expectedResult) }; // primary monitor not topmost / leftmost - if there are screens // that are further left or higher up than the primary monitor @@ -291,7 +343,7 @@ public static IEnumerable GetTestCases() new(0, 0, 5120, 1440), }; activatedLocation = new(-960, 60); - previewLayout = new PreviewLayout( + expectedResult = new PreviewLayout( virtualScreen: new(-1920, -480, 7040, 1920), screens: screens, activatedScreenIndex: 0, @@ -321,7 +373,7 @@ public static IEnumerable GetTestCases() contentBounds: new(204, 60, 500, 132) ), }); - yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, previewLayout) }; + yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, expectedResult) }; } [TestMethod] diff --git a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Models/Drawing/SizeInfoTests.cs b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Models/Drawing/SizeInfoTests.cs index e6c33e3d7a87..9871d0526cdd 100644 --- a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Models/Drawing/SizeInfoTests.cs +++ b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Models/Drawing/SizeInfoTests.cs @@ -15,45 +15,49 @@ public sealed class ScaleToFitTests { public sealed class TestCase { - public TestCase(SizeInfo obj, SizeInfo bounds, SizeInfo expectedResult) + public TestCase(SizeInfo source, SizeInfo bounds, SizeInfo expectedResult, decimal scalingRatio) { - this.Obj = obj; + this.Source = source; this.Bounds = bounds; this.ExpectedResult = expectedResult; + this.ScalingRatio = scalingRatio; } - public SizeInfo Obj { get; } + public SizeInfo Source { get; } public SizeInfo Bounds { get; } public SizeInfo ExpectedResult { get; } + + public decimal ScalingRatio { get; } } public static IEnumerable GetTestCases() { // identity tests - 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)), }; + yield return new object[] { new TestCase(new(512, 384), new(512, 384), new(512, 384), 1), }; + yield return new object[] { new TestCase(new(1024, 768), new(1024, 768), new(1024, 768), 1), }; // general tests - 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)), }; + yield return new object[] { new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536), 4), }; + yield return new object[] { new TestCase(new(2048, 1536), new(1024, 768), new(1024, 768), 0.5m), }; // scale to fit width - yield return new object[] { 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), 4), }; // scale to fit height - yield return new object[] { 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), 4), }; } [TestMethod] [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] public void RunTestCases(TestCase data) { - var actual = data.Obj.ScaleToFit(data.Bounds); + var actual = data.Source.ScaleToFit(data.Bounds, out var scalingRatio); var expected = data.ExpectedResult; Assert.AreEqual(expected.Width, actual.Width); Assert.AreEqual(expected.Height, actual.Height); + Assert.AreEqual(scalingRatio, data.ScalingRatio); } } diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/ConfigHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/ConfigHelper.cs new file mode 100644 index 000000000000..a853d77a8d6c --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/ConfigHelper.cs @@ -0,0 +1,94 @@ +// 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.Globalization; +using System.Linq; + +namespace MouseJump.Common.Helpers; + +public static class ConfigHelper +{ + public static Color? ToUnnamedColor(Color? value) + { + if (!value.HasValue) + { + return null; + } + + var color = value.Value; + return Color.FromArgb(color.A, color.R, color.G, color.B); + } + + public static string? SerializeToConfigColorString(Color? value) + { + if (!value.HasValue) + { + return null; + } + + var color = value.Value; + return color switch + { + Color { IsNamedColor: true } => + $"{nameof(Color)}.{color.Name}", + Color { IsSystemColor: true } => + $"{nameof(SystemColors)}.{color.Name}", + _ => + $"#{color.R:X2}{color.G:X2}{color.B:X2}", + }; + } + + public static Color? DeserializeFromConfigColorString(string? value) + { + if (string.IsNullOrEmpty(value)) + { + return null; + } + + // e.g. "#AABBCC" + if (value.StartsWith('#')) + { + var culture = CultureInfo.InvariantCulture; + if ((value.Length == 7) + && int.TryParse(value[1..3], NumberStyles.HexNumber, culture, out var r) + && int.TryParse(value[3..5], NumberStyles.HexNumber, culture, out var g) + && int.TryParse(value[5..7], NumberStyles.HexNumber, culture, out var b)) + { + return Color.FromArgb(0xFF, r, g, b); + } + } + + const StringComparison comparison = StringComparison.InvariantCulture; + + // e.g. "Color.Red" + const string colorPrefix = $"{nameof(Color)}."; + if (value.StartsWith(colorPrefix, comparison)) + { + var colorName = value[colorPrefix.Length..]; + var property = typeof(Color).GetProperties() + .SingleOrDefault(property => property.Name == colorName); + if (property is not null) + { + return (Color?)property.GetValue(null, null); + } + } + + // e.g. "SystemColors.Highlight" + const string systemColorPrefix = $"{nameof(SystemColors)}."; + if (value.StartsWith(systemColorPrefix, comparison)) + { + var colorName = value[systemColorPrefix.Length..]; + var property = typeof(SystemColors).GetProperties() + .SingleOrDefault(property => property.Name == colorName); + if (property is not null) + { + return (Color?)property.GetValue(null, null); + } + } + + return null; + } +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/DrawingHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/DrawingHelper.cs index 278e07039fc9..668e90dd3d67 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Helpers/DrawingHelper.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/DrawingHelper.cs @@ -102,8 +102,13 @@ private static void DrawRaisedBorder( return; } + if (borderStyle.Color is null) + { + return; + } + // draw the main box border - using var borderBrush = new SolidBrush(borderStyle.Color); + using var borderBrush = new SolidBrush(borderStyle.Color.Value); var borderRegion = new Region(boxBounds.BorderBounds.ToRectangle()); borderRegion.Exclude(boxBounds.PaddingBounds.ToRectangle()); graphics.FillRegion(borderBrush, borderRegion); diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/LayoutHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/LayoutHelper.cs index 791d1f7ffb8f..02dea7e6c0a2 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Helpers/LayoutHelper.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/LayoutHelper.cs @@ -46,16 +46,13 @@ public static PreviewLayout GetPreviewLayout( .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() + .ScaleToFit(maxContentSize, out var scalingRatio) + .Round() + .Clamp(maxContentSize) .PlaceAt(0, 0) .Offset(previewStyle.CanvasStyle.MarginStyle.Left, previewStyle.CanvasStyle.MarginStyle.Top) .Offset(previewStyle.CanvasStyle.BorderStyle.Left, previewStyle.CanvasStyle.BorderStyle.Top) @@ -82,16 +79,16 @@ public static PreviewLayout GetPreviewLayout( screen => LayoutHelper.GetBoxBoundsFromOuterBounds( screen .Offset(builder.VirtualScreen.Location.ToSize().Invert()) - .Scale(screenScalingRatio) + .Scale(scalingRatio) .Offset(builder.PreviewBounds.ContentBounds.Location.ToSize()) - .Truncate(), + .Round(), previewStyle.ScreenStyle)) .ToList(); return builder.Build(); } - internal static RectangleInfo GetCombinedScreenBounds(List screens) + public static RectangleInfo GetCombinedScreenBounds(List screens) { return screens.Skip(1).Aggregate( seed: screens.First(), @@ -107,7 +104,7 @@ internal static RectangleInfo GetCombinedScreenBounds(List screen /// A object that represents the bounds of the different areas of the box. /// Thrown when or is null. /// Thrown when any of the styles in is null. - public static BoxBounds GetBoxBoundsFromContentBounds( + internal static BoxBounds GetBoxBoundsFromContentBounds( RectangleInfo contentBounds, BoxStyle boxStyle) { @@ -135,7 +132,7 @@ public static BoxBounds GetBoxBoundsFromContentBounds( /// A object that represents the bounds of the different areas of the box. /// Thrown when or is null. /// Thrown when any of the styles in is null. - public static BoxBounds GetBoxBoundsFromOuterBounds( + internal static BoxBounds GetBoxBoundsFromOuterBounds( RectangleInfo outerBounds, BoxStyle boxStyle) { diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/MouseHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/MouseHelper.cs index 935495887860..443b252aa356 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Helpers/MouseHelper.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/MouseHelper.cs @@ -102,7 +102,7 @@ public static void SetCursorPosition(PointInfo location) /// See https://github.com/microsoft/PowerToys/issues/24523 /// https://github.com/microsoft/PowerToys/pull/24527 /// - internal static void SimulateMouseMovementEvent(PointInfo location) + private static void SimulateMouseMovementEvent(PointInfo location) { var inputs = new User32.INPUT[] { diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/StyleHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/StyleHelper.cs index 2f02ae507d07..ae2f97264dcc 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Helpers/StyleHelper.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/StyleHelper.cs @@ -10,9 +10,9 @@ namespace MouseJump.Common.Helpers; public static class StyleHelper { /// - /// Default v2 preview style + /// Compact (legacy) preview style /// - public static readonly PreviewStyle DefaultPreviewStyle = new( + public static readonly PreviewStyle CompactPreviewStyle = new( canvasSize: new( width: 1600, height: 1200 @@ -25,7 +25,7 @@ public static class StyleHelper depth: 0 ), paddingStyle: new( - all: 4 + all: 0 ), backgroundStyle: new( color1: Color.FromArgb(0xFF, 0x0D, 0x57, 0xD2), @@ -34,12 +34,12 @@ public static class StyleHelper ), screenStyle: new( marginStyle: new( - all: 4 + all: 0 ), borderStyle: new( color: Color.FromArgb(0xFF, 0x22, 0x22, 0x22), - all: 12, - depth: 4 + all: 0, + depth: 0 ), paddingStyle: PaddingStyle.Empty, backgroundStyle: new( @@ -50,9 +50,9 @@ public static class StyleHelper ); /// - /// Legacy preview style + /// Bezelled preview style /// - public static readonly PreviewStyle LegacyPreviewStyle = new( + public static readonly PreviewStyle BezelledPreviewStyle = new( canvasSize: new( width: 1600, height: 1200 @@ -65,7 +65,7 @@ public static class StyleHelper depth: 0 ), paddingStyle: new( - all: 0 + all: 4 ), backgroundStyle: new( color1: Color.FromArgb(0xFF, 0x0D, 0x57, 0xD2), @@ -74,12 +74,12 @@ public static class StyleHelper ), screenStyle: new( marginStyle: new( - all: 0 + all: 4 ), borderStyle: new( color: Color.FromArgb(0xFF, 0x22, 0x22, 0x22), - all: 0, - depth: 0 + all: 12, + depth: 4 ), paddingStyle: PaddingStyle.Empty, backgroundStyle: new( diff --git a/src/modules/MouseUtils/MouseJump.Common/Imaging/StaticImageRegionCopyService.cs b/src/modules/MouseUtils/MouseJump.Common/Imaging/StaticImageRegionCopyService.cs index 6808ff09e633..5f0304e4cc45 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Imaging/StaticImageRegionCopyService.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Imaging/StaticImageRegionCopyService.cs @@ -2,6 +2,8 @@ // 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.Drawing2D; + using MouseJump.Common.Models.Drawing; namespace MouseJump.Common.Imaging; @@ -31,6 +33,11 @@ public void CopyImageRegion( RectangleInfo sourceBounds, RectangleInfo targetBounds) { + // prevent the background bleeding through into screen images + // (see https://github.com/mikeclayton/FancyMouse/issues/44) + targetGraphics.PixelOffsetMode = PixelOffsetMode.Half; + targetGraphics.InterpolationMode = InterpolationMode.NearestNeighbor; + targetGraphics.DrawImage( image: this.SourceImage, destRect: targetBounds.ToRectangle(), diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/BoxBounds.cs b/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/BoxBounds.cs index 0c2d81a19615..5d4d15e8ffe7 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/BoxBounds.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/BoxBounds.cs @@ -26,7 +26,7 @@ public sealed class BoxBounds */ - public BoxBounds( + internal BoxBounds( RectangleInfo outerBounds, RectangleInfo marginBounds, RectangleInfo borderBounds, diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/RectangleInfo.cs b/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/RectangleInfo.cs index e0f8c3b7ef9d..ab5bec86768f 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/RectangleInfo.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/RectangleInfo.cs @@ -203,6 +203,15 @@ public RectangleInfo Offset(SizeInfo amount) => public RectangleInfo Offset(decimal dx, decimal dy) => new(this.X + dx, this.Y + dy, this.Width, this.Height); + public RectangleInfo Round() => + this.Round(0); + + public RectangleInfo Round(int decimals) => new( + Math.Round(this.X, decimals), + Math.Round(this.Y, decimals), + Math.Round(this.Width, decimals), + Math.Round(this.Height, decimals)); + /// /// Returns a new that is a scaled version of the current rectangle. /// The dimensions of the new rectangle are calculated by multiplying the current rectangle's dimensions by the scaling factor. diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/ScreenInfo.cs b/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/ScreenInfo.cs index b1e9c9327a23..6a7862236f12 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/ScreenInfo.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/ScreenInfo.cs @@ -12,6 +12,8 @@ public sealed class ScreenInfo { public ScreenInfo(int handle, bool primary, RectangleInfo displayArea, RectangleInfo workingArea) { + // this.Handle is a HMONITOR that has been cast to an int because we don't want + // to expose the HMONITOR type outside the current assembly. this.Handle = handle; this.Primary = primary; this.DisplayArea = displayArea ?? throw new ArgumentNullException(nameof(displayArea)); diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/SizeInfo.cs b/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/SizeInfo.cs index ffb0b4de106f..dc605ebb39dd 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/SizeInfo.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/SizeInfo.cs @@ -33,6 +33,20 @@ public decimal Height get; } + public SizeInfo Clamp(SizeInfo max) + { + return new( + width: Math.Clamp(this.Width, 0, max.Width), + height: Math.Clamp(this.Height, 0, max.Height)); + } + + public SizeInfo Clamp(decimal maxWidth, decimal maxHeight) + { + return new( + width: Math.Clamp(this.Width, 0, maxWidth), + height: Math.Clamp(this.Height, 0, maxHeight)); + } + public SizeInfo Enlarge(BorderStyle border) => new( this.Width + border.Horizontal, @@ -43,6 +57,17 @@ public SizeInfo Enlarge(PaddingStyle padding) => this.Width + padding.Horizontal, this.Height + padding.Vertical); + /// + /// Rounds down the width and height of this size to the nearest whole number. + /// + /// A new instance with floored dimensions. + public SizeInfo Floor() + { + return new SizeInfo( + Math.Floor(this.Width), + Math.Floor(this.Height)); + } + /// /// Calculates the intersection of this size with another size, resulting in a size that represents /// the overlapping dimensions. Both sizes must be non-negative. @@ -69,19 +94,6 @@ public SizeInfo Intersect(SizeInfo size) public SizeInfo Invert() => new(-this.Width, -this.Height); - public SizeInfo Scale(decimal scalingFactor) => new( - this.Width * scalingFactor, - this.Height * scalingFactor); - - public SizeInfo Shrink(BorderStyle border) => - new(this.Width - border.Horizontal, this.Height - border.Vertical); - - public SizeInfo Shrink(MarginStyle margin) => - new(this.Width - margin.Horizontal, this.Height - margin.Vertical); - - public SizeInfo Shrink(PaddingStyle padding) => - new(this.Width - padding.Horizontal, this.Height - padding.Vertical); - /// /// Creates a new instance representing a rectangle with this size, /// positioned at the specified coordinates. @@ -92,32 +104,39 @@ public SizeInfo Shrink(PaddingStyle padding) => public RectangleInfo PlaceAt(decimal x, decimal y) => new(x, y, this.Width, this.Height); + public SizeInfo Round() => + this.Round(0); + + public SizeInfo Round(int decimals) => new( + Math.Round(this.Width, decimals), + Math.Round(this.Height, decimals)); + + public SizeInfo Scale(decimal scalingFactor) => new( + this.Width * scalingFactor, + this.Height * scalingFactor); + /// /// Scales this size to fit within the bounds of another size, while maintaining the aspect ratio. /// /// The size to fit this size into. /// A new instance representing the scaled size. - public SizeInfo ScaleToFit(SizeInfo bounds) + public SizeInfo ScaleToFit(SizeInfo bounds, out decimal scalingRatio) { var widthRatio = bounds.Width / this.Width; var heightRatio = bounds.Height / this.Height; - return widthRatio.CompareTo(heightRatio) switch + switch (widthRatio.CompareTo(heightRatio)) { - < 0 => new(bounds.Width, this.Height * widthRatio), - 0 => bounds, - > 0 => new(this.Width * heightRatio, bounds.Height), - }; - } - - /// - /// Rounds down the width and height of this size to the nearest whole number. - /// - /// A new instance with floored dimensions. - public SizeInfo Floor() - { - return new SizeInfo( - Math.Floor(this.Width), - Math.Floor(this.Height)); + case < 0: + scalingRatio = widthRatio; + return new(bounds.Width, this.Height * widthRatio); + case 0: + // widthRatio and heightRatio are the same, so just pick one + scalingRatio = widthRatio; + return bounds; + case > 0: + scalingRatio = heightRatio; + return new(this.Width * heightRatio, bounds.Height); + } } /// @@ -140,6 +159,15 @@ public decimal ScaleToFitRatio(SizeInfo bounds) return scalingRatio; } + 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); + public Size ToSize() => new((int)this.Width, (int)this.Height); public Point ToPoint() => new((int)this.Width, (int)this.Height); diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Settings/PreviewType.cs b/src/modules/MouseUtils/MouseJump.Common/Models/Settings/PreviewType.cs new file mode 100644 index 000000000000..5a3e8076c7ea --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Models/Settings/PreviewType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MouseJump.Common.Models.Settings; + +public enum PreviewType +{ + Custom = 0, + Compact = 1, + Bezelled = 2, +} diff --git a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/BorderStyle.cs b/src/modules/MouseUtils/MouseJump.Common/Models/Styles/BorderStyle.cs index a8773b535d7d..eb0ac423c5bc 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Models/Styles/BorderStyle.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Models/Styles/BorderStyle.cs @@ -9,14 +9,14 @@ namespace MouseJump.Common.Models.Styles; /// public sealed class BorderStyle { - public static readonly BorderStyle Empty = new(Color.Transparent, 0, 0); + public static readonly BorderStyle Empty = new(null, 0, 0); - public BorderStyle(Color color, decimal all, decimal depth) + 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) + public BorderStyle(Color? color, decimal left, decimal top, decimal right, decimal bottom, decimal depth) { this.Color = color; this.Left = left; @@ -26,7 +26,7 @@ public BorderStyle(Color color, decimal left, decimal top, decimal right, decima this.Depth = depth; } - public Color Color + public Color? Color { get; } diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs index 88ee061c4ee2..9833d9e4b77a 100644 --- a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs +++ b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs @@ -183,7 +183,7 @@ public void ShowPreview() var screens = ScreenHelper.GetAllScreens().Select(screen => screen.DisplayArea).ToList(); var activatedLocation = MouseHelper.GetCursorPosition(); this.PreviewLayout = LayoutHelper.GetPreviewLayout( - previewStyle: StyleHelper.LegacyPreviewStyle.WithCanvasSize( + previewStyle: StyleHelper.CompactPreviewStyle.WithCanvasSize( new( appSettings.Properties.ThumbnailSize.Width, appSettings.Properties.ThumbnailSize.Height diff --git a/src/modules/MouseUtils/MouseJumpUI/Program.cs b/src/modules/MouseUtils/MouseJumpUI/Program.cs index 8bd9b183e17d..2438053c5319 100644 --- a/src/modules/MouseUtils/MouseJumpUI/Program.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Program.cs @@ -82,29 +82,4 @@ private static void Main(string[] args) Application.Run(); } - - private static MouseJumpSettings ReadSettings() - { - var settingsUtils = new SettingsUtils(); - var settingsPath = settingsUtils.GetSettingsFilePath(MouseJumpSettings.ModuleName); - if (!File.Exists(settingsPath)) - { - var scaffoldSettings = new MouseJumpSettings(); - settingsUtils.SaveSettings(JsonSerializer.Serialize(scaffoldSettings), MouseJumpSettings.ModuleName); - } - - var settings = new MouseJumpSettings(); - try - { - settings = settingsUtils.GetSettings(MouseJumpSettings.ModuleName); - } - catch (Exception ex) - { - var errorMessage = $"There was a problem reading the configuration file. Error: {ex.GetType()} {ex.Message}"; - Logger.LogInfo(errorMessage); - Logger.LogDebug(errorMessage); - } - - return settings; - } } diff --git a/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs b/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs index 3712d5b1ea56..33bad29bd929 100644 --- a/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs +++ b/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs @@ -27,6 +27,101 @@ public MouseJumpThumbnailSize ThumbnailSize set; } + /// + /// Gets or sets the preview type. + /// Allowed values are "compact", "bezelled", "custom" + /// + [JsonPropertyName("preview_type")] + public string PreviewType + { + get; + set; + } + + [JsonPropertyName("background_color_1")] + public string BackgroundColor1 + { + get; + set; + } + + [JsonPropertyName("background_color_2")] + public string BackgroundColor2 + { + get; + set; + } + + [JsonPropertyName("border_thickness")] + public int BorderThickness + { + get; + set; + } + + [JsonPropertyName("border_color")] + public string BorderColor + { + get; + set; + } + + [JsonPropertyName("border_3d_depth")] + public int Border3dDepth + { + get; + set; + } + + [JsonPropertyName("border_padding")] + public int BorderPadding + { + get; + set; + } + + [JsonPropertyName("bezel_thickness")] + public int BezelThickness + { + get; + set; + } + + [JsonPropertyName("bezel_color")] + public string BezelColor + { + get; + set; + } + + [JsonPropertyName("bezel_3d_depth")] + public int Bezel3dDepth + { + get; + set; + } + + [JsonPropertyName("screen_margin")] + public int ScreenMargin + { + get; + set; + } + + [JsonPropertyName("screen_color_1")] + public string ScreenColor1 + { + get; + set; + } + + [JsonPropertyName("screen_color_2")] + public string ScreenColor2 + { + get; + set; + } + public MouseJumpProperties() { ActivationShortcut = DefaultActivationShortcut; diff --git a/src/settings-ui/Settings.UI/Converters/MouseJumpPreviewTypeConverter.cs b/src/settings-ui/Settings.UI/Converters/MouseJumpPreviewTypeConverter.cs new file mode 100644 index 000000000000..cfc980a8cea1 --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/MouseJumpPreviewTypeConverter.cs @@ -0,0 +1,69 @@ +// 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 Microsoft.UI.Xaml.Data; +using MouseJump.Common.Models.Settings; + +namespace Microsoft.PowerToys.Settings.UI.Converters +{ + public sealed partial class MouseJumpPreviewTypeConverter : IValueConverter + { + private static readonly PreviewType[] PreviewTypeOrder = + [ + PreviewType.Compact, PreviewType.Bezelled, PreviewType.Custom, + ]; + + private static readonly PreviewType DefaultPreviewType = PreviewType.Bezelled; + + // Receives a string as a parameter and returns an int representing the index + // to select in the Segmented control on the Mouse Jump settings page + public object Convert(object value, Type targetType, object parameter, string language) + { + var previewType = MouseJumpPreviewTypeConverter.DefaultPreviewType; + + if (value is not string previewTypeName) + { + // the value isn't a string so just use the default preview type + } + else if (Enum.IsDefined(typeof(PreviewType), previewTypeName)) + { + // there's a case-sensitive match for the value + previewType = Enum.Parse(previewTypeName); + } + else if (Enum.TryParse(previewTypeName, true, out var previewTypeResult)) + { + // there's a case-insensitive match for the value + previewType = previewTypeResult; + } + + return Array.IndexOf( + MouseJumpPreviewTypeConverter.PreviewTypeOrder, + previewType); + } + + // Receives an int as a parameter that represents the selected index in the Segmented + // control on the Mouse Jump settings page, and returns the name of the PreviewType enum + // for that index. + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + var previewType = MouseJumpPreviewTypeConverter.DefaultPreviewType; + + if (value is not int segmentedIndex) + { + // the value isn't an int so just use the default preview type + } + else if ((segmentedIndex < 0) || (segmentedIndex > MouseJumpPreviewTypeConverter.PreviewTypeOrder.Length)) + { + // not a valid selected index so just use the default preview type + } + else + { + previewType = MouseJumpPreviewTypeConverter.PreviewTypeOrder[segmentedIndex]; + } + + return previewType.ToString(); + } + } +} diff --git a/src/settings-ui/Settings.UI/Images/MouseJump-Desktop.png b/src/settings-ui/Settings.UI/Images/MouseJump-Desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..27637601134d311754a5186c6747166d01bda81c GIT binary patch literal 26840 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%0Ap?DfPx?V^$X0={qK_y^=gGA?n+;2fF`rT01 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 dFa5q3>mxVsM` + + + + + @@ -82,6 +87,7 @@ + @@ -120,6 +126,9 @@ $(DefaultXamlRuntime) + + $(DefaultXamlRuntime) + $(DefaultXamlRuntime) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml index 8830f7ba0bf5..b92cf4d4584b 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml @@ -12,6 +12,15 @@ AutomationProperties.LandmarkType="Main" mc:Ignorable="d"> + + + + + + + + + @@ -112,6 +121,132 @@ + + + + + + + + + + + + + + + + + + + + + + +