Skip to content
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3b37f13
explorer-gallery-photos-launcher.wh.cpp
jakubix30 Aug 8, 2026
ba86866
Update metadata for Gallery to Photos app launcher
jakubix30 Aug 8, 2026
9ba8643
Update hooking method for BrowseObject in Explorer
jakubix30 Aug 8, 2026
a6eeb32
Refactor Gallery launcher to open Photos app
jakubix30 Aug 8, 2026
2ae2401
Refactor event handling and improve thread safety
jakubix30 Aug 8, 2026
787eaec
Refactor settings loading and command execution
jakubix30 Aug 9, 2026
5ba8e0a
Refactor code for target command handling and hooks
jakubix30 Aug 9, 2026
25643e0
Fix missing newline at end of file
jakubix30 Aug 9, 2026
f96522f
Enhance navigation interception and command handling
jakubix30 Aug 9, 2026
edb3d9c
Refactor target PID list management and cleanup
jakubix30 Aug 10, 2026
1f7b95f
Enhance launch command to support arguments
jakubix30 Aug 10, 2026
786cb3f
Refactor PIDLIST handling and add async launch support
jakubix30 Aug 10, 2026
51d89e5
Implement cleanup for worker thread and handles
jakubix30 Aug 11, 2026
9aa0c02
Improve resource management for worker thread
jakubix30 Aug 11, 2026
4e11240
Refactor Gallery navigation handling and descriptions
jakubix30 Aug 12, 2026
0841da0
Improve descriptions and fix launch event handling
jakubix30 Aug 12, 2026
57d5f4a
Refine command descriptions and async handling
jakubix30 Aug 12, 2026
a4d79b1
Merge branch 'ramensoftware:main' into main
jakubix30 Aug 12, 2026
3eb0f22
Add newline at end of explorer-gallery-photos-launcher file
jakubix30 Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
336 changes: 336 additions & 0 deletions mods/explorer-gallery-photos-launcher.wh.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
// ==WindhawkMod==
// @id explorer-gallery-photos-launcher
// @name Open Photos App instead of Gallery
// @name:pl-PL Otwieraj aplikację Zdjęcia zamiast Galerii
// @description Intercepts Gallery navigation in File Explorer and launches the Microsoft Photos app (ms-photos:) instead.
// @description:pl-PL Przechwytuje nawigację do Galerii w Eksploratorze plików i uruchamia zamiast niej aplikację Zdjęcia (ms-photos:).
// @version 2.0
// @author Jäkubix
// @github https://github.com/jakubix30
// @include %SystemRoot%\explorer.exe
// @architecture x86-64
// @compilerOptions -lole32
// ==/WindhawkMod==

// ==WindhawkModReadme==
/*
# Open Photos App instead of Gallery

Intercepts navigation to the "Gallery" folder in the Windows File Explorer navigation pane
and launches the Microsoft Photos app (`ms-photos:`) instead of opening the built-in Gallery view.

*(Note: Only in-Explorer navigation is redirected. Opening Gallery from a pinned Start menu shortcut won't be intercepted. Using the Back/Forward history buttons to return to Gallery will also bypass the hook.)*

![Demonstration](https://raw.githubusercontent.com/jakubix30/explorer-gallery-photos-launcher/main/demo.gif)

## Features

- Works with **ExplorerPatcher** (Win10 Ribbon overlay fix) and standard Windows 11
- Hooks at the COM/Symbol level (`CShellBrowser::BrowseObject`) — completely UI-independent
- Configurable launch command and arguments (useful if you prefer a different photo viewer).
*/
// ==/WindhawkModReadme==

// ==WindhawkModSettings==
/*
- targetCommand: "ms-photos:"
$name: Command to launch
$name:pl-PL: Komenda do uruchomienia
$description: "Protocol or program launched instead of navigating to the Gallery folder."
$description:pl-PL: Protokół lub program uruchamiany zamiast nawigacji do folderu Galerii.
- targetArguments: ""
$name: Command arguments
$name:pl-PL: Argumenty komendy
$description: "Optional arguments to pass to the command (only applies when the command is an executable, not a protocol)."
$description:pl-PL: Opcjonalne argumenty przekazywane do komendy.
*/
// ==/WindhawkModSettings==

#include <shlobj.h>
#include <shellapi.h>
#include <windhawk_utils.h>
#include <atomic>
#include <mutex>

static std::atomic<PIDLIST_ABSOLUTE> g_targetPidl{nullptr};
static std::atomic<bool> g_explorerFrameHooked{false};
static std::atomic<bool> g_unloading{false};
static HMODULE g_explorerFrameRef = nullptr;

// ---- BrowseObject Original & Hook ----
typedef HRESULT(STDMETHODCALLTYPE *BrowseObject_t)(void *pThis,
PCUIDLIST_RELATIVE pidl,
UINT wFlags);
static BrowseObject_t g_BrowseObject_Original = nullptr;

