@@ -15,15 +15,24 @@ public class GlobalKeyboardHook
15
15
{
16
16
// Windows API constants for keyboard hook
17
17
private const int WH_KEYBOARD_LL = 13 ; // Low-level keyboard input hook
18
+ private const int WM_KEYDOWN = 0x0100 ; // Key down message
18
19
private const int WM_KEYUP = 0x0101 ; // Key up message
20
+ private const int WM_SYSKEYDOWN = 0x0104 ; // System key down (Alt combinations)
21
+ private const int WM_SYSKEYUP = 0x0105 ; // System key up (Alt combinations)
19
22
private const int VK_LMENU = 0xA4 ; // Left Alt key
20
23
private const int VK_RMENU = 0xA5 ; // Right Alt key
24
+ private const int VK_LSHIFT = 0xA0 ; // Left Shift key
25
+ private const int VK_RSHIFT = 0xA1 ; // Right Shift key
26
+ private const int VK_SHIFT = 0x10 ; // Generic Shift key
21
27
22
28
// Static fields for the hook
23
29
private static LowLevelKeyboardProc _proc = HookCallback ;
24
30
private static IntPtr _hookID = IntPtr . Zero ;
25
- private static InputLanguage ? _currentLanguage ;
26
- private static Action ? _onLanguageChange ;
31
+ private static uint _currentInputLanguage = 0 ;
32
+ private static Action < string > ? _onLanguageChange ;
33
+ private static System . Threading . Timer ? _languageCheckTimer ;
34
+ private static bool _leftAltPressed = false ;
35
+ private static bool _leftShiftPressed = false ;
27
36
28
37
/// <summary>
29
38
/// Delegate for the low-level keyboard procedure
@@ -33,11 +42,17 @@ public class GlobalKeyboardHook
33
42
/// <summary>
34
43
/// Initializes the global keyboard hook with a callback for language changes
35
44
/// </summary>
36
- /// <param name="onLanguageChange">Action to execute when language changes</param>
37
- public GlobalKeyboardHook ( Action onLanguageChange )
45
+ /// <param name="onLanguageChange">Action to execute when language changes, receives the new language name </param>
46
+ public GlobalKeyboardHook ( Action < string > onLanguageChange )
38
47
{
39
48
_onLanguageChange = onLanguageChange ;
40
- _currentLanguage = InputLanguage . CurrentInputLanguage ;
49
+
50
+ // Get the initial keyboard layout for the foreground window's thread
51
+ var foregroundWindow = GetForegroundWindow ( ) ;
52
+ var threadId = GetWindowThreadProcessId ( foregroundWindow , IntPtr . Zero ) ;
53
+ var keyboardLayout = GetKeyboardLayout ( threadId ) ;
54
+ _currentInputLanguage = ( uint ) ( ( long ) keyboardLayout & 0xFFFF ) ;
55
+
41
56
_hookID = SetHook ( _proc ) ;
42
57
}
43
58
@@ -58,7 +73,7 @@ private static IntPtr SetHook(LowLevelKeyboardProc proc)
58
73
59
74
/// <summary>
60
75
/// Callback function that processes keyboard events from the hook
61
- /// Monitors for Alt key releases and checks for language changes
76
+ /// Monitors for potential language switching keys and checks for language changes
62
77
/// </summary>
63
78
/// <param name="nCode">Hook code</param>
64
79
/// <param name="wParam">Message identifier</param>
@@ -72,15 +87,49 @@ private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
72
87
// Get the virtual key code from the keyboard input structure
73
88
int vkCode = Marshal . ReadInt32 ( lParam ) ;
74
89
75
- // Check if this is a key release event
76
- if ( wParam == ( IntPtr ) WM_KEYUP )
90
+ // Check if this is a key press or release event
91
+ bool isKeyDown = wParam == ( IntPtr ) WM_KEYDOWN || wParam == ( IntPtr ) WM_SYSKEYDOWN ;
92
+ bool isKeyUp = wParam == ( IntPtr ) WM_KEYUP || wParam == ( IntPtr ) WM_SYSKEYUP ;
93
+
94
+ // Track Left Alt and Left Shift key states
95
+ if ( vkCode == VK_LMENU )
77
96
{
78
- // Check if Alt key was released (left or right Alt)
79
- if ( vkCode == VK_LMENU || vkCode == VK_RMENU )
97
+ if ( isKeyDown )
98
+ {
99
+ _leftAltPressed = true ;
100
+ Debug . WriteLine ( "Left Alt pressed" ) ;
101
+ }
102
+ else if ( isKeyUp )
103
+ {
104
+ _leftAltPressed = false ;
105
+ Debug . WriteLine ( "Left Alt released" ) ;
106
+
107
+ // Check for language change when Alt is released if Shift was also pressed
108
+ if ( _leftShiftPressed )
109
+ {
110
+ Debug . WriteLine ( "Alt+Shift combination detected (Alt released) - checking for language change" ) ;
111
+ ScheduleLanguageCheck ( ) ;
112
+ }
113
+ }
114
+ }
115
+ else if ( vkCode == VK_LSHIFT )
116
+ {
117
+ if ( isKeyDown )
118
+ {
119
+ _leftShiftPressed = true ;
120
+ Debug . WriteLine ( "Left Shift pressed" ) ;
121
+ }
122
+ else if ( isKeyUp )
80
123
{
81
- // Check for language change when Alt is released
82
- // This catches Alt+Shift language switching
83
- CheckLanguageChange ( ) ;
124
+ _leftShiftPressed = false ;
125
+ Debug . WriteLine ( "Left Shift released" ) ;
126
+
127
+ // Check for language change when Shift is released if Alt was also pressed
128
+ if ( _leftAltPressed )
129
+ {
130
+ Debug . WriteLine ( "Alt+Shift combination detected (Shift released) - checking for language change" ) ;
131
+ ScheduleLanguageCheck ( ) ;
132
+ }
84
133
}
85
134
}
86
135
}
@@ -89,25 +138,84 @@ private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
89
138
return CallNextHookEx ( _hookID , nCode , wParam , lParam ) ;
90
139
}
91
140
141
+ /// <summary>
142
+ /// Schedules a single delayed language check
143
+ /// </summary>
144
+ private static void ScheduleLanguageCheck ( )
145
+ {
146
+ // Cancel any existing timer to prevent duplicates
147
+ _languageCheckTimer ? . Dispose ( ) ;
148
+
149
+ // Schedule a single check after a delay to allow the language change to complete
150
+ _languageCheckTimer = new System . Threading . Timer (
151
+ callback : _ => CheckLanguageChange ( ) ,
152
+ state : null ,
153
+ dueTime : TimeSpan . FromMilliseconds ( 250 ) ,
154
+ period : Timeout . InfiniteTimeSpan
155
+ ) ;
156
+ }
157
+
92
158
/// <summary>
93
159
/// Checks if the input language has changed and triggers the callback if it has
94
160
/// </summary>
95
161
private static void CheckLanguageChange ( )
96
162
{
97
- var newLanguage = InputLanguage . CurrentInputLanguage ;
98
- if ( newLanguage != _currentLanguage )
163
+ try
164
+ {
165
+ // Get the keyboard layout for the foreground window's thread instead of current thread
166
+ var foregroundWindow = GetForegroundWindow ( ) ;
167
+ var threadId = GetWindowThreadProcessId ( foregroundWindow , IntPtr . Zero ) ;
168
+ var keyboardLayout = GetKeyboardLayout ( threadId ) ;
169
+ var newInputLanguage = ( uint ) ( ( long ) keyboardLayout & 0xFFFF ) ;
170
+
171
+ // Get language name for debugging
172
+ string currentLangName = GetLanguageName ( _currentInputLanguage ) ;
173
+ string newLangName = GetLanguageName ( newInputLanguage ) ;
174
+
175
+ Debug . WriteLine ( $ "Current: { currentLangName } (0x{ _currentInputLanguage : X4} ), New: { newLangName } (0x{ newInputLanguage : X4} ), Thread: { threadId } , Layout: 0x{ ( long ) keyboardLayout : X8} ") ;
176
+
177
+ if ( newInputLanguage != _currentInputLanguage )
178
+ {
179
+ Debug . WriteLine ( $ "Language changed from { currentLangName } to { newLangName } ") ;
180
+ _currentInputLanguage = newInputLanguage ;
181
+ _onLanguageChange ? . Invoke ( newLangName ) ;
182
+ }
183
+ }
184
+ catch ( Exception ex )
99
185
{
100
- _currentLanguage = newLanguage ;
101
- _onLanguageChange ? . Invoke ( ) ;
186
+ Debug . WriteLine ( $ "Error checking language change: { ex . Message } " ) ;
187
+ // Silently ignore errors to prevent disrupting the hook
102
188
}
103
189
}
104
190
191
+ /// <summary>
192
+ /// Gets a readable language name from language ID for debugging and audio file matching
193
+ /// </summary>
194
+ private static string GetLanguageName ( uint langId )
195
+ {
196
+ return langId switch
197
+ {
198
+ 0x0409 => "English" ,
199
+ 0x0809 => "English" ,
200
+ 0x0402 => "Bulgarian" ,
201
+ 0x0407 => "German" ,
202
+ 0x040C => "French" ,
203
+ 0x0410 => "Italian" ,
204
+ 0x0C0A => "Spanish" ,
205
+ 0x0419 => "Russian" ,
206
+ 0x041F => "Turkish" ,
207
+ _ => $ "Unknown"
208
+ } ;
209
+ }
210
+
105
211
/// <summary>
106
212
/// Disposes of the keyboard hook resources
107
213
/// </summary>
108
214
public void Dispose ( )
109
215
{
110
216
UnhookWindowsHookEx ( _hookID ) ;
217
+ _languageCheckTimer ? . Dispose ( ) ;
218
+ _languageCheckTimer = null ;
111
219
}
112
220
113
221
// Windows API function imports for keyboard hook functionality
@@ -138,4 +246,22 @@ private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
138
246
/// </summary>
139
247
[ DllImport ( "kernel32.dll" , CharSet = CharSet . Auto , SetLastError = true ) ]
140
248
private static extern IntPtr GetModuleHandle ( string lpModuleName ) ;
249
+
250
+ /// <summary>
251
+ /// Retrieves the active input locale identifier (keyboard layout) for the specified thread
252
+ /// </summary>
253
+ [ DllImport ( "user32.dll" ) ]
254
+ private static extern IntPtr GetKeyboardLayout ( uint idThread ) ;
255
+
256
+ /// <summary>
257
+ /// Retrieves a handle to the foreground window
258
+ /// </summary>
259
+ [ DllImport ( "user32.dll" ) ]
260
+ private static extern IntPtr GetForegroundWindow ( ) ;
261
+
262
+ /// <summary>
263
+ /// Retrieves the identifier of the thread that created the specified window
264
+ /// </summary>
265
+ [ DllImport ( "user32.dll" ) ]
266
+ private static extern uint GetWindowThreadProcessId ( IntPtr hWnd , IntPtr processId ) ;
141
267
}
0 commit comments