@@ -15,15 +15,24 @@ public class GlobalKeyboardHook
1515{
1616 // Windows API constants for keyboard hook
1717 private const int WH_KEYBOARD_LL = 13 ; // Low-level keyboard input hook
18+ private const int WM_KEYDOWN = 0x0100 ; // Key down message
1819 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)
1922 private const int VK_LMENU = 0xA4 ; // Left Alt key
2023 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
2127
2228 // Static fields for the hook
2329 private static LowLevelKeyboardProc _proc = HookCallback ;
2430 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 ;
2736
2837 /// <summary>
2938 /// Delegate for the low-level keyboard procedure
@@ -33,11 +42,17 @@ public class GlobalKeyboardHook
3342 /// <summary>
3443 /// Initializes the global keyboard hook with a callback for language changes
3544 /// </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 )
3847 {
3948 _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+
4156 _hookID = SetHook ( _proc ) ;
4257 }
4358
@@ -58,7 +73,7 @@ private static IntPtr SetHook(LowLevelKeyboardProc proc)
5873
5974 /// <summary>
6075 /// 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
6277 /// </summary>
6378 /// <param name="nCode">Hook code</param>
6479 /// <param name="wParam">Message identifier</param>
@@ -72,15 +87,49 @@ private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
7287 // Get the virtual key code from the keyboard input structure
7388 int vkCode = Marshal . ReadInt32 ( lParam ) ;
7489
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 )
7796 {
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 )
80123 {
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+ }
84133 }
85134 }
86135 }
@@ -89,25 +138,84 @@ private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
89138 return CallNextHookEx ( _hookID , nCode , wParam , lParam ) ;
90139 }
91140
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+
92158 /// <summary>
93159 /// Checks if the input language has changed and triggers the callback if it has
94160 /// </summary>
95161 private static void CheckLanguageChange ( )
96162 {
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 )
99185 {
100- _currentLanguage = newLanguage ;
101- _onLanguageChange ? . Invoke ( ) ;
186+ Debug . WriteLine ( $ "Error checking language change: { ex . Message } " ) ;
187+ // Silently ignore errors to prevent disrupting the hook
102188 }
103189 }
104190
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+
105211 /// <summary>
106212 /// Disposes of the keyboard hook resources
107213 /// </summary>
108214 public void Dispose ( )
109215 {
110216 UnhookWindowsHookEx ( _hookID ) ;
217+ _languageCheckTimer ? . Dispose ( ) ;
218+ _languageCheckTimer = null ;
111219 }
112220
113221 // Windows API function imports for keyboard hook functionality
@@ -138,4 +246,22 @@ private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
138246 /// </summary>
139247 [ DllImport ( "kernel32.dll" , CharSet = CharSet . Auto , SetLastError = true ) ]
140248 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 ) ;
141267}
0 commit comments