Skip to content

Commit 542f6af

Browse files
committed
Fix stuck modifier keys in GlobalKeyboardListener on desktop lock/unlock. Provide a public DesktopLockNotifierForm.
1 parent 90baad0 commit 542f6af

File tree

1 file changed

+87
-1
lines changed

1 file changed

+87
-1
lines changed

Src/GlobalKeyboardListener.cs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
3+
using System.ComponentModel;
4+
using System.Runtime.InteropServices;
35
using System.Windows.Forms;
46

57
namespace RT.Util
@@ -30,13 +32,22 @@ public sealed class GlobalKeyboardListener : IDisposable
3032
/// <summary>Keeps the managed delegate referenced so that the garbage collector doesn’t collect it.</summary>
3133
private WinAPI.KeyboardHookProc _hook;
3234

35+
private DesktopLockNotifierForm _lockNotifier;
36+
private Timer _afterLockTimer;
37+
3338
/// <summary>
3439
/// Initializes a new instance of the <see cref="GlobalKeyboardListener" /> class and installs the keyboard hook.</summary>
3540
public GlobalKeyboardListener()
3641
{
3742
IntPtr hInstance = WinAPI.LoadLibrary("User32");
3843
_hook = hookProc; // don’t remove this or the garbage collector will collect it while the global hook still tries to access it
3944
_hHook = WinAPI.SetWindowsHookEx(WinAPI.WH_KEYBOARD_LL, _hook, IntPtr.Zero, 0);
45+
_lockNotifier = new DesktopLockNotifierForm();
46+
_lockNotifier.SessionLocked += recheckPossibleSwallowedKeys;
47+
_lockNotifier.SessionUnlocked += recheckPossibleSwallowedKeys;
48+
_afterLockTimer = new Timer();
49+
_afterLockTimer.Interval = 1000;
50+
_afterLockTimer.Tick += recheckPossibleSwallowedKeysTimer;
4051
}
4152

4253
private bool _disposed = false;
@@ -55,6 +66,9 @@ public void Dispose()
5566
if (!_disposed)
5667
{
5768
_disposed = true;
69+
_lockNotifier.Close();
70+
_lockNotifier.Dispose();
71+
_afterLockTimer.Dispose();
5872
WinAPI.UnhookWindowsHookEx(_hHook);
5973
}
6074
}
@@ -133,6 +147,36 @@ private int hookProc(int code, int wParam, ref WinAPI.KeyboardHookStruct lParam)
133147
}
134148
return WinAPI.CallNextHookEx(_hHook, code, wParam, ref lParam);
135149
}
150+
151+
private void recheckPossibleSwallowedKeys(object sender, EventArgs e)
152+
{
153+
// When the desktop is locked with a quick press of Win+L, the up events for these keypresses are swallowed, even though all subsequent keypresses
154+
// get through to the handler even though we're on the lock screen already. In this scenario the "Session Locked" notification can arrive while the keys are
155+
// actually still down, even though the up events will still be swallowed shortly after, breaking this check. Hence the repeat on timeout.
156+
// A similar issue occurs with Ctrl/Alt when unlocking with Ctrl+Alt+Del, and it similarly requires a timer to re-check the keys.
157+
recheckPossibleSwallowedKeys();
158+
_afterLockTimer.Start();
159+
// All of this key swallowing on lock/unlock means that Up/Down events from this Global Keyboard Listener are not always paired correctly.
160+
// We do not attempt to emulate the swallowed events even for the modifier keys (which are easier because we track their state already).
161+
}
162+
163+
private void recheckPossibleSwallowedKeysTimer(object sender, EventArgs e)
164+
{
165+
_afterLockTimer.Stop();
166+
recheckPossibleSwallowedKeys();
167+
}
168+
169+
private void recheckPossibleSwallowedKeys()
170+
{
171+
if (_ctrl && WinAPI.GetKeyState((int) Keys.LControlKey) >= 0 && WinAPI.GetKeyState((int) Keys.RControlKey) >= 0)
172+
_ctrl = false;
173+
if (_alt && WinAPI.GetKeyState((int) Keys.LMenu) >= 0 && WinAPI.GetKeyState((int) Keys.RMenu) >= 0)
174+
_alt = false;
175+
if (_shift && WinAPI.GetKeyState((int) Keys.LShiftKey) >= 0 && WinAPI.GetKeyState((int) Keys.RShiftKey) >= 0)
176+
_shift = false;
177+
if (_win && WinAPI.GetKeyState((int) Keys.LWin) >= 0 && WinAPI.GetKeyState((int) Keys.RWin) >= 0)
178+
_win = false;
179+
}
136180
}
137181

138182
/// <summary>Encapsulates the current state of modifier keys.</summary>
@@ -189,4 +233,46 @@ public GlobalKeyEventArgs(Keys virtualKeyCode, int scanCode, ModifierKeysState m
189233

190234
/// <summary>Used to trigger the KeyUp/KeyDown events in <see cref="GlobalKeyboardListener" />.</summary>
191235
public delegate void GlobalKeyEventHandler(object sender, GlobalKeyEventArgs e);
236+
237+
/// <summary>
238+
/// Subscribes to desktop (session) lock/unlock notifications and exposes events for these. It's untested whether
239+
/// disposing correctly unsubscribes from the notifications, so you should call Close and then Dispose to shut down
240+
/// the notifier.</summary>
241+
public class DesktopLockNotifierForm : Form
242+
{
243+
/// <summary>Constructor.</summary>
244+
public DesktopLockNotifierForm()
245+
{
246+
WTSRegisterSessionNotification(Handle, 0 /*NOTIFY_FOR_THIS_SESSION*/);
247+
}
248+
/// <summary>Close handler.</summary>
249+
protected override void OnClosing(CancelEventArgs e)
250+
{
251+
WTSUnRegisterSessionNotification(Handle);
252+
base.OnClosing(e);
253+
}
254+
/// <summary>Triggers every time the desktop (session) is locked.</summary>
255+
public event EventHandler SessionLocked;
256+
/// <summary>Triggers every time the desktop (session) is unlocked.</summary>
257+
public event EventHandler SessionUnlocked;
258+
259+
/// <summary>Message handler.</summary>
260+
protected override void WndProc(ref Message m)
261+
{
262+
if (m.Msg == 0x2b1/*WM_WTSSESSION_CHANGE*/)
263+
{
264+
int value = m.WParam.ToInt32();
265+
if (value == 7 /*WTS_SESSION_LOCK*/)
266+
SessionLocked?.Invoke(this, EventArgs.Empty);
267+
if (value == 8 /*WTS_SESSION_UNLOCK*/)
268+
SessionUnlocked?.Invoke(this, EventArgs.Empty);
269+
}
270+
base.WndProc(ref m);
271+
}
272+
273+
[DllImport("wtsapi32.dll", SetLastError = true)]
274+
static extern bool WTSRegisterSessionNotification(IntPtr hWnd, [MarshalAs(UnmanagedType.U4)] int dwFlags);
275+
[DllImport("WtsApi32.dll")]
276+
static extern bool WTSUnRegisterSessionNotification(IntPtr hWnd);
277+
}
192278
}

0 commit comments

Comments
 (0)