diff --git a/mods/taskbar-primary-on-secondary-monitor.wh.cpp b/mods/taskbar-primary-on-secondary-monitor.wh.cpp new file mode 100644 index 000000000..91e8e7deb --- /dev/null +++ b/mods/taskbar-primary-on-secondary-monitor.wh.cpp @@ -0,0 +1,426 @@ +// ==WindhawkMod== +// @id taskbar-primary-on-secondary-monitor +// @name Primary taskbar on secondary monitor +// @description Move the primary taskbar, including the tray icons, notifications, action center, etc. to another monitor +// @version 1.0 +// @author m417z +// @github https://github.com/m417z +// @twitter https://twitter.com/m417z +// @homepage https://m417z.com/ +// @include explorer.exe +// @architecture x86-64 +// @compilerOptions -lversion +// ==/WindhawkMod== + +// Source code is published under The GNU General Public License v3.0. +// +// For bug reports and feature requests, please open an issue here: +// https://github.com/ramensoftware/windhawk-mods/issues +// +// For pull requests, development takes place here: +// https://github.com/m417z/my-windhawk-mods + +// ==WindhawkModReadme== +/* +# Primary taskbar on secondary monitor + +Move the primary taskbar, including the tray icons, notifications, action +center, etc. to another monitor. + +![Demonstration](https://i.imgur.com/hFU9oyK.gif) +*/ +// ==/WindhawkModReadme== + +// ==WindhawkModSettings== +/* +- monitor: 2 + $name: Monitor + $description: >- + The monitor number to have the primary taskbar on +- oldTaskbarOnWin11: false + $name: Customize the old taskbar on Windows 11 + $description: >- + Enable this option to customize the old taskbar on Windows 11 (if using + ExplorerPatcher or a similar tool). +*/ +// ==/WindhawkModSettings== + +#include + +#include + +#include + +struct { + int monitor; + bool oldTaskbarOnWin11; +} g_settings; + +enum class WinVersion { + Unsupported, + Win10, + Win11, + Win11_24H2, +}; + +WinVersion g_winVersion; + +std::atomic g_initialized; +std::atomic g_explorerPatcherInitialized; + +std::atomic g_unloading; + +HWND GetTaskbarWnd() { + HWND hTaskbarWnd = FindWindow(L"Shell_TrayWnd", nullptr); + + DWORD processId = 0; + if (!hTaskbarWnd || !GetWindowThreadProcessId(hTaskbarWnd, &processId) || + processId != GetCurrentProcessId()) { + return nullptr; + } + + return hTaskbarWnd; +} + +HMONITOR GetMonitorById(int monitorId) { + HMONITOR monitorResult = nullptr; + int currentMonitorId = 0; + + auto monitorEnumProc = [&monitorResult, ¤tMonitorId, + monitorId](HMONITOR hMonitor) -> BOOL { + if (currentMonitorId == monitorId) { + monitorResult = hMonitor; + return FALSE; + } + currentMonitorId++; + return TRUE; + }; + + EnumDisplayMonitors( + nullptr, nullptr, + [](HMONITOR hMonitor, HDC hdc, LPRECT lprcMonitor, + LPARAM dwData) WINAPI -> BOOL { + auto& proc = *reinterpret_cast(dwData); + return proc(hMonitor); + }, + reinterpret_cast(&monitorEnumProc)); + + return monitorResult; +} + +using MonitorFromPoint_t = decltype(&MonitorFromPoint); +MonitorFromPoint_t MonitorFromPoint_Original; +HMONITOR WINAPI MonitorFromPoint_Hook(POINT pt, DWORD dwFlags) { + Wh_Log(L">"); + + if (pt.x == 0 && pt.y == 0 && g_settings.monitor >= 1) { + if (HMONITOR monitor = GetMonitorById(g_settings.monitor - 1)) { + return monitor; + } + } + + return MonitorFromPoint_Original(pt, dwFlags); +} + +using TrayUI__SetStuckMonitor_t = HRESULT(WINAPI*)(void* pThis, + HMONITOR monitor); +TrayUI__SetStuckMonitor_t TrayUI__SetStuckMonitor_Original; +HRESULT WINAPI TrayUI__SetStuckMonitor_Hook(void* pThis, HMONITOR monitor) { + Wh_Log(L">"); + + if (!g_unloading && g_settings.monitor >= 1) { + monitor = GetMonitorById(g_settings.monitor - 1); + } else { + monitor = nullptr; + } + + if (!monitor) { + monitor = MonitorFromPoint_Original({0, 0}, MONITOR_DEFAULTTONEAREST); + } + + return TrayUI__SetStuckMonitor_Original(pThis, monitor); +} + +VS_FIXEDFILEINFO* GetModuleVersionInfo(HMODULE hModule, UINT* puPtrLen) { + void* pFixedFileInfo = nullptr; + UINT uPtrLen = 0; + + HRSRC hResource = + FindResource(hModule, MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION); + if (hResource) { + HGLOBAL hGlobal = LoadResource(hModule, hResource); + if (hGlobal) { + void* pData = LockResource(hGlobal); + if (pData) { + if (!VerQueryValue(pData, L"\\", &pFixedFileInfo, &uPtrLen) || + uPtrLen == 0) { + pFixedFileInfo = nullptr; + uPtrLen = 0; + } + } + } + } + + if (puPtrLen) { + *puPtrLen = uPtrLen; + } + + return (VS_FIXEDFILEINFO*)pFixedFileInfo; +} + +WinVersion GetExplorerVersion() { + VS_FIXEDFILEINFO* fixedFileInfo = GetModuleVersionInfo(nullptr, nullptr); + if (!fixedFileInfo) { + return WinVersion::Unsupported; + } + + WORD major = HIWORD(fixedFileInfo->dwFileVersionMS); + WORD minor = LOWORD(fixedFileInfo->dwFileVersionMS); + WORD build = HIWORD(fixedFileInfo->dwFileVersionLS); + WORD qfe = LOWORD(fixedFileInfo->dwFileVersionLS); + + Wh_Log(L"Version: %u.%u.%u.%u", major, minor, build, qfe); + + switch (major) { + case 10: + if (build < 22000) { + return WinVersion::Win10; + } else if (build < 26100) { + return WinVersion::Win11; + } else { + return WinVersion::Win11_24H2; + } + break; + } + + return WinVersion::Unsupported; +} + +bool HookExplorerPatcherSymbols(HMODULE explorerPatcherModule) { + if (g_explorerPatcherInitialized.exchange(true)) { + return true; + } + + if (g_winVersion >= WinVersion::Win11) { + g_winVersion = WinVersion::Win10; + } + + struct EXPLORER_PATCHER_HOOK { + PCSTR symbol; + void** pOriginalFunction; + void* hookFunction = nullptr; + bool optional = false; + }; + + EXPLORER_PATCHER_HOOK hooks[] = { + {R"(?_SetStuckMonitor@TrayUI@@QEAAJPEAUHMONITOR__@@@Z)", + (void**)&TrayUI__SetStuckMonitor_Original, + (void*)TrayUI__SetStuckMonitor_Hook}, + }; + + bool succeeded = true; + + for (const auto& hook : hooks) { + void* ptr = (void*)GetProcAddress(explorerPatcherModule, hook.symbol); + if (!ptr) { + Wh_Log(L"ExplorerPatcher symbol%s doesn't exist: %S", + hook.optional ? L" (optional)" : L"", hook.symbol); + if (!hook.optional) { + succeeded = false; + } + continue; + } + + if (hook.hookFunction) { + Wh_SetFunctionHook(ptr, hook.hookFunction, hook.pOriginalFunction); + } else { + *hook.pOriginalFunction = ptr; + } + } + + if (g_initialized) { + Wh_ApplyHookOperations(); + } + + return succeeded; +} + +bool HandleModuleIfExplorerPatcher(HMODULE module) { + WCHAR moduleFilePath[MAX_PATH]; + switch ( + GetModuleFileName(module, moduleFilePath, ARRAYSIZE(moduleFilePath))) { + case 0: + case ARRAYSIZE(moduleFilePath): + return false; + } + + PCWSTR moduleFileName = wcsrchr(moduleFilePath, L'\\'); + if (!moduleFileName) { + return false; + } + + moduleFileName++; + + if (_wcsnicmp(L"ep_taskbar.", moduleFileName, sizeof("ep_taskbar.") - 1) != + 0) { + return true; + } + + Wh_Log(L"ExplorerPatcher taskbar loaded: %s", moduleFileName); + return HookExplorerPatcherSymbols(module); +} + +void HandleLoadedExplorerPatcher() { + HMODULE hMods[1024]; + DWORD cbNeeded; + if (EnumProcessModules(GetCurrentProcess(), hMods, sizeof(hMods), + &cbNeeded)) { + for (size_t i = 0; i < cbNeeded / sizeof(HMODULE); i++) { + HandleModuleIfExplorerPatcher(hMods[i]); + } + } +} + +using LoadLibraryExW_t = decltype(&LoadLibraryExW); +LoadLibraryExW_t LoadLibraryExW_Original; +HMODULE WINAPI LoadLibraryExW_Hook(LPCWSTR lpLibFileName, + HANDLE hFile, + DWORD dwFlags) { + HMODULE module = LoadLibraryExW_Original(lpLibFileName, hFile, dwFlags); + if (module && !((ULONG_PTR)module & 3) && !g_explorerPatcherInitialized) { + HandleModuleIfExplorerPatcher(module); + } + + return module; +} + +bool HookTaskbarSymbols() { + HMODULE module; + if (g_winVersion <= WinVersion::Win10) { + module = GetModuleHandle(nullptr); + } else { + module = LoadLibrary(L"taskbar.dll"); + if (!module) { + Wh_Log(L"Couldn't load taskbar.dll"); + return false; + } + } + + // Taskbar.dll, explorer.exe + WindhawkUtils::SYMBOL_HOOK symbolHooks[] = { + { + { + // Windows 11. + LR"(public: long __cdecl TrayUI::_SetStuckMonitor(struct HMONITOR__ *))", + + // Windows 10. + LR"(public: void __cdecl TrayUI::_SetStuckMonitor(struct HMONITOR__ *))", + }, + &TrayUI__SetStuckMonitor_Original, + TrayUI__SetStuckMonitor_Hook, + }, + }; + + if (!HookSymbols(module, symbolHooks, ARRAYSIZE(symbolHooks))) { + Wh_Log(L"HookSymbols failed"); + return false; + } + + return true; +} + +void ApplySettings() { + HWND hTaskbarWnd = GetTaskbarWnd(); + if (!hTaskbarWnd) { + return; + } + + // Trigger CTray::_HandleDisplayChange. + SendMessage(hTaskbarWnd, 0x5B8, 0, 0); +} + +void LoadSettings() { + g_settings.monitor = Wh_GetIntSetting(L"monitor"); + g_settings.oldTaskbarOnWin11 = Wh_GetIntSetting(L"oldTaskbarOnWin11"); +} + +BOOL Wh_ModInit() { + Wh_Log(L">"); + + LoadSettings(); + + g_winVersion = GetExplorerVersion(); + if (g_winVersion == WinVersion::Unsupported) { + Wh_Log(L"Unsupported Windows version"); + return FALSE; + } + + if (g_settings.oldTaskbarOnWin11) { + bool hasWin10Taskbar = g_winVersion < WinVersion::Win11_24H2; + + if (g_winVersion >= WinVersion::Win11) { + g_winVersion = WinVersion::Win10; + } + + if (hasWin10Taskbar && !HookTaskbarSymbols()) { + return FALSE; + } + } else if (!HookTaskbarSymbols()) { + return FALSE; + } + + HandleLoadedExplorerPatcher(); + + HMODULE kernelBaseModule = GetModuleHandle(L"kernelbase.dll"); + FARPROC pKernelBaseLoadLibraryExW = + GetProcAddress(kernelBaseModule, "LoadLibraryExW"); + Wh_SetFunctionHook((void*)pKernelBaseLoadLibraryExW, + (void*)LoadLibraryExW_Hook, + (void**)&LoadLibraryExW_Original); + + WindhawkUtils::Wh_SetFunctionHookT(MonitorFromPoint, MonitorFromPoint_Hook, + &MonitorFromPoint_Original); + + g_initialized = true; + + return TRUE; +} + +void Wh_ModAfterInit() { + Wh_Log(L">"); + + // Try again in case there's a race between the previous attempt and the + // LoadLibraryExW hook. + if (!g_explorerPatcherInitialized) { + HandleLoadedExplorerPatcher(); + } + + ApplySettings(); +} + +void Wh_ModBeforeUninit() { + Wh_Log(L">"); + + g_unloading = true; + + ApplySettings(); +} + +void Wh_ModUninit() { + Wh_Log(L">"); +} + +BOOL Wh_ModSettingsChanged(BOOL* bReload) { + Wh_Log(L">"); + + bool prevOldTaskbarOnWin11 = g_settings.oldTaskbarOnWin11; + + LoadSettings(); + + *bReload = g_settings.oldTaskbarOnWin11 != prevOldTaskbarOnWin11; + if (!*bReload) { + ApplySettings(); + } + + return TRUE; +}