Skip to content

Commit ef0ff1b

Browse files
committed
Initial setup of CursorKeeper
1 parent cb7b692 commit ef0ff1b

File tree

9 files changed

+334
-0
lines changed

9 files changed

+334
-0
lines changed

CursorKeeper.sln

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.12.35707.178 d17.12
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CursorKeeper", "CursorKeeper\CursorKeeper.csproj", "{D72F4513-6DDE-4933-924F-38F044FE8D0F}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{D72F4513-6DDE-4933-924F-38F044FE8D0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{D72F4513-6DDE-4933-924F-38F044FE8D0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{D72F4513-6DDE-4933-924F-38F044FE8D0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{D72F4513-6DDE-4933-924F-38F044FE8D0F}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
EndGlobal

CursorKeeper/CursorKeeper.csproj

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>WinExe</OutputType>
4+
<TargetFramework>net8.0-windows</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>disable</ImplicitUsings>
7+
<UseWindowsForms>true</UseWindowsForms>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<None Remove="Resources\tray-icon16.png" />
11+
</ItemGroup>
12+
<ItemGroup>
13+
<EmbeddedResource Include="Resources\tray-icon16.png" />
14+
</ItemGroup>
15+
</Project>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System;
2+
using System.Drawing;
3+
using System.Windows.Forms;
4+
using CursorKeeper.Services.CursorConstraint;
5+
6+
namespace CursorKeeper
7+
{
8+
public sealed class CursorKeeperContext : ApplicationContext, IDisposable
9+
{
10+
private readonly NotifyIcon _trayIcon;
11+
private readonly ICursorConstraintService _cursorService;
12+
private readonly ToolStripMenuItem _enableMenuItem;
13+
private bool _disposed;
14+
15+
public CursorKeeperContext()
16+
{
17+
(_trayIcon, _enableMenuItem) = InitializeComponents();
18+
_cursorService = new CursorConstraintService();
19+
_cursorService.EnableConstraints(); // Start with constraints enabled
20+
}
21+
22+
private (NotifyIcon icon, ToolStripMenuItem menuItem) InitializeComponents()
23+
{
24+
// Create context menu
25+
var contextMenu = new ContextMenuStrip();
26+
var enableMenuItem = new ToolStripMenuItem("Disable CursorKeeper", null, ToggleConstraints);
27+
var exitMenuItem = new ToolStripMenuItem("Exit", null, Exit);
28+
29+
contextMenu.Items.AddRange(new ToolStripItem[]
30+
{
31+
enableMenuItem,
32+
new ToolStripSeparator(),
33+
exitMenuItem
34+
});
35+
36+
// Create tray icon
37+
var icon = new NotifyIcon
38+
{
39+
Icon = CreateCursorIcon(),
40+
ContextMenuStrip = contextMenu,
41+
Text = "CursorKeeper - Active",
42+
Visible = true
43+
};
44+
45+
icon.MouseClick += (s, e) =>
46+
{
47+
if (e.Button == MouseButtons.Left)
48+
icon.ContextMenuStrip.Show(Cursor.Position);
49+
};
50+
51+
return (icon, enableMenuItem);
52+
}
53+
54+
private static Icon CreateCursorIcon()
55+
{
56+
using var stream = typeof(CursorKeeperContext).Assembly
57+
.GetManifestResourceStream("CursorKeeper.Resources.tray-icon16.png");
58+
if (stream == null)
59+
throw new InvalidOperationException("Could not load tray icon resource");
60+
61+
using var bitmap = new Bitmap(stream);
62+
return Icon.FromHandle(bitmap.GetHicon());
63+
}
64+
65+
private void ToggleConstraints(object? sender, EventArgs e)
66+
{
67+
if (_cursorService.IsEnabled)
68+
{
69+
_cursorService.DisableConstraints();
70+
_enableMenuItem.Text = "Enable CursorKeeper";
71+
_trayIcon.Text = "CursorKeeper - Inactive";
72+
}
73+
else
74+
{
75+
_cursorService.EnableConstraints();
76+
_enableMenuItem.Text = "Disable CursorKeeper";
77+
_trayIcon.Text = "CursorKeeper - Active";
78+
}
79+
}
80+
81+
private void Exit(object? sender, EventArgs e)
82+
{
83+
Dispose();
84+
Application.Exit();
85+
}
86+
87+
public void Dispose()
88+
{
89+
if (_disposed) return;
90+
91+
_trayIcon.Visible = false;
92+
_trayIcon.Dispose();
93+
_cursorService.Dispose();
94+
_disposed = true;
95+
96+
GC.SuppressFinalize(this);
97+
}
98+
}
99+
}

CursorKeeper/Program.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
using System.Windows.Forms;
3+
4+
namespace CursorKeeper
5+
{
6+
public class Program
7+
{
8+
[STAThread]
9+
static void Main()
10+
{
11+
Application.EnableVisualStyles();
12+
Application.SetCompatibleTextRenderingDefault(false);
13+
Application.SetHighDpiMode(HighDpiMode.SystemAware);
14+
Application.Run(new CursorKeeperContext());
15+
}
16+
}
17+
}
638 Bytes
Loading
Lines changed: 35 additions & 0 deletions
Loading
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using System;
2+
using System.Drawing;
3+
using System.Runtime.InteropServices;
4+
using System.Windows.Forms;
5+
6+
namespace CursorKeeper.Services.CursorConstraint
7+
{
8+
public sealed class CursorConstraintService : ICursorConstraintService
9+
{
10+
private const int WH_MOUSE_LL = 14;
11+
private const int WM_MOUSEMOVE = 0x200;
12+
private const int HC_ACTION = 0;
13+
private const int EDGE_PADDING = 2;
14+
15+
private IntPtr _hookHandle = IntPtr.Zero;
16+
private readonly MouseHookDelegate _hookDelegate;
17+
private readonly Screen _primaryScreen;
18+
private Point _lastValidPosition;
19+
private bool _disposed;
20+
21+
public bool IsEnabled { get; private set; }
22+
23+
private delegate IntPtr MouseHookDelegate(int nCode, IntPtr wParam, ref MSLLHOOKSTRUCT lParam);
24+
25+
[StructLayout(LayoutKind.Sequential)]
26+
private struct POINT
27+
{
28+
public int x;
29+
public int y;
30+
}
31+
32+
[StructLayout(LayoutKind.Sequential)]
33+
private struct MSLLHOOKSTRUCT
34+
{
35+
public POINT pt;
36+
public int mouseData;
37+
public int flags;
38+
public int time;
39+
public IntPtr dwExtraInfo;
40+
}
41+
42+
[DllImport("user32.dll")]
43+
private static extern IntPtr SetWindowsHookEx(int idHook, MouseHookDelegate lpfn, IntPtr hMod, uint dwThreadId);
44+
45+
[DllImport("user32.dll")]
46+
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
47+
48+
[DllImport("user32.dll")]
49+
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, ref MSLLHOOKSTRUCT lParam);
50+
51+
[DllImport("user32.dll")]
52+
private static extern bool SetCursorPos(int x, int y);
53+
54+
public CursorConstraintService()
55+
{
56+
_hookDelegate = MouseHookCallback;
57+
_primaryScreen = Screen.PrimaryScreen ?? throw new InvalidOperationException("No primary screen found");
58+
_lastValidPosition = Cursor.Position;
59+
}
60+
61+
public void EnableConstraints()
62+
{
63+
if (IsEnabled) return;
64+
65+
_hookHandle = SetWindowsHookEx(WH_MOUSE_LL, _hookDelegate, IntPtr.Zero, 0);
66+
if (_hookHandle == IntPtr.Zero)
67+
throw new InvalidOperationException("Failed to set mouse hook");
68+
69+
IsEnabled = true;
70+
}
71+
72+
public void DisableConstraints()
73+
{
74+
if (!IsEnabled || _hookHandle == IntPtr.Zero) return;
75+
76+
UnhookWindowsHookEx(_hookHandle);
77+
_hookHandle = IntPtr.Zero;
78+
IsEnabled = false;
79+
}
80+
81+
private IntPtr MouseHookCallback(int nCode, IntPtr wParam, ref MSLLHOOKSTRUCT lParam)
82+
{
83+
if (nCode == HC_ACTION && wParam == (IntPtr)WM_MOUSEMOVE)
84+
{
85+
var currentPos = new Point(lParam.pt.x, lParam.pt.y);
86+
87+
if (!IsPositionInPrimaryScreen(currentPos))
88+
{
89+
var newPos = CalculateValidPosition(currentPos);
90+
SetCursorPos(newPos.X, newPos.Y);
91+
_lastValidPosition = newPos;
92+
return (IntPtr)1;
93+
}
94+
95+
_lastValidPosition = currentPos;
96+
}
97+
98+
return CallNextHookEx(_hookHandle, nCode, wParam, ref lParam);
99+
}
100+
101+
private bool IsPositionInPrimaryScreen(Point position)
102+
{
103+
var paddedBounds = new Rectangle(
104+
_primaryScreen.Bounds.Left + EDGE_PADDING,
105+
_primaryScreen.Bounds.Top + EDGE_PADDING,
106+
_primaryScreen.Bounds.Width - (2 * EDGE_PADDING),
107+
_primaryScreen.Bounds.Height - (2 * EDGE_PADDING)
108+
);
109+
110+
return paddedBounds.Contains(position);
111+
}
112+
113+
private Point CalculateValidPosition(Point currentPos) =>
114+
new(
115+
Math.Max(_primaryScreen.Bounds.Left + EDGE_PADDING,
116+
Math.Min(currentPos.X, _primaryScreen.Bounds.Right - EDGE_PADDING)),
117+
Math.Max(_primaryScreen.Bounds.Top + EDGE_PADDING,
118+
Math.Min(currentPos.Y, _primaryScreen.Bounds.Bottom - EDGE_PADDING))
119+
);
120+
121+
public void Dispose()
122+
{
123+
if (_disposed) return;
124+
125+
DisableConstraints();
126+
_disposed = true;
127+
128+
GC.SuppressFinalize(this);
129+
}
130+
}
131+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace CursorKeeper.Services.CursorConstraint
8+
{
9+
public interface ICursorConstraintService : IDisposable
10+
{
11+
bool IsEnabled { get; }
12+
void EnableConstraints();
13+
void DisableConstraints();
14+
}
15+
}

screenshots/tray-icon.png

2.48 KB
Loading

0 commit comments

Comments
 (0)