mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
* [Mouse Jump] - screenshot performance (#24607) * [Mouse Jump] - add words to spellchecker (#24607) * [Mouse Jump] - progressive activation (#24607) * [Mouse Jump] - fixing build (#24607) * [Mouse Jump] - fixing jump location, add unit tests, refactor (#24607) * [Mouse Jump] - removed testing code (#24607) * [Mouse Jump] added failing tests for * [Mouse Jump] - fix problem with some monitor layouts (#24607) * [Mouse Jump] - cleaning up some comments and namepsaces * [Mouse Jump] - added another screen layout test (#24607)
This commit is contained in:
25
.github/actions/spell-check/expect.txt
vendored
25
.github/actions/spell-check/expect.txt
vendored
@@ -49,6 +49,7 @@ angularsen
|
||||
Animatable
|
||||
ansicolor
|
||||
ANull
|
||||
ANDSCANS
|
||||
AOC
|
||||
aocfnapldcnfbofgmbbllojgocaelgdd
|
||||
APARTMENTTHREADED
|
||||
@@ -139,7 +140,9 @@ BITMAPINFOHEADER
|
||||
bitmask
|
||||
BITSPIXEL
|
||||
bla
|
||||
BLACKONWHITE
|
||||
Blockquotes
|
||||
Blt
|
||||
blogs
|
||||
BLUEGRAY
|
||||
Bluetooth
|
||||
@@ -185,6 +188,7 @@ CALG
|
||||
callbackptr
|
||||
Cangjie
|
||||
CANRENAME
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
CAtl
|
||||
cch
|
||||
@@ -258,6 +262,7 @@ colorformat
|
||||
colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
COLORONCOLOR
|
||||
colorpicker
|
||||
COLORREF
|
||||
comctl
|
||||
@@ -305,6 +310,7 @@ CProj
|
||||
CREATESCHEDULEDTASK
|
||||
CREATESTRUCT
|
||||
CREATEWINDOWFAILED
|
||||
createcompatibledc
|
||||
critsec
|
||||
Crossdevice
|
||||
CRSEL
|
||||
@@ -368,6 +374,7 @@ dcomp
|
||||
dcompi
|
||||
DComposition
|
||||
DCR
|
||||
DCs
|
||||
DDevice
|
||||
ddf
|
||||
DDxgi
|
||||
@@ -388,6 +395,7 @@ DEFERERASE
|
||||
DEFPUSHBUTTON
|
||||
deinitialization
|
||||
DELA
|
||||
DELETESCANS
|
||||
deletethis
|
||||
Delimarsky
|
||||
dend
|
||||
@@ -442,6 +450,7 @@ drawingcolor
|
||||
dreamsofameaningfullife
|
||||
drivedetectionwarning
|
||||
dshow
|
||||
DSTINVERT
|
||||
dutil
|
||||
DVASPECT
|
||||
DVASPECTINFO
|
||||
@@ -1035,6 +1044,8 @@ Melman
|
||||
MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
menurc
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metadatas
|
||||
metafile
|
||||
mfapi
|
||||
@@ -1193,6 +1204,7 @@ NOINHERITLAYOUT
|
||||
NOINTERFACE
|
||||
NOLINKINFO
|
||||
NOMINMAX
|
||||
NOMIRRORBITMAP
|
||||
NOMOVE
|
||||
NONAME
|
||||
nonclient
|
||||
@@ -1224,6 +1236,8 @@ notmatch
|
||||
Noto
|
||||
NOTOPMOST
|
||||
NOTRACK
|
||||
NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
NOUPDATE
|
||||
NOZORDER
|
||||
NPH
|
||||
@@ -1273,6 +1287,7 @@ openxmlformats
|
||||
OPTIMIZEFORINVOKE
|
||||
ORAW
|
||||
ORPHANEDDIALOGTITLE
|
||||
ORSCANS
|
||||
oss
|
||||
ostr
|
||||
OSVERSIONINFOEX
|
||||
@@ -1304,8 +1319,11 @@ PArgb
|
||||
parray
|
||||
PARTIALCONFIRMATIONDIALOGTITLE
|
||||
pasteplain
|
||||
PATCOPY
|
||||
pathcch
|
||||
Pathto
|
||||
PATINVERT
|
||||
PATPAINT
|
||||
PAUDIO
|
||||
pbc
|
||||
Pbgra
|
||||
@@ -1547,6 +1565,7 @@ Roamable
|
||||
robmensching
|
||||
Roboto
|
||||
rooler
|
||||
rop
|
||||
roslyn
|
||||
Rothera
|
||||
roundf
|
||||
@@ -1716,8 +1735,12 @@ spsi
|
||||
spsia
|
||||
spsrm
|
||||
spsv
|
||||
SRCAND
|
||||
SRCCOPY
|
||||
SRCERASE
|
||||
Srch
|
||||
SRCINVERT
|
||||
SRCPAINT
|
||||
sre
|
||||
Srednekolymsk
|
||||
SResize
|
||||
@@ -2039,6 +2062,7 @@ website
|
||||
wekyb
|
||||
Wevtapi
|
||||
wgpocpl
|
||||
WHITEONBLACK
|
||||
whitespaces
|
||||
WIC
|
||||
wifi
|
||||
@@ -2073,6 +2097,7 @@ winevt
|
||||
winexe
|
||||
winforms
|
||||
winfx
|
||||
wingdi
|
||||
winget
|
||||
wingetcreate
|
||||
Winhook
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MouseJumpUI.Drawing.Models;
|
||||
|
||||
namespace MouseJumpUI.UnitTests.Drawing;
|
||||
|
||||
[TestClass]
|
||||
public static class RectangleInfoTests
|
||||
{
|
||||
[TestClass]
|
||||
public class CenterTests
|
||||
{
|
||||
public class TestCase
|
||||
{
|
||||
public TestCase(RectangleInfo rectangle, PointInfo point, RectangleInfo expectedResult)
|
||||
{
|
||||
this.Rectangle = rectangle;
|
||||
this.Point = point;
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public RectangleInfo Rectangle { get; set; }
|
||||
|
||||
public PointInfo Point { get; set; }
|
||||
|
||||
public RectangleInfo ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// zero-sized
|
||||
yield return new[] { new TestCase(new(0, 0, 0, 0), new(0, 0), new(0, 0, 0, 0)), };
|
||||
|
||||
// zero-origin
|
||||
yield return new[] { new TestCase(new(0, 0, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
|
||||
yield return new[] { new TestCase(new(0, 0, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
|
||||
yield return new[] { new TestCase(new(0, 0, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
|
||||
|
||||
// non-zero origin
|
||||
yield return new[] { new TestCase(new(1000, 2000, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
|
||||
yield return new[] { new TestCase(new(1000, 2000, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
|
||||
yield return new[] { new TestCase(new(1000, 2000, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
|
||||
|
||||
// negative result
|
||||
yield return new[] { new TestCase(new(0, 0, 1000, 1200), new(300, 300), new(-200, -300, 1000, 1200)), };
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
|
||||
public void RunTestCases(TestCase data)
|
||||
{
|
||||
var actual = data.Rectangle.Center(data.Point);
|
||||
var expected = data.ExpectedResult;
|
||||
Assert.AreEqual(expected.X, actual.X);
|
||||
Assert.AreEqual(expected.Y, actual.Y);
|
||||
Assert.AreEqual(expected.Width, actual.Width);
|
||||
Assert.AreEqual(expected.Height, actual.Height);
|
||||
}
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class ClampTests
|
||||
{
|
||||
public class TestCase
|
||||
{
|
||||
public TestCase(RectangleInfo inner, RectangleInfo outer, RectangleInfo expectedResult)
|
||||
{
|
||||
this.Inner = inner;
|
||||
this.Outer = outer;
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public RectangleInfo Inner { get; set; }
|
||||
|
||||
public RectangleInfo Outer { get; set; }
|
||||
|
||||
public RectangleInfo ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// already inside - obj fills bounds exactly
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(0, 0, 100, 100), new(0, 0, 100, 100), new(0, 0, 100, 100)),
|
||||
};
|
||||
|
||||
// already inside - obj exactly in each corner
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(0, 0, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(100, 0, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(0, 100, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(100, 100, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)),
|
||||
};
|
||||
|
||||
// move inside - obj outside each corner
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(-50, -50, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(250, -50, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(-50, 250, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(150, 150, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)),
|
||||
};
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
|
||||
public void RunTestCases(TestCase data)
|
||||
{
|
||||
var actual = data.Inner.Clamp(data.Outer);
|
||||
var expected = data.ExpectedResult;
|
||||
Assert.AreEqual(expected.X, actual.X);
|
||||
Assert.AreEqual(expected.Y, actual.Y);
|
||||
Assert.AreEqual(expected.Width, actual.Width);
|
||||
Assert.AreEqual(expected.Height, actual.Height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MouseJumpUI.Drawing.Models;
|
||||
|
||||
namespace MouseJumpUI.UnitTests.Drawing;
|
||||
|
||||
public sealed class SizeInfoTests
|
||||
{
|
||||
[TestClass]
|
||||
public class ScaleToFitTests
|
||||
{
|
||||
public class TestCase
|
||||
{
|
||||
public TestCase(SizeInfo obj, SizeInfo bounds, SizeInfo expectedResult)
|
||||
{
|
||||
this.Obj = obj;
|
||||
this.Bounds = bounds;
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public SizeInfo Obj { get; set; }
|
||||
|
||||
public SizeInfo Bounds { get; set; }
|
||||
|
||||
public SizeInfo ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// identity tests
|
||||
yield return new[] { new TestCase(new(512, 384), new(512, 384), new(512, 384)), };
|
||||
yield return new[] { new TestCase(new(1024, 768), new(1024, 768), new(1024, 768)), };
|
||||
|
||||
// general tests
|
||||
yield return new[] { new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536)), };
|
||||
yield return new[] { new TestCase(new(2048, 1536), new(1024, 768), new(1024, 768)), };
|
||||
|
||||
// scale to fit width
|
||||
yield return new[] { new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)), };
|
||||
|
||||
// scale to fit height
|
||||
yield return new[] { new TestCase(new(512, 384), new(4096, 1536), new(2048, 1536)), };
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
|
||||
public void RunTestCases(TestCase data)
|
||||
{
|
||||
var actual = data.Obj.ScaleToFit(data.Bounds);
|
||||
var expected = data.ExpectedResult;
|
||||
Assert.AreEqual(expected.Width, actual.Width);
|
||||
Assert.AreEqual(expected.Height, actual.Height);
|
||||
}
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class ScaleToFitRatioTests
|
||||
{
|
||||
public class TestCase
|
||||
{
|
||||
public TestCase(SizeInfo obj, SizeInfo bounds, decimal expectedResult)
|
||||
{
|
||||
this.Obj = obj;
|
||||
this.Bounds = bounds;
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public SizeInfo Obj { get; set; }
|
||||
|
||||
public SizeInfo Bounds { get; set; }
|
||||
|
||||
public decimal ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// identity tests
|
||||
yield return new[] { new TestCase(new(512, 384), new(512, 384), 1), };
|
||||
yield return new[] { new TestCase(new(1024, 768), new(1024, 768), 1), };
|
||||
|
||||
// general tests
|
||||
yield return new[] { new TestCase(new(512, 384), new(2048, 1536), 4), };
|
||||
yield return new[] { new TestCase(new(2048, 1536), new(1024, 768), 0.5M), };
|
||||
|
||||
// scale to fit width
|
||||
yield return new[] { new TestCase(new(512, 384), new(2048, 3072), 4), };
|
||||
|
||||
// scale to fit height
|
||||
yield return new[] { new TestCase(new(512, 384), new(4096, 1536), 4), };
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
|
||||
public void RunTestCases(TestCase data)
|
||||
{
|
||||
var actual = data.Obj.ScaleToFitRatio(data.Bounds);
|
||||
var expected = data.ExpectedResult;
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MouseJumpUI.Drawing.Models;
|
||||
using MouseJumpUI.Helpers;
|
||||
|
||||
namespace MouseJumpUI.UnitTests.Helpers;
|
||||
|
||||
[TestClass]
|
||||
public static class DrawingHelperTests
|
||||
{
|
||||
[TestClass]
|
||||
public class CalculateLayoutInfoTests
|
||||
{
|
||||
public class TestCase
|
||||
{
|
||||
public TestCase(LayoutConfig layoutConfig, LayoutInfo expectedResult)
|
||||
{
|
||||
this.LayoutConfig = layoutConfig;
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public LayoutConfig LayoutConfig { get; set; }
|
||||
|
||||
public LayoutInfo ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// happy path - check the preview form is shown
|
||||
// at the correct size and position on a single screen
|
||||
//
|
||||
// +----------------+
|
||||
// | |
|
||||
// | 0 |
|
||||
// | |
|
||||
// +----------------+
|
||||
var layoutConfig = new LayoutConfig(
|
||||
virtualScreen: new(0, 0, 5120, 1440),
|
||||
screenBounds: new List<Rectangle>
|
||||
{
|
||||
new(0, 0, 5120, 1440),
|
||||
},
|
||||
activatedLocation: new(5120 / 2, 1440 / 2),
|
||||
activatedScreen: 0,
|
||||
maximumFormSize: new(1600, 1200),
|
||||
formPadding: new(5, 5, 5, 5),
|
||||
previewPadding: new(0, 0, 0, 0));
|
||||
var layoutInfo = new LayoutInfo(
|
||||
layoutConfig: layoutConfig,
|
||||
formBounds: new(1760, 491.40625M, 1600, 457.1875M),
|
||||
previewBounds: new(0, 0, 1590, 447.1875M),
|
||||
screenBounds: new List<RectangleInfo>
|
||||
{
|
||||
new(0, 0, 1590, 447.1875M),
|
||||
},
|
||||
activatedScreen: new(0, 0, 5120, 1440));
|
||||
yield return new[] { new TestCase(layoutConfig, layoutInfo) };
|
||||
|
||||
// primary monitor not topmost / leftmost - if there are screens
|
||||
// that are further left or higher than the primary monitor
|
||||
// they'll have negative coordinates which has caused some
|
||||
// issues with calculations in the past. this test will make
|
||||
// sure we handle negative coordinates gracefully
|
||||
//
|
||||
// +-------+
|
||||
// | 0 +----------------+
|
||||
// +-------+ |
|
||||
// | 1 |
|
||||
// | |
|
||||
// +----------------+
|
||||
layoutConfig = new LayoutConfig(
|
||||
virtualScreen: new(-1920, -472, 7040, 1912),
|
||||
screenBounds: new List<Rectangle>
|
||||
{
|
||||
new(-1920, -472, 1920, 1080),
|
||||
new(0, 0, 5120, 1440),
|
||||
},
|
||||
activatedLocation: new(-960, -236),
|
||||
activatedScreen: 0,
|
||||
maximumFormSize: new(1600, 1200),
|
||||
formPadding: new(5, 5, 5, 5),
|
||||
previewPadding: new(0, 0, 0, 0));
|
||||
layoutInfo = new LayoutInfo(
|
||||
layoutConfig: layoutConfig,
|
||||
formBounds: new(
|
||||
-1760,
|
||||
-456.91477M, // -236 - (((decimal)(1600-10) / 7040 * 1912) + 10) / 2
|
||||
1600,
|
||||
441.829545M // ((decimal)(1600-10) / 7040 * 1912) + 10
|
||||
),
|
||||
previewBounds: new(0, 0, 1590, 431.829545M),
|
||||
screenBounds: new List<RectangleInfo>
|
||||
{
|
||||
new(0, 0, 433.63636M, 243.92045M),
|
||||
new(433.63636M, 106.602270M, 1156.36363M, 325.22727M),
|
||||
},
|
||||
activatedScreen: new(-1920, -472, 1920, 1080));
|
||||
yield return new[] { new TestCase(layoutConfig, layoutInfo) };
|
||||
|
||||
// check we handle rounding errors in scaling the preview form
|
||||
// that might make the form *larger* than the current screen -
|
||||
// e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768
|
||||
// with a 5px form padding border:
|
||||
//
|
||||
// ((decimal)1014 / 7168) * 7168 = 1014.0000000000000000000000002
|
||||
//
|
||||
// +----------------+
|
||||
// | |
|
||||
// | 1 +-------+
|
||||
// | | 0 |
|
||||
// +----------------+-------+
|
||||
layoutConfig = new LayoutConfig(
|
||||
virtualScreen: new(0, 0, 7168, 1440),
|
||||
screenBounds: new List<Rectangle>
|
||||
{
|
||||
new(6144, 0, 1024, 768),
|
||||
new(0, 0, 6144, 1440),
|
||||
},
|
||||
activatedLocation: new(6656, 384),
|
||||
activatedScreen: 0,
|
||||
maximumFormSize: new(1600, 1200),
|
||||
formPadding: new(5, 5, 5, 5),
|
||||
previewPadding: new(0, 0, 0, 0));
|
||||
layoutInfo = new LayoutInfo(
|
||||
layoutConfig: layoutConfig,
|
||||
formBounds: new(6144, 277.14732M, 1024, 213.70535M),
|
||||
previewBounds: new(0, 0, 1014, 203.70535M),
|
||||
screenBounds: new List<RectangleInfo>
|
||||
{
|
||||
new(869.14285M, 0, 144.85714M, 108.642857M),
|
||||
new(0, 0, 869.142857M, 203.705357M),
|
||||
},
|
||||
activatedScreen: new(6144, 0, 1024, 768));
|
||||
yield return new[] { new TestCase(layoutConfig, layoutInfo) };
|
||||
|
||||
// check we handle rounding errors in scaling the preview form
|
||||
// that might make the form a pixel *smaller* than the current screen -
|
||||
// e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768
|
||||
// with a 5px form padding border:
|
||||
//
|
||||
// ((decimal)1280 / 7424) * 7424 = 1279.9999999999999999999999999
|
||||
//
|
||||
// +----------------+
|
||||
// | |
|
||||
// | 1 +-------+
|
||||
// | | 0 |
|
||||
// +----------------+-------+
|
||||
layoutConfig = new LayoutConfig(
|
||||
virtualScreen: new(0, 0, 7424, 1440),
|
||||
screenBounds: new List<Rectangle>
|
||||
{
|
||||
new(6144, 0, 1280, 768),
|
||||
new(0, 0, 6144, 1440),
|
||||
},
|
||||
activatedLocation: new(6784, 384),
|
||||
activatedScreen: 0,
|
||||
maximumFormSize: new(1600, 1200),
|
||||
formPadding: new(5, 5, 5, 5),
|
||||
previewPadding: new(0, 0, 0, 0));
|
||||
layoutInfo = new LayoutInfo(
|
||||
layoutConfig: layoutConfig,
|
||||
formBounds: new(
|
||||
6144,
|
||||
255.83189M, // (768 - (((decimal)(1280-10) / 7424 * 1440) + 10)) / 2
|
||||
1280,
|
||||
256.33620M // ((decimal)(1280 - 10) / 7424 * 1440) + 10
|
||||
),
|
||||
previewBounds: new(0, 0, 1270, 246.33620M),
|
||||
screenBounds: new List<RectangleInfo>
|
||||
{
|
||||
new(1051.03448M, 0, 218.96551M, 131.37931M),
|
||||
new(0, 0M, 1051.03448M, 246.33620M),
|
||||
},
|
||||
activatedScreen: new(6144, 0, 1280, 768));
|
||||
yield return new[] { new TestCase(layoutConfig, layoutInfo) };
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
|
||||
public void RunTestCases(TestCase data)
|
||||
{
|
||||
// note - even if values are within 0.0001M of each other they could
|
||||
// still round to different values - e.g.
|
||||
// (int)1279.999999999999 -> 1279
|
||||
// vs
|
||||
// (int)1280.000000000000 -> 1280
|
||||
// so we'll compare the raw values, *and* convert to an int-based
|
||||
// Rectangle to compare rounded values
|
||||
var actual = DrawingHelper.CalculateLayoutInfo(data.LayoutConfig);
|
||||
var expected = data.ExpectedResult;
|
||||
Assert.AreEqual(expected.FormBounds.X, actual.FormBounds.X, 0.00001M, "FormBounds.X");
|
||||
Assert.AreEqual(expected.FormBounds.Y, actual.FormBounds.Y, 0.00001M, "FormBounds.Y");
|
||||
Assert.AreEqual(expected.FormBounds.Width, actual.FormBounds.Width, 0.00001M, "FormBounds.Width");
|
||||
Assert.AreEqual(expected.FormBounds.Height, actual.FormBounds.Height, 0.00001M, "FormBounds.Height");
|
||||
Assert.AreEqual(expected.FormBounds.ToRectangle(), actual.FormBounds.ToRectangle(), "FormBounds.ToRectangle");
|
||||
Assert.AreEqual(expected.PreviewBounds.X, actual.PreviewBounds.X, 0.00001M, "PreviewBounds.X");
|
||||
Assert.AreEqual(expected.PreviewBounds.Y, actual.PreviewBounds.Y, 0.00001M, "PreviewBounds.Y");
|
||||
Assert.AreEqual(expected.PreviewBounds.Width, actual.PreviewBounds.Width, 0.00001M, "PreviewBounds.Width");
|
||||
Assert.AreEqual(expected.PreviewBounds.Height, actual.PreviewBounds.Height, 0.00001M, "PreviewBounds.Height");
|
||||
Assert.AreEqual(expected.PreviewBounds.ToRectangle(), actual.PreviewBounds.ToRectangle(), "PreviewBounds.ToRectangle");
|
||||
Assert.AreEqual(expected.ScreenBounds.Count, actual.ScreenBounds.Count, "ScreenBounds.Count");
|
||||
for (var i = 0; i < expected.ScreenBounds.Count; i++)
|
||||
{
|
||||
Assert.AreEqual(expected.ScreenBounds[i].X, actual.ScreenBounds[i].X, 0.00001M, $"ScreenBounds[{i}].X");
|
||||
Assert.AreEqual(expected.ScreenBounds[i].Y, actual.ScreenBounds[i].Y, 0.00001M, $"ScreenBounds[{i}].Y");
|
||||
Assert.AreEqual(expected.ScreenBounds[i].Width, actual.ScreenBounds[i].Width, 0.00001M, $"ScreenBounds[{i}].Width");
|
||||
Assert.AreEqual(expected.ScreenBounds[i].Height, actual.ScreenBounds[i].Height, 0.00001M, $"ScreenBounds[{i}].Height");
|
||||
Assert.AreEqual(expected.ScreenBounds[i].ToRectangle(), actual.ScreenBounds[i].ToRectangle(), "ActivatedScreen.ToRectangle");
|
||||
}
|
||||
|
||||
Assert.AreEqual(expected.ActivatedScreen.X, actual.ActivatedScreen.X, "ActivatedScreen.X");
|
||||
Assert.AreEqual(expected.ActivatedScreen.Y, actual.ActivatedScreen.Y, "ActivatedScreen.Y");
|
||||
Assert.AreEqual(expected.ActivatedScreen.Width, actual.ActivatedScreen.Width, "ActivatedScreen.Width");
|
||||
Assert.AreEqual(expected.ActivatedScreen.Height, actual.ActivatedScreen.Height, "ActivatedScreen.Height");
|
||||
Assert.AreEqual(expected.ActivatedScreen.ToRectangle(), actual.ActivatedScreen.ToRectangle(), "ActivatedScreen.ToRectangle");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,514 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace MouseJumpUI.Helpers.Tests;
|
||||
|
||||
[TestClass]
|
||||
public static class LayoutHelperTests
|
||||
{
|
||||
[TestClass]
|
||||
public class CenterObjectTests
|
||||
{
|
||||
public class TestCase
|
||||
{
|
||||
public TestCase(Size obj, Point midpoint, Point expectedResult)
|
||||
{
|
||||
this.Obj = obj;
|
||||
this.Midpoint = midpoint;
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public Size Obj { get; set; }
|
||||
|
||||
public Point Midpoint { get; set; }
|
||||
|
||||
public Point ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// zero-sized object should center exactly on the midpoint
|
||||
yield return new[] { new TestCase(new(0, 0), new(0, 0), new(0, 0)), };
|
||||
|
||||
// odd-sized objects should center above/left of the midpoint
|
||||
yield return new[] { new TestCase(new(1, 1), new(1, 1), new(0, 0)), };
|
||||
yield return new[] { new TestCase(new(1, 1), new(5, 5), new(4, 4)), };
|
||||
|
||||
// even-sized objects should center exactly on the midpoint
|
||||
yield return new[] { new TestCase(new(2, 2), new(1, 1), new(0, 0)), };
|
||||
yield return new[] { new TestCase(new(2, 2), new(5, 5), new(4, 4)), };
|
||||
yield return new[] { new TestCase(new(800, 600), new(1000, 1000), new(600, 700)), };
|
||||
|
||||
// negative result position
|
||||
yield return new[] { new TestCase(new(1000, 1200), new(300, 300), new(-200, -300)), };
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
|
||||
public void RunTestCases(TestCase data)
|
||||
{
|
||||
var actual = LayoutHelper.CenterObject(data.Obj, data.Midpoint);
|
||||
var expected = data.ExpectedResult;
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class CombineRegionsTests
|
||||
{
|
||||
public class TestCase
|
||||
{
|
||||
public TestCase(List<Rectangle> bounds, Rectangle expectedResult)
|
||||
{
|
||||
this.Bounds = bounds;
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public List<Rectangle> Bounds { get; set; }
|
||||
|
||||
public Rectangle ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// empty list
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(
|
||||
new(),
|
||||
Rectangle.Empty),
|
||||
};
|
||||
|
||||
// empty bounds
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(
|
||||
new()
|
||||
{
|
||||
Rectangle.Empty,
|
||||
},
|
||||
Rectangle.Empty),
|
||||
};
|
||||
|
||||
// single region
|
||||
//
|
||||
// +---+
|
||||
// | 0 |
|
||||
// +---+
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(
|
||||
new()
|
||||
{
|
||||
new(100, 100, 100, 100),
|
||||
},
|
||||
new(100, 100, 100, 100)),
|
||||
};
|
||||
|
||||
// multi-monitor desktop
|
||||
//
|
||||
// +----------------+
|
||||
// | |
|
||||
// | 1 +-------+
|
||||
// | | 0 |
|
||||
// +----------------+-------+
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(
|
||||
new()
|
||||
{
|
||||
new(5120, 0, 1920, 1080),
|
||||
new(0, 0, 5120, 1440),
|
||||
},
|
||||
new(0, 0, 7040, 1440)),
|
||||
};
|
||||
|
||||
// multi-monitor desktop
|
||||
//
|
||||
// note - windows puts the *primary* monitor at the origin (0,0),
|
||||
// so screens positioned *above* or *left* will have negative coordinates
|
||||
//
|
||||
// +-------+
|
||||
// | 0 |
|
||||
// +-------+--------+
|
||||
// | |
|
||||
// | 1 |
|
||||
// | |
|
||||
// +----------------+
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(
|
||||
new()
|
||||
{
|
||||
new(0, -1000, 1920, 1080),
|
||||
new(0, 0, 5120, 1440),
|
||||
},
|
||||
new(0, -1000, 5120, 2440)),
|
||||
};
|
||||
|
||||
// multi-monitor desktop
|
||||
//
|
||||
// note - windows puts the *primary* monitor at the origin (0,0),
|
||||
// so screens positioned *above* or *left* will have negative coordinates
|
||||
//
|
||||
// +-------+----------------+
|
||||
// | 0 | |
|
||||
// +-------+ 1 |
|
||||
// | |
|
||||
// +----------------+
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(
|
||||
new()
|
||||
{
|
||||
new(-1920, 0, 1920, 1080),
|
||||
new(0, 0, 5120, 1440),
|
||||
},
|
||||
new(-1920, 0, 7040, 1440)),
|
||||
};
|
||||
|
||||
// non-contiguous regions
|
||||
//
|
||||
// +---+
|
||||
// | 0 | +-------+
|
||||
// +---+ | |
|
||||
// | 1 |
|
||||
// | |
|
||||
// +-------+
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(
|
||||
new()
|
||||
{
|
||||
new(0, 0, 100, 100),
|
||||
new(200, 150, 200, 200),
|
||||
},
|
||||
new(0, 0, 400, 350)),
|
||||
};
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
|
||||
public void RunTestCases(TestCase data)
|
||||
{
|
||||
var actual = LayoutHelper.CombineRegions(data.Bounds);
|
||||
var expected = data.ExpectedResult;
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class GetMidpointTests
|
||||
{
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class MoveInsideTests
|
||||
{
|
||||
public class TestCase
|
||||
{
|
||||
public TestCase(Rectangle obj, Rectangle bounds, Rectangle expectedResult)
|
||||
{
|
||||
this.Obj = obj;
|
||||
this.Bounds = bounds;
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public Rectangle Obj { get; set; }
|
||||
|
||||
public Rectangle Bounds { get; set; }
|
||||
|
||||
public Rectangle ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// already inside - obj fills bounds exactly
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(0, 0, 100, 100), new(0, 0, 100, 100), new(0, 0, 100, 100)),
|
||||
};
|
||||
|
||||
// already inside - obj exactly in each corner
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(0, 0, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(100, 0, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(0, 100, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(100, 100, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)),
|
||||
};
|
||||
|
||||
// move inside - obj outside each corner
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(-50, -50, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(250, -50, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(-50, 250, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(150, 150, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)),
|
||||
};
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
|
||||
public void RunTestCases(TestCase data)
|
||||
{
|
||||
var actual = LayoutHelper.MoveInside(data.Obj, data.Bounds);
|
||||
var expected = data.ExpectedResult;
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class ScaleLocationTests
|
||||
{
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class ScaleToFitTests
|
||||
{
|
||||
public class TestCase
|
||||
{
|
||||
public TestCase(Size obj, Size bounds, Size expectedResult)
|
||||
{
|
||||
this.Obj = obj;
|
||||
this.Bounds = bounds;
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public Size Obj { get; set; }
|
||||
|
||||
public Size Bounds { get; set; }
|
||||
|
||||
public Size ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// identity tests
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(0, 0), new(0, 0), new(0, 0)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(512, 384), new(512, 384), new(512, 384)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(1024, 768), new(1024, 768), new(1024, 768)),
|
||||
};
|
||||
|
||||
// integer scaling factor tests
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536)),
|
||||
};
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(2048, 1536), new(1024, 768), new(1024, 768)),
|
||||
};
|
||||
|
||||
// scale to fit width
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)),
|
||||
};
|
||||
|
||||
// scale to fit height
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(512, 384), new(4096, 1536), new(2048, 1536)),
|
||||
};
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
|
||||
public void RunTestCases(TestCase data)
|
||||
{
|
||||
var actual = LayoutHelper.ScaleToFit(data.Obj, data.Bounds);
|
||||
var expected = data.ExpectedResult;
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class GetPreviewFormBoundsTests
|
||||
{
|
||||
public class TestCase
|
||||
{
|
||||
public TestCase(
|
||||
Rectangle desktopBounds,
|
||||
Point cursorPosition,
|
||||
Rectangle currentMonitorBounds,
|
||||
Size maximumPreviewImageSize,
|
||||
Size previewImagePadding,
|
||||
Rectangle expectedResult)
|
||||
{
|
||||
this.DesktopBounds = desktopBounds;
|
||||
this.CursorPosition = cursorPosition;
|
||||
this.CurrentMonitorBounds = currentMonitorBounds;
|
||||
this.MaximumPreviewImageSize = maximumPreviewImageSize;
|
||||
this.PreviewImagePadding = previewImagePadding;
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public Rectangle DesktopBounds { get; set; }
|
||||
|
||||
public Point CursorPosition { get; set; }
|
||||
|
||||
public Rectangle CurrentMonitorBounds { get; set; }
|
||||
|
||||
public Size MaximumPreviewImageSize { get; set; }
|
||||
|
||||
public Size PreviewImagePadding { get; set; }
|
||||
|
||||
public Rectangle ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// multi-monitor desktop
|
||||
//
|
||||
// +----------------+
|
||||
// | |
|
||||
// | 1 +-------+
|
||||
// | | 0 |
|
||||
// +----------------+-------+
|
||||
//
|
||||
// clicked near top left corner so that the
|
||||
// preview box overhangs the top and left
|
||||
//
|
||||
// +----------------+
|
||||
// | * |
|
||||
// | 1 +-------+
|
||||
// | | 0 |
|
||||
// +----------------+-------+
|
||||
//
|
||||
// form is centered on mouse cursor and then
|
||||
// nudged back into the top left corner
|
||||
//
|
||||
// +-----+----------+
|
||||
// | * | |
|
||||
// +-----+ 1 +-------+
|
||||
// | | 0 |
|
||||
// +----------------+-------+
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(
|
||||
desktopBounds: new(-5120, -359, 7040, 1440),
|
||||
cursorPosition: new(-5020, -259),
|
||||
currentMonitorBounds: new(-5120, -359, 5120, 1440),
|
||||
maximumPreviewImageSize: new(1600, 1200),
|
||||
previewImagePadding: new(10, 10),
|
||||
expectedResult: new(-5120, -359, 1610, 337)),
|
||||
};
|
||||
|
||||
// multi-monitor desktop
|
||||
//
|
||||
// +----------------+
|
||||
// | |
|
||||
// | 1 +-------+
|
||||
// | | 0 |
|
||||
// +----------------+-------+
|
||||
//
|
||||
// clicked in the center of the second monitor
|
||||
//
|
||||
// +----------------+
|
||||
// | |
|
||||
// | * +-------+
|
||||
// | | 0 |
|
||||
// +----------------+-------+
|
||||
//
|
||||
// form is centered on the mouse cursor
|
||||
//
|
||||
// +----------------+
|
||||
// | +-----+ |
|
||||
// | | * | +-------+
|
||||
// | +-----+ | 0 |
|
||||
// +----------------+-------+
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(
|
||||
desktopBounds: new(-5120, -359, 7040, 1440),
|
||||
cursorPosition: new(-2560, 361),
|
||||
currentMonitorBounds: new(-5120, -359, 5120, 1440),
|
||||
maximumPreviewImageSize: new(1600, 1200),
|
||||
previewImagePadding: new(10, 10),
|
||||
expectedResult: new(-3365, 192, 1610, 337)),
|
||||
};
|
||||
|
||||
// multi-monitor desktop
|
||||
//
|
||||
// +----------------+
|
||||
// | |
|
||||
// | 1 +-------+
|
||||
// | | 0 |
|
||||
// +----------------+-------+
|
||||
//
|
||||
// clicked in the center of the monitor
|
||||
//
|
||||
// +----------------+
|
||||
// | |
|
||||
// | * +-------+
|
||||
// | | 0 |
|
||||
// +----------------+-------+
|
||||
//
|
||||
// max preview is larger than monitor,
|
||||
// form is scaled to monitor size, with
|
||||
// consideration for image padding
|
||||
//
|
||||
// *----------------*
|
||||
// |+--------------+|
|
||||
// || * |+-------+
|
||||
// |+--------------+| 0 |
|
||||
// +----------------+-------+
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(
|
||||
desktopBounds: new(-5120, -359, 7040, 1440),
|
||||
cursorPosition: new(-2560, 361),
|
||||
currentMonitorBounds: new(-5120, -359, 5120, 1440),
|
||||
maximumPreviewImageSize: new(160000, 120000),
|
||||
previewImagePadding: new(10, 10),
|
||||
expectedResult: new(-5120, -166, 5120, 1055)),
|
||||
};
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
|
||||
public void RunTestCases(TestCase data)
|
||||
{
|
||||
var actual = LayoutHelper.GetPreviewFormBounds(
|
||||
desktopBounds: data.DesktopBounds,
|
||||
activatedPosition: data.CursorPosition,
|
||||
activatedMonitorBounds: data.CurrentMonitorBounds,
|
||||
maximumThumbnailImageSize: data.MaximumPreviewImageSize,
|
||||
thumbnailImagePadding: data.PreviewImagePadding);
|
||||
var expected = data.ExpectedResult;
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MouseJumpUI.Drawing.Models;
|
||||
using MouseJumpUI.Helpers;
|
||||
|
||||
namespace MouseJumpUI.UnitTests.Helpers;
|
||||
|
||||
[TestClass]
|
||||
public static class MouseHelperTests
|
||||
{
|
||||
[TestClass]
|
||||
public class GetJumpLocationTests
|
||||
{
|
||||
public class TestCase
|
||||
{
|
||||
public TestCase(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds, PointInfo expectedResult)
|
||||
{
|
||||
this.PreviewLocation = previewLocation;
|
||||
this.PreviewSize = previewSize;
|
||||
this.DesktopBounds = desktopBounds;
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public PointInfo PreviewLocation { get; set; }
|
||||
|
||||
public SizeInfo PreviewSize { get; set; }
|
||||
|
||||
public RectangleInfo DesktopBounds { get; set; }
|
||||
|
||||
public PointInfo ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// screen corners and midpoint with a zero origin
|
||||
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(0, 0, 1600, 1200), new(0, 0)) };
|
||||
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(0, 0, 1600, 1200), new(1600, 0)) };
|
||||
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(0, 0, 1600, 1200), new(0, 1200)) };
|
||||
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(0, 0, 1600, 1200), new(1600, 1200)) };
|
||||
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(0, 0, 1600, 1200), new(800, 600)) };
|
||||
|
||||
// screen corners and midpoint with a positive origin
|
||||
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 1000)) };
|
||||
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 1000)) };
|
||||
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 2200)) };
|
||||
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 2200)) };
|
||||
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(1000, 1000, 1600, 1200), new(1800, 1600)) };
|
||||
|
||||
// screen corners and midpoint with a negative origin
|
||||
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, -1000)) };
|
||||
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, -1000)) };
|
||||
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, 200)) };
|
||||
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, 200)) };
|
||||
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(-1000, -1000, 1600, 1200), new(-200, -400)) };
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
|
||||
public void RunTestCases(TestCase data)
|
||||
{
|
||||
var actual = MouseHelper.GetJumpLocation(
|
||||
data.PreviewLocation,
|
||||
data.PreviewSize,
|
||||
data.DesktopBounds);
|
||||
var expected = data.ExpectedResult;
|
||||
Assert.AreEqual(expected.X, actual.X);
|
||||
Assert.AreEqual(expected.Y, actual.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace MouseJumpUI.Drawing.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a collection of values needed for calculating the MainForm layout.
|
||||
/// </summary>
|
||||
public sealed class LayoutConfig
|
||||
{
|
||||
public LayoutConfig(
|
||||
Rectangle virtualScreen,
|
||||
IEnumerable<Rectangle> screenBounds,
|
||||
Point activatedLocation,
|
||||
int activatedScreen,
|
||||
Size maximumFormSize,
|
||||
Padding formPadding,
|
||||
Padding previewPadding)
|
||||
{
|
||||
// make sure the virtual screen entirely contains all of the individual screen bounds
|
||||
ArgumentNullException.ThrowIfNull(screenBounds);
|
||||
if (screenBounds.Any(screen => !virtualScreen.Contains(screen)))
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(virtualScreen)}' must contain all of the screens in '{nameof(screenBounds)}'", nameof(virtualScreen));
|
||||
}
|
||||
|
||||
this.VirtualScreen = new RectangleInfo(virtualScreen);
|
||||
this.ScreenBounds = new(
|
||||
screenBounds.Select(screen => new RectangleInfo(screen)).ToList());
|
||||
this.ActivatedLocation = new(activatedLocation);
|
||||
this.ActivatedScreen = activatedScreen;
|
||||
this.MaximumFormSize = new(maximumFormSize);
|
||||
this.FormPadding = new(formPadding);
|
||||
this.PreviewPadding = new(previewPadding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the coordinates of the entire virtual screen.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Virtual Screen is the bounding rectangle of all the monitors.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen
|
||||
/// </remarks>
|
||||
public RectangleInfo VirtualScreen
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bounds of all of the screens connected to the system.
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<RectangleInfo> ScreenBounds
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the point where the cursor was located when the form was activated.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// The preview form will be centered on this location unless there are any
|
||||
/// constraints such as the being too close to edge of a screen, in which case
|
||||
/// the form will be displayed as close as possible to this location.
|
||||
/// </summary>
|
||||
public PointInfo ActivatedLocation
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the screen the cursor was on when the form was activated.
|
||||
/// </summary>
|
||||
public int ActivatedScreen
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum size of the screen preview form.
|
||||
/// </summary>
|
||||
public SizeInfo MaximumFormSize
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the padding border around the screen preview form.
|
||||
/// </summary>
|
||||
public PaddingInfo FormPadding
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the padding border inside the screen preview image.
|
||||
/// </summary>
|
||||
public PaddingInfo PreviewPadding
|
||||
{
|
||||
get;
|
||||
}
|
||||
}
|
||||
111
src/modules/MouseUtils/MouseJumpUI/Drawing/Models/LayoutInfo.cs
Normal file
111
src/modules/MouseUtils/MouseJumpUI/Drawing/Models/LayoutInfo.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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;
|
||||
|
||||
namespace MouseJumpUI.Drawing.Models;
|
||||
|
||||
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<RectangleInfo> ScreenBounds
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public RectangleInfo? ActivatedScreen
|
||||
{
|
||||
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(),
|
||||
activatedScreen: this.ActivatedScreen ?? throw new InvalidOperationException());
|
||||
}
|
||||
}
|
||||
|
||||
public LayoutInfo(
|
||||
LayoutConfig layoutConfig,
|
||||
RectangleInfo formBounds,
|
||||
RectangleInfo previewBounds,
|
||||
IEnumerable<RectangleInfo> screenBounds,
|
||||
RectangleInfo activatedScreen)
|
||||
{
|
||||
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.ActivatedScreen = activatedScreen ?? throw new ArgumentNullException(nameof(activatedScreen));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original LayoutConfig settings used to calculate coordinates.
|
||||
/// </summary>
|
||||
public LayoutConfig LayoutConfig
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size and location of the preview form.
|
||||
/// </summary>
|
||||
public RectangleInfo FormBounds
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size and location of the preview image.
|
||||
/// </summary>
|
||||
public RectangleInfo PreviewBounds
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public ReadOnlyCollection<RectangleInfo> ScreenBounds
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public RectangleInfo ActivatedScreen
|
||||
{
|
||||
get;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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.Drawing.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable version of a System.Windows.Forms.Padding object with some extra utility methods.
|
||||
/// </summary>
|
||||
public sealed class PaddingInfo
|
||||
{
|
||||
public PaddingInfo(decimal all)
|
||||
: this(all, all, all, all)
|
||||
{
|
||||
}
|
||||
|
||||
public PaddingInfo(Padding padding)
|
||||
: this(padding.Left, padding.Top, padding.Right, padding.Bottom)
|
||||
{
|
||||
}
|
||||
|
||||
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}" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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.Drawing.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable version of a System.Drawing.Point object with some extra utility methods.
|
||||
/// </summary>
|
||||
public sealed class PointInfo
|
||||
{
|
||||
public PointInfo(decimal x, decimal y)
|
||||
{
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
}
|
||||
|
||||
public PointInfo(Point point)
|
||||
: this(point.X, point.Y)
|
||||
{
|
||||
}
|
||||
|
||||
public decimal X
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public decimal Y
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public SizeInfo Size => 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}" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace MouseJumpUI.Drawing.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable version of a System.Drawing.Rectangle object with some extra utility methods.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
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}" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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.Drawing.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable version of a System.Drawing.Size object with some extra utility methods.
|
||||
/// </summary>
|
||||
public sealed class SizeInfo
|
||||
{
|
||||
public SizeInfo(decimal width, decimal height)
|
||||
{
|
||||
this.Width = width;
|
||||
this.Height = height;
|
||||
}
|
||||
|
||||
public SizeInfo(Size size)
|
||||
: this(size.Width, size.Height)
|
||||
{
|
||||
}
|
||||
|
||||
public decimal Width
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public decimal Height
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public SizeInfo 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),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the scaling ratio to scale obj by so that it fits inside the specified bounds
|
||||
/// without distorting the aspect ratio.
|
||||
/// </summary>
|
||||
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}" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
237
src/modules/MouseUtils/MouseJumpUI/Helpers/DrawingHelper.cs
Normal file
237
src/modules/MouseUtils/MouseJumpUI/Helpers/DrawingHelper.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using MouseJumpUI.Drawing.Models;
|
||||
using MouseJumpUI.NativeMethods.Core;
|
||||
using MouseJumpUI.NativeWrappers;
|
||||
|
||||
namespace MouseJumpUI.Helpers;
|
||||
|
||||
internal static class DrawingHelper
|
||||
{
|
||||
public static LayoutInfo CalculateLayoutInfo(
|
||||
LayoutConfig layoutConfig)
|
||||
{
|
||||
if (layoutConfig is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutConfig));
|
||||
}
|
||||
|
||||
var builder = new LayoutInfo.Builder
|
||||
{
|
||||
LayoutConfig = layoutConfig,
|
||||
};
|
||||
|
||||
builder.ActivatedScreen = layoutConfig.ScreenBounds[layoutConfig.ActivatedScreen];
|
||||
|
||||
// work out the maximum *constrained* form size
|
||||
// * can't be bigger than the activated screen
|
||||
// * can't be bigger than the max form size
|
||||
var maxFormSize = builder.ActivatedScreen.Size
|
||||
.Intersect(layoutConfig.MaximumFormSize);
|
||||
|
||||
// the drawing area for screen images is inside the
|
||||
// form border and inside the preview border
|
||||
var maxDrawingSize = maxFormSize
|
||||
.Shrink(layoutConfig.FormPadding)
|
||||
.Shrink(layoutConfig.PreviewPadding);
|
||||
|
||||
// scale the virtual screen to fit inside the drawing bounds
|
||||
var scalingRatio = layoutConfig.VirtualScreen.Size
|
||||
.ScaleToFitRatio(maxDrawingSize);
|
||||
|
||||
// position the drawing bounds inside the preview border
|
||||
var drawingBounds = layoutConfig.VirtualScreen.Size
|
||||
.ScaleToFit(maxDrawingSize)
|
||||
.PlaceAt(layoutConfig.PreviewPadding.Left, layoutConfig.PreviewPadding.Top);
|
||||
|
||||
// now we know the size of the drawing area we can work out the preview size
|
||||
builder.PreviewBounds = drawingBounds.Enlarge(layoutConfig.PreviewPadding);
|
||||
|
||||
// ... and the form size
|
||||
// * center the form to the activated position, but nudge it back
|
||||
// inside the visible area of the activated screen if it falls outside
|
||||
builder.FormBounds = builder.PreviewBounds.Size
|
||||
.PlaceAt(0, 0)
|
||||
.Enlarge(layoutConfig.FormPadding)
|
||||
.Center(layoutConfig.ActivatedLocation)
|
||||
.Clamp(builder.ActivatedScreen);
|
||||
|
||||
// now calculate the positions of each of the screen images on the preview
|
||||
builder.ScreenBounds = layoutConfig.ScreenBounds
|
||||
.Select(
|
||||
screen => screen
|
||||
.Offset(layoutConfig.VirtualScreen.Location.Size.Negate())
|
||||
.Scale(scalingRatio)
|
||||
.Offset(layoutConfig.PreviewPadding.Left, layoutConfig.PreviewPadding.Top))
|
||||
.ToList();
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resize and position the specified form.
|
||||
/// </summary>
|
||||
public static void PositionForm(
|
||||
Form form, RectangleInfo formBounds)
|
||||
{
|
||||
// note - do this in two steps rather than "this.Bounds = formBounds" as there
|
||||
// appears to be an issue in WinForms with dpi scaling even when using PerMonitorV2,
|
||||
// where the form scaling uses either the *primary* screen scaling or the *previous*
|
||||
// screen's scaling when the form is moved to a different screen. i've got no idea
|
||||
// *why*, but the exact sequence of calls below seems to be a workaround...
|
||||
// see https://github.com/mikeclayton/FancyMouse/issues/2
|
||||
var bounds = formBounds.ToRectangle();
|
||||
form.Location = bounds.Location;
|
||||
_ = form.PointToScreen(Point.Empty);
|
||||
form.Size = bounds.Size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw the preview background.
|
||||
/// </summary>
|
||||
public static void DrawPreviewBackground(
|
||||
Graphics previewGraphics, RectangleInfo previewBounds, IEnumerable<RectangleInfo> screenBounds)
|
||||
{
|
||||
using var backgroundBrush = new LinearGradientBrush(
|
||||
previewBounds.Location.ToPoint(),
|
||||
previewBounds.Size.ToPoint(),
|
||||
Color.FromArgb(13, 87, 210), // light blue
|
||||
Color.FromArgb(3, 68, 192)); // darker blue
|
||||
|
||||
// it's faster to build a region with the screen areas excluded
|
||||
// and fill that than it is to fill the entire bounding rectangle
|
||||
var backgroundRegion = new Region(previewBounds.ToRectangle());
|
||||
foreach (var screen in screenBounds)
|
||||
{
|
||||
backgroundRegion.Exclude(screen.ToRectangle());
|
||||
}
|
||||
|
||||
previewGraphics.FillRegion(backgroundBrush, backgroundRegion);
|
||||
}
|
||||
|
||||
public static void EnsureDesktopDeviceContext(ref HWND desktopHwnd, ref HDC desktopHdc)
|
||||
{
|
||||
if (desktopHwnd.IsNull)
|
||||
{
|
||||
desktopHwnd = User32.GetDesktopWindow();
|
||||
}
|
||||
|
||||
if (desktopHdc.IsNull)
|
||||
{
|
||||
desktopHdc = User32.GetWindowDC(desktopHwnd);
|
||||
}
|
||||
}
|
||||
|
||||
public static void FreeDesktopDeviceContext(ref HWND desktopHwnd, ref HDC desktopHdc)
|
||||
{
|
||||
if (!desktopHwnd.IsNull && !desktopHdc.IsNull)
|
||||
{
|
||||
_ = User32.ReleaseDC(desktopHwnd, desktopHdc);
|
||||
}
|
||||
|
||||
desktopHwnd = HWND.Null;
|
||||
desktopHdc = HDC.Null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the device context handle exists, and creates a new one from the
|
||||
/// Graphics object if not.
|
||||
/// </summary>
|
||||
public static void EnsurePreviewDeviceContext(Graphics previewGraphics, ref HDC previewHdc)
|
||||
{
|
||||
if (previewHdc.IsNull)
|
||||
{
|
||||
previewHdc = new HDC(previewGraphics.GetHdc());
|
||||
_ = Gdi32.SetStretchBltMode(previewHdc, MouseJumpUI.NativeMethods.Gdi32.STRETCH_BLT_MODE.STRETCH_HALFTONE);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Free the specified device context handle if it exists.
|
||||
/// </summary>
|
||||
public static void FreePreviewDeviceContext(Graphics previewGraphics, ref HDC previewHdc)
|
||||
{
|
||||
if ((previewGraphics is not null) && !previewHdc.IsNull)
|
||||
{
|
||||
previewGraphics.ReleaseHdc(previewHdc.Value);
|
||||
previewHdc = HDC.Null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw placeholder images for any non-activated screens on the preview.
|
||||
/// Will release the specified device context handle if it needs to draw anything.
|
||||
/// </summary>
|
||||
public static void DrawPreviewPlaceholders(
|
||||
Graphics previewGraphics, IEnumerable<RectangleInfo> screenBounds)
|
||||
{
|
||||
// we can exclude the activated screen because we've already draw
|
||||
// the screen capture image for that one on the preview
|
||||
if (screenBounds.Any())
|
||||
{
|
||||
var brush = Brushes.Black;
|
||||
previewGraphics.FillRectangles(brush, screenBounds.Select(screen => screen.ToRectangle()).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws screen captures from the specified desktop handle onto the target device context.
|
||||
/// </summary>
|
||||
public static void DrawPreviewScreen(
|
||||
HDC sourceHdc,
|
||||
HDC targetHdc,
|
||||
RectangleInfo sourceBounds,
|
||||
RectangleInfo targetBounds)
|
||||
{
|
||||
var source = sourceBounds.ToRectangle();
|
||||
var target = targetBounds.ToRectangle();
|
||||
_ = Gdi32.StretchBlt(
|
||||
targetHdc,
|
||||
target.X,
|
||||
target.Y,
|
||||
target.Width,
|
||||
target.Height,
|
||||
sourceHdc,
|
||||
source.X,
|
||||
source.Y,
|
||||
source.Width,
|
||||
source.Height,
|
||||
MouseJumpUI.NativeMethods.Gdi32.ROP_CODE.SRCCOPY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws screen captures from the specified desktop handle onto the target device context.
|
||||
/// </summary>
|
||||
public static void DrawPreviewScreens(
|
||||
HDC sourceHdc,
|
||||
HDC targetHdc,
|
||||
IList<RectangleInfo> sourceBounds,
|
||||
IList<RectangleInfo> targetBounds)
|
||||
{
|
||||
for (var i = 0; i < sourceBounds.Count; i++)
|
||||
{
|
||||
var source = sourceBounds[i].ToRectangle();
|
||||
var target = targetBounds[i].ToRectangle();
|
||||
_ = Gdi32.StretchBlt(
|
||||
targetHdc,
|
||||
target.X,
|
||||
target.Y,
|
||||
target.Width,
|
||||
target.Height,
|
||||
sourceHdc,
|
||||
source.X,
|
||||
source.Y,
|
||||
source.Width,
|
||||
source.Height,
|
||||
MouseJumpUI.NativeMethods.Gdi32.ROP_CODE.SRCCOPY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
|
||||
namespace MouseJumpUI.Helpers;
|
||||
|
||||
internal static class LayoutHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Center an object on the given origin.
|
||||
/// </summary>
|
||||
public static Point CenterObject(Size obj, Point origin)
|
||||
{
|
||||
return new Point(
|
||||
x: (int)(origin.X - ((float)obj.Width / 2)),
|
||||
y: (int)(origin.Y - ((float)obj.Height / 2)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines the specified regions and returns the smallest rectangle that contains them.
|
||||
/// </summary>
|
||||
/// <param name="regions">The regions to combine.</param>
|
||||
/// <returns>
|
||||
/// Returns the smallest rectangle that contains all the specified regions.
|
||||
/// </returns>
|
||||
public static Rectangle CombineRegions(List<Rectangle> regions)
|
||||
{
|
||||
if (regions == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(regions));
|
||||
}
|
||||
|
||||
if (regions.Count == 0)
|
||||
{
|
||||
return Rectangle.Empty;
|
||||
}
|
||||
|
||||
var combined = regions.Aggregate(
|
||||
seed: regions[0],
|
||||
func: Rectangle.Union);
|
||||
return combined;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the midpoint of the given region.
|
||||
/// </summary>
|
||||
public static Point GetMidpoint(Rectangle region)
|
||||
{
|
||||
return new Point(
|
||||
(region.Left + region.Right) / 2,
|
||||
(region.Top + region.Bottom) / 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the largest Size object that can fit inside
|
||||
/// all of the given sizes. (Equivalent to a Size
|
||||
/// object with the smallest Width and smallest Height from
|
||||
/// all of the specified sizes).
|
||||
/// </summary>
|
||||
public static Size IntersectSizes(params Size[] sizes)
|
||||
{
|
||||
return new Size(
|
||||
sizes.Min(s => s.Width),
|
||||
sizes.Min(s => s.Height));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the location to move the inner rectangle so that it sits entirely inside
|
||||
/// the outer rectangle. Returns the inner rectangle's current position if it is
|
||||
/// already inside the outer rectangle.
|
||||
/// </summary>
|
||||
public static Rectangle MoveInside(Rectangle inner, Rectangle outer)
|
||||
{
|
||||
if ((inner.Width > outer.Width) || (inner.Height > outer.Height))
|
||||
{
|
||||
throw new ArgumentException($"{nameof(inner)} cannot be larger than {nameof(outer)}.");
|
||||
}
|
||||
|
||||
return inner with
|
||||
{
|
||||
X = Math.Clamp(inner.X, outer.X, outer.Right - inner.Width),
|
||||
Y = Math.Clamp(inner.Y, outer.Y, outer.Bottom - inner.Height),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales a location within a reference region onto a new region
|
||||
/// so that it's proportionally in the same position in the new region.
|
||||
/// </summary>
|
||||
public static Point ScaleLocation(Rectangle originalBounds, Point originalLocation, Rectangle scaledBounds)
|
||||
{
|
||||
return new Point(
|
||||
(int)(originalLocation.X / (double)originalBounds.Width * scaledBounds.Width) + scaledBounds.Left,
|
||||
(int)(originalLocation.Y / (double)originalBounds.Height * scaledBounds.Height) + scaledBounds.Top);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scale an object to fit inside the specified bounds while maintaining aspect ratio.
|
||||
/// </summary>
|
||||
public static Size ScaleToFit(Size obj, Size bounds)
|
||||
{
|
||||
if (bounds.Width == 0 || bounds.Height == 0)
|
||||
{
|
||||
return Size.Empty;
|
||||
}
|
||||
|
||||
var widthRatio = (double)obj.Width / bounds.Width;
|
||||
var heightRatio = (double)obj.Height / bounds.Height;
|
||||
var scaledSize = (widthRatio > heightRatio)
|
||||
? bounds with
|
||||
{
|
||||
Height = (int)(obj.Height / widthRatio),
|
||||
}
|
||||
: bounds with
|
||||
{
|
||||
Width = (int)(obj.Width / heightRatio),
|
||||
};
|
||||
return scaledSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the position to show the preview form based on a number of factors.
|
||||
/// </summary>
|
||||
/// <param name="desktopBounds">
|
||||
/// The bounds of the entire desktop / virtual screen. Might start at a negative
|
||||
/// x, y if a non-primary screen is located left of or above the primary screen.
|
||||
/// </param>
|
||||
/// <param name="activatedPosition">
|
||||
/// The current position of the cursor on the virtual desktop.
|
||||
/// </param>
|
||||
/// <param name="activatedMonitorBounds">
|
||||
/// The bounds of the screen the cursor is currently on. Might start at a negative
|
||||
/// x, y if a non-primary screen is located left of or above the primary screen.
|
||||
/// </param>
|
||||
/// <param name="maximumThumbnailImageSize">
|
||||
/// The largest allowable size of the preview image. This is literally the just
|
||||
/// image itself, not including padding around the image.
|
||||
/// </param>
|
||||
/// <param name="thumbnailImagePadding">
|
||||
/// The total width and height of padding around the preview image.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The size and location to use when showing the preview image form.
|
||||
/// </returns>
|
||||
public static Rectangle GetPreviewFormBounds(
|
||||
Rectangle desktopBounds,
|
||||
Point activatedPosition,
|
||||
Rectangle activatedMonitorBounds,
|
||||
Size maximumThumbnailImageSize,
|
||||
Size thumbnailImagePadding)
|
||||
{
|
||||
// see https://learn.microsoft.com/en-gb/windows/win32/gdi/the-virtual-screen
|
||||
// calculate the maximum size the form is allowed to be
|
||||
var maxFormSize = LayoutHelper.IntersectSizes(
|
||||
new[]
|
||||
{
|
||||
// can't be bigger than the current screen
|
||||
activatedMonitorBounds.Size,
|
||||
|
||||
// can't be bigger than the max preview image
|
||||
// *plus* the padding around the preview image
|
||||
// (max thumbnail image size doesn't include the padding)
|
||||
maximumThumbnailImageSize + thumbnailImagePadding,
|
||||
});
|
||||
|
||||
// calculate the actual form size by scaling the entire
|
||||
// desktop bounds into the max thumbnail size while accounting
|
||||
// for the size of the padding around the preview
|
||||
var thumbnailImageSize = LayoutHelper.ScaleToFit(
|
||||
obj: desktopBounds.Size,
|
||||
bounds: maxFormSize - thumbnailImagePadding);
|
||||
var formSize = thumbnailImageSize + thumbnailImagePadding;
|
||||
|
||||
// center the form to the activated position, but nudge it back
|
||||
// inside the visible area of the screen if it falls outside
|
||||
var formBounds = LayoutHelper.MoveInside(
|
||||
inner: new Rectangle(
|
||||
LayoutHelper.CenterObject(
|
||||
obj: formSize,
|
||||
origin: activatedPosition),
|
||||
formSize),
|
||||
outer: activatedMonitorBounds);
|
||||
|
||||
return formBounds;
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,9 @@ namespace MouseJumpUI.Helpers
|
||||
|
||||
private static string GetCallerInfo()
|
||||
{
|
||||
StackTrace stackTrace = new StackTrace();
|
||||
|
||||
var stackTrace = new StackTrace();
|
||||
var methodName = stackTrace.GetFrame(3)?.GetMethod();
|
||||
var className = methodName?.DeclaringType.Name;
|
||||
var className = methodName?.DeclaringType?.Name;
|
||||
return "[Method]: " + methodName?.Name + " [Class]: " + className;
|
||||
}
|
||||
}
|
||||
|
||||
87
src/modules/MouseUtils/MouseJumpUI/Helpers/MouseHelper.cs
Normal file
87
src/modules/MouseUtils/MouseJumpUI/Helpers/MouseHelper.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using MouseJumpUI.Drawing.Models;
|
||||
|
||||
namespace MouseJumpUI.Helpers;
|
||||
|
||||
internal static class MouseHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates where to move the cursor to by projecting a point from
|
||||
/// the preview image onto the desktop and using that as the target location.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The preview image origin is (0, 0) but the desktop origin may be non-zero,
|
||||
/// or even negative if the primary monitor is not the at the top-left of the
|
||||
/// entire desktop rectangle, so results may contain negative coordinates.
|
||||
/// </remarks>
|
||||
public static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds)
|
||||
{
|
||||
return previewLocation
|
||||
.Scale(previewSize.ScaleToFitRatio(desktopBounds.Size))
|
||||
.Offset(desktopBounds.Location);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the cursor to the specified location.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See https://github.com/mikeclayton/FancyMouse/pull/3
|
||||
/// </remarks>
|
||||
public static void JumpCursor(PointInfo location)
|
||||
{
|
||||
// set the new cursor position *twice* - the cursor sometimes end up in
|
||||
// the wrong place if we try to cross the dead space between non-aligned
|
||||
// monitors - e.g. when trying to move the cursor from (a) to (b) we can
|
||||
// *sometimes* - for no clear reason - end up at (c) instead.
|
||||
//
|
||||
// +----------------+
|
||||
// |(c) (b) |
|
||||
// | |
|
||||
// | |
|
||||
// | |
|
||||
// +---------+ |
|
||||
// | (a) | |
|
||||
// +---------+----------------+
|
||||
//
|
||||
// setting the position a second time seems to fix this and moves the
|
||||
// cursor to the expected location (b)
|
||||
var point = location.ToPoint();
|
||||
Cursor.Position = point;
|
||||
Cursor.Position = point;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an input simulating an absolute mouse move to the new location.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See https://github.com/microsoft/PowerToys/issues/24523
|
||||
/// https://github.com/microsoft/PowerToys/pull/24527
|
||||
/// </remarks>
|
||||
public static void SimulateMouseMovementEvent(Point location)
|
||||
{
|
||||
var mouseMoveInput = new NativeMethods.INPUT
|
||||
{
|
||||
type = NativeMethods.INPUTTYPE.INPUT_MOUSE,
|
||||
data = new NativeMethods.InputUnion
|
||||
{
|
||||
mi = new NativeMethods.MOUSEINPUT
|
||||
{
|
||||
dx = NativeMethods.CalculateAbsoluteCoordinateX(location.X),
|
||||
dy = NativeMethods.CalculateAbsoluteCoordinateY(location.Y),
|
||||
mouseData = 0,
|
||||
dwFlags = (uint)NativeMethods.MOUSE_INPUT_FLAGS.MOUSEEVENTF_MOVE
|
||||
| (uint)NativeMethods.MOUSE_INPUT_FLAGS.MOUSEEVENTF_ABSOLUTE,
|
||||
time = 0,
|
||||
dwExtraInfo = 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
var inputs = new NativeMethods.INPUT[] { mouseMoveInput };
|
||||
_ = NativeMethods.SendInput(1, inputs, NativeMethods.INPUT.Size);
|
||||
}
|
||||
}
|
||||
@@ -5,108 +5,107 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MouseJumpUI.Helpers
|
||||
namespace MouseJumpUI.Helpers;
|
||||
|
||||
// Win32 functions required for temporary workaround for issue #1273
|
||||
internal static class NativeMethods
|
||||
{
|
||||
// Win32 functions required for temporary workaround for issue #1273
|
||||
internal class NativeMethods
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern int GetSystemMetrics(SystemMetric smIndex);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct INPUT
|
||||
{
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
||||
internal INPUTTYPE type;
|
||||
internal InputUnion data;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern int GetSystemMetrics(SystemMetric smIndex);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct INPUT
|
||||
internal static int Size
|
||||
{
|
||||
internal INPUTTYPE type;
|
||||
internal InputUnion data;
|
||||
|
||||
internal static int Size
|
||||
{
|
||||
get { return Marshal.SizeOf(typeof(INPUT)); }
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
internal struct InputUnion
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
internal MOUSEINPUT mi;
|
||||
[FieldOffset(0)]
|
||||
internal KEYBDINPUT ki;
|
||||
[FieldOffset(0)]
|
||||
internal HARDWAREINPUT hi;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct MOUSEINPUT
|
||||
{
|
||||
internal int dx;
|
||||
internal int dy;
|
||||
internal int mouseData;
|
||||
internal uint dwFlags;
|
||||
internal uint time;
|
||||
internal UIntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct KEYBDINPUT
|
||||
{
|
||||
internal short wVk;
|
||||
internal short wScan;
|
||||
internal uint dwFlags;
|
||||
internal int time;
|
||||
internal UIntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct HARDWAREINPUT
|
||||
{
|
||||
internal int uMsg;
|
||||
internal short wParamL;
|
||||
internal short wParamH;
|
||||
}
|
||||
|
||||
internal enum INPUTTYPE : uint
|
||||
{
|
||||
INPUT_MOUSE = 0,
|
||||
INPUT_KEYBOARD = 1,
|
||||
INPUT_HARDWARE = 2,
|
||||
}
|
||||
|
||||
internal enum MOUSE_INPUT_FLAGS : uint
|
||||
{
|
||||
MOUSEEVENTF_MOVE = 0x0001,
|
||||
MOUSEEVENTF_LEFTDOWN = 0x0002,
|
||||
MOUSEEVENTF_LEFTUP = 0x0004,
|
||||
MOUSEEVENTF_RIGHTDOWN = 0x0008,
|
||||
MOUSEEVENTF_RIGHTUP = 0x0010,
|
||||
MOUSEEVENTF_MIDDLEDOWN = 0x0020,
|
||||
MOUSEEVENTF_MIDDLEUP = 0x0040,
|
||||
MOUSEEVENTF_XDOWN = 0x0080,
|
||||
MOUSEEVENTF_XUP = 0x0100,
|
||||
MOUSEEVENTF_WHEEL = 0x0800,
|
||||
MOUSEEVENTF_HWHEEL = 0x1000,
|
||||
MOUSEEVENTF_MOVE_NOCOALESCE = 0x2000,
|
||||
MOUSEEVENTF_VIRTUALDESK = 0x4000,
|
||||
MOUSEEVENTF_ABSOLUTE = 0x8000,
|
||||
}
|
||||
|
||||
internal enum SystemMetric
|
||||
{
|
||||
SM_CXSCREEN = 0,
|
||||
SM_CYSCREEN = 1,
|
||||
}
|
||||
|
||||
internal static int CalculateAbsoluteCoordinateX(int x)
|
||||
{
|
||||
return (x * 65536) / GetSystemMetrics(SystemMetric.SM_CXSCREEN);
|
||||
}
|
||||
|
||||
internal static int CalculateAbsoluteCoordinateY(int y)
|
||||
{
|
||||
return (y * 65536) / GetSystemMetrics(SystemMetric.SM_CYSCREEN);
|
||||
get { return Marshal.SizeOf(typeof(INPUT)); }
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
internal struct InputUnion
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
internal MOUSEINPUT mi;
|
||||
[FieldOffset(0)]
|
||||
internal KEYBDINPUT ki;
|
||||
[FieldOffset(0)]
|
||||
internal HARDWAREINPUT hi;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct MOUSEINPUT
|
||||
{
|
||||
internal int dx;
|
||||
internal int dy;
|
||||
internal int mouseData;
|
||||
internal uint dwFlags;
|
||||
internal uint time;
|
||||
internal UIntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct KEYBDINPUT
|
||||
{
|
||||
internal short wVk;
|
||||
internal short wScan;
|
||||
internal uint dwFlags;
|
||||
internal int time;
|
||||
internal UIntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct HARDWAREINPUT
|
||||
{
|
||||
internal int uMsg;
|
||||
internal short wParamL;
|
||||
internal short wParamH;
|
||||
}
|
||||
|
||||
internal enum INPUTTYPE : uint
|
||||
{
|
||||
INPUT_MOUSE = 0,
|
||||
INPUT_KEYBOARD = 1,
|
||||
INPUT_HARDWARE = 2,
|
||||
}
|
||||
|
||||
internal enum MOUSE_INPUT_FLAGS : uint
|
||||
{
|
||||
MOUSEEVENTF_MOVE = 0x0001,
|
||||
MOUSEEVENTF_LEFTDOWN = 0x0002,
|
||||
MOUSEEVENTF_LEFTUP = 0x0004,
|
||||
MOUSEEVENTF_RIGHTDOWN = 0x0008,
|
||||
MOUSEEVENTF_RIGHTUP = 0x0010,
|
||||
MOUSEEVENTF_MIDDLEDOWN = 0x0020,
|
||||
MOUSEEVENTF_MIDDLEUP = 0x0040,
|
||||
MOUSEEVENTF_XDOWN = 0x0080,
|
||||
MOUSEEVENTF_XUP = 0x0100,
|
||||
MOUSEEVENTF_WHEEL = 0x0800,
|
||||
MOUSEEVENTF_HWHEEL = 0x1000,
|
||||
MOUSEEVENTF_MOVE_NOCOALESCE = 0x2000,
|
||||
MOUSEEVENTF_VIRTUALDESK = 0x4000,
|
||||
MOUSEEVENTF_ABSOLUTE = 0x8000,
|
||||
}
|
||||
|
||||
internal enum SystemMetric
|
||||
{
|
||||
SM_CXSCREEN = 0,
|
||||
SM_CYSCREEN = 1,
|
||||
}
|
||||
|
||||
internal static int CalculateAbsoluteCoordinateX(int x)
|
||||
{
|
||||
return (x * 65536) / GetSystemMetrics(SystemMetric.SM_CXSCREEN);
|
||||
}
|
||||
|
||||
internal static int CalculateAbsoluteCoordinateY(int y)
|
||||
{
|
||||
return (y * 65536) / GetSystemMetrics(SystemMetric.SM_CYSCREEN);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ partial class MainForm
|
||||
Thumbnail.Location = new System.Drawing.Point(5, 5);
|
||||
Thumbnail.Name = "Thumbnail";
|
||||
Thumbnail.Size = new System.Drawing.Size(790, 440);
|
||||
Thumbnail.SizeMode = PictureBoxSizeMode.StretchImage;
|
||||
Thumbnail.SizeMode = PictureBoxSizeMode.Normal;
|
||||
Thumbnail.TabIndex = 1;
|
||||
Thumbnail.TabStop = false;
|
||||
Thumbnail.Click += Thumbnail_Click;
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using MouseJumpUI.Drawing.Models;
|
||||
using MouseJumpUI.Helpers;
|
||||
using MouseJumpUI.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI;
|
||||
|
||||
@@ -33,172 +36,170 @@ internal partial class MainForm : Form
|
||||
|
||||
private void MainForm_Deactivate(object sender, EventArgs e)
|
||||
{
|
||||
// dispose the existing image if there is one
|
||||
if (Thumbnail.Image != null)
|
||||
{
|
||||
Thumbnail.Image.Dispose();
|
||||
Thumbnail.Image = null;
|
||||
}
|
||||
|
||||
this.Close();
|
||||
}
|
||||
|
||||
// Sends an input simulating an absolute mouse move to the new location.
|
||||
private void SimulateMouseMovementEvent(Point location)
|
||||
{
|
||||
NativeMethods.INPUT mouseMoveInput = new NativeMethods.INPUT
|
||||
{
|
||||
type = NativeMethods.INPUTTYPE.INPUT_MOUSE,
|
||||
data = new NativeMethods.InputUnion
|
||||
{
|
||||
mi = new NativeMethods.MOUSEINPUT
|
||||
{
|
||||
dx = NativeMethods.CalculateAbsoluteCoordinateX(location.X),
|
||||
dy = NativeMethods.CalculateAbsoluteCoordinateY(location.Y),
|
||||
mouseData = 0,
|
||||
dwFlags = (uint)NativeMethods.MOUSE_INPUT_FLAGS.MOUSEEVENTF_MOVE | (uint)NativeMethods.MOUSE_INPUT_FLAGS.MOUSEEVENTF_ABSOLUTE,
|
||||
time = 0,
|
||||
dwExtraInfo = 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
NativeMethods.INPUT[] inputs = new NativeMethods.INPUT[] { mouseMoveInput };
|
||||
_ = NativeMethods.SendInput(1, inputs, NativeMethods.INPUT.Size);
|
||||
}
|
||||
|
||||
private void Thumbnail_Click(object sender, EventArgs e)
|
||||
{
|
||||
var mouseEventArgs = (MouseEventArgs)e;
|
||||
Logger.LogInfo($"Reporting mouse event args \n\tbutton = {mouseEventArgs.Button}\n\tlocation = {mouseEventArgs.Location} ");
|
||||
|
||||
if (mouseEventArgs.Button == MouseButtons.Left)
|
||||
{
|
||||
// plain click - move mouse pointer
|
||||
var desktopBounds = LayoutHelper.CombineRegions(
|
||||
Screen.AllScreens.Select(
|
||||
screen => screen.Bounds).ToList());
|
||||
Logger.LogInfo($"desktop bounds = {desktopBounds}");
|
||||
|
||||
var mouseEvent = (MouseEventArgs)e;
|
||||
|
||||
var scaledLocation = LayoutHelper.ScaleLocation(
|
||||
originalBounds: Thumbnail.Bounds,
|
||||
originalLocation: new Point(mouseEvent.X, mouseEvent.Y),
|
||||
scaledBounds: desktopBounds);
|
||||
Logger.LogInfo($"scaled location = {scaledLocation}");
|
||||
|
||||
// set the new cursor position *twice* - the cursor sometimes end up in
|
||||
// the wrong place if we try to cross the dead space between non-aligned
|
||||
// monitors - e.g. when trying to move the cursor from (a) to (b) we can
|
||||
// *sometimes* - for no clear reason - end up at (c) instead.
|
||||
//
|
||||
// +----------------+
|
||||
// |(c) (b) |
|
||||
// | |
|
||||
// | |
|
||||
// | |
|
||||
// +---------+ |
|
||||
// | (a) | |
|
||||
// +---------+----------------+
|
||||
//
|
||||
// setting the position a second time seems to fix this and moves the
|
||||
// cursor to the expected location (b) - for more details see
|
||||
// https://github.com/mikeclayton/FancyMouse/pull/3
|
||||
Cursor.Position = scaledLocation;
|
||||
Cursor.Position = scaledLocation;
|
||||
|
||||
// Simulate mouse input for handlers that won't just catch the Cursor change
|
||||
SimulateMouseMovementEvent(scaledLocation);
|
||||
|
||||
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent());
|
||||
}
|
||||
|
||||
this.Close();
|
||||
}
|
||||
|
||||
public void ShowThumbnail()
|
||||
{
|
||||
if (this.Thumbnail.Image != null)
|
||||
if (this.Thumbnail.Image is not null)
|
||||
{
|
||||
var tmp = this.Thumbnail.Image;
|
||||
this.Thumbnail.Image = null;
|
||||
tmp.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void Thumbnail_Click(object sender, EventArgs e)
|
||||
{
|
||||
var mouseEventArgs = (MouseEventArgs)e;
|
||||
Logger.LogInfo(string.Join(
|
||||
'\n',
|
||||
$"Reporting mouse event args",
|
||||
$"\tbutton = {mouseEventArgs.Button}",
|
||||
$"\tlocation = {mouseEventArgs.Location}"));
|
||||
|
||||
if (mouseEventArgs.Button == MouseButtons.Left)
|
||||
{
|
||||
// plain click - move mouse pointer
|
||||
var scaledLocation = MouseHelper.GetJumpLocation(
|
||||
new PointInfo(mouseEventArgs.X, mouseEventArgs.Y),
|
||||
new SizeInfo(this.Thumbnail.Size),
|
||||
new RectangleInfo(SystemInformation.VirtualScreen));
|
||||
Logger.LogInfo($"scaled location = {scaledLocation}");
|
||||
MouseHelper.JumpCursor(scaledLocation);
|
||||
|
||||
// Simulate mouse input for handlers that won't just catch the Cursor change
|
||||
MouseHelper.SimulateMouseMovementEvent(scaledLocation.ToPoint());
|
||||
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent());
|
||||
}
|
||||
|
||||
this.OnDeactivate(EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void ShowThumbnail()
|
||||
{
|
||||
var screens = Screen.AllScreens;
|
||||
foreach (var i in Enumerable.Range(0, screens.Length))
|
||||
{
|
||||
var screen = screens[i];
|
||||
Logger.LogInfo($"screen[{i}] = \"{screen.DeviceName}\"\n\tprimary = {screen.Primary}\n\tbounds = {screen.Bounds}\n\tworking area = {screen.WorkingArea}");
|
||||
Logger.LogInfo(string.Join(
|
||||
'\n',
|
||||
$"screen[{i}] = \"{screen.DeviceName}\"",
|
||||
$"\tprimary = {screen.Primary}",
|
||||
$"\tbounds = {screen.Bounds}",
|
||||
$"\tworking area = {screen.WorkingArea}"));
|
||||
}
|
||||
|
||||
var desktopBounds = LayoutHelper.CombineRegions(
|
||||
screens.Select(screen => screen.Bounds).ToList());
|
||||
Logger.LogInfo(
|
||||
$"desktop bounds = {desktopBounds}");
|
||||
// collect together some values that we need for calculating layout
|
||||
var activatedLocation = Cursor.Position;
|
||||
var layoutConfig = new LayoutConfig(
|
||||
virtualScreen: SystemInformation.VirtualScreen,
|
||||
screenBounds: Screen.AllScreens.Select(screen => screen.Bounds),
|
||||
activatedLocation: activatedLocation,
|
||||
activatedScreen: Array.IndexOf(Screen.AllScreens, Screen.FromPoint(activatedLocation)),
|
||||
maximumFormSize: new Size(1600, 1200),
|
||||
formPadding: this.panel1.Padding,
|
||||
previewPadding: new Padding(0));
|
||||
Logger.LogInfo(string.Join(
|
||||
'\n',
|
||||
$"Layout config",
|
||||
$"-------------",
|
||||
$"virtual screen = {layoutConfig.VirtualScreen}",
|
||||
$"activated location = {layoutConfig.ActivatedLocation}",
|
||||
$"activated screen = {layoutConfig.ActivatedScreen}",
|
||||
$"maximum form size = {layoutConfig.MaximumFormSize}",
|
||||
$"form padding = {layoutConfig.FormPadding}",
|
||||
$"preview padding = {layoutConfig.PreviewPadding}"));
|
||||
|
||||
var activatedPosition = Cursor.Position;
|
||||
Logger.LogInfo(
|
||||
$"activated position = {activatedPosition}");
|
||||
// calculate the layout coordinates for everything
|
||||
var layoutInfo = DrawingHelper.CalculateLayoutInfo(layoutConfig);
|
||||
Logger.LogInfo(string.Join(
|
||||
'\n',
|
||||
$"Layout info",
|
||||
$"-----------",
|
||||
$"form bounds = {layoutInfo.FormBounds}",
|
||||
$"preview bounds = {layoutInfo.PreviewBounds}",
|
||||
$"activated screen = {layoutInfo.ActivatedScreen}"));
|
||||
|
||||
var previewImagePadding = new Size(
|
||||
panel1.Padding.Left + panel1.Padding.Right,
|
||||
panel1.Padding.Top + panel1.Padding.Bottom);
|
||||
Logger.LogInfo(
|
||||
$"image padding = {previewImagePadding}");
|
||||
DrawingHelper.PositionForm(this, layoutInfo.FormBounds);
|
||||
|
||||
var maxThumbnailSize = new Size(1600, 1200);
|
||||
var formBounds = LayoutHelper.GetPreviewFormBounds(
|
||||
desktopBounds: desktopBounds,
|
||||
activatedPosition: activatedPosition,
|
||||
activatedMonitorBounds: Screen.FromPoint(activatedPosition).Bounds,
|
||||
maximumThumbnailImageSize: maxThumbnailSize,
|
||||
thumbnailImagePadding: previewImagePadding);
|
||||
Logger.LogInfo(
|
||||
$"form bounds = {formBounds}");
|
||||
// initialize the preview image
|
||||
var preview = new Bitmap(
|
||||
(int)layoutInfo.PreviewBounds.Width,
|
||||
(int)layoutInfo.PreviewBounds.Height,
|
||||
PixelFormat.Format32bppArgb);
|
||||
this.Thumbnail.Image = preview;
|
||||
|
||||
// take a screenshot of the entire desktop
|
||||
// see https://learn.microsoft.com/en-gb/windows/win32/gdi/the-virtual-screen
|
||||
var screenshot = new Bitmap(desktopBounds.Width, desktopBounds.Height, PixelFormat.Format32bppArgb);
|
||||
using (var graphics = Graphics.FromImage(screenshot))
|
||||
using var previewGraphics = Graphics.FromImage(preview);
|
||||
|
||||
DrawingHelper.DrawPreviewBackground(previewGraphics, layoutInfo.PreviewBounds, layoutInfo.ScreenBounds);
|
||||
|
||||
var desktopHwnd = HWND.Null;
|
||||
var desktopHdc = HDC.Null;
|
||||
var previewHdc = HDC.Null;
|
||||
try
|
||||
{
|
||||
// note - it *might* be faster to capture each monitor individually and assemble them into
|
||||
// a single image ourselves as we *may* not have to transfer all of the blank pixels
|
||||
// that are outside the desktop bounds - e.g. the *** in the ascii art below
|
||||
//
|
||||
// +----------------+********
|
||||
// | |********
|
||||
// | 1 +-------+
|
||||
// | | |
|
||||
// +----------------+ 0 |
|
||||
// *****************| |
|
||||
// *****************+-------+
|
||||
//
|
||||
// for very irregular monitor layouts this *might* be a big percentage of the rectangle
|
||||
// containing the desktop bounds.
|
||||
//
|
||||
// then again, it might not make much difference at all - we'd need to do some perf tests
|
||||
graphics.CopyFromScreen(desktopBounds.Left, desktopBounds.Top, 0, 0, desktopBounds.Size);
|
||||
DrawingHelper.EnsureDesktopDeviceContext(ref desktopHwnd, ref desktopHdc);
|
||||
|
||||
// we have to capture the screen where we're going to show the form first
|
||||
// as the form will obscure the screen as soon as it's visible
|
||||
var activatedStopwatch = Stopwatch.StartNew();
|
||||
DrawingHelper.EnsurePreviewDeviceContext(previewGraphics, ref previewHdc);
|
||||
DrawingHelper.DrawPreviewScreen(
|
||||
desktopHdc,
|
||||
previewHdc,
|
||||
layoutConfig.ScreenBounds[layoutConfig.ActivatedScreen],
|
||||
layoutInfo.ScreenBounds[layoutConfig.ActivatedScreen]);
|
||||
activatedStopwatch.Stop();
|
||||
|
||||
// show the placeholder images if it looks like it might take a while
|
||||
// to capture the remaining screenshot images
|
||||
if (activatedStopwatch.ElapsedMilliseconds > 250)
|
||||
{
|
||||
var activatedArea = layoutConfig.ScreenBounds[layoutConfig.ActivatedScreen].Area;
|
||||
var totalArea = layoutConfig.ScreenBounds.Sum(screen => screen.Area);
|
||||
if ((activatedArea / totalArea) < 0.5M)
|
||||
{
|
||||
// we need to release the device context handle before we can draw the placeholders
|
||||
// using the Graphics object otherwise we'll get an error from GDI saying
|
||||
// "Object is currently in use elsewhere"
|
||||
DrawingHelper.FreePreviewDeviceContext(previewGraphics, ref previewHdc);
|
||||
DrawingHelper.DrawPreviewPlaceholders(
|
||||
previewGraphics,
|
||||
layoutInfo.ScreenBounds.Where((_, idx) => idx != layoutConfig.ActivatedScreen));
|
||||
MainForm.ShowPreview(this);
|
||||
}
|
||||
}
|
||||
|
||||
// draw the remaining screen captures (if any) on the preview image
|
||||
var sourceScreens = layoutConfig.ScreenBounds.Where((_, idx) => idx != layoutConfig.ActivatedScreen).ToList();
|
||||
if (sourceScreens.Any())
|
||||
{
|
||||
DrawingHelper.EnsurePreviewDeviceContext(previewGraphics, ref previewHdc);
|
||||
DrawingHelper.DrawPreviewScreens(
|
||||
desktopHdc,
|
||||
previewHdc,
|
||||
sourceScreens,
|
||||
layoutInfo.ScreenBounds.Where((_, idx) => idx != layoutConfig.ActivatedScreen).ToList());
|
||||
MainForm.ShowPreview(this);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DrawingHelper.FreeDesktopDeviceContext(ref desktopHwnd, ref desktopHdc);
|
||||
DrawingHelper.FreePreviewDeviceContext(previewGraphics, ref previewHdc);
|
||||
}
|
||||
|
||||
// resize and position the form
|
||||
// note - do this in two steps rather than "this.Bounds = formBounds" as there
|
||||
// appears to be an issue in WinForms with dpi scaling even when using PerMonitorV2,
|
||||
// where the form scaling uses either the *primary* screen scaling or the *previous*
|
||||
// screen's scaling when the form is moved to a different screen. i've got no idea
|
||||
// *why*, but the exact sequence of calls below seems to be a workaround...
|
||||
// see https://github.com/mikeclayton/FancyMouse/issues/2
|
||||
this.Location = formBounds.Location;
|
||||
_ = this.PointToScreen(Point.Empty);
|
||||
this.Size = formBounds.Size;
|
||||
|
||||
// update the preview image
|
||||
this.Thumbnail.Image = screenshot;
|
||||
|
||||
this.Show();
|
||||
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent());
|
||||
|
||||
// we have to activate the form to make sure the deactivate event fires
|
||||
MainForm.ShowPreview(this);
|
||||
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent());
|
||||
this.Activate();
|
||||
}
|
||||
|
||||
private static void ShowPreview(MainForm form)
|
||||
{
|
||||
if (!form.Visible)
|
||||
{
|
||||
form.Show();
|
||||
}
|
||||
|
||||
form.Thumbnail.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<StartupObject>MouseJumpUI.Program</StartupObject>
|
||||
<SelfContained>true</SelfContained>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- SelfContained=true requires RuntimeIdentifier to be set -->
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MouseJumpUI.NativeMethods.Core;
|
||||
|
||||
/// <summary>
|
||||
/// A Boolean variable (should be TRUE or FALSE).
|
||||
/// This type is declared in WinDef.h as follows:
|
||||
/// typedef int BOOL;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
|
||||
/// </remarks>
|
||||
internal readonly struct BOOL
|
||||
{
|
||||
public readonly int Value;
|
||||
|
||||
public BOOL(int value)
|
||||
{
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
public BOOL(bool value)
|
||||
{
|
||||
this.Value = value ? 1 : 0;
|
||||
}
|
||||
|
||||
public static implicit operator bool(BOOL value) => value.Value != 0;
|
||||
|
||||
public static implicit operator BOOL(bool value) => new(value);
|
||||
|
||||
public static implicit operator int(BOOL value) => value.Value;
|
||||
|
||||
public static implicit operator BOOL(int value) => new(value);
|
||||
}
|
||||
29
src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HDC.cs
Normal file
29
src/modules/MouseUtils/MouseJumpUI/NativeMethods/Core/HDC.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods.Core;
|
||||
|
||||
/// <summary>
|
||||
/// A handle to a device context (DC).
|
||||
/// This type is declared in WinDef.h as follows:
|
||||
/// typedef HANDLE HDC;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
|
||||
/// </remarks>
|
||||
internal readonly struct HDC
|
||||
{
|
||||
public static readonly HDC Null = new(IntPtr.Zero);
|
||||
|
||||
public readonly IntPtr Value;
|
||||
|
||||
public HDC(IntPtr value)
|
||||
{
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
public bool IsNull => this.Value == HDC.Null.Value;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods.Core;
|
||||
|
||||
/// <summary>
|
||||
/// A handle to a window.
|
||||
/// This type is declared in WinDef.h as follows:
|
||||
/// typedef HANDLE HWND;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
|
||||
/// </remarks>
|
||||
internal readonly struct HWND
|
||||
{
|
||||
public static readonly HWND Null = new(IntPtr.Zero);
|
||||
|
||||
public readonly IntPtr Value;
|
||||
|
||||
public HWND(IntPtr value)
|
||||
{
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
public bool IsNull => this.Value == HWND.Null.Value;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
|
||||
internal static partial class Gdi32
|
||||
{
|
||||
/// <summary>
|
||||
/// A raster-operation code. These codes define how the color data for the source
|
||||
/// rectangle is to be combined with the color data for the destination rectangle
|
||||
/// to achieve the final color.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-bitblt
|
||||
/// </remarks>
|
||||
public enum ROP_CODE : uint
|
||||
{
|
||||
BLACKNESS = 0x00000042,
|
||||
CAPTUREBLT = 0x40000000,
|
||||
DSTINVERT = 0x00550009,
|
||||
MERGECOPY = 0x00C000CA,
|
||||
MERGEPAINT = 0x00BB0226,
|
||||
NOMIRRORBITMAP = 0x80000000,
|
||||
NOTSRCCOPY = 0x00330008,
|
||||
NOTSRCERASE = 0x001100A6,
|
||||
PATCOPY = 0x00F00021,
|
||||
PATINVERT = 0x005A0049,
|
||||
PATPAINT = 0x00FB0A09,
|
||||
SRCAND = 0x008800C6,
|
||||
SRCCOPY = 0x00CC0020,
|
||||
SRCERASE = 0x00440328,
|
||||
SRCINVERT = 0x00660046,
|
||||
SRCPAINT = 0x00EE0086,
|
||||
WHITENESS = 0x00FF0062,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
|
||||
internal static partial class Gdi32
|
||||
{
|
||||
/// <summary>
|
||||
/// The stretching mode.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-setstretchbltmode
|
||||
/// </remarks>
|
||||
public enum STRETCH_BLT_MODE : int
|
||||
{
|
||||
BLACKONWHITE = 1,
|
||||
COLORONCOLOR = 3,
|
||||
HALFTONE = 4,
|
||||
WHITEONBLACK = 2,
|
||||
STRETCH_ANDSCANS = STRETCH_BLT_MODE.BLACKONWHITE,
|
||||
STRETCH_DELETESCANS = STRETCH_BLT_MODE.COLORONCOLOR,
|
||||
STRETCH_HALFTONE = STRETCH_BLT_MODE.HALFTONE,
|
||||
STRETCH_ORSCANS = STRETCH_BLT_MODE.WHITEONBLACK,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using MouseJumpUI.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
|
||||
internal static partial class Gdi32
|
||||
{
|
||||
/// <summary>
|
||||
/// The SetStretchBltMode function sets the bitmap stretching mode in the specified device context.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is the previous stretching mode.
|
||||
/// If the function fails, the return value is zero.
|
||||
/// This function can return the following value.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-setstretchbltmode
|
||||
/// </remarks>
|
||||
[LibraryImport(Libraries.Gdi32)]
|
||||
public static partial int SetStretchBltMode(
|
||||
HDC hdc,
|
||||
STRETCH_BLT_MODE mode);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using MouseJumpUI.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
|
||||
internal static partial class Gdi32
|
||||
{
|
||||
/// <summary>
|
||||
/// The StretchBlt function copies a bitmap from a source rectangle into a destination
|
||||
/// rectangle, stretching or compressing the bitmap to fit the dimensions of the
|
||||
/// destination rectangle, if necessary. The system stretches or compresses the bitmap
|
||||
/// according to the stretching mode currently set in the destination device context.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is nonzero.
|
||||
/// If the function fails, the return value is zero.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-stretchblt
|
||||
/// </remarks>
|
||||
[LibraryImport(Libraries.Gdi32)]
|
||||
public static partial BOOL StretchBlt(
|
||||
HDC hdcDest,
|
||||
int xDest,
|
||||
int yDest,
|
||||
int wDest,
|
||||
int hDest,
|
||||
HDC hdcSrc,
|
||||
int xSrc,
|
||||
int ySrc,
|
||||
int wSrc,
|
||||
int hSrc,
|
||||
ROP_CODE rop);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
|
||||
internal static class Libraries
|
||||
{
|
||||
public const string Gdi32 = "gdi32";
|
||||
public const string User32 = "user32";
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using MouseJumpUI.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a handle to the desktop window. The desktop window covers the entire
|
||||
/// screen. The desktop window is the area on top of which other windows are painted.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The return value is a handle to the desktop window.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdesktopwindow
|
||||
/// </remarks>
|
||||
[LibraryImport(Libraries.User32)]
|
||||
public static partial HWND GetDesktopWindow();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using MouseJumpUI.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
||||
/// <summary>
|
||||
/// The GetWindowDC function retrieves the device context (DC) for the entire window,
|
||||
/// including title bar, menus, and scroll bars. A window device context permits painting
|
||||
/// anywhere in a window, because the origin of the device context is the upper-left
|
||||
/// corner of the window instead of the client area.
|
||||
///
|
||||
/// GetWindowDC assigns default attributes to the window device context each time it
|
||||
/// retrieves the device context. Previous attributes are lost.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// If the function succeeds, the return value is a handle to a device context for the specified window.
|
||||
/// If the function fails, the return value is NULL, indicating an error or an invalid hWnd parameter.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowdc
|
||||
/// </remarks>
|
||||
[LibraryImport(Libraries.User32)]
|
||||
public static partial HDC GetWindowDC(
|
||||
HWND hWnd);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using MouseJumpUI.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeMethods;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
||||
/// <summary>
|
||||
/// The ReleaseDC function releases a device context (DC), freeing it for use by other
|
||||
/// applications. The effect of the ReleaseDC function depends on the type of DC. It
|
||||
/// frees only common and window DCs. It has no effect on class or private DCs.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The return value indicates whether the DC was released. If the DC was released, the return value is 1.
|
||||
/// If the DC was not released, the return value is zero.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-releasedc
|
||||
/// </remarks>
|
||||
[LibraryImport(Libraries.User32)]
|
||||
public static partial int ReleaseDC(
|
||||
HWND hWnd,
|
||||
HDC hDC);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeWrappers;
|
||||
|
||||
internal static partial class Gdi32
|
||||
{
|
||||
public static int SetStretchBltMode(HDC hdc, NativeMethods.Gdi32.STRETCH_BLT_MODE mode)
|
||||
{
|
||||
var result = NativeMethods.Gdi32.SetStretchBltMode(hdc, mode);
|
||||
|
||||
return result == 0
|
||||
? throw new InvalidOperationException(
|
||||
$"{nameof(Gdi32.SetStretchBltMode)} returned {result}")
|
||||
: result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using MouseJumpUI.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeWrappers;
|
||||
|
||||
internal static partial class Gdi32
|
||||
{
|
||||
public static BOOL StretchBlt(
|
||||
HDC hdcDest,
|
||||
int xDest,
|
||||
int yDest,
|
||||
int wDest,
|
||||
int hDest,
|
||||
HDC hdcSrc,
|
||||
int xSrc,
|
||||
int ySrc,
|
||||
int wSrc,
|
||||
int hSrc,
|
||||
NativeMethods.Gdi32.ROP_CODE rop)
|
||||
{
|
||||
var result = NativeMethods.Gdi32.StretchBlt(
|
||||
hdcDest,
|
||||
xDest,
|
||||
yDest,
|
||||
wDest,
|
||||
hDest,
|
||||
hdcSrc,
|
||||
xSrc,
|
||||
ySrc,
|
||||
wSrc,
|
||||
hSrc,
|
||||
rop);
|
||||
|
||||
return result
|
||||
? result
|
||||
: throw new InvalidOperationException(
|
||||
$"{nameof(Gdi32.StretchBlt)} returned {result.Value}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeWrappers;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
||||
public static HWND GetDesktopWindow()
|
||||
{
|
||||
return NativeMethods.User32.GetDesktopWindow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeWrappers;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
||||
public static HDC GetWindowDC(HWND hWnd)
|
||||
{
|
||||
var hdc = NativeMethods.User32.GetWindowDC(hWnd);
|
||||
|
||||
return hdc.IsNull
|
||||
? throw new InvalidOperationException(
|
||||
$"{nameof(User32.GetWindowDC)} returned null")
|
||||
: hdc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.NativeMethods.Core;
|
||||
|
||||
namespace MouseJumpUI.NativeWrappers;
|
||||
|
||||
internal static partial class User32
|
||||
{
|
||||
public static int ReleaseDC(HWND hWnd, HDC hDC)
|
||||
{
|
||||
var result = NativeMethods.User32.ReleaseDC(hWnd, hDC);
|
||||
|
||||
return result == 0
|
||||
? throw new InvalidOperationException(
|
||||
$"{nameof(User32.ReleaseDC)} returned {result}")
|
||||
: result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user