static PIDLIST_ABSOLUTE EnsureTargetPidl() {
PIDLIST_ABSOLUTE pidl = g_targetPidl.load(std::memory_order_acquire);
if (pidl) {
return pidl;
}

constexpr WCHAR kGalleryParsingName[] = L"::{e88865ea-0e1c-4e20-9aa6-edcd0212c87c}";
PIDLIST_ABSOLUTE newPidl = nullptr;
HRESULT hr = SHParseDisplayName(kGalleryParsingName, nullptr, &newPidl, 0, nullptr);
if (FAILED(hr) || !newPidl) {
Wh_Log(L"SHParseDisplayName failed: 0x%08X", (unsigned)hr);
return nullptr;
}

PIDLIST_ABSOLUTE expected = nullptr;
if (!g_targetPidl.compare_exchange_strong(expected, newPidl,
std::memory_order_release,
std::memory_order_acquire)) {
CoTaskMemFree(newPidl); // lost the race
return expected;
}
return newPidl;
}

// Asynchronous launch parameters
static HANDLE g_launchEvent = NULL;
static HANDLE g_stopEvent = NULL;
static HANDLE g_workerThread = NULL;
static std::once_flag g_workerInitOnce;

static void CleanupResources() {
if (g_stopEvent) {
SetEvent(g_stopEvent);
}
if (g_workerThread) {
WaitForSingleObject(g_workerThread, INFINITE);
CloseHandle(g_workerThread);
g_workerThread = NULL;
}
if (g_launchEvent) {
CloseHandle(g_launchEvent);
g_launchEvent = NULL;
}
if (g_stopEvent) {
CloseHandle(g_stopEvent);
g_stopEvent = NULL;
}
if (g_explorerFrameRef) {
FreeLibrary(g_explorerFrameRef);
g_explorerFrameRef = nullptr;
}
}

static DWORD WINAPI LaunchWorkerProc(LPVOID) {
HRESULT hrCo = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

HANDLE handles[] = {g_stopEvent, g_launchEvent};
for (;;) {
DWORD r = MsgWaitForMultipleObjectsEx(ARRAYSIZE(handles), handles, INFINITE,
QS_ALLINPUT, MWMO_INPUTAVAILABLE);
if (r == WAIT_OBJECT_0) {
break; // stop requested
}
if (r == WAIT_OBJECT_0 + 1) {
// Don't start a new launch if a stop was requested in the meantime.
if (WaitForSingleObject(g_stopEvent, 0) == WAIT_OBJECT_0) {
break;
}

// Read target dynamically, preventing use-after-free and threading issues.
WindhawkUtils::StringSetting cmd = WindhawkUtils::StringSetting::make(L"targetCommand");
WindhawkUtils::StringSetting args = WindhawkUtils::StringSetting::make(L"targetArguments");

PCWSTR target = cmd.get()[0] ? cmd.get() : L"ms-photos:";
PCWSTR targetArgs = args.get()[0] ? args.get() : nullptr;

Wh_Log(L"Launching app asynchronously: %s %s", target, targetArgs ? targetArgs : L"");

SHELLEXECUTEINFOW sei = {sizeof(sei)};
// SEE_MASK_ASYNCOK allows shell interactions (like UAC prompts) to offload to a background
// thread without blocking our worker, ensuring Wh_ModUninit can always complete a clean join.
sei.fMask = SEE_MASK_ASYNCOK | SEE_MASK_FLAG_NO_UI;
sei.lpVerb = nullptr; // Default verb is used for maximum compatibility
sei.lpFile = target;
sei.lpParameters = targetArgs;
sei.nShow = SW_SHOWNORMAL;

if (!ShellExecuteExW(&sei)) {
Wh_Log(L"ShellExecuteExW(%s) failed: %u", target, GetLastError());
}
} else if (r == WAIT_OBJECT_0 + ARRAYSIZE(handles)) {
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
} else {
break; // WAIT_FAILED
}
}

if (SUCCEEDED(hrCo)) {
CoUninitialize();
}
return 0;
}

static void EnsureWorkerThread() {
std::call_once(g_workerInitOnce, [] {
g_launchEvent = CreateEventW(NULL, FALSE, FALSE, NULL); // auto-reset
g_stopEvent = CreateEventW(NULL, TRUE, FALSE, NULL); // manual-reset

if (g_launchEvent && g_stopEvent) {
g_workerThread = CreateThread(NULL, 0, LaunchWorkerProc, NULL, 0, NULL);
if (!g_workerThread) {
Wh_Log(L"Failed to create worker thread.");
}
} else {
Wh_Log(L"Failed to create thread synchronization events.");
}
});
}

// Hook on BrowseObject
static HRESULT STDMETHODCALLTYPE BrowseObject_Hook(void *pThis,
PCUIDLIST_RELATIVE pidl,
UINT wFlags) {
// Ignore relative or navigation-history PIDLs
if (wFlags & (SBSP_RELATIVE | SBSP_PARENT | SBSP_NAVIGATEBACK | SBSP_NAVIGATEFORWARD)) {
return g_BrowseObject_Original(pThis, pidl, wFlags);
}

// Lazy load the PIDL on the UI thread to guarantee COM readiness
PIDLIST_ABSOLUTE target = EnsureTargetPidl();

if (target && pidl) {
if (ILIsEqual(reinterpret_cast<PCIDLIST_ABSOLUTE>(pidl), target)) {

Wh_Log(L"Intercepted Gallery navigation! Triggering async launch.");

EnsureWorkerThread();
if (g_launchEvent) {
SetEvent(g_launchEvent);
}

// Block original navigation without tricking the address bar into switching state
return HRESULT_FROM_WIN32(ERROR_CANCELLED);
}
}

return g_BrowseObject_Original(pThis, pidl, wFlags);
}

