mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-05 10:46:33 +02:00
[MouseJump][CQ]Refactor "common" classes into a separate project (#34333)
* [Mouse Jump] - moving common code to MouseJump.Common project - #25482 * [Mouse Jump] - fixing warnings in MouseJump.Common - #25482 * [MouseJump] - cherrypick 5653b4 - Exclude MouseJump Common tests from the checks # Conflicts: # src/modules/MouseUtils/MouseJump.Common.UnitTests/MouseJump.Common.UnitTests.csproj * [mOuSEjUMP] - cherry pick 61aab9 - Fix ci build issues # Conflicts: # src/modules/MouseUtils/MouseJump.Common.UnitTests/MouseJump.Common.UnitTests.csproj * [Mouse Jump] - remove project type guids - #25482 * [Mouse Jump] - simplify mousejump *.csproj files - #25482 * [Mouse Jump] - fixing broken tests - #25482 * [Mouse Jump] - fixing broken build - #25482 * [Mouse Jump] - editing MouseJump.Common.UnitTests.csproj - #25482 * [Mouse Jump] - editing MouseJump.Common.csproj (UseWindowsForms=true) - #25482 * [Mouse Jump] - fixing spellcheck - #25482 * [MouseJump] - enabled implicit usings - #25482 * [Mouse Jump] - re-add csproj attributes - #27511 * ci: Fix signing of Common dll --------- Co-authored-by: Clayton <mike.clayton@delinian.com>
This commit is contained in:
245
src/modules/MouseUtils/MouseJump.Common/Helpers/DrawingHelper.cs
Normal file
245
src/modules/MouseUtils/MouseJump.Common/Helpers/DrawingHelper.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
|
||||
using MouseJump.Common.Imaging;
|
||||
using MouseJump.Common.Models.Drawing;
|
||||
using MouseJump.Common.Models.Layout;
|
||||
using MouseJump.Common.Models.Styles;
|
||||
|
||||
namespace MouseJump.Common.Helpers;
|
||||
|
||||
public static class DrawingHelper
|
||||
{
|
||||
public static Bitmap RenderPreview(
|
||||
PreviewLayout previewLayout,
|
||||
IImageRegionCopyService imageCopyService,
|
||||
Action<Bitmap>? previewImageCreatedCallback = null,
|
||||
Action? previewImageUpdatedCallback = null)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
// initialize the preview image
|
||||
var previewBounds = previewLayout.PreviewBounds.OuterBounds.ToRectangle();
|
||||
var previewImage = new Bitmap(previewBounds.Width, previewBounds.Height, PixelFormat.Format32bppPArgb);
|
||||
var previewGraphics = Graphics.FromImage(previewImage);
|
||||
previewImageCreatedCallback?.Invoke(previewImage);
|
||||
|
||||
DrawingHelper.DrawRaisedBorder(previewGraphics, previewLayout.PreviewStyle.CanvasStyle, previewLayout.PreviewBounds);
|
||||
DrawingHelper.DrawBackgroundFill(
|
||||
previewGraphics,
|
||||
previewLayout.PreviewStyle.CanvasStyle,
|
||||
previewLayout.PreviewBounds,
|
||||
[]);
|
||||
|
||||
// sort the source and target screen areas into the order we want to
|
||||
// draw them, putting the activated screen first (we need to capture
|
||||
// and draw the activated screen before we show the form because
|
||||
// otherwise we'll capture the form as part of the screenshot!)
|
||||
var sourceScreens = new List<RectangleInfo> { previewLayout.Screens[previewLayout.ActivatedScreenIndex] }
|
||||
.Concat(previewLayout.Screens.Where((_, idx) => idx != previewLayout.ActivatedScreenIndex))
|
||||
.ToList();
|
||||
var targetScreens = new List<BoxBounds> { previewLayout.ScreenshotBounds[previewLayout.ActivatedScreenIndex] }
|
||||
.Concat(previewLayout.ScreenshotBounds.Where((_, idx) => idx != previewLayout.ActivatedScreenIndex))
|
||||
.ToList();
|
||||
|
||||
// draw all the screenshot bezels
|
||||
foreach (var screenshotBounds in previewLayout.ScreenshotBounds)
|
||||
{
|
||||
DrawingHelper.DrawRaisedBorder(
|
||||
previewGraphics, previewLayout.PreviewStyle.ScreenStyle, screenshotBounds);
|
||||
}
|
||||
|
||||
var refreshRequired = false;
|
||||
var placeholdersDrawn = false;
|
||||
for (var i = 0; i < sourceScreens.Count; i++)
|
||||
{
|
||||
imageCopyService.CopyImageRegion(previewGraphics, sourceScreens[i], targetScreens[i].ContentBounds);
|
||||
refreshRequired = true;
|
||||
|
||||
// show the placeholder images and show the form if it looks like it might take
|
||||
// a while to capture the remaining screenshot images (but only if there are any)
|
||||
if (stopwatch.ElapsedMilliseconds > 250)
|
||||
{
|
||||
// draw placeholder backgrounds for any undrawn screens
|
||||
if (!placeholdersDrawn)
|
||||
{
|
||||
DrawingHelper.DrawScreenPlaceholders(
|
||||
previewGraphics,
|
||||
previewLayout.PreviewStyle.ScreenStyle,
|
||||
targetScreens.GetRange(i + 1, targetScreens.Count - i - 1));
|
||||
placeholdersDrawn = true;
|
||||
}
|
||||
|
||||
previewImageUpdatedCallback?.Invoke();
|
||||
refreshRequired = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (refreshRequired)
|
||||
{
|
||||
previewImageUpdatedCallback?.Invoke();
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
return previewImage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a border shape with an optional raised 3d highlight and shadow effect.
|
||||
/// </summary>
|
||||
private static void DrawRaisedBorder(
|
||||
Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds)
|
||||
{
|
||||
var borderStyle = boxStyle.BorderStyle;
|
||||
if ((borderStyle.Horizontal == 0) || (borderStyle.Vertical == 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// draw the main box border
|
||||
using var borderBrush = new SolidBrush(borderStyle.Color);
|
||||
var borderRegion = new Region(boxBounds.BorderBounds.ToRectangle());
|
||||
borderRegion.Exclude(boxBounds.PaddingBounds.ToRectangle());
|
||||
graphics.FillRegion(borderBrush, borderRegion);
|
||||
|
||||
// draw the highlight and shadow
|
||||
var bounds = boxBounds.BorderBounds.ToRectangle();
|
||||
using var highlight = new Pen(Color.FromArgb(0x44, 0xFF, 0xFF, 0xFF));
|
||||
using var shadow = new Pen(Color.FromArgb(0x44, 0x00, 0x00, 0x00));
|
||||
|
||||
var outer = (
|
||||
Left: bounds.Left,
|
||||
Top: bounds.Top,
|
||||
Right: bounds.Right - 1,
|
||||
Bottom: bounds.Bottom - 1
|
||||
);
|
||||
var inner = (
|
||||
Left: bounds.Left + (int)borderStyle.Left - 1,
|
||||
Top: bounds.Top + (int)borderStyle.Top - 1,
|
||||
Right: bounds.Right - (int)borderStyle.Right,
|
||||
Bottom: bounds.Bottom - (int)borderStyle.Bottom
|
||||
);
|
||||
|
||||
for (var i = 0; i < borderStyle.Depth; i++)
|
||||
{
|
||||
// left edge
|
||||
if (borderStyle.Left >= i * 2)
|
||||
{
|
||||
graphics.DrawLine(highlight, outer.Left, outer.Top, outer.Left, outer.Bottom);
|
||||
graphics.DrawLine(shadow, inner.Left, inner.Top, inner.Left, inner.Bottom);
|
||||
}
|
||||
|
||||
// top edge
|
||||
if (borderStyle.Top >= i * 2)
|
||||
{
|
||||
graphics.DrawLine(highlight, outer.Left, outer.Top, outer.Right, outer.Top);
|
||||
graphics.DrawLine(shadow, inner.Left, inner.Top, inner.Right, inner.Top);
|
||||
}
|
||||
|
||||
// right edge
|
||||
if (borderStyle.Right >= i * 2)
|
||||
{
|
||||
graphics.DrawLine(highlight, inner.Right, inner.Top, inner.Right, inner.Bottom);
|
||||
graphics.DrawLine(shadow, outer.Right, outer.Top, outer.Right, outer.Bottom);
|
||||
}
|
||||
|
||||
// bottom edge
|
||||
if (borderStyle.Bottom >= i * 2)
|
||||
{
|
||||
graphics.DrawLine(highlight, inner.Left, inner.Bottom, inner.Right, inner.Bottom);
|
||||
graphics.DrawLine(shadow, outer.Left, outer.Bottom, outer.Right, outer.Bottom);
|
||||
}
|
||||
|
||||
// shrink the outer border for the next iteration
|
||||
outer = (
|
||||
outer.Left + 1,
|
||||
outer.Top + 1,
|
||||
outer.Right - 1,
|
||||
outer.Bottom - 1
|
||||
);
|
||||
|
||||
// enlarge the inner border for the next iteration
|
||||
inner = (
|
||||
inner.Left - 1,
|
||||
inner.Top - 1,
|
||||
inner.Right + 1,
|
||||
inner.Bottom + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a gradient-filled background shape.
|
||||
/// </summary>
|
||||
private static void DrawBackgroundFill(
|
||||
Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds, IEnumerable<RectangleInfo> excludeBounds)
|
||||
{
|
||||
var backgroundBounds = boxBounds.PaddingBounds;
|
||||
|
||||
using var backgroundBrush = DrawingHelper.GetBackgroundStyleBrush(boxStyle.BackgroundStyle, backgroundBounds);
|
||||
if (backgroundBrush == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 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(backgroundBounds.ToRectangle());
|
||||
foreach (var exclude in excludeBounds)
|
||||
{
|
||||
backgroundRegion.Exclude(exclude.ToRectangle());
|
||||
}
|
||||
|
||||
graphics.FillRegion(backgroundBrush, backgroundRegion);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws placeholder background images for the specified screens on the preview.
|
||||
/// </summary>
|
||||
private static void DrawScreenPlaceholders(
|
||||
Graphics graphics, BoxStyle screenStyle, IList<BoxBounds> screenBounds)
|
||||
{
|
||||
if (screenBounds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenStyle?.BackgroundStyle?.Color1 == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var brush = new SolidBrush(screenStyle.BackgroundStyle.Color1.Value);
|
||||
graphics.FillRectangles(brush, screenBounds.Select(bounds => bounds.PaddingBounds.ToRectangle()).ToArray());
|
||||
}
|
||||
|
||||
private static Brush? GetBackgroundStyleBrush(BackgroundStyle backgroundStyle, RectangleInfo backgroundBounds)
|
||||
{
|
||||
var backgroundBrush = backgroundStyle switch
|
||||
{
|
||||
{ Color1: not null, Color2: not null } =>
|
||||
/* draw a gradient fill if both colors are specified */
|
||||
new LinearGradientBrush(
|
||||
backgroundBounds.ToRectangle(),
|
||||
backgroundStyle.Color1.Value,
|
||||
backgroundStyle.Color2.Value,
|
||||
LinearGradientMode.ForwardDiagonal),
|
||||
{ Color1: not null } =>
|
||||
/* draw a solid fill if only one color is specified */
|
||||
new SolidBrush(
|
||||
backgroundStyle.Color1.Value),
|
||||
{ Color2: not null } =>
|
||||
/* draw a solid fill if only one color is specified */
|
||||
new SolidBrush(
|
||||
backgroundStyle.Color2.Value),
|
||||
_ => (Brush?)null,
|
||||
};
|
||||
return backgroundBrush;
|
||||
}
|
||||
}
|
||||
156
src/modules/MouseUtils/MouseJump.Common/Helpers/LayoutHelper.cs
Normal file
156
src/modules/MouseUtils/MouseJump.Common/Helpers/LayoutHelper.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
// 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 MouseJump.Common.Models.Drawing;
|
||||
using MouseJump.Common.Models.Layout;
|
||||
using MouseJump.Common.Models.Styles;
|
||||
|
||||
namespace MouseJump.Common.Helpers;
|
||||
|
||||
public static class LayoutHelper
|
||||
{
|
||||
public static PreviewLayout GetPreviewLayout(
|
||||
PreviewStyle previewStyle, List<RectangleInfo> screens, PointInfo activatedLocation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(previewStyle);
|
||||
ArgumentNullException.ThrowIfNull(screens);
|
||||
|
||||
if (screens.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("Value must contain at least one item.", nameof(screens));
|
||||
}
|
||||
|
||||
var builder = new PreviewLayout.Builder();
|
||||
builder.Screens = screens.ToList();
|
||||
|
||||
// calculate the bounding rectangle for the virtual screen
|
||||
builder.VirtualScreen = LayoutHelper.GetCombinedScreenBounds(builder.Screens);
|
||||
|
||||
// find the screen that contains the activated location - this is the
|
||||
// one we'll show the preview form on
|
||||
var activatedScreen = builder.Screens.Single(
|
||||
screen => screen.Contains(activatedLocation));
|
||||
builder.ActivatedScreenIndex = builder.Screens.IndexOf(activatedScreen);
|
||||
|
||||
// work out the maximum allowed size of the preview form:
|
||||
// * can't be bigger than the activated screen
|
||||
// * can't be bigger than the configured canvas size
|
||||
var maxPreviewSize = activatedScreen.Size
|
||||
.Intersect(previewStyle.CanvasSize);
|
||||
|
||||
// the "content area" (i.e. drawing area) for screenshots is inside the
|
||||
// preview border and inside the preview padding (if any)
|
||||
var maxContentSize = maxPreviewSize
|
||||
.Shrink(previewStyle.CanvasStyle.MarginStyle)
|
||||
.Shrink(previewStyle.CanvasStyle.BorderStyle)
|
||||
.Shrink(previewStyle.CanvasStyle.PaddingStyle);
|
||||
|
||||
// scale the virtual screen to fit inside the content area
|
||||
var screenScalingRatio = builder.VirtualScreen.Size
|
||||
.ScaleToFitRatio(maxContentSize);
|
||||
|
||||
// work out the actual size of the "content area" by scaling the virtual screen
|
||||
// to fit inside the maximum content area while maintaining its aspect ration.
|
||||
// we'll also offset it to allow for any margins, borders and padding
|
||||
var contentBounds = builder.VirtualScreen.Size
|
||||
.Scale(screenScalingRatio)
|
||||
.Floor()
|
||||
.PlaceAt(0, 0)
|
||||
.Offset(previewStyle.CanvasStyle.MarginStyle.Left, previewStyle.CanvasStyle.MarginStyle.Top)
|
||||
.Offset(previewStyle.CanvasStyle.BorderStyle.Left, previewStyle.CanvasStyle.BorderStyle.Top)
|
||||
.Offset(previewStyle.CanvasStyle.PaddingStyle.Left, previewStyle.CanvasStyle.PaddingStyle.Top);
|
||||
|
||||
// now we know the actual size of the content area we can work outwards to
|
||||
// get the size of the background bounds including margins, borders and padding
|
||||
builder.PreviewStyle = previewStyle;
|
||||
builder.PreviewBounds = LayoutHelper.GetBoxBoundsFromContentBounds(
|
||||
contentBounds,
|
||||
previewStyle.CanvasStyle);
|
||||
|
||||
// ... and then the size and position of the preview form on the activated screen
|
||||
// * center the form to the activated position, but nudge it back
|
||||
// inside the visible area of the activated screen if it falls outside
|
||||
var formBounds = builder.PreviewBounds.OuterBounds
|
||||
.Center(activatedLocation)
|
||||
.Clamp(activatedScreen);
|
||||
builder.FormBounds = formBounds;
|
||||
|
||||
// now calculate the positions of each of the screenshot images on the preview
|
||||
builder.ScreenshotBounds = builder.Screens
|
||||
.Select(
|
||||
screen => LayoutHelper.GetBoxBoundsFromOuterBounds(
|
||||
screen
|
||||
.Offset(builder.VirtualScreen.Location.ToSize().Invert())
|
||||
.Scale(screenScalingRatio)
|
||||
.Offset(builder.PreviewBounds.ContentBounds.Location.ToSize())
|
||||
.Truncate(),
|
||||
previewStyle.ScreenStyle))
|
||||
.ToList();
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
internal static RectangleInfo GetCombinedScreenBounds(List<RectangleInfo> screens)
|
||||
{
|
||||
return screens.Skip(1).Aggregate(
|
||||
seed: screens.First(),
|
||||
(bounds, screen) => bounds.Union(screen));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the bounds of the various areas of a box, given the content bounds and the box style.
|
||||
/// Starts with the content bounds and works outward, enlarging the content bounds by the padding, border, and margin sizes to calculate the outer bounds of the box.
|
||||
/// </summary>
|
||||
/// <param name="contentBounds">The content bounds of the box.</param>
|
||||
/// <param name="boxStyle">The style of the box, which includes the sizes of the margin, border, and padding areas.</param>
|
||||
/// <returns>A <see cref="BoxBounds"/> object that represents the bounds of the different areas of the box.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="contentBounds"/> or <paramref name="boxStyle"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when any of the styles in <paramref name="boxStyle"/> is null.</exception>
|
||||
public static BoxBounds GetBoxBoundsFromContentBounds(
|
||||
RectangleInfo contentBounds,
|
||||
BoxStyle boxStyle)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(contentBounds);
|
||||
ArgumentNullException.ThrowIfNull(boxStyle);
|
||||
if (boxStyle.PaddingStyle == null || boxStyle.BorderStyle == null || boxStyle.MarginStyle == null)
|
||||
{
|
||||
throw new ArgumentException(null, nameof(boxStyle));
|
||||
}
|
||||
|
||||
var paddingBounds = contentBounds.Enlarge(boxStyle.PaddingStyle);
|
||||
var borderBounds = paddingBounds.Enlarge(boxStyle.BorderStyle);
|
||||
var marginBounds = borderBounds.Enlarge(boxStyle.MarginStyle);
|
||||
var outerBounds = marginBounds;
|
||||
return new(
|
||||
outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the bounds of the various areas of a box, given the outer bounds and the box style.
|
||||
/// This method starts with the outer bounds and works inward, shrinking the outer bounds by the margin, border, and padding sizes to calculate the content bounds of the box.
|
||||
/// </summary>
|
||||
/// <param name="outerBounds">The outer bounds of the box.</param>
|
||||
/// <param name="boxStyle">The style of the box, which includes the sizes of the margin, border, and padding areas.</param>
|
||||
/// <returns>A <see cref="BoxBounds"/> object that represents the bounds of the different areas of the box.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="outerBounds"/> or <paramref name="boxStyle"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when any of the styles in <paramref name="boxStyle"/> is null.</exception>
|
||||
public static BoxBounds GetBoxBoundsFromOuterBounds(
|
||||
RectangleInfo outerBounds,
|
||||
BoxStyle boxStyle)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(outerBounds);
|
||||
ArgumentNullException.ThrowIfNull(boxStyle);
|
||||
if (outerBounds == null || boxStyle.MarginStyle == null || boxStyle.BorderStyle == null || boxStyle.PaddingStyle == null)
|
||||
{
|
||||
throw new ArgumentException(null, nameof(boxStyle));
|
||||
}
|
||||
|
||||
var marginBounds = outerBounds;
|
||||
var borderBounds = marginBounds.Shrink(boxStyle.MarginStyle);
|
||||
var paddingBounds = borderBounds.Shrink(boxStyle.BorderStyle);
|
||||
var contentBounds = paddingBounds.Shrink(boxStyle.PaddingStyle);
|
||||
return new(
|
||||
outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds);
|
||||
}
|
||||
}
|
||||
144
src/modules/MouseUtils/MouseJump.Common/Helpers/MouseHelper.cs
Normal file
144
src/modules/MouseUtils/MouseJump.Common/Helpers/MouseHelper.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using MouseJump.Common.Models.Drawing;
|
||||
using MouseJump.Common.NativeMethods;
|
||||
using static MouseJump.Common.NativeMethods.Core;
|
||||
using static MouseJump.Common.NativeMethods.User32;
|
||||
|
||||
namespace MouseJump.Common.Helpers;
|
||||
|
||||
public 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>
|
||||
/// Get the current position of the cursor.
|
||||
/// </summary>
|
||||
public static PointInfo GetCursorPosition()
|
||||
{
|
||||
var lpPoint = new LPPOINT(new POINT(0, 0));
|
||||
var result = User32.GetCursorPos(lpPoint);
|
||||
if (!result)
|
||||
{
|
||||
throw new Win32Exception(
|
||||
Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
var point = lpPoint.ToStructure();
|
||||
lpPoint.Free();
|
||||
|
||||
return new PointInfo(
|
||||
point.x, point.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the cursor to the specified location.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See https://github.com/mikeclayton/FancyMouse/pull/3
|
||||
/// </remarks>
|
||||
public static void SetCursorPosition(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 target = location.ToPoint();
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result = User32.SetCursorPos(target.X, target.Y);
|
||||
if (!result)
|
||||
{
|
||||
throw new Win32Exception(
|
||||
Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
var current = MouseHelper.GetCursorPosition();
|
||||
if ((current.X == target.X) || (current.Y == target.Y))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// temporary workaround for issue #1273
|
||||
MouseHelper.SimulateMouseMovementEvent(location);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
internal static void SimulateMouseMovementEvent(PointInfo location)
|
||||
{
|
||||
var inputs = new User32.INPUT[]
|
||||
{
|
||||
new(
|
||||
type: INPUT_TYPE.INPUT_MOUSE,
|
||||
data: new INPUT.DUMMYUNIONNAME(
|
||||
mi: new MOUSEINPUT(
|
||||
dx: (int)MouseHelper.CalculateAbsoluteCoordinateX(location.X),
|
||||
dy: (int)MouseHelper.CalculateAbsoluteCoordinateY(location.Y),
|
||||
mouseData: 0,
|
||||
dwFlags: MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE,
|
||||
time: 0,
|
||||
dwExtraInfo: ULONG_PTR.Null))),
|
||||
};
|
||||
var result = User32.SendInput(
|
||||
(UINT)inputs.Length,
|
||||
new LPINPUT(inputs),
|
||||
INPUT.Size * inputs.Length);
|
||||
if (result != inputs.Length)
|
||||
{
|
||||
throw new Win32Exception(
|
||||
Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
|
||||
private static decimal CalculateAbsoluteCoordinateX(decimal x)
|
||||
{
|
||||
// If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535.
|
||||
// see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
|
||||
return (x * 65535) / User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
}
|
||||
|
||||
private static decimal CalculateAbsoluteCoordinateY(decimal y)
|
||||
{
|
||||
// If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535.
|
||||
// see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
|
||||
return (y * 65535) / User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// 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.ComponentModel;
|
||||
|
||||
using MouseJump.Common.Models.Drawing;
|
||||
using MouseJump.Common.NativeMethods;
|
||||
using static MouseJump.Common.NativeMethods.Core;
|
||||
using static MouseJump.Common.NativeMethods.User32;
|
||||
|
||||
namespace MouseJump.Common.Helpers;
|
||||
|
||||
public static class ScreenHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Duplicates functionality available in System.Windows.Forms.SystemInformation
|
||||
/// to reduce the dependency on WinForms
|
||||
/// </summary>
|
||||
private static RectangleInfo GetVirtualScreen()
|
||||
{
|
||||
return new(
|
||||
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_XVIRTUALSCREEN),
|
||||
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_YVIRTUALSCREEN),
|
||||
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXVIRTUALSCREEN),
|
||||
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYVIRTUALSCREEN));
|
||||
}
|
||||
|
||||
public static IEnumerable<ScreenInfo> GetAllScreens()
|
||||
{
|
||||
// enumerate the monitors attached to the system
|
||||
var hMonitors = new List<HMONITOR>();
|
||||
var callback = new User32.MONITORENUMPROC(
|
||||
(hMonitor, hdcMonitor, lprcMonitor, dwData) =>
|
||||
{
|
||||
hMonitors.Add(hMonitor);
|
||||
return true;
|
||||
});
|
||||
var result = User32.EnumDisplayMonitors(HDC.Null, LPCRECT.Null, callback, LPARAM.Null);
|
||||
if (!result)
|
||||
{
|
||||
throw new Win32Exception(
|
||||
result.Value,
|
||||
$"{nameof(User32.EnumDisplayMonitors)} failed with return code {result.Value}");
|
||||
}
|
||||
|
||||
// get detailed info about each monitor
|
||||
foreach (var hMonitor in hMonitors)
|
||||
{
|
||||
var monitorInfoPtr = new LPMONITORINFO(
|
||||
new MONITORINFO((DWORD)MONITORINFO.Size, RECT.Empty, RECT.Empty, 0));
|
||||
result = User32.GetMonitorInfoW(hMonitor, monitorInfoPtr);
|
||||
if (!result)
|
||||
{
|
||||
throw new Win32Exception(
|
||||
result.Value,
|
||||
$"{nameof(User32.GetMonitorInfoW)} failed with return code {result.Value}");
|
||||
}
|
||||
|
||||
var monitorInfo = monitorInfoPtr.ToStructure();
|
||||
monitorInfoPtr.Free();
|
||||
|
||||
yield return new ScreenInfo(
|
||||
handle: hMonitor,
|
||||
primary: monitorInfo.dwFlags.HasFlag(User32.MONITOR_INFO_FLAGS.MONITORINFOF_PRIMARY),
|
||||
displayArea: new RectangleInfo(
|
||||
monitorInfo.rcMonitor.left,
|
||||
monitorInfo.rcMonitor.top,
|
||||
monitorInfo.rcMonitor.right - monitorInfo.rcMonitor.left,
|
||||
monitorInfo.rcMonitor.bottom - monitorInfo.rcMonitor.top),
|
||||
workingArea: new RectangleInfo(
|
||||
monitorInfo.rcWork.left,
|
||||
monitorInfo.rcWork.top,
|
||||
monitorInfo.rcWork.right - monitorInfo.rcWork.left,
|
||||
monitorInfo.rcWork.bottom - monitorInfo.rcWork.top));
|
||||
}
|
||||
}
|
||||
|
||||
public static ScreenInfo GetScreenFromPoint(
|
||||
List<ScreenInfo> screens,
|
||||
PointInfo pt)
|
||||
{
|
||||
// get the monitor handle from the point
|
||||
var hMonitor = User32.MonitorFromPoint(
|
||||
new((int)pt.X, (int)pt.Y),
|
||||
User32.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
|
||||
if (hMonitor.IsNull)
|
||||
{
|
||||
throw new InvalidOperationException($"no monitor found for point {pt}");
|
||||
}
|
||||
|
||||
// find the screen with the given monitor handle
|
||||
var screen = screens
|
||||
.Single(item => item.Handle == hMonitor);
|
||||
return screen;
|
||||
}
|
||||
}
|
||||
101
src/modules/MouseUtils/MouseJump.Common/Helpers/StyleHelper.cs
Normal file
101
src/modules/MouseUtils/MouseJump.Common/Helpers/StyleHelper.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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 MouseJump.Common.Models.Drawing;
|
||||
using MouseJump.Common.Models.Styles;
|
||||
|
||||
namespace MouseJump.Common.Helpers;
|
||||
|
||||
public static class StyleHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Default v2 preview style
|
||||
/// </summary>
|
||||
public static readonly PreviewStyle DefaultPreviewStyle = new(
|
||||
canvasSize: new(
|
||||
width: 1600,
|
||||
height: 1200
|
||||
),
|
||||
canvasStyle: new(
|
||||
marginStyle: MarginStyle.Empty,
|
||||
borderStyle: new(
|
||||
color: SystemColors.Highlight,
|
||||
all: 6,
|
||||
depth: 0
|
||||
),
|
||||
paddingStyle: new(
|
||||
all: 4
|
||||
),
|
||||
backgroundStyle: new(
|
||||
color1: Color.FromArgb(0xFF, 0x0D, 0x57, 0xD2),
|
||||
color2: Color.FromArgb(0xFF, 0x03, 0x44, 0xC0)
|
||||
)
|
||||
),
|
||||
screenStyle: new(
|
||||
marginStyle: new(
|
||||
all: 4
|
||||
),
|
||||
borderStyle: new(
|
||||
color: Color.FromArgb(0xFF, 0x22, 0x22, 0x22),
|
||||
all: 12,
|
||||
depth: 4
|
||||
),
|
||||
paddingStyle: PaddingStyle.Empty,
|
||||
backgroundStyle: new(
|
||||
color1: Color.MidnightBlue,
|
||||
color2: Color.MidnightBlue
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Legacy preview style
|
||||
/// </summary>
|
||||
public static readonly PreviewStyle LegacyPreviewStyle = new(
|
||||
canvasSize: new(
|
||||
width: 1600,
|
||||
height: 1200
|
||||
),
|
||||
canvasStyle: new(
|
||||
marginStyle: MarginStyle.Empty,
|
||||
borderStyle: new(
|
||||
color: SystemColors.Highlight,
|
||||
all: 6,
|
||||
depth: 0
|
||||
),
|
||||
paddingStyle: new(
|
||||
all: 0
|
||||
),
|
||||
backgroundStyle: new(
|
||||
color1: Color.FromArgb(0xFF, 0x0D, 0x57, 0xD2),
|
||||
color2: Color.FromArgb(0xFF, 0x03, 0x44, 0xC0)
|
||||
)
|
||||
),
|
||||
screenStyle: new(
|
||||
marginStyle: new(
|
||||
all: 0
|
||||
),
|
||||
borderStyle: new(
|
||||
color: Color.FromArgb(0xFF, 0x22, 0x22, 0x22),
|
||||
all: 0,
|
||||
depth: 0
|
||||
),
|
||||
paddingStyle: PaddingStyle.Empty,
|
||||
backgroundStyle: new(
|
||||
color1: Color.MidnightBlue,
|
||||
color2: Color.MidnightBlue
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
public static PreviewStyle WithCanvasSize(this PreviewStyle previewStyle, SizeInfo canvasSize)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(previewStyle);
|
||||
ArgumentNullException.ThrowIfNull(canvasSize);
|
||||
return new PreviewStyle(
|
||||
canvasSize: canvasSize,
|
||||
canvasStyle: previewStyle.CanvasStyle,
|
||||
screenStyle: previewStyle.ScreenStyle);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user