[MouseJump]Refactor code to allow later introduction of customizable appearance (#32838)

* [Mouse Jump] - move code shared with FancyMouse into "Common" folder (#25482)

* [Mouse Jump] - updates to NativeMethods (#25482)

* [Mouse Jump] - added new drawing / layout / style classes (#25482)

* [Mouse Jump] - new style-based preview rendering (actual preview visual style unchanged) (#25482)

* [Mouse Jump] - add words to spell checker (#25482)

* [Mouse Jump] - small tweak to error handling (#25482)

* [Mouse Jump] - fixed failing test (#25482)
This commit is contained in:
Michael Clayton
2024-06-12 16:30:18 +01:00
committed by GitHub
parent 3e07b9b8f4
commit 651f2e4bd8
83 changed files with 2968 additions and 954 deletions

View File

@@ -1,184 +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.Drawing.Drawing2D;
using System.Linq;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.NativeMethods;
using static MouseJumpUI.NativeMethods.Core;
namespace MouseJumpUI.Helpers;
internal static class DrawingHelper
{
/// <summary>
/// Draw the gradient-filled 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);
if (desktopHdc.IsNull)
{
throw new InvalidOperationException(
$"{nameof(User32.GetWindowDC)} returned null");
}
}
}
public static void FreeDesktopDeviceContext(ref HWND desktopHwnd, ref HDC desktopHdc)
{
if (!desktopHwnd.IsNull && !desktopHdc.IsNull)
{
var result = User32.ReleaseDC(desktopHwnd, desktopHdc);
if (result == 0)
{
throw new InvalidOperationException(
$"{nameof(User32.ReleaseDC)} returned {result}");
}
}
desktopHwnd = HWND.Null;
desktopHdc = HDC.Null;
}
/// <summary>
/// Checks if the device context handle exists, and creates a new one from the
/// specified Graphics object if not.
/// </summary>
public static void EnsurePreviewDeviceContext(Graphics previewGraphics, ref HDC previewHdc)
{
if (previewHdc.IsNull)
{
previewHdc = new HDC(previewGraphics.GetHdc());
var result = Gdi32.SetStretchBltMode(previewHdc, Gdi32.STRETCH_BLT_MODE.STRETCH_HALFTONE);
if (result == 0)
{
throw new InvalidOperationException(
$"{nameof(Gdi32.SetStretchBltMode)} returned {result}");
}
}
}
/// <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 DrawPreviewScreenPlaceholders(
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 a screen capture 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();
var result = Gdi32.StretchBlt(
targetHdc,
target.X,
target.Y,
target.Width,
target.Height,
sourceHdc,
source.X,
source.Y,
source.Width,
source.Height,
Gdi32.ROP_CODE.SRCCOPY);
if (!result)
{
throw new InvalidOperationException(
$"{nameof(Gdi32.StretchBlt)} returned {result.Value}");
}
}
/// <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();
var result = Gdi32.StretchBlt(
targetHdc,
target.X,
target.Y,
target.Width,
target.Height,
sourceHdc,
source.X,
source.Y,
source.Width,
source.Height,
Gdi32.ROP_CODE.SRCCOPY);
if (!result)
{
throw new InvalidOperationException(
$"{nameof(Gdi32.StretchBlt)} returned {result.Value}");
}
}
}
}

View File

@@ -1,89 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.Models.Layout;
namespace MouseJumpUI.Helpers;
internal static class LayoutHelper
{
public static LayoutInfo CalculateLayoutInfo(
LayoutConfig layoutConfig)
{
ArgumentNullException.ThrowIfNull(layoutConfig);
var builder = new LayoutInfo.Builder
{
LayoutConfig = layoutConfig,
};
builder.ActivatedScreenBounds = layoutConfig.Screens[layoutConfig.ActivatedScreenIndex].Bounds;
// 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.ActivatedScreenBounds.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.VirtualScreenBounds.Size
.ScaleToFitRatio(maxDrawingSize);
// position the drawing bounds inside the preview border
var drawingBounds = layoutConfig.VirtualScreenBounds.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
.Enlarge(layoutConfig.FormPadding)
.Center(layoutConfig.ActivatedLocation)
.Clamp(builder.ActivatedScreenBounds);
// now calculate the positions of each of the screen images on the preview
builder.ScreenBounds = layoutConfig.Screens
.Select(
screen => screen.Bounds
.Offset(layoutConfig.VirtualScreenBounds.Location.ToSize().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;
}
}

View File

@@ -1,137 +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.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.NativeMethods;
using static MouseJumpUI.NativeMethods.Core;
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>
/// 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 point = location.ToPoint();
for (var i = 0; i < 2; i++)
{
var result = User32.SetCursorPos(point.X, point.Y);
if (!result)
{
throw new Win32Exception(
Marshal.GetLastWin32Error());
}
}
// 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>
public static void SimulateMouseMovementEvent(PointInfo location)
{
var inputs = new User32.INPUT[]
{
new(
type: User32.INPUT_TYPE.INPUT_MOUSE,
data: new User32.INPUT.DUMMYUNIONNAME(
mi: new User32.MOUSEINPUT(
dx: (int)MouseHelper.CalculateAbsoluteCoordinateX(location.X),
dy: (int)MouseHelper.CalculateAbsoluteCoordinateY(location.Y),
mouseData: 0,
dwFlags: User32.MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | User32.MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE,
time: 0,
dwExtraInfo: ULONG_PTR.Null))),
};
var result = User32.SendInput(
(uint)inputs.Length,
new User32.LPINPUT(inputs),
User32.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(User32.SYSTEM_METRICS_INDEX.SM_CXSCREEN);
}
internal 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(User32.SYSTEM_METRICS_INDEX.SM_CYSCREEN);
}
}

View File

@@ -1,94 +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.ComponentModel;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.Models.Screen;
using MouseJumpUI.NativeMethods;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.NativeMethods.User32;
namespace MouseJumpUI.Helpers;
internal static class ScreenHelper
{
/// <summary>
/// Duplicates functionality available in System.Windows.Forms.SystemInformation
/// to reduce the dependency on WinForms
/// </summary>
public 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 result = User32.EnumDisplayMonitors(
HDC.Null,
LPCRECT.Null,
(unnamedParam1, unnamedParam2, unnamedParam3, unnamedParam4) =>
{
hMonitors.Add(unnamedParam1);
return true;
},
LPARAM.Null);
if (!result)
{
throw new Win32Exception(
$"{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((uint)MONITORINFO.Size, RECT.Empty, RECT.Empty, 0));
result = User32.GetMonitorInfoW(hMonitor, monitorInfoPtr);
if (!result)
{
throw new Win32Exception(
$"{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 HMONITOR MonitorFromPoint(
PointInfo pt)
{
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}");
}
return hMonitor;
}
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Styles;
namespace MouseJumpUI.Helpers;
internal 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);
}
}