// Hook explorerframe.dll symbols
static bool HookExplorerFrame(HMODULE hEF, bool applyNow) {
// Bail out if module is invalid or mod is unloading
if (!hEF || g_unloading.load(std::memory_order_acquire)) return false;

bool expected = false;
// Fast path: bail if we already successfully hooked or attempted to hook.
if (!g_explorerFrameHooked.compare_exchange_strong(expected, true)) {
return true;
}

// Hold a reference so the module is not unloaded out from under the hook
if (!GetModuleHandleExW(0, L"explorerframe.dll", &g_explorerFrameRef)) {
Wh_Log(L"Failed to retain reference to explorerframe.dll: %u", GetLastError());
}

WindhawkUtils::SYMBOL_HOOK explorerframe_dll_hooks[] = {
{
{
LR"(public: virtual long __cdecl CShellBrowser::BrowseObject(struct _ITEMIDLIST_RELATIVE const __unaligned *,unsigned int))",
LR"(public: virtual long __cdecl CShellBrowser::BrowseObject(struct _ITEMIDLIST_RELATIVE const * __ptr64,unsigned int))",
LR"(public: virtual long __cdecl CShellBrowser::BrowseObject(struct _ITEMIDLIST const *,unsigned int))",
},
&g_BrowseObject_Original,
BrowseObject_Hook,
},
};

if (WindhawkUtils::HookSymbols(hEF, explorerframe_dll_hooks, ARRAYSIZE(explorerframe_dll_hooks))) {
Wh_Log(L"Successfully registered BrowseObject hooks");
if (applyNow) {
if (!Wh_ApplyHookOperations()) {
Wh_Log(L"Wh_ApplyHookOperations failed");
}
}
return true;
}

// Hook failed - latch remains true to prevent repeated HookSymbols failure retries
Wh_Log(L"Failed to hook ExplorerFrame symbols. Mod will remain inactive until restarted/reloaded.");
return false;
}

// LoadLibraryExW hook to catch late loading of explorerframe.dll
using LoadLibraryExW_t = decltype(&LoadLibraryExW);
static LoadLibraryExW_t g_LoadLibraryExW_Original = nullptr;

static HMODULE WINAPI LoadLibraryExW_Hook(LPCWSTR lpLibFileName,
HANDLE hFile,
DWORD dwFlags) {
HMODULE hMod = g_LoadLibraryExW_Original(lpLibFileName, hFile, dwFlags);

// Check if explorerframe.dll is now available (whether it was the direct target or an indirect dependency)
if (hMod && !g_unloading.load(std::memory_order_acquire) &&
!g_explorerFrameHooked.load(std::memory_order_relaxed)) {
if (HMODULE hEF = GetModuleHandleW(L"explorerframe.dll")) {
HookExplorerFrame(hEF, true);
}
}

return hMod;
}

BOOL Wh_ModInit() {
Wh_Log(L"Initializing mod v" WH_MOD_VERSION);

bool delayLoadingNeeded = true;

// If explorerframe.dll is already loaded, hook it immediately
HMODULE hEF = GetModuleHandleW(L"explorerframe.dll");
if (hEF) {
if (HookExplorerFrame(hEF, false)) {
delayLoadingNeeded = false;
} else {
CleanupResources();
return FALSE;
}
}

if (delayLoadingNeeded) {
// Hook LoadLibraryExW from kernelbase.dll to monitor for explorerframe.dll late-loading
HMODULE hKernelBase = GetModuleHandleW(L"kernelbase.dll");
if (hKernelBase) {
auto pLoadLibraryExW = (LoadLibraryExW_t)GetProcAddress(hKernelBase, "LoadLibraryExW");
if (pLoadLibraryExW) {
WindhawkUtils::SetFunctionHook(pLoadLibraryExW,
LoadLibraryExW_Hook,
&g_LoadLibraryExW_Original);
}
}
}

return TRUE;
}

void Wh_ModAfterInit() {
// Catch cases where explorerframe.dll loaded during mod initialization
HMODULE hEF = GetModuleHandleW(L"explorerframe.dll");
if (hEF) {
HookExplorerFrame(hEF, true);
}
}

void Wh_ModBeforeUninit() {
// Let hooks know we are shutting down to prevent a late-loading race condition
g_unloading.store(true, std::memory_order_release);
}

void Wh_ModUninit() {
Wh_Log(L"Uninitializing mod");

CleanupResources();

PIDLIST_ABSOLUTE target = g_targetPidl.exchange(nullptr, std::memory_order_acquire);
if (target) {
CoTaskMemFree(target);
}
}
Loading