diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 5a8c5e280c..d657c65ff3 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -102,6 +102,7 @@ backtracer bbwe bck BESTEFFORT +bezelled bhid BIF bigbar @@ -725,6 +726,7 @@ ISSEPARATOR ITask ith ITHUMBNAIL +ITwoWayPipeMessageIPCManaged IUI IUnknown IUse diff --git a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/DrawingHelperTests.cs b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/DrawingHelperTests.cs index 5ba0a0eef8..f6c6c51831 100644 --- a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/DrawingHelperTests.cs +++ b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/DrawingHelperTests.cs @@ -2,8 +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 System.Globalization; using System.Reflection; - +using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using MouseJump.Common.Helpers; using MouseJump.Common.Imaging; @@ -16,7 +17,7 @@ namespace MouseJump.Common.UnitTests.Helpers; public static class DrawingHelperTests { [TestClass] - public sealed class GetPreviewLayoutTests + public sealed class RenderPreviewTests { public sealed class TestCase { @@ -46,7 +47,7 @@ public static class DrawingHelperTests yield return new object[] { new TestCase( - previewStyle: StyleHelper.DefaultPreviewStyle, + previewStyle: StyleHelper.BezelledPreviewStyle, screens: new List() { new(0, 0, 500, 500), @@ -62,7 +63,7 @@ public static class DrawingHelperTests yield return new object[] { new TestCase( - previewStyle: StyleHelper.DefaultPreviewStyle, + previewStyle: StyleHelper.BezelledPreviewStyle, screens: new List() { new(5120, 349, 1920, 1080), @@ -79,7 +80,7 @@ public static class DrawingHelperTests public void RunTestCases(TestCase data) { // load the fake desktop image - using var desktopImage = GetPreviewLayoutTests.LoadImageResource(data.DesktopImageFilename); + using var desktopImage = RenderPreviewTests.LoadImageResource(data.DesktopImageFilename); // draw the preview image var previewLayout = LayoutHelper.GetPreviewLayout( @@ -90,28 +91,29 @@ public static class DrawingHelperTests using var actual = DrawingHelper.RenderPreview(previewLayout, imageCopyService); // load the expected image - var expected = GetPreviewLayoutTests.LoadImageResource(data.ExpectedImageFilename); + var expected = RenderPreviewTests.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 = $"{typeof(DrawingHelperTests).Namespace}.{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); + var message = new StringBuilder(); + message.AppendLine(CultureInfo.InvariantCulture, $"Embedded resource '{resourceName}' does not exist."); + message.AppendLine($"Known resources:"); + foreach (var name in resourceNames) + { + message.AppendLine(name); + } + + throw new InvalidOperationException(message.ToString()); } var stream = assembly.GetManifestResourceStream(resourceName) @@ -121,7 +123,7 @@ public static class DrawingHelperTests } /// - /// 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 5bdd4af443..64b7d0cef2 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 static class LayoutHelperTests 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 class LayoutHelperTests 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 class LayoutHelperTests 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 class LayoutHelperTests 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 class LayoutHelperTests 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 class LayoutHelperTests 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 class LayoutHelperTests 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/Helpers/_test-win11-expected.png b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/_test-win11-expected.png index 9d4b1612d0..1816a9125a 100644 Binary files a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/_test-win11-expected.png and b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Helpers/_test-win11-expected.png differ diff --git a/src/modules/MouseUtils/MouseJump.Common.UnitTests/Models/Drawing/SizeInfoTests.cs b/src/modules/MouseUtils/MouseJump.Common.UnitTests/Models/Drawing/SizeInfoTests.cs index e6c33e3d7a..9871d0526c 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 static class SizeInfoTests { 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 0000000000..bc2e67203d --- /dev/null +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/ConfigHelper.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; + +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 278e07039f..668e90dd3d 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Helpers/DrawingHelper.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/DrawingHelper.cs @@ -102,8 +102,13 @@ public static class DrawingHelper 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 791d1f7ffb..589ff42178 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 class LayoutHelper .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 class LayoutHelper 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(), diff --git a/src/modules/MouseUtils/MouseJump.Common/Helpers/MouseHelper.cs b/src/modules/MouseUtils/MouseJump.Common/Helpers/MouseHelper.cs index 9354958878..443b252aa3 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 class MouseHelper /// 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 2f02ae507d..ae2f97264d 100644 --- a/src/modules/MouseUtils/MouseJump.Common/Helpers/StyleHelper.cs +++ b/src/modules/MouseUtils/MouseJump.Common/Helpers/StyleHelper.cs @@ -10,49 +10,9 @@ namespace MouseJump.Common.Helpers; public static class StyleHelper { /// - /// Default v2 preview style + /// Compact (legacy) preview style /// - 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 - ) - ) - ); - - /// - /// Legacy preview style - /// - public static readonly PreviewStyle LegacyPreviewStyle = new( + public static readonly PreviewStyle CompactPreviewStyle = new( canvasSize: new( width: 1600, height: 1200 @@ -89,6 +49,46 @@ public static class StyleHelper ) ); + /// + /// Bezelled preview style + /// + public static readonly PreviewStyle BezelledPreviewStyle = 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 + ) + ) + ); + public static PreviewStyle WithCanvasSize(this PreviewStyle previewStyle, SizeInfo canvasSize) { ArgumentNullException.ThrowIfNull(previewStyle); diff --git a/src/modules/MouseUtils/MouseJump.Common/Imaging/StaticImageRegionCopyService.cs b/src/modules/MouseUtils/MouseJump.Common/Imaging/StaticImageRegionCopyService.cs index 6808ff09e6..5f0304e4cc 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 sealed class StaticImageRegionCopyService : IImageRegionCopyService 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/RectangleInfo.cs b/src/modules/MouseUtils/MouseJump.Common/Models/Drawing/RectangleInfo.cs index e0f8c3b7ef..ab5bec8676 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 sealed class RectangleInfo 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 b1e9c9327a..6a7862236f 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 ffb0b4de10..dc605ebb39 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 sealed class SizeInfo 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 sealed class SizeInfo 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 sealed class SizeInfo 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 sealed class SizeInfo 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 sealed class SizeInfo 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 0000000000..5a3e8076c7 --- /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 a8773b535d..eb0ac423c5 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 sealed class BorderStyle this.Depth = depth; } - public Color Color + public Color? Color { get; } diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs index cfd10fbd78..efe721e873 100644 --- a/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs @@ -10,6 +10,10 @@ using System.Threading; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Utilities; +using MouseJump.Common.Helpers; +using MouseJump.Common.Models.Drawing; +using MouseJump.Common.Models.Settings; +using MouseJump.Common.Models.Styles; namespace MouseJumpUI.Helpers; @@ -93,4 +97,65 @@ internal sealed class SettingsHelper { this.CurrentSettings = this.LoadSettings(); } + + public static PreviewStyle GetActivePreviewStyle(MouseJumpSettings settings) + { + var previewType = Enum.TryParse(settings.Properties.PreviewType, true, out var previewTypeResult) + ? previewTypeResult + : PreviewType.Bezelled; + + var canvasSize = new SizeInfo( + settings.Properties.ThumbnailSize.Width, + settings.Properties.ThumbnailSize.Height); + + var properties = settings.Properties; + + var previewStyle = previewType switch + { + PreviewType.Compact => StyleHelper.CompactPreviewStyle.WithCanvasSize(canvasSize), + PreviewType.Bezelled => StyleHelper.BezelledPreviewStyle.WithCanvasSize(canvasSize), + PreviewType.Custom => new PreviewStyle( + canvasSize: canvasSize, + canvasStyle: new( + marginStyle: new(0), + borderStyle: new( + color: ConfigHelper.DeserializeFromConfigColorString( + properties.BorderColor), + all: properties.BorderThickness, + depth: properties.Border3dDepth + ), + paddingStyle: new( + all: properties.BorderPadding + ), + backgroundStyle: new( + color1: ConfigHelper.DeserializeFromConfigColorString( + properties.BackgroundColor1), + color2: ConfigHelper.DeserializeFromConfigColorString( + properties.BackgroundColor2) + ) + ), + screenStyle: new( + marginStyle: new( + all: properties.ScreenMargin + ), + borderStyle: new( + color: ConfigHelper.DeserializeFromConfigColorString( + properties.BezelColor), + all: properties.BezelThickness, + depth: properties.Bezel3dDepth + ), + paddingStyle: new(0), + backgroundStyle: new( + color1: ConfigHelper.DeserializeFromConfigColorString( + properties.ScreenColor1), + color2: ConfigHelper.DeserializeFromConfigColorString( + properties.ScreenColor2) + ) + )), + _ => throw new InvalidOperationException( + $"Unhandled {nameof(PreviewType)} '{previewType}'"), + }; + + return previewStyle; + } } diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs index 1c6e94a1a9..d4b2ca75de 100644 --- a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs +++ b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs @@ -183,12 +183,9 @@ internal sealed partial class MainForm : Form 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 - )), + previewStyle: SettingsHelper.GetActivePreviewStyle(appSettings), screens: screens, activatedLocation: activatedLocation); diff --git a/src/modules/MouseUtils/MouseJumpUI/Models/Drawing/PaddingInfo.cs b/src/modules/MouseUtils/MouseJumpUI/Models/Drawing/PaddingInfo.cs deleted file mode 100644 index 9437bf0531..0000000000 --- a/src/modules/MouseUtils/MouseJumpUI/Models/Drawing/PaddingInfo.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Windows.Forms; - -namespace MouseJumpUI.Models.Drawing; - -/// -/// Immutable version of a System.Windows.Forms.Padding object with some extra utility methods. -/// -public sealed class PaddingInfo -{ - public PaddingInfo(decimal all) - : this(all, all, all, all) - { - } - - public PaddingInfo(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}" + - "}"; - } -} diff --git a/src/modules/MouseUtils/MouseJumpUI/Models/Drawing/PointInfo.cs b/src/modules/MouseUtils/MouseJumpUI/Models/Drawing/PointInfo.cs deleted file mode 100644 index 40e452a378..0000000000 --- a/src/modules/MouseUtils/MouseJumpUI/Models/Drawing/PointInfo.cs +++ /dev/null @@ -1,53 +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.Drawing; - -namespace MouseJumpUI.Models.Drawing; - -/// -/// Immutable version of a System.Drawing.Point object with some extra utility methods. -/// -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; - } - - public SizeInfo ToSize() - { - return new((int)this.X, (int)this.Y); - } - - 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 override string ToString() - { - return "{" + - $"{nameof(this.X)}={this.X}," + - $"{nameof(this.Y)}={this.Y}" + - "}"; - } -} diff --git a/src/modules/MouseUtils/MouseJumpUI/Models/Drawing/RectangleInfo.cs b/src/modules/MouseUtils/MouseJumpUI/Models/Drawing/RectangleInfo.cs deleted file mode 100644 index af46b85470..0000000000 --- a/src/modules/MouseUtils/MouseJumpUI/Models/Drawing/RectangleInfo.cs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Drawing; - -namespace MouseJumpUI.Models.Drawing; - -/// -/// Immutable version of a System.Drawing.Rectangle object with some extra utility methods. -/// -public sealed class RectangleInfo -{ - 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; - } - - public decimal Left => this.X; - - public decimal Top => this.Y; - - public decimal Right => this.X + this.Width; - - public decimal Bottom => this.Y + this.Height; - - public SizeInfo Size => new(this.Width, this.Height); - - public PointInfo Location => new(this.X, this.Y); - - public decimal Area => this.Width * this.Height; - - /// - /// Adapted from https://github.com/dotnet/runtime - /// See https://github.com/dotnet/runtime/blob/dfd618dc648ba9b11dd0f8034f78113d69f223cd/src/libraries/System.Drawing.Primitives/src/System/Drawing/Rectangle.cs - /// - public 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); - - public RectangleInfo Enlarge(PaddingInfo padding) => new( - this.X + padding.Left, - this.Y + padding.Top, - this.Width + padding.Horizontal, - this.Height + padding.Vertical); - - public RectangleInfo Offset(SizeInfo amount) => this.Offset(amount.Width, amount.Height); - - public RectangleInfo Offset(decimal dx, decimal dy) => new(this.X + dx, this.Y + dy, this.Width, this.Height); - - public RectangleInfo Scale(decimal scalingFactor) => new( - this.X * scalingFactor, - this.Y * scalingFactor, - this.Width * scalingFactor, - this.Height * scalingFactor); - - public RectangleInfo Center(PointInfo point) => new( - x: point.X - (this.Width / 2), - y: point.Y - (this.Height / 2), - width: this.Width, - height: this.Height); - - public PointInfo Midpoint => new( - x: this.X + (this.Width / 2), - y: this.Y + (this.Height / 2)); - - 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); - } - - 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}" + - "}"; - } -} diff --git a/src/modules/MouseUtils/MouseJumpUI/Models/Drawing/SizeInfo.cs b/src/modules/MouseUtils/MouseJumpUI/Models/Drawing/SizeInfo.cs deleted file mode 100644 index a91febc23c..0000000000 --- a/src/modules/MouseUtils/MouseJumpUI/Models/Drawing/SizeInfo.cs +++ /dev/null @@ -1,87 +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; - -namespace MouseJumpUI.Models.Drawing; - -/// -/// Immutable version of a System.Drawing.Size object with some extra utility methods. -/// -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 Negate() => new(-this.Width, -this.Height); - - public SizeInfo Shrink(PaddingInfo padding) => new(this.Width - padding.Horizontal, this.Height - padding.Vertical); - - public SizeInfo Intersect(SizeInfo size) => new( - Math.Min(this.Width, size.Width), - Math.Min(this.Height, size.Height)); - - public RectangleInfo PlaceAt(decimal x, decimal y) => new(x, y, this.Width, this.Height); - - 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), - }; - } - - /// - /// Get the scaling ratio to scale obj by so that it fits inside the specified bounds - /// without distorting the aspect ratio. - /// - 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}" + - "}"; - } -} diff --git a/src/modules/MouseUtils/MouseJumpUI/Models/Layout/LayoutConfig.cs b/src/modules/MouseUtils/MouseJumpUI/Models/Layout/LayoutConfig.cs deleted file mode 100644 index 85aae9ab6f..0000000000 --- a/src/modules/MouseUtils/MouseJumpUI/Models/Layout/LayoutConfig.cs +++ /dev/null @@ -1,123 +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.Collections.ObjectModel; -using System.Linq; - -using MouseJumpUI.Models.Drawing; -using MouseJumpUI.Models.Screen; - -namespace MouseJumpUI.Models.Layout; - -/// -/// Represents a collection of values needed for calculating the MainForm layout. -/// -public sealed class LayoutConfig -{ - public LayoutConfig( - RectangleInfo virtualScreenBounds, - List screens, - PointInfo activatedLocation, - int activatedScreenIndex, - int activatedScreenNumber, - SizeInfo maximumFormSize, - PaddingInfo formPadding, - PaddingInfo previewPadding) - { - // make sure the virtual screen entirely contains all of the individual screen bounds - ArgumentNullException.ThrowIfNull(virtualScreenBounds); - ArgumentNullException.ThrowIfNull(screens); - if (screens.Any(screen => !virtualScreenBounds.Contains(screen.Bounds))) - { - throw new ArgumentException($"'{nameof(virtualScreenBounds)}' must contain all of the screens in '{nameof(screens)}'", nameof(virtualScreenBounds)); - } - - this.VirtualScreenBounds = virtualScreenBounds; - this.Screens = new(screens.ToList()); - this.ActivatedLocation = activatedLocation; - this.ActivatedScreenIndex = activatedScreenIndex; - this.ActivatedScreenNumber = activatedScreenNumber; - this.MaximumFormSize = maximumFormSize; - this.FormPadding = formPadding; - this.PreviewPadding = previewPadding; - } - - /// - /// Gets the coordinates of the entire virtual screen. - /// - /// - /// The Virtual Screen is the bounding rectangle of all the monitors. - /// https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen - /// - public RectangleInfo VirtualScreenBounds - { - get; - } - - /// - /// Gets a collection containing the individual screens connected to the system. - /// - public ReadOnlyCollection Screens - { - get; - } - - /// - /// Gets the point where the cursor was located when the form was activated. - /// - /// - /// The preview form will be centered on this location unless there are any - /// constraints such as being too close to edge of a screen, in which case - /// the form will be displayed centered as close as possible to this location. - /// - public PointInfo ActivatedLocation - { - get; - } - - /// - /// Gets the index of the screen the cursor was on when the form was activated. - /// The value is an index into the ScreenBounds array and is 0-indexed as a result. - /// - public int ActivatedScreenIndex - { - get; - } - - /// - /// Gets the screen number the cursor was on when the form was activated. - /// The value matches the screen numbering scheme in the "Display Settings" dialog - /// and is 1-indexed as a result. - /// - public int ActivatedScreenNumber - { - get; - } - - /// - /// Gets the maximum size of the screen preview form. - /// - public SizeInfo MaximumFormSize - { - get; - } - - /// - /// Gets the padding border around the screen preview form. - /// - public PaddingInfo FormPadding - { - get; - } - - /// - /// Gets the padding border inside the screen preview image. - /// - public PaddingInfo PreviewPadding - { - get; - } -} diff --git a/src/modules/MouseUtils/MouseJumpUI/Models/Layout/LayoutInfo.cs b/src/modules/MouseUtils/MouseJumpUI/Models/Layout/LayoutInfo.cs deleted file mode 100644 index fee84a0d85..0000000000 --- a/src/modules/MouseUtils/MouseJumpUI/Models/Layout/LayoutInfo.cs +++ /dev/null @@ -1,113 +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.Collections.ObjectModel; -using System.Linq; - -using MouseJumpUI.Models.Drawing; - -namespace MouseJumpUI.Models.Layout; - -public sealed class LayoutInfo -{ - public sealed class Builder - { - public Builder() - { - this.ScreenBounds = new(); - } - - public LayoutConfig? LayoutConfig - { - get; - set; - } - - public RectangleInfo? FormBounds - { - get; - set; - } - - public RectangleInfo? PreviewBounds - { - get; - set; - } - - public List ScreenBounds - { - get; - set; - } - - public RectangleInfo? ActivatedScreenBounds - { - get; - set; - } - - public LayoutInfo Build() - { - return new LayoutInfo( - layoutConfig: this.LayoutConfig ?? throw new InvalidOperationException(), - formBounds: this.FormBounds ?? throw new InvalidOperationException(), - previewBounds: this.PreviewBounds ?? throw new InvalidOperationException(), - screenBounds: this.ScreenBounds ?? throw new InvalidOperationException(), - activatedScreenBounds: this.ActivatedScreenBounds ?? throw new InvalidOperationException()); - } - } - - public LayoutInfo( - LayoutConfig layoutConfig, - RectangleInfo formBounds, - RectangleInfo previewBounds, - IEnumerable screenBounds, - RectangleInfo activatedScreenBounds) - { - this.LayoutConfig = layoutConfig ?? throw new ArgumentNullException(nameof(layoutConfig)); - this.FormBounds = formBounds ?? throw new ArgumentNullException(nameof(formBounds)); - this.PreviewBounds = previewBounds ?? throw new ArgumentNullException(nameof(previewBounds)); - this.ScreenBounds = new( - (screenBounds ?? throw new ArgumentNullException(nameof(screenBounds))) - .ToList()); - this.ActivatedScreenBounds = activatedScreenBounds ?? throw new ArgumentNullException(nameof(activatedScreenBounds)); - } - - /// - /// Gets the original LayoutConfig settings used to calculate coordinates. - /// - public LayoutConfig LayoutConfig - { - get; - } - - /// - /// Gets the size and location of the preview form. - /// - public RectangleInfo FormBounds - { - get; - } - - /// - /// Gets the size and location of the preview image. - /// - public RectangleInfo PreviewBounds - { - get; - } - - public ReadOnlyCollection ScreenBounds - { - get; - } - - public RectangleInfo ActivatedScreenBounds - { - get; - } -} diff --git a/src/modules/MouseUtils/MouseJumpUI/Models/Screen/ScreenInfo.cs b/src/modules/MouseUtils/MouseJumpUI/Models/Screen/ScreenInfo.cs deleted file mode 100644 index 2d05d19c1f..0000000000 --- a/src/modules/MouseUtils/MouseJumpUI/Models/Screen/ScreenInfo.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; - -using MouseJumpUI.Models.Drawing; - -namespace MouseJumpUI.Models.Screen; - -/// -/// Immutable version of a System.Windows.Forms.Screen object so we don't need to -/// take a dependency on WinForms just for screen info. -/// -public sealed class ScreenInfo -{ - internal ScreenInfo(int 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 Bounds => - this.DisplayArea; - - public RectangleInfo WorkingArea - { - get; - } -} diff --git a/src/modules/MouseUtils/MouseJumpUI/Program.cs b/src/modules/MouseUtils/MouseJumpUI/Program.cs index cb9916c33d..d327e80304 100644 --- a/src/modules/MouseUtils/MouseJumpUI/Program.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Program.cs @@ -3,9 +3,7 @@ // See the LICENSE file in the project root for more information. using System; -using System.IO; using System.Reflection; -using System.Text.Json; using System.Threading; using System.Windows.Forms; using System.Windows.Threading; @@ -97,29 +95,4 @@ internal static class Program cancellationTokenSource.Cancel(); Application.Exit(); } - - 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 0c26e86124..33bad29bd9 100644 --- a/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs +++ b/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs @@ -14,10 +14,113 @@ namespace Microsoft.PowerToys.Settings.UI.Library public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x44); [JsonPropertyName("activation_shortcut")] - public HotkeySettings ActivationShortcut { get; set; } + public HotkeySettings ActivationShortcut + { + get; + set; + } [JsonPropertyName("thumbnail_size")] - public MouseJumpThumbnailSize ThumbnailSize { get; set; } + public MouseJumpThumbnailSize ThumbnailSize + { + get; + 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() { diff --git a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs index fbeb1eaf68..450e6aec93 100644 --- a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs +++ b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs @@ -7,6 +7,8 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using MouseJump.Common.Helpers; +using MouseJump.Common.Models.Settings; namespace Microsoft.PowerToys.Settings.UI.Library { @@ -26,7 +28,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library { Name = ModuleName; Properties = new MouseJumpProperties(); - Version = "1.0"; + Version = "1.1"; } public void Save(ISettingsUtils settingsUtils) @@ -47,7 +49,74 @@ namespace Microsoft.PowerToys.Settings.UI.Library // This can be utilized in the future if the settings.json file is to be modified/deleted. public bool UpgradeSettingsConfiguration() { - return false; + /* + v1.0 - initial version + + * DefaultActivationShortcut + * activation_shortcut + * thumbnail_size + * name + * version + */ + var upgraded = false; + + if (this.Version == "1.0") + { + /* + v1.1 - added preview style settings + + * preview_type + * background_color_1 + * background_color_2 + * border_thickness + * border_color + * border_3d_depth + * border_padding + * bezel_thickness + * bezel_color + * bezel_3d_depth + * screen_margin + * screen_color_1 + * screen_color_2 + */ + this.Version = "1.1"; + + // note - there's an issue where ITwoWayPipeMessageIPCManagedMethods.Send overwrites + // the settings file version as "1.0" regardless of the actual version. as a result, + // the UpgradeSettingsConfiguration can get triggered even if the config has already + // been upgraded, so we need to do an additional check to make sure values haven't + // already been upgraded before we overwrite them with default values. + if (string.IsNullOrEmpty(this.Properties.PreviewType)) + { + // set default values for custom preview style + var previewStyle = StyleHelper.BezelledPreviewStyle; + this.Properties.PreviewType = PreviewType.Bezelled.ToString(); + this.Properties.BackgroundColor1 = ConfigHelper.SerializeToConfigColorString( + ConfigHelper.ToUnnamedColor(previewStyle.CanvasStyle.BackgroundStyle.Color1)); + this.Properties.BackgroundColor2 = ConfigHelper.SerializeToConfigColorString( + ConfigHelper.ToUnnamedColor(previewStyle.CanvasStyle.BackgroundStyle.Color2)); + this.Properties.BorderThickness = (int)previewStyle.CanvasStyle.BorderStyle.Top; + this.Properties.BorderColor = ConfigHelper.SerializeToConfigColorString( + ConfigHelper.ToUnnamedColor(previewStyle.CanvasStyle.BorderStyle.Color)); + this.Properties.Border3dDepth = (int)previewStyle.CanvasStyle.BorderStyle.Depth; + this.Properties.BorderPadding = (int)previewStyle.CanvasStyle.PaddingStyle.Top; + this.Properties.BezelThickness = (int)previewStyle.ScreenStyle.BorderStyle.Top; + this.Properties.BezelColor = ConfigHelper.SerializeToConfigColorString( + ConfigHelper.ToUnnamedColor(previewStyle.ScreenStyle.BorderStyle.Color)); + this.Properties.Bezel3dDepth = (int)previewStyle.ScreenStyle.BorderStyle.Depth; + this.Properties.ScreenMargin = (int)previewStyle.ScreenStyle.MarginStyle.Top; + this.Properties.ScreenColor1 = ConfigHelper.SerializeToConfigColorString( + ConfigHelper.ToUnnamedColor(previewStyle.ScreenStyle.BackgroundStyle.Color1)); + this.Properties.ScreenColor2 = ConfigHelper.SerializeToConfigColorString( + ConfigHelper.ToUnnamedColor(previewStyle.ScreenStyle.BackgroundStyle.Color2)); + } + + // we still need to flag the settings as "upgraded" so that the new version gets written + // back to the config file, even if we didn't actually change and setting values + upgraded = true; + } + + return upgraded; } } } diff --git a/src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj b/src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj index 37e239ed3b..a967dd28a7 100644 --- a/src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj +++ b/src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj @@ -21,6 +21,7 @@ + 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 0000000000..cfc980a8ce --- /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 0000000000..d3ae47b2d4 Binary files /dev/null and b/src/settings-ui/Settings.UI/Images/MouseJump-Desktop.png differ diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj index d5f72b1c43..7a465fbd01 100644 --- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj +++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj @@ -49,6 +49,11 @@ + + + + + @@ -84,6 +89,7 @@ + @@ -122,6 +128,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 new file mode 100644 index 0000000000..b92cf4d458 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + Launch Registry Preview "Registry Preview" is the name of the utility + + Mouse Jump + Refers to the utility name + Quickly move the mouse pointer long distances. "Mouse Jump" is the name of the utility. Mouse is the hardware mouse. - - Mouse Jump - Refers to the utility name + + Enable Mouse Jump + "Mouse Jump" is the name of the utility. Customize the shortcut to turn on or off this mode @@ -4138,9 +4142,124 @@ Activate by holding the key for the character you want to add an accent to, then Activation shortcut - - Enable Mouse Jump - "Mouse Jump" is the name of the utility. + + Thumbnail Size + + + Constrain thumbnail image size to a maximum of + + + pixels + + + Maximum height (px) + px = pixels + + + Maximum width (px) + px = pixels + + + Appearance + + + Preview style + + + Select a predefined style, or apply a custom one + + + Compact + + + Bezelled + + + Custom + + + Copy to Custom preview style + + + Background color 1 + + + The start color for the background gradient fill on the preview image + + + Background color 2 + + + The end color for the background gradient fill on the preview image + + + Border thickness + + + The thickness (in pixels) of the border that surrounds the preview image + + + Border color + + + The color of the border that surrounds the preview image + + + Border 3D depth + + + The width (in pixels) of the 3d effect on the border that surrounds the preview image + + + Border padding + + + The amount of padding to draw between the border that surrounds the main preview image and the screen images + + + Bezel thickness + + + The thickness (in pixels) of the border that surrounds the individual screen images + + + Bezel color + + + The color of the border that surrounds the individual screen images + + + Bezel 3D depth + + + The width (in pixels) of the 3d effect on the border that surrounds individual screen images + + + Screen spacing + + + The width (in pixels) of the margin drawn between individual screen images + + + Screen color 1 + + + The start color for the background gradient fill on individual screen images + + + Screen color 2 + + + The end color for the background gradient fill on individual screen images + + + Copy to Custom preview style + + + This will replace the current settings in the Custom preview style. + + + Copy 127.0.0.1, ::1, ... @@ -4168,23 +4287,6 @@ Activate by holding the key for the character you want to add an accent to, then Using this shortcut may prevent non-text paste actions (e.g. images, files) or built-in paste plain text actions in other applications from functioning. - - Thumbnail Size - - - Constrain thumbnail image size to a maximum of - - - pixels - - - Maximum height (px) - px = pixels - - - Maximum width (px) - px = pixels - A lightning fast file preview feature for Windows. {Locked="Windows"} diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs index 5533a6f04f..e307b40606 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs @@ -14,7 +14,7 @@ using Microsoft.PowerToys.Settings.Utilities; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public class MouseUtilsViewModel : Observable + public partial class MouseUtilsViewModel : Observable { private ISettingsUtils SettingsUtils { get; set; } @@ -24,8 +24,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private MouseHighlighterSettings MouseHighlighterSettingsConfig { get; set; } - private MouseJumpSettings MouseJumpSettingsConfig { get; set; } - private MousePointerCrosshairsSettings MousePointerCrosshairsSettingsConfig { get; set; } public MouseUtilsViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, ISettingsRepository findMyMouseSettingsRepository, ISettingsRepository mouseHighlighterSettingsRepository, ISettingsRepository mouseJumpSettingsRepository, ISettingsRepository mousePointerCrosshairsSettingsRepository, Func ipcMSGCallBackFunc) @@ -80,10 +78,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels _highlightFadeDurationMs = MouseHighlighterSettingsConfig.Properties.HighlightFadeDurationMs.Value; _highlighterAutoActivate = MouseHighlighterSettingsConfig.Properties.AutoActivate.Value; - ArgumentNullException.ThrowIfNull(mouseJumpSettingsRepository); - - MouseJumpSettingsConfig = mouseJumpSettingsRepository.SettingsConfig; - MouseJumpSettingsConfig.Properties.ThumbnailSize.PropertyChanged += MouseJumpThumbnailSizePropertyChanged; + this.InitializeMouseJumpSettings(mouseJumpSettingsRepository); ArgumentNullException.ThrowIfNull(mousePointerCrosshairsSettingsRepository); @@ -138,17 +133,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels _isMouseHighlighterEnabled = GeneralSettingsConfig.Enabled.MouseHighlighter; } - _jumpEnabledGpoRuleConfiguration = GPOWrapper.GetConfiguredMouseJumpEnabledValue(); - if (_jumpEnabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _jumpEnabledGpoRuleConfiguration == GpoRuleConfigured.Enabled) - { - // Get the enabled state from GPO. - _jumpEnabledStateIsGPOConfigured = true; - _isMouseJumpEnabled = _jumpEnabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; - } - else - { - _isMouseJumpEnabled = GeneralSettingsConfig.Enabled.MouseJump; - } + this.InitializeMouseJumpEnabledValues(); _mousePointerCrosshairsEnabledGpoRuleConfiguration = GPOWrapper.GetConfiguredMousePointerCrosshairsEnabledValue(); if (_mousePointerCrosshairsEnabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _mousePointerCrosshairsEnabledGpoRuleConfiguration == GpoRuleConfigured.Enabled) @@ -657,87 +642,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels SettingsUtils.SaveSettings(MouseHighlighterSettingsConfig.ToJsonString(), MouseHighlighterSettings.ModuleName); } - public bool IsMouseJumpEnabled - { - get => _isMouseJumpEnabled; - set - { - if (_jumpEnabledStateIsGPOConfigured) - { - // If it's GPO configured, shouldn't be able to change this state. - return; - } - - if (_isMouseJumpEnabled != value) - { - _isMouseJumpEnabled = value; - - GeneralSettingsConfig.Enabled.MouseJump = value; - OnPropertyChanged(nameof(_isMouseJumpEnabled)); - - OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); - SendConfigMSG(outgoing.ToString()); - - NotifyMouseJumpPropertyChanged(); - } - } - } - - public bool IsJumpEnabledGpoConfigured - { - get => _jumpEnabledStateIsGPOConfigured; - } - - public HotkeySettings MouseJumpActivationShortcut - { - get - { - return MouseJumpSettingsConfig.Properties.ActivationShortcut; - } - - set - { - if (MouseJumpSettingsConfig.Properties.ActivationShortcut != value) - { - MouseJumpSettingsConfig.Properties.ActivationShortcut = value ?? MouseJumpSettingsConfig.Properties.DefaultActivationShortcut; - NotifyMouseJumpPropertyChanged(); - } - } - } - - public MouseJumpThumbnailSize MouseJumpThumbnailSize - { - get - { - return MouseJumpSettingsConfig.Properties.ThumbnailSize; - } - - set - { - if ((MouseJumpSettingsConfig.Properties.ThumbnailSize.Width != value?.Width) - && (MouseJumpSettingsConfig.Properties.ThumbnailSize.Height != value?.Height)) - { - MouseJumpSettingsConfig.Properties.ThumbnailSize = value; - NotifyMouseJumpPropertyChanged(); - } - } - } - - public void MouseJumpThumbnailSizePropertyChanged(object sender, PropertyChangedEventArgs e) - { - NotifyMouseJumpPropertyChanged(nameof(MouseJumpThumbnailSize)); - } - - public void NotifyMouseJumpPropertyChanged([CallerMemberName] string propertyName = null) - { - OnPropertyChanged(propertyName); - - SndMouseJumpSettings outsettings = new SndMouseJumpSettings(MouseJumpSettingsConfig); - SndModuleSettings ipcMessage = new SndModuleSettings(outsettings); - SendConfigMSG(ipcMessage.ToJsonString()); - SettingsUtils.SaveSettings(MouseJumpSettingsConfig.ToJsonString(), MouseJumpSettings.ModuleName); - } - public bool IsMousePointerCrosshairsEnabled { get => _isMousePointerCrosshairsEnabled; @@ -1017,10 +921,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private int _highlightFadeDurationMs; private bool _highlighterAutoActivate; - private GpoRuleConfigured _jumpEnabledGpoRuleConfiguration; - private bool _jumpEnabledStateIsGPOConfigured; - private bool _isMouseJumpEnabled; - private GpoRuleConfigured _mousePointerCrosshairsEnabledGpoRuleConfiguration; private bool _mousePointerCrosshairsEnabledStateIsGPOConfigured; private bool _isMousePointerCrosshairsEnabled; diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel_MouseJump.cs b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel_MouseJump.cs new file mode 100644 index 0000000000..2818c34a99 --- /dev/null +++ b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel_MouseJump.cs @@ -0,0 +1,513 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using global::PowerToys.GPOWrapper; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; +using MouseJump.Common.Helpers; +using MouseJump.Common.Imaging; +using MouseJump.Common.Models.Drawing; +using MouseJump.Common.Models.Settings; +using MouseJump.Common.Models.Styles; + +namespace Microsoft.PowerToys.Settings.UI.ViewModels +{ + public partial class MouseUtilsViewModel : Observable + { + private GpoRuleConfigured _jumpEnabledGpoRuleConfiguration; + private bool _jumpEnabledStateIsGPOConfigured; + private bool _isMouseJumpEnabled; + + internal MouseJumpSettings MouseJumpSettingsConfig { get; set; } + + private void InitializeMouseJumpSettings(ISettingsRepository mouseJumpSettingsRepository) + { + ArgumentNullException.ThrowIfNull(mouseJumpSettingsRepository); + this.MouseJumpSettingsConfig = mouseJumpSettingsRepository.SettingsConfig; + this.MouseJumpSettingsConfig.Properties.ThumbnailSize.PropertyChanged += this.MouseJumpThumbnailSizePropertyChanged; + } + + private void InitializeMouseJumpEnabledValues() + { + _jumpEnabledGpoRuleConfiguration = GPOWrapper.GetConfiguredMouseJumpEnabledValue(); + if (_jumpEnabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _jumpEnabledGpoRuleConfiguration == GpoRuleConfigured.Enabled) + { + // Get the enabled state from GPO. + _jumpEnabledStateIsGPOConfigured = true; + _isMouseJumpEnabled = _jumpEnabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; + } + else + { + _isMouseJumpEnabled = GeneralSettingsConfig.Enabled.MouseJump; + } + } + + public bool IsMouseJumpEnabled + { + get => _isMouseJumpEnabled; + set + { + if (_jumpEnabledStateIsGPOConfigured) + { + // If it's GPO configured, shouldn't be able to change this state. + return; + } + + if (_isMouseJumpEnabled != value) + { + _isMouseJumpEnabled = value; + + GeneralSettingsConfig.Enabled.MouseJump = value; + OnPropertyChanged(nameof(_isMouseJumpEnabled)); + + OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); + SendConfigMSG(outgoing.ToString()); + + NotifyMouseJumpPropertyChanged(); + } + } + } + + public bool IsJumpEnabledGpoConfigured + { + get => _jumpEnabledStateIsGPOConfigured; + } + + public HotkeySettings MouseJumpActivationShortcut + { + get + { + return MouseJumpSettingsConfig.Properties.ActivationShortcut; + } + + set + { + if (MouseJumpSettingsConfig.Properties.ActivationShortcut != value) + { + MouseJumpSettingsConfig.Properties.ActivationShortcut = value ?? MouseJumpSettingsConfig.Properties.DefaultActivationShortcut; + NotifyMouseJumpPropertyChanged(); + } + } + } + + public MouseJumpThumbnailSize MouseJumpThumbnailSize + { + get + { + return MouseJumpSettingsConfig.Properties.ThumbnailSize; + } + + set + { + if ((MouseJumpSettingsConfig.Properties.ThumbnailSize.Width != value?.Width) + && (MouseJumpSettingsConfig.Properties.ThumbnailSize.Height != value?.Height)) + { + MouseJumpSettingsConfig.Properties.ThumbnailSize = value; + NotifyMouseJumpPropertyChanged(); + } + } + } + + 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; + } + + private static Lazy MouseJumpDesktopImage => new( + () => MouseUtilsViewModel.LoadImageResource("UI/Images/MouseJump-Desktop.png") + ); + + public ImageSource MouseJumpPreviewImage + { + get + { + // keep these in sync with the layout of "Images\MouseJump-Desktop.png" + var screens = new List() + { + /* + these magic numbers are the pixel dimensions of the individual screens on the + fake desktop image - "Images\MouseJump-Desktop.png" - used to generate the + preview image in the Settings UI properties page for Mouse Jump. if you update + the fake desktop image be sure to update these values as well. + */ + new(635, 172, 272, 168), + new(0, 0, 635, 339), + }; + var desktopSize = LayoutHelper.GetCombinedScreenBounds(screens).Size; + /* + magic number 283 is the content height left in the settings card after removing the top and bottom chrome: + + 300px settings card height - 1px top border - 7px top margin - 8px bottom margin - 1px bottom border = 283px image height + + this ensures we get a preview image scaled at 100% so borders etc are shown at exact pixel sizes in the preview + */ + var canvasSize = new SizeInfo(desktopSize.Width, 283).Clamp(desktopSize); + + var previewType = Enum.TryParse(this.MouseJumpPreviewType, true, out var previewTypeResult) + ? previewTypeResult + : PreviewType.Bezelled; + var previewStyle = previewType switch + { + PreviewType.Compact => StyleHelper.CompactPreviewStyle.WithCanvasSize(desktopSize), + PreviewType.Bezelled => StyleHelper.BezelledPreviewStyle.WithCanvasSize(desktopSize), + PreviewType.Custom => new PreviewStyle( + canvasSize: canvasSize, + canvasStyle: new( + marginStyle: new(0), + borderStyle: new( + color: ConfigHelper.DeserializeFromConfigColorString( + this.MouseJumpBorderColor), + all: this.MouseJumpBorderThickness, + depth: this.MouseJumpBorder3dDepth + ), + paddingStyle: new( + all: this.MouseJumpBorderPadding + ), + backgroundStyle: new( + color1: ConfigHelper.DeserializeFromConfigColorString( + this.MouseJumpBackgroundColor1), + color2: ConfigHelper.DeserializeFromConfigColorString( + this.MouseJumpBackgroundColor2) + ) + ), + screenStyle: new( + marginStyle: new( + all: this.MouseJumpScreenMargin + ), + borderStyle: new( + color: ConfigHelper.DeserializeFromConfigColorString( + this.MouseJumpBezelColor), + all: this.MouseJumpBezelThickness, + depth: this.MouseJumpBezel3dDepth + ), + paddingStyle: new(0), + backgroundStyle: new( + color1: ConfigHelper.DeserializeFromConfigColorString( + this.MouseJumpScreenColor1), + color2: ConfigHelper.DeserializeFromConfigColorString( + this.MouseJumpScreenColor2) + ) + )), + _ => throw new InvalidOperationException( + $"Unhandled {nameof(MouseJumpPreviewType)} '{previewType}'"), + }; + + var previewLayout = LayoutHelper.GetPreviewLayout( + previewStyle: previewStyle, + screens: screens, + activatedLocation: new(0, 0)); + + var desktopImage = MouseUtilsViewModel.MouseJumpDesktopImage.Value; + var imageCopyService = new StaticImageRegionCopyService(desktopImage); + using var previewImage = DrawingHelper.RenderPreview( + previewLayout, + imageCopyService); + + // save the image to a memory stream + using var stream = new MemoryStream(); + previewImage.Save(stream, ImageFormat.Png); + stream.Position = 0; + + // load the memory stream into a bitmap image + var bitmap = new BitmapImage(); + var rnd = stream.AsRandomAccessStream(); + bitmap.DecodePixelWidth = previewImage.Width; + bitmap.DecodePixelHeight = previewImage.Height; + bitmap.SetSource(rnd); + return bitmap; + } + } + + public string MouseJumpPreviewType + { + get + { + return MouseJumpSettingsConfig.Properties.PreviewType; + } + + set + { + if (value != MouseJumpSettingsConfig.Properties.PreviewType) + { + MouseJumpSettingsConfig.Properties.PreviewType = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public string MouseJumpBackgroundColor1 + { + get + { + var value = MouseJumpSettingsConfig.Properties.BackgroundColor1; + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#000000"; + return value; + } + + set + { + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#000000"; + if (!value.Equals(MouseJumpSettingsConfig.Properties.BackgroundColor1, StringComparison.OrdinalIgnoreCase)) + { + MouseJumpSettingsConfig.Properties.BackgroundColor1 = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public string MouseJumpBackgroundColor2 + { + get + { + var value = MouseJumpSettingsConfig.Properties.BackgroundColor2; + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#000000"; + return value; + } + + set + { + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#000000"; + if (!value.Equals(MouseJumpSettingsConfig.Properties.BackgroundColor2, StringComparison.OrdinalIgnoreCase)) + { + MouseJumpSettingsConfig.Properties.BackgroundColor2 = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public int MouseJumpBorderThickness + { + get + { + return MouseJumpSettingsConfig.Properties.BorderThickness; + } + + set + { + if (value != MouseJumpSettingsConfig.Properties.BorderThickness) + { + MouseJumpSettingsConfig.Properties.BorderThickness = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public string MouseJumpBorderColor + { + get + { + var value = MouseJumpSettingsConfig.Properties.BorderColor; + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#000000"; + return value; + } + + set + { + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#000000"; + if (!value.Equals(MouseJumpSettingsConfig.Properties.BorderColor, StringComparison.OrdinalIgnoreCase)) + { + MouseJumpSettingsConfig.Properties.BorderColor = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public int MouseJumpBorder3dDepth + { + get + { + return MouseJumpSettingsConfig.Properties.Border3dDepth; + } + + set + { + if (value != MouseJumpSettingsConfig.Properties.Border3dDepth) + { + MouseJumpSettingsConfig.Properties.Border3dDepth = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public int MouseJumpBorderPadding + { + get + { + return MouseJumpSettingsConfig.Properties.BorderPadding; + } + + set + { + if (value != MouseJumpSettingsConfig.Properties.BorderPadding) + { + MouseJumpSettingsConfig.Properties.BorderPadding = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public int MouseJumpBezelThickness + { + get + { + return MouseJumpSettingsConfig.Properties.BezelThickness; + } + + set + { + if (value != MouseJumpSettingsConfig.Properties.BezelThickness) + { + MouseJumpSettingsConfig.Properties.BezelThickness = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public string MouseJumpBezelColor + { + get + { + var value = MouseJumpSettingsConfig.Properties.BezelColor; + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#000000"; + return value; + } + + set + { + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#000000"; + if (!value.Equals(MouseJumpSettingsConfig.Properties.BezelColor, StringComparison.OrdinalIgnoreCase)) + { + MouseJumpSettingsConfig.Properties.BezelColor = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public int MouseJumpBezel3dDepth + { + get + { + return MouseJumpSettingsConfig.Properties.Bezel3dDepth; + } + + set + { + if (value != MouseJumpSettingsConfig.Properties.Bezel3dDepth) + { + MouseJumpSettingsConfig.Properties.Bezel3dDepth = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public int MouseJumpScreenMargin + { + get + { + return MouseJumpSettingsConfig.Properties.ScreenMargin; + } + + set + { + if (value != MouseJumpSettingsConfig.Properties.ScreenMargin) + { + MouseJumpSettingsConfig.Properties.ScreenMargin = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public string MouseJumpScreenColor1 + { + get + { + var value = MouseJumpSettingsConfig.Properties.ScreenColor1; + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#000000"; + return value; + } + + set + { + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#000000"; + if (!value.Equals(MouseJumpSettingsConfig.Properties.ScreenColor1, StringComparison.OrdinalIgnoreCase)) + { + MouseJumpSettingsConfig.Properties.ScreenColor1 = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public string MouseJumpScreenColor2 + { + get + { + var value = MouseJumpSettingsConfig.Properties.ScreenColor2; + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#000000"; + return value; + } + + set + { + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#000000"; + if (!value.Equals(MouseJumpSettingsConfig.Properties.ScreenColor2, StringComparison.OrdinalIgnoreCase)) + { + MouseJumpSettingsConfig.Properties.ScreenColor2 = value; + NotifyMouseJumpPropertyChanged(); + NotifyMouseJumpPropertyChanged(nameof(this.MouseJumpPreviewImage)); + } + } + } + + public void MouseJumpThumbnailSizePropertyChanged(object sender, PropertyChangedEventArgs e) + { + NotifyMouseJumpPropertyChanged(nameof(MouseJumpThumbnailSize)); + } + + public void NotifyMouseJumpPropertyChanged([CallerMemberName] string propertyName = null) + { + OnPropertyChanged(propertyName); + + SndMouseJumpSettings outsettings = new SndMouseJumpSettings(MouseJumpSettingsConfig); + SndModuleSettings ipcMessage = new SndModuleSettings(outsettings); + SendConfigMSG(ipcMessage.ToJsonString()); + SettingsUtils.SaveSettings(MouseJumpSettingsConfig.ToJsonString(), MouseJumpSettings.ModuleName); + } + } +}