Skip to content

Commit

Permalink
[MouseJump] added Mouse Jump style controls to Settings UI (microsoft…
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeclayton committed Oct 21, 2024
1 parent 6dcbd29 commit 1cfb30b
Show file tree
Hide file tree
Showing 25 changed files with 1,256 additions and 141 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static IEnumerable<object[]> GetTestCases()
yield return new object[]
{
new TestCase(
previewStyle: StyleHelper.DefaultPreviewStyle,
previewStyle: StyleHelper.BezelledPreviewStyle,
screens: new List<RectangleInfo>()
{
new(0, 0, 500, 500),
Expand All @@ -62,7 +62,7 @@ public static IEnumerable<object[]> GetTestCases()
yield return new object[]
{
new TestCase(
previewStyle: StyleHelper.DefaultPreviewStyle,
previewStyle: StyleHelper.BezelledPreviewStyle,
screens: new List<RectangleInfo>()
{
new(5120, 349, 1920, 1080),
Expand Down Expand Up @@ -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)
Expand All @@ -121,7 +114,7 @@ private static Bitmap LoadImageResource(string filename)
}

/// <summary>
/// Naive / brute force image comparison - we can optimise this later :-)
/// Naive / brute force image comparison - we can optimize this later :-)
/// </summary>
private static void AssertImagesEqual(Bitmap expected, Bitmap actual)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public TestCase(PreviewStyle previewStyle, List<RectangleInfo> screens, PointInf
public static IEnumerable<object[]> GetTestCases()
{
// happy path - single screen with 50% scaling,
// *has* a preview borders but *no* screenshot borders
// *has* a preview border but *no* screenshot borders
//
// +----------------+
// | |
Expand Down Expand Up @@ -160,7 +160,7 @@ public static IEnumerable<object[]> 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,
Expand All @@ -183,7 +183,7 @@ public static IEnumerable<object[]> 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
Expand Down Expand Up @@ -217,7 +217,7 @@ public static IEnumerable<object[]> 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,
Expand All @@ -240,7 +240,59 @@ public static IEnumerable<object[]> 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<RectangleInfo>
{
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
Expand Down Expand Up @@ -291,7 +343,7 @@ public static IEnumerable<object[]> 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,
Expand Down Expand Up @@ -321,7 +373,7 @@ public static IEnumerable<object[]> 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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<object[]> 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);
}
}

Expand Down
94 changes: 94 additions & 0 deletions src/modules/MouseUtils/MouseJump.Common/Helpers/ConfigHelper.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
19 changes: 8 additions & 11 deletions src/modules/MouseUtils/MouseJump.Common/Helpers/LayoutHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<RectangleInfo> screens)
public static RectangleInfo GetCombinedScreenBounds(List<RectangleInfo> screens)
{
return screens.Skip(1).Aggregate(
seed: screens.First(),
Expand All @@ -107,7 +104,7 @@ internal static RectangleInfo GetCombinedScreenBounds(List<RectangleInfo> screen
/// <returns>A <see cref="BoxBounds"/> object that represents the bounds of the different areas of the box.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="contentBounds"/> or <paramref name="boxStyle"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any of the styles in <paramref name="boxStyle"/> is null.</exception>
public static BoxBounds GetBoxBoundsFromContentBounds(
internal static BoxBounds GetBoxBoundsFromContentBounds(
RectangleInfo contentBounds,
BoxStyle boxStyle)
{
Expand Down Expand Up @@ -135,7 +132,7 @@ public static BoxBounds GetBoxBoundsFromContentBounds(
/// <returns>A <see cref="BoxBounds"/> object that represents the bounds of the different areas of the box.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="outerBounds"/> or <paramref name="boxStyle"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any of the styles in <paramref name="boxStyle"/> is null.</exception>
public static BoxBounds GetBoxBoundsFromOuterBounds(
internal static BoxBounds GetBoxBoundsFromOuterBounds(
RectangleInfo outerBounds,
BoxStyle boxStyle)
{
Expand Down
Loading

1 comment on commit 1cfb30b

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@check-spelling-bot Report

🔴 Please review

See the 📜action log or 📝 job summary for details.

Unrecognized words (1)

bezelled

Previously acknowledged words that are now absent applayout appsfolder buildtask cswinrt directshow DOPUS GBarm netcore nugets QDir SYSTEMSETTINGS SYSTEMWOW telem TOTALCMD USEPOSITION USESIZE winappdriver xplorer 🫥
To accept these unrecognized words as correct and remove the previously acknowledged and now absent words, you could run the following commands

... in a clone of the [email protected]:mikeclayton/PowerToys.git repository
on the dev/mikeclayton/mousejump-styles-part-3 branch (ℹ️ how do I use this?):

curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/v0.0.22/apply.pl' |
perl - 'https://github.com/mikeclayton/PowerToys/actions/runs/11447788435/attempts/1'
Available 📚 dictionaries could cover words (expected and unrecognized) not in the 📘 dictionary

This includes both expected items (1932) from .github/actions/spell-check/expect.txt and unrecognized words (1)

Dictionary Entries Covers Uniquely
cspell:r/src/r.txt 543 1 1
cspell:cpp/src/people.txt 23 1
cspell:cpp/src/ecosystem.txt 51 1

Consider adding them (in .github/workflows/spelling2.yml) for uses: check-spelling/[email protected] in its with:

      with:
        extra_dictionaries:
          cspell:r/src/r.txt
          cspell:cpp/src/people.txt
          cspell:cpp/src/ecosystem.txt

To stop checking additional dictionaries, add (in .github/workflows/spelling2.yml) for uses: check-spelling/[email protected] in its with:

check_extra_dictionaries: ''
If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

Please sign in to comment.