Skip to content

Commit 7ae12c1

Browse files
fixed the stability of the logic
1 parent a7f5fe0 commit 7ae12c1

File tree

6 files changed

+267
-32
lines changed

6 files changed

+267
-32
lines changed

.vscode/launch.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "Launch Desktop App",
6+
"type": "coreclr",
7+
"request": "launch",
8+
"program": "${workspaceFolder}/Desktop/bin/Debug/net9.0-windows/Desktop.exe",
9+
"args": [],
10+
"cwd": "${workspaceFolder}/Desktop",
11+
"console": "internalConsole",
12+
"stopAtEntry": false,
13+
"preLaunchTask": "build"
14+
}
15+
]
16+
}

.vscode/tasks.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "build",
6+
"command": "dotnet",
7+
"type": "process",
8+
"args": [
9+
"build",
10+
"${workspaceFolder}/Desktop/Desktop.csproj",
11+
"/property:GenerateFullPaths=true",
12+
"/consoleloggerparameters:NoSummary"
13+
],
14+
"group": "build",
15+
"presentation": {
16+
"reveal": "silent"
17+
},
18+
"problemMatcher": "$msCompile"
19+
},
20+
{
21+
"label": "publish",
22+
"command": "dotnet",
23+
"type": "process",
24+
"args": [
25+
"publish",
26+
"${workspaceFolder}/Desktop/Desktop.csproj",
27+
"--configuration",
28+
"Release",
29+
"--output",
30+
"${workspaceFolder}/Desktop/bin/Release/net9.0-windows/publish/"
31+
],
32+
"group": "build",
33+
"presentation": {
34+
"reveal": "always"
35+
},
36+
"problemMatcher": "$msCompile"
37+
}
38+
]
39+
}

Desktop/Desktop.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
<Product>Input Language Screamer</Product>
1313
<Description>Plays language-specific MP3 audio files when Windows input language changes using Alt+Shift</Description>
1414
<Copyright>© 2025 OneBit Software</Copyright>
15-
<AssemblyVersion>1.1.0.0</AssemblyVersion>
16-
<FileVersion>1.1.0.0</FileVersion>
15+
<AssemblyVersion>1.2.0.0</AssemblyVersion>
16+
<FileVersion>1.2.0.0</FileVersion>
1717

1818
<!-- Publishing Configuration -->
1919
<PublishSingleFile>true</PublishSingleFile>

Desktop/GlobalKeyboardHook.cs

Lines changed: 143 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)