diff --git a/ExplorerPatcher/ExplorerPatcher.vcxproj b/ExplorerPatcher/ExplorerPatcher.vcxproj index 5c66d0a3d..68b867e62 100644 --- a/ExplorerPatcher/ExplorerPatcher.vcxproj +++ b/ExplorerPatcher/ExplorerPatcher.vcxproj @@ -245,6 +245,9 @@ true + + true + true diff --git a/ExplorerPatcher/ExplorerPatcher.vcxproj.filters b/ExplorerPatcher/ExplorerPatcher.vcxproj.filters index 4da58ee89..e8e49b9c5 100644 --- a/ExplorerPatcher/ExplorerPatcher.vcxproj.filters +++ b/ExplorerPatcher/ExplorerPatcher.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -178,6 +178,9 @@ Source Files + + Source Files + Source Files diff --git a/ExplorerPatcher/StartSearchFix.c b/ExplorerPatcher/StartSearchFix.c new file mode 100644 index 000000000..de074a969 --- /dev/null +++ b/ExplorerPatcher/StartSearchFix.c @@ -0,0 +1,334 @@ +#include "StartSearchFix.h" +#include +#include + +#define MAX_BUFFERED_KEYS 64 + +typedef struct +{ + DWORD vk; + DWORD scan; +} BufferedKey; + +static HHOOK g_hHook = NULL; +static HANDLE g_hHookThread = NULL; +static DWORD g_dwHookThreadId = 0; + +static volatile BOOL g_isTransitioning = FALSE; +static volatile BOOL g_searchActive = FALSE; +static DWORD g_transitionStartTick = 0; + +static CRITICAL_SECTION g_csBuffer; +static BufferedKey g_keyBuffer[MAX_BUFFERED_KEYS]; +static int g_keyBufferCount = 0; + +// Process name cache +static DWORD g_lastCheckedPid = 0; +static WCHAR g_lastCheckedPName[MAX_PATH] = { 0 }; +static CRITICAL_SECTION g_csPidCache; + +static void BufferKey(DWORD vk, DWORD scan) +{ + EnterCriticalSection(&g_csBuffer); + if (g_keyBufferCount < MAX_BUFFERED_KEYS) + { + g_keyBuffer[g_keyBufferCount].vk = vk; + g_keyBuffer[g_keyBufferCount].scan = scan; + g_keyBufferCount++; + } + LeaveCriticalSection(&g_csBuffer); +} + +static int CopyAndClearBuffer(BufferedKey* dest, int maxCount) +{ + int count = 0; + EnterCriticalSection(&g_csBuffer); + count = (g_keyBufferCount < maxCount) ? g_keyBufferCount : maxCount; + for (int i = 0; i < count; i++) + { + dest[i] = g_keyBuffer[i]; + } + g_keyBufferCount = 0; + LeaveCriticalSection(&g_csBuffer); + return count; +} + +static void GetCachedProcessName(DWORD pid, WCHAR* outName, DWORD cchMax) +{ + if (pid == 0 || !outName || cchMax == 0) + { + if (outName && cchMax > 0) outName[0] = L'\0'; + return; + } + + EnterCriticalSection(&g_csPidCache); + if (pid == g_lastCheckedPid && g_lastCheckedPName[0] != L'\0') + { + wcsncpy_s(outName, cchMax, g_lastCheckedPName, _TRUNCATE); + LeaveCriticalSection(&g_csPidCache); + return; + } + + g_lastCheckedPid = pid; + g_lastCheckedPName[0] = L'\0'; + + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnapshot != INVALID_HANDLE_VALUE) + { + PROCESSENTRY32W pe; + pe.dwSize = sizeof(PROCESSENTRY32W); + if (Process32FirstW(hSnapshot, &pe)) + { + do + { + if (pe.th32ProcessID == pid) + { + wcsncpy_s(g_lastCheckedPName, MAX_PATH, pe.szExeFile, _TRUNCATE); + break; + } + } while (Process32NextW(hSnapshot, &pe)); + } + CloseHandle(hSnapshot); + } + + wcsncpy_s(outName, cchMax, g_lastCheckedPName, _TRUNCATE); + LeaveCriticalSection(&g_csPidCache); +} + +static BOOL IsTypingKey(DWORD vk) +{ + // A-Z + if (vk >= 0x41 && vk <= 0x5A) return TRUE; + // 0-9 + if (vk >= 0x30 && vk <= 0x39) return TRUE; + // Numpad 0-9 + if (vk >= 0x60 && vk <= 0x69) return TRUE; + // Space, Backspace + if (vk == VK_SPACE || vk == VK_BACK) return TRUE; + // OEM keys for international/Turkish layouts + if (vk >= 0xBA && vk <= 0xC0) return TRUE; + if (vk >= 0xDB && vk <= 0xDF) return TRUE; + if (vk == 0xE2) return TRUE; + + return FALSE; +} + +static DWORD WINAPI TransitionWorker(LPVOID lpParam) +{ + UNREFERENCED_PARAMETER(lpParam); + + // Step 1: Dismiss Start Menu cleanly via ESC + keybd_event(VK_ESCAPE, 0x01, 0, 0); + Sleep(20); + keybd_event(VK_ESCAPE, 0x01, KEYEVENTF_KEYUP, 0); + + // Wait 180ms for StartMenuExperienceHost dismissal animation to finish completely + Sleep(180); + + // Ensure Win key is released (Never touch Alt or Ctrl to prevent Menu Bar mode) + keybd_event(VK_LWIN, 0x5B, KEYEVENTF_KEYUP, 0); + Sleep(10); + + // Step 2: Send Win + S via clean hardware scan codes + keybd_event(VK_LWIN, 0x5B, 0, 0); // Win Down (scan 0x5B) + Sleep(30); + keybd_event(0x53, 0x1F, 0, 0); // S Down (scan 0x1F) + Sleep(30); + keybd_event(0x53, 0x1F, KEYEVENTF_KEYUP, 0); // S Up + Sleep(30); + keybd_event(VK_LWIN, 0x5B, KEYEVENTF_KEYUP, 0);// Win Up + Sleep(20); + + g_searchActive = TRUE; + + // Step 3: Wait 400ms for Search flyout animation and cursor focus + Sleep(400); + + // Step 4: Replay buffered keys with hardware scan codes + BufferedKey replayKeys[MAX_BUFFERED_KEYS]; + int count = CopyAndClearBuffer(replayKeys, MAX_BUFFERED_KEYS); + + for (int i = 0; i < count; i++) + { + BYTE scan = (replayKeys[i].scan != 0) ? (BYTE)replayKeys[i].scan : (BYTE)MapVirtualKeyW(replayKeys[i].vk, MAPVK_VK_TO_VSC); + keybd_event((BYTE)replayKeys[i].vk, scan, 0, 0); + Sleep(20); + keybd_event((BYTE)replayKeys[i].vk, scan, KEYEVENTF_KEYUP, 0); + Sleep(20); + } + + g_isTransitioning = FALSE; + return 0; +} + +static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) +{ + if (nCode >= 0) + { + KBDLLHOOKSTRUCT* pKbd = (KBDLLHOOKSTRUCT*)lParam; + + // Injected keystrokes pass through untouched + if (pKbd->flags & LLKHF_INJECTED) + { + return CallNextHookEx(g_hHook, nCode, wParam, lParam); + } + + DWORD vk = pKbd->vkCode; + BOOL isKeyDown = (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN); + + // Track Windows Key + if (vk == VK_LWIN || vk == VK_RWIN) + { + if (isKeyDown) + { + g_searchActive = FALSE; + g_isTransitioning = FALSE; + } + return CallNextHookEx(g_hHook, nCode, wParam, lParam); + } + + // Track ESC + if (vk == VK_ESCAPE && isKeyDown) + { + g_searchActive = FALSE; + g_isTransitioning = FALSE; + return CallNextHookEx(g_hHook, nCode, wParam, lParam); + } + + // Track Win + S (opened search directly) + if (isKeyDown && vk == 0x53 && ((GetKeyState(VK_LWIN) & 0x8000) || (GetKeyState(VK_RWIN) & 0x8000))) + { + g_searchActive = TRUE; + return CallNextHookEx(g_hHook, nCode, wParam, lParam); + } + + // Modifiers (Ctrl or Alt) held down -> pass through (preserves Alt+Tab, Ctrl+C, etc.) + if ((GetKeyState(VK_CONTROL) & 0x8000) || (GetKeyState(VK_MENU) & 0x8000)) + { + return CallNextHookEx(g_hHook, nCode, wParam, lParam); + } + + // Typing keys + if (isKeyDown && IsTypingKey(vk)) + { + DWORD now = GetTickCount(); + + // Safety watchdog: reset if transition takes longer than 1500ms + if (g_isTransitioning && (now - g_transitionStartTick > 1500)) + { + g_isTransitioning = FALSE; + } + + // Buffer keys during transition + if (g_isTransitioning) + { + BufferKey(vk, pKbd->scanCode); + return 1; // Swallow + } + + // Check foreground process + HWND hwndFg = GetForegroundWindow(); + DWORD pid = 0; + if (hwndFg) GetWindowThreadProcessId(hwndFg, &pid); + + WCHAR pName[MAX_PATH] = { 0 }; + GetCachedProcessName(pid, pName, MAX_PATH); + + BOOL isTarget = FALSE; + if (_wcsicmp(pName, L"SearchHost.exe") == 0 || + _wcsicmp(pName, L"StartMenuExperienceHost.exe") == 0 || + _wcsicmp(pName, L"ShellExperienceHost.exe") == 0) + { + isTarget = TRUE; + } + else if (_wcsicmp(pName, L"explorer.exe") == 0) + { + WCHAR clsName[64] = { 0 }; + GetClassNameW(hwndFg, clsName, 64); + isTarget = (_wcsicmp(clsName, L"ApplicationFrameWindow") == 0 || + _wcsicmp(clsName, L"Windows.UI.Core.CoreWindow") == 0); + } + + if (!isTarget) + { + g_searchActive = FALSE; + return CallNextHookEx(g_hHook, nCode, wParam, lParam); + } + + if (g_searchActive) + { + return CallNextHookEx(g_hHook, nCode, wParam, lParam); + } + + // Begin transition + g_isTransitioning = TRUE; + g_transitionStartTick = now; + + EnterCriticalSection(&g_csBuffer); + g_keyBufferCount = 0; + g_keyBuffer[0].vk = vk; + g_keyBuffer[0].scan = pKbd->scanCode; + g_keyBufferCount = 1; + LeaveCriticalSection(&g_csBuffer); + + HANDLE hThread = CreateThread(NULL, 0, TransitionWorker, NULL, 0, NULL); + if (hThread) CloseHandle(hThread); + + return 1; // Swallow initial trigger key + } + } + + return CallNextHookEx(g_hHook, nCode, wParam, lParam); +} + +static DWORD WINAPI HookThreadProc(LPVOID lpParam) +{ + UNREFERENCED_PARAMETER(lpParam); + + g_hHook = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandleW(NULL), 0); + if (!g_hHook) + { + return 1; + } + + MSG msg; + while (GetMessageW(&msg, NULL, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + + if (g_hHook) + { + UnhookWindowsHookEx(g_hHook); + g_hHook = NULL; + } + + return 0; +} + +void StartSearchFix_Init(void) +{ + InitializeCriticalSection(&g_csBuffer); + InitializeCriticalSection(&g_csPidCache); + + g_hHookThread = CreateThread(NULL, 0, HookThreadProc, NULL, 0, &g_dwHookThreadId); +} + +void StartSearchFix_Uninit(void) +{ + if (g_dwHookThreadId != 0) + { + PostThreadMessageW(g_dwHookThreadId, WM_QUIT, 0, 0); + } + + if (g_hHookThread) + { + WaitForSingleObject(g_hHookThread, 2000); + CloseHandle(g_hHookThread); + g_hHookThread = NULL; + } + + DeleteCriticalSection(&g_csBuffer); + DeleteCriticalSection(&g_csPidCache); +} diff --git a/ExplorerPatcher/StartSearchFix.h b/ExplorerPatcher/StartSearchFix.h new file mode 100644 index 000000000..751a2b022 --- /dev/null +++ b/ExplorerPatcher/StartSearchFix.h @@ -0,0 +1,25 @@ +#ifndef _H_STARTSEARCHFIX_H_ +#define _H_STARTSEARCHFIX_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * StartSearchFix: + * Fixes the Windows 10 Full-screen Start Menu search Z-Order bug on Windows 11. + * When typing in full-screen Start, intercepts the initial key, cleanly dismisses + * the Start Menu via ESC (180ms settle), invokes Windows 11 Search via hardware + * Win + S (400ms focus settle), and replays buffered keystrokes with scan codes. + */ + +void StartSearchFix_Init(void); +void StartSearchFix_Uninit(void); + +#ifdef __cplusplus +} +#endif + +#endif // _H_STARTSEARCHFIX_H_ diff --git a/ExplorerPatcher/dllmain.c b/ExplorerPatcher/dllmain.c index 5d7b99d0d..74df90728 100644 --- a/ExplorerPatcher/dllmain.c +++ b/ExplorerPatcher/dllmain.c @@ -56,6 +56,9 @@ DWORD32 global_ubr; #include #pragma comment(lib, "Userenv.lib") #endif +#if WITH_MAIN_PATCHER +#include "StartSearchFix.h" +#endif #define CHECKFOREGROUNDELAPSED_TIMEOUT 300 #define POPUPMENU_SAFETOREMOVE_TIMEOUT 300 @@ -11555,6 +11558,9 @@ DWORD Inject(BOOL bIsExplorer) VnPatchIAT(hMyTaskbar, "USER32.dll", "GetClientRect", TaskbarCenter_GetClientRectHook); VnPatchIAT(hExplorer, "SHCORE.dll", (LPCSTR)190, TaskbarCenter_SHWindowsPolicy); printf("Initialized taskbar centering module.\n"); +#if WITH_MAIN_PATCHER + StartSearchFix_Init(); +#endif @@ -13411,6 +13417,9 @@ BOOL WINAPI DllMain( case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: +#if WITH_MAIN_PATCHER + StartSearchFix_Uninit(); +#endif break; } return TRUE;