mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 19:57:57 +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
|
Animatable
|
||||||
ansicolor
|
ansicolor
|
||||||
ANull
|
ANull
|
||||||
|
ANDSCANS
|
||||||
AOC
|
AOC
|
||||||
aocfnapldcnfbofgmbbllojgocaelgdd
|
aocfnapldcnfbofgmbbllojgocaelgdd
|
||||||
APARTMENTTHREADED
|
APARTMENTTHREADED
|
||||||
@@ -139,7 +140,9 @@ BITMAPINFOHEADER
|
|||||||
bitmask
|
bitmask
|
||||||
BITSPIXEL
|
BITSPIXEL
|
||||||
bla
|
bla
|
||||||
|
BLACKONWHITE
|
||||||
Blockquotes
|
Blockquotes
|
||||||
|
Blt
|
||||||
blogs
|
blogs
|
||||||
BLUEGRAY
|
BLUEGRAY
|
||||||
Bluetooth
|
Bluetooth
|
||||||
@@ -185,6 +188,7 @@ CALG
|
|||||||
callbackptr
|
callbackptr
|
||||||
Cangjie
|
Cangjie
|
||||||
CANRENAME
|
CANRENAME
|
||||||
|
CAPTUREBLT
|
||||||
CAPTURECHANGED
|
CAPTURECHANGED
|
||||||
CAtl
|
CAtl
|
||||||
cch
|
cch
|
||||||
@@ -258,6 +262,7 @@ colorformat
|
|||||||
colorhistory
|
colorhistory
|
||||||
colorhistorylimit
|
colorhistorylimit
|
||||||
COLORKEY
|
COLORKEY
|
||||||
|
COLORONCOLOR
|
||||||
colorpicker
|
colorpicker
|
||||||
COLORREF
|
COLORREF
|
||||||
comctl
|
comctl
|
||||||
@@ -305,6 +310,7 @@ CProj
|
|||||||
CREATESCHEDULEDTASK
|
CREATESCHEDULEDTASK
|
||||||
CREATESTRUCT
|
CREATESTRUCT
|
||||||
CREATEWINDOWFAILED
|
CREATEWINDOWFAILED
|
||||||
|
createcompatibledc
|
||||||
critsec
|
critsec
|
||||||
Crossdevice
|
Crossdevice
|
||||||
CRSEL
|
CRSEL
|
||||||
@@ -368,6 +374,7 @@ dcomp
|
|||||||
dcompi
|
dcompi
|
||||||
DComposition
|
DComposition
|
||||||
DCR
|
DCR
|
||||||
|
DCs
|
||||||
DDevice
|
DDevice
|
||||||
ddf
|
ddf
|
||||||
DDxgi
|
DDxgi
|
||||||
@@ -388,6 +395,7 @@ DEFERERASE
|
|||||||
DEFPUSHBUTTON
|
DEFPUSHBUTTON
|
||||||
deinitialization
|
deinitialization
|
||||||
DELA
|
DELA
|
||||||
|
DELETESCANS
|
||||||
deletethis
|
deletethis
|
||||||
Delimarsky
|
Delimarsky
|
||||||
dend
|
dend
|
||||||
@@ -442,6 +450,7 @@ drawingcolor
|
|||||||
dreamsofameaningfullife
|
dreamsofameaningfullife
|
||||||
drivedetectionwarning
|
drivedetectionwarning
|
||||||
dshow
|
dshow
|
||||||
|
DSTINVERT
|
||||||
dutil
|
dutil
|
||||||
DVASPECT
|
DVASPECT
|
||||||
DVASPECTINFO
|
DVASPECTINFO
|
||||||
@@ -1035,6 +1044,8 @@ Melman
|
|||||||
MENUITEMINFO
|
MENUITEMINFO
|
||||||
MENUITEMINFOW
|
MENUITEMINFOW
|
||||||
menurc
|
menurc
|
||||||
|
MERGECOPY
|
||||||
|
MERGEPAINT
|
||||||
Metadatas
|
Metadatas
|
||||||
metafile
|
metafile
|
||||||
mfapi
|
mfapi
|
||||||
@@ -1193,6 +1204,7 @@ NOINHERITLAYOUT
|
|||||||
NOINTERFACE
|
NOINTERFACE
|
||||||
NOLINKINFO
|
NOLINKINFO
|
||||||
NOMINMAX
|
NOMINMAX
|
||||||
|
NOMIRRORBITMAP
|
||||||
NOMOVE
|
NOMOVE
|
||||||
NONAME
|
NONAME
|
||||||
nonclient
|
nonclient
|
||||||
@@ -1224,6 +1236,8 @@ notmatch
|
|||||||
Noto
|
Noto
|
||||||
NOTOPMOST
|
NOTOPMOST
|
||||||
NOTRACK
|
NOTRACK
|
||||||
|
NOTSRCCOPY
|
||||||
|
NOTSRCERASE
|
||||||
NOUPDATE
|
NOUPDATE
|
||||||
NOZORDER
|
NOZORDER
|
||||||
NPH
|
NPH
|
||||||
@@ -1273,6 +1287,7 @@ openxmlformats
|
|||||||
OPTIMIZEFORINVOKE
|
OPTIMIZEFORINVOKE
|
||||||
ORAW
|
ORAW
|
||||||
ORPHANEDDIALOGTITLE
|
ORPHANEDDIALOGTITLE
|
||||||
|
ORSCANS
|
||||||
oss
|
oss
|
||||||
ostr
|
ostr
|
||||||
OSVERSIONINFOEX
|
OSVERSIONINFOEX
|
||||||
@@ -1304,8 +1319,11 @@ PArgb
|
|||||||
parray
|
parray
|
||||||
PARTIALCONFIRMATIONDIALOGTITLE
|
PARTIALCONFIRMATIONDIALOGTITLE
|
||||||
pasteplain
|
pasteplain
|
||||||
|
PATCOPY
|
||||||
pathcch
|
pathcch
|
||||||
Pathto
|
Pathto
|
||||||
|
PATINVERT
|
||||||
|
PATPAINT
|
||||||
PAUDIO
|
PAUDIO
|
||||||
pbc
|
pbc
|
||||||
Pbgra
|
Pbgra
|
||||||
@@ -1547,6 +1565,7 @@ Roamable
|
|||||||
robmensching
|
robmensching
|
||||||
Roboto
|
Roboto
|
||||||
rooler
|
rooler
|
||||||
|
rop
|
||||||
roslyn
|
roslyn
|
||||||
Rothera
|
Rothera
|
||||||
roundf
|
roundf
|
||||||
@@ -1716,8 +1735,12 @@ spsi
|
|||||||
spsia
|
spsia
|
||||||
spsrm
|
spsrm
|
||||||
spsv
|
spsv
|
||||||
|
SRCAND
|
||||||
SRCCOPY
|
SRCCOPY
|
||||||
|
SRCERASE
|
||||||
Srch
|
Srch
|
||||||
|
SRCINVERT
|
||||||
|
SRCPAINT
|
||||||
sre
|
sre
|
||||||
Srednekolymsk
|
Srednekolymsk
|
||||||
SResize
|
SResize
|
||||||
@@ -2039,6 +2062,7 @@ website
|
|||||||
wekyb
|
wekyb
|
||||||
Wevtapi
|
Wevtapi
|
||||||
wgpocpl
|
wgpocpl
|
||||||
|
WHITEONBLACK
|
||||||
whitespaces
|
whitespaces
|
||||||
WIC
|
WIC
|
||||||
wifi
|
wifi
|
||||||
@@ -2073,6 +2097,7 @@ winevt
|
|||||||
winexe
|
winexe
|
||||||
winforms
|
winforms
|
||||||
winfx
|
winfx
|
||||||
|
wingdi
|
||||||
winget
|
winget
|
||||||
wingetcreate
|
wingetcreate
|
||||||
Winhook
|
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()
|
private static string GetCallerInfo()
|
||||||
{
|
{
|
||||||
StackTrace stackTrace = new StackTrace();
|
var stackTrace = new StackTrace();
|
||||||
|
|
||||||
var methodName = stackTrace.GetFrame(3)?.GetMethod();
|
var methodName = stackTrace.GetFrame(3)?.GetMethod();
|
||||||
var className = methodName?.DeclaringType.Name;
|
var className = methodName?.DeclaringType?.Name;
|
||||||
return "[Method]: " + methodName?.Name + " [Class]: " + className;
|
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;
|
||||||
using System.Runtime.InteropServices;
|
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
|
[DllImport("user32.dll")]
|
||||||
internal class NativeMethods
|
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 INPUTTYPE type;
|
||||||
internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
internal InputUnion data;
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
internal static int Size
|
||||||
internal static extern int GetSystemMetrics(SystemMetric smIndex);
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
public struct INPUT
|
|
||||||
{
|
{
|
||||||
internal INPUTTYPE type;
|
get { return Marshal.SizeOf(typeof(INPUT)); }
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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.Location = new System.Drawing.Point(5, 5);
|
||||||
Thumbnail.Name = "Thumbnail";
|
Thumbnail.Name = "Thumbnail";
|
||||||
Thumbnail.Size = new System.Drawing.Size(790, 440);
|
Thumbnail.Size = new System.Drawing.Size(790, 440);
|
||||||
Thumbnail.SizeMode = PictureBoxSizeMode.StretchImage;
|
Thumbnail.SizeMode = PictureBoxSizeMode.Normal;
|
||||||
Thumbnail.TabIndex = 1;
|
Thumbnail.TabIndex = 1;
|
||||||
Thumbnail.TabStop = false;
|
Thumbnail.TabStop = false;
|
||||||
Thumbnail.Click += Thumbnail_Click;
|
Thumbnail.Click += Thumbnail_Click;
|
||||||
|
|||||||
@@ -3,11 +3,14 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Drawing.Imaging;
|
using System.Drawing.Imaging;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using MouseJumpUI.Drawing.Models;
|
||||||
using MouseJumpUI.Helpers;
|
using MouseJumpUI.Helpers;
|
||||||
|
using MouseJumpUI.NativeMethods.Core;
|
||||||
|
|
||||||
namespace MouseJumpUI;
|
namespace MouseJumpUI;
|
||||||
|
|
||||||
@@ -33,172 +36,170 @@ internal partial class MainForm : Form
|
|||||||
|
|
||||||
private void MainForm_Deactivate(object sender, EventArgs e)
|
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();
|
this.Close();
|
||||||
}
|
|
||||||
|
|
||||||
// Sends an input simulating an absolute mouse move to the new location.
|
if (this.Thumbnail.Image is not null)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
var tmp = this.Thumbnail.Image;
|
var tmp = this.Thumbnail.Image;
|
||||||
this.Thumbnail.Image = null;
|
this.Thumbnail.Image = null;
|
||||||
tmp.Dispose();
|
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;
|
var screens = Screen.AllScreens;
|
||||||
foreach (var i in Enumerable.Range(0, screens.Length))
|
foreach (var i in Enumerable.Range(0, screens.Length))
|
||||||
{
|
{
|
||||||
var screen = screens[i];
|
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(
|
// collect together some values that we need for calculating layout
|
||||||
screens.Select(screen => screen.Bounds).ToList());
|
var activatedLocation = Cursor.Position;
|
||||||
Logger.LogInfo(
|
var layoutConfig = new LayoutConfig(
|
||||||
$"desktop bounds = {desktopBounds}");
|
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;
|
// calculate the layout coordinates for everything
|
||||||
Logger.LogInfo(
|
var layoutInfo = DrawingHelper.CalculateLayoutInfo(layoutConfig);
|
||||||
$"activated position = {activatedPosition}");
|
Logger.LogInfo(string.Join(
|
||||||
|
'\n',
|
||||||
|
$"Layout info",
|
||||||
|
$"-----------",
|
||||||
|
$"form bounds = {layoutInfo.FormBounds}",
|
||||||
|
$"preview bounds = {layoutInfo.PreviewBounds}",
|
||||||
|
$"activated screen = {layoutInfo.ActivatedScreen}"));
|
||||||
|
|
||||||
var previewImagePadding = new Size(
|
DrawingHelper.PositionForm(this, layoutInfo.FormBounds);
|
||||||
panel1.Padding.Left + panel1.Padding.Right,
|
|
||||||
panel1.Padding.Top + panel1.Padding.Bottom);
|
|
||||||
Logger.LogInfo(
|
|
||||||
$"image padding = {previewImagePadding}");
|
|
||||||
|
|
||||||
var maxThumbnailSize = new Size(1600, 1200);
|
// initialize the preview image
|
||||||
var formBounds = LayoutHelper.GetPreviewFormBounds(
|
var preview = new Bitmap(
|
||||||
desktopBounds: desktopBounds,
|
(int)layoutInfo.PreviewBounds.Width,
|
||||||
activatedPosition: activatedPosition,
|
(int)layoutInfo.PreviewBounds.Height,
|
||||||
activatedMonitorBounds: Screen.FromPoint(activatedPosition).Bounds,
|
PixelFormat.Format32bppArgb);
|
||||||
maximumThumbnailImageSize: maxThumbnailSize,
|
this.Thumbnail.Image = preview;
|
||||||
thumbnailImagePadding: previewImagePadding);
|
|
||||||
Logger.LogInfo(
|
|
||||||
$"form bounds = {formBounds}");
|
|
||||||
|
|
||||||
// take a screenshot of the entire desktop
|
using var previewGraphics = Graphics.FromImage(preview);
|
||||||
// see https://learn.microsoft.com/en-gb/windows/win32/gdi/the-virtual-screen
|
|
||||||
var screenshot = new Bitmap(desktopBounds.Width, desktopBounds.Height, PixelFormat.Format32bppArgb);
|
DrawingHelper.DrawPreviewBackground(previewGraphics, layoutInfo.PreviewBounds, layoutInfo.ScreenBounds);
|
||||||
using (var graphics = Graphics.FromImage(screenshot))
|
|
||||||
|
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
|
DrawingHelper.EnsureDesktopDeviceContext(ref desktopHwnd, ref desktopHdc);
|
||||||
// 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
|
// 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);
|
||||||
// | 1 +-------+
|
DrawingHelper.DrawPreviewScreen(
|
||||||
// | | |
|
desktopHdc,
|
||||||
// +----------------+ 0 |
|
previewHdc,
|
||||||
// *****************| |
|
layoutConfig.ScreenBounds[layoutConfig.ActivatedScreen],
|
||||||
// *****************+-------+
|
layoutInfo.ScreenBounds[layoutConfig.ActivatedScreen]);
|
||||||
//
|
activatedStopwatch.Stop();
|
||||||
// for very irregular monitor layouts this *might* be a big percentage of the rectangle
|
|
||||||
// containing the desktop bounds.
|
// show the placeholder images if it looks like it might take a while
|
||||||
//
|
// to capture the remaining screenshot images
|
||||||
// then again, it might not make much difference at all - we'd need to do some perf tests
|
if (activatedStopwatch.ElapsedMilliseconds > 250)
|
||||||
graphics.CopyFromScreen(desktopBounds.Left, desktopBounds.Top, 0, 0, desktopBounds.Size);
|
{
|
||||||
|
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
|
// 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();
|
this.Activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ShowPreview(MainForm form)
|
||||||
|
{
|
||||||
|
if (!form.Visible)
|
||||||
|
{
|
||||||
|
form.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
form.Thumbnail.Refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<StartupObject>MouseJumpUI.Program</StartupObject>
|
<StartupObject>MouseJumpUI.Program</StartupObject>
|
||||||
<SelfContained>true</SelfContained>
|
<SelfContained>true</SelfContained>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- SelfContained=true requires RuntimeIdentifier to be set -->
|
<!-- 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