From d53c39a2c61045a62b090a0bcc5e683fb95c6c68 Mon Sep 17 00:00:00 2001 From: b1xcy Date: Mon, 10 Aug 2026 22:06:11 +0800 Subject: [PATCH 1/9] Route Explorer clipboard paste through FastCopy Keep Ctrl+C and Ctrl+X in Explorer so the Shell produces its standard IDataObject, including the preferred copy or move drop effect. Start a packaged background helper at sign-in and app launch. It combines RegisterHotKey with WH_KEYBOARD_LL to observe Ctrl+V, suppress Explorer's native accelerator, and deduplicate the two input paths. Resolve the foreground Explorer folder through IShellWindows, using the active ShellView first and LocationURL as a tab-compatible fallback. Read filesystem items through IShellItemArray with CF_HDROP fallback, serialize them in FastCopy's existing task-file format, and launch the fastcopy protocol. Clear the clipboard after a move; on any failure, temporarily release the hotkey and replay native Ctrl+V. Add a localized settings toggle backed by HKCU and package the helper as an enabled startup task. Verified: user-tested file and folder copy/move with Ctrl+C/Ctrl+X then Ctrl+V using the signed 1.2.1.8 MSIX; FastCopyKeyboardHook Package|x64 builds with 0 warnings and 0 errors; git diff --cached --check passes. --- FastCopy.sln | 32 ++ FastCopy/App.xaml.cpp | 2 + FastCopy/FastCopy.vcxproj | 11 +- FastCopy/KeyboardHookController.cpp | 69 ++++ FastCopy/KeyboardHookController.h | 9 + FastCopy/Package.appxmanifest | 12 +- FastCopy/SettingsViewModel.cpp | 9 + FastCopy/SettingsViewModel.h | 3 + FastCopy/SettingsViewModel.idl | 1 + FastCopy/SettingsWindow.xaml | 10 + FastCopy/Strings/de-DE/Resources.resw | 6 + FastCopy/Strings/en-US/Resources.resw | 6 + FastCopy/Strings/es-ES/Resources.resw | 6 + FastCopy/Strings/zh-CN/Resources.resw | 6 + .../ClipboardFileTransfer.cpp | 143 ++++++++ FastCopyKeyboardHook/ClipboardFileTransfer.h | 13 + FastCopyKeyboardHook/ExplorerWindow.cpp | 185 ++++++++++ FastCopyKeyboardHook/ExplorerWindow.h | 9 + .../FastCopyKeyboardHook.vcxproj | 79 +++++ FastCopyKeyboardHook/FastCopyLauncher.cpp | 113 +++++++ FastCopyKeyboardHook/FastCopyLauncher.h | 9 + FastCopyKeyboardHook/main.cpp | 316 ++++++++++++++++++ Public/KeyboardHookSettings.h | 59 ++++ 23 files changed, 1106 insertions(+), 2 deletions(-) create mode 100644 FastCopy/KeyboardHookController.cpp create mode 100644 FastCopy/KeyboardHookController.h create mode 100644 FastCopyKeyboardHook/ClipboardFileTransfer.cpp create mode 100644 FastCopyKeyboardHook/ClipboardFileTransfer.h create mode 100644 FastCopyKeyboardHook/ExplorerWindow.cpp create mode 100644 FastCopyKeyboardHook/ExplorerWindow.h create mode 100644 FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj create mode 100644 FastCopyKeyboardHook/FastCopyLauncher.cpp create mode 100644 FastCopyKeyboardHook/FastCopyLauncher.h create mode 100644 FastCopyKeyboardHook/main.cpp create mode 100644 Public/KeyboardHookSettings.h diff --git a/FastCopy.sln b/FastCopy.sln index 9d17202..bdeea7d 100644 --- a/FastCopy.sln +++ b/FastCopy.sln @@ -26,6 +26,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RobocopyInjection", "RobocopyInjection\RobocopyInjection.vcxproj", "{3CFC5459-E40B-4049-A227-81BD5675F3D7}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FastCopyKeyboardHook", "FastCopyKeyboardHook\FastCopyKeyboardHook.vcxproj", "{83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -210,6 +212,36 @@ Global {3CFC5459-E40B-4049-A227-81BD5675F3D7}.Release|x64.Build.0 = Release|x64 {3CFC5459-E40B-4049-A227-81BD5675F3D7}.Release|x86.ActiveCfg = Release|Win32 {3CFC5459-E40B-4049-A227-81BD5675F3D7}.Release|x86.Build.0 = Release|Win32 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Debug|Any CPU.ActiveCfg = Debug|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Debug|Any CPU.Build.0 = Debug|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Debug|ARM.ActiveCfg = Debug|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Debug|ARM.Build.0 = Debug|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Debug|arm64.ActiveCfg = Debug|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Debug|arm64.Build.0 = Debug|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Debug|x64.ActiveCfg = Debug|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Debug|x64.Build.0 = Debug|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Debug|x86.ActiveCfg = Debug|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Debug|x86.Build.0 = Debug|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Package|Any CPU.ActiveCfg = Package|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Package|Any CPU.Build.0 = Package|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Package|ARM.ActiveCfg = Package|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Package|ARM.Build.0 = Package|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Package|arm64.ActiveCfg = Package|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Package|arm64.Build.0 = Package|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Package|x64.ActiveCfg = Package|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Package|x64.Build.0 = Package|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Package|x86.ActiveCfg = Package|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Package|x86.Build.0 = Package|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Release|Any CPU.ActiveCfg = Release|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Release|Any CPU.Build.0 = Release|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Release|ARM.ActiveCfg = Release|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Release|ARM.Build.0 = Release|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Release|arm64.ActiveCfg = Release|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Release|arm64.Build.0 = Release|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Release|x64.ActiveCfg = Release|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Release|x64.Build.0 = Release|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Release|x86.ActiveCfg = Release|x64 + {83E7518D-7BA4-4A13-B2C9-4AB98DA778DE}.Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FastCopy/App.xaml.cpp b/FastCopy/App.xaml.cpp index 8e84db0..c162290 100644 --- a/FastCopy/App.xaml.cpp +++ b/FastCopy/App.xaml.cpp @@ -20,6 +20,7 @@ #include "RobocopyViewModel.h" #include "CommandLineHandler.h" #include "Settings.h" +#include "KeyboardHookController.h" namespace winrt::FastCopy::implementation @@ -61,6 +62,7 @@ namespace winrt::FastCopy::implementation { try { + KeyboardHookController::Start(); CommandLineHandler::AppLaunchMode == AppLaunchMode::LaunchSettings ? launchSettings() : normalLaunch(); } catch (winrt::hresult_error const& e) diff --git a/FastCopy/FastCopy.vcxproj b/FastCopy/FastCopy.vcxproj index fa273d2..0d93657 100644 --- a/FastCopy/FastCopy.vcxproj +++ b/FastCopy/FastCopy.vcxproj @@ -217,6 +217,7 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll + @@ -285,6 +286,7 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll + @@ -430,6 +432,7 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll + FileCompareViewModel.idl Code @@ -693,6 +696,9 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll true true + + true + true @@ -703,6 +709,9 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll {17f52e4d-1e98-4288-9aa1-20a384171876} + + {83e7518d-7ba4-4a13-b2c9-4ab98da778de} + @@ -811,4 +820,4 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll - \ No newline at end of file + + +bool KeyboardHookController::IsEnabled() +{ + return KeyboardHookSettings::IsEnabled(); +} + +void KeyboardHookController::SetEnabled(bool enabled) +{ + if (!KeyboardHookSettings::SetEnabled(enabled)) + { + return; + } + + enabled ? Start() : Stop(); +} + +void KeyboardHookController::Start() +{ + if (!IsEnabled()) + { + return; + } + + std::wstring modulePath(32768, L'\0'); + auto const length = GetModuleFileNameW(nullptr, modulePath.data(), static_cast(modulePath.size())); + if (length == 0 || length == modulePath.size()) + { + return; + } + modulePath.resize(length); + + auto const hookPath = std::filesystem::path{ modulePath }.parent_path() / L"FastCopyKeyboardHook.exe"; + if (!std::filesystem::exists(hookPath)) + { + return; + } + + STARTUPINFOW startupInfo{ sizeof(startupInfo) }; + PROCESS_INFORMATION processInfo{}; + if (CreateProcessW( + hookPath.c_str(), + nullptr, + nullptr, + nullptr, + FALSE, + CREATE_NO_WINDOW, + nullptr, + hookPath.parent_path().c_str(), + &startupInfo, + &processInfo)) + { + CloseHandle(processInfo.hThread); + CloseHandle(processInfo.hProcess); + } +} + +void KeyboardHookController::Stop() +{ + auto const window = FindWindowW(KeyboardHookSettings::WindowClassName, nullptr); + if (window) + { + PostMessageW(window, WM_CLOSE, 0, 0); + } +} diff --git a/FastCopy/KeyboardHookController.h b/FastCopy/KeyboardHookController.h new file mode 100644 index 0000000..4f09351 --- /dev/null +++ b/FastCopy/KeyboardHookController.h @@ -0,0 +1,9 @@ +#pragma once + +namespace KeyboardHookController +{ + [[nodiscard]] bool IsEnabled(); + void SetEnabled(bool enabled); + void Start(); + void Stop(); +} diff --git a/FastCopy/Package.appxmanifest b/FastCopy/Package.appxmanifest index a9d2d61..900a72f 100644 --- a/FastCopy/Package.appxmanifest +++ b/FastCopy/Package.appxmanifest @@ -52,7 +52,17 @@ FastCopy - + + + + + diff --git a/FastCopy/SettingsViewModel.cpp b/FastCopy/SettingsViewModel.cpp index 73fa0c2..2137b3b 100644 --- a/FastCopy/SettingsViewModel.cpp +++ b/FastCopy/SettingsViewModel.cpp @@ -6,6 +6,7 @@ #include "Global.h" #include "SettingsChangeListener.h" #include "RenameUtils.h" +#include "KeyboardHookController.h" namespace winrt::FastCopy::implementation { @@ -17,6 +18,14 @@ namespace winrt::FastCopy::implementation { m_model.Set(Settings::Notify, value); } + bool SettingsViewModel::KeyboardIntegration() + { + return KeyboardHookController::IsEnabled(); + } + void SettingsViewModel::KeyboardIntegration(bool value) + { + KeyboardHookController::SetEnabled(value); + } int SettingsViewModel::RenameBehavior() { return m_model.Get(Settings::RenameBehavior, 1); diff --git a/FastCopy/SettingsViewModel.h b/FastCopy/SettingsViewModel.h index 00ff98e..16b3426 100644 --- a/FastCopy/SettingsViewModel.h +++ b/FastCopy/SettingsViewModel.h @@ -13,6 +13,9 @@ namespace winrt::FastCopy::implementation bool Notify(); void Notify(bool value); + bool KeyboardIntegration(); + void KeyboardIntegration(bool value); + int RenameBehavior(); void RenameBehavior(int value); diff --git a/FastCopy/SettingsViewModel.idl b/FastCopy/SettingsViewModel.idl index 8639e94..6f7af34 100644 --- a/FastCopy/SettingsViewModel.idl +++ b/FastCopy/SettingsViewModel.idl @@ -7,6 +7,7 @@ SettingsViewModel(); Boolean Notify; + Boolean KeyboardIntegration; Int32 RenameBehavior; String RenameSuffix; Int32 MultipleWindowBehavior; diff --git a/FastCopy/SettingsWindow.xaml b/FastCopy/SettingsWindow.xaml index 60a3308..c738936 100644 --- a/FastCopy/SettingsWindow.xaml +++ b/FastCopy/SettingsWindow.xaml @@ -775,6 +775,16 @@ Style="{StaticResource ToggleSwitchLeftLabelStyle}" /> + + + + + + + Bei Fertigstellung benachrichtigen + + Explorer-Tastaturintegration + + + RoboCopyEx beim Einfügen von Dateien mit Strg+V im Datei-Explorer verwenden + FastCopy-Einstellungen diff --git a/FastCopy/Strings/en-US/Resources.resw b/FastCopy/Strings/en-US/Resources.resw index c785629..5c26f5a 100644 --- a/FastCopy/Strings/en-US/Resources.resw +++ b/FastCopy/Strings/en-US/Resources.resw @@ -219,6 +219,12 @@ Notify upon finish + + Explorer keyboard integration + + + Use RoboCopyEx when pasting files with Ctrl+V in File Explorer + This software will not work properly. Please re-install Windows! diff --git a/FastCopy/Strings/es-ES/Resources.resw b/FastCopy/Strings/es-ES/Resources.resw index 4d5b2c1..0ffe5aa 100644 --- a/FastCopy/Strings/es-ES/Resources.resw +++ b/FastCopy/Strings/es-ES/Resources.resw @@ -195,6 +195,12 @@ Notificar al acabar + + Integración de teclado del Explorador + + + Usar RoboCopyEx al pegar archivos con Ctrl+V en el Explorador de archivos + Ajustes de FastCopy diff --git a/FastCopy/Strings/zh-CN/Resources.resw b/FastCopy/Strings/zh-CN/Resources.resw index 3af9545..dfb8380 100644 --- a/FastCopy/Strings/zh-CN/Resources.resw +++ b/FastCopy/Strings/zh-CN/Resources.resw @@ -219,6 +219,12 @@ 复制完成后通知 + + 资源管理器快捷键集成 + + + 在文件资源管理器中按 Ctrl+V 时使用 RoboCopyEx + 软件无法正常工作,请重新安装Windows! diff --git a/FastCopyKeyboardHook/ClipboardFileTransfer.cpp b/FastCopyKeyboardHook/ClipboardFileTransfer.cpp new file mode 100644 index 0000000..baffe2c --- /dev/null +++ b/FastCopyKeyboardHook/ClipboardFileTransfer.cpp @@ -0,0 +1,143 @@ +#include "ClipboardFileTransfer.h" + +#include +#include +#include +#include +#include + +namespace +{ + using Microsoft::WRL::ComPtr; + + std::optional GetPreferredDropEffect(IDataObject* dataObject) + { + FORMATETC format{}; + format.cfFormat = static_cast(RegisterClipboardFormatW(CFSTR_PREFERREDDROPEFFECT)); + format.dwAspect = DVASPECT_CONTENT; + format.lindex = -1; + format.tymed = TYMED_HGLOBAL; + + STGMEDIUM medium{}; + if (FAILED(dataObject->GetData(&format, &medium))) + { + return std::nullopt; + } + + std::optional effect; + if (medium.tymed == TYMED_HGLOBAL && GlobalSize(medium.hGlobal) >= sizeof(DWORD)) + { + auto const value = static_cast(GlobalLock(medium.hGlobal)); + if (value) + { + effect = *value; + GlobalUnlock(medium.hGlobal); + } + } + ReleaseStgMedium(&medium); + return effect; + } + + std::optional> ReadShellItems(IDataObject* dataObject) + { + ComPtr items; + if (FAILED(SHCreateShellItemArrayFromDataObject(dataObject, IID_PPV_ARGS(&items)))) + { + return std::nullopt; + } + + DWORD itemCount{}; + if (FAILED(items->GetCount(&itemCount)) || itemCount == 0) + { + return std::nullopt; + } + + std::vector paths; + paths.reserve(itemCount); + for (DWORD index = 0; index < itemCount; ++index) + { + ComPtr item; + PWSTR path{}; + if (FAILED(items->GetItemAt(index, &item)) || + FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &path))) + { + return std::nullopt; + } + paths.emplace_back(path); + CoTaskMemFree(path); + } + return paths; + } + + std::optional> ReadDropFiles(IDataObject* dataObject) + { + FORMATETC format{}; + format.cfFormat = CF_HDROP; + format.dwAspect = DVASPECT_CONTENT; + format.lindex = -1; + format.tymed = TYMED_HGLOBAL; + + STGMEDIUM medium{}; + if (FAILED(dataObject->GetData(&format, &medium))) + { + return std::nullopt; + } + + auto const drop = static_cast(GlobalLock(medium.hGlobal)); + if (!drop) + { + ReleaseStgMedium(&medium); + return std::nullopt; + } + + std::vector paths; + auto const count = DragQueryFileW(drop, 0xFFFFFFFF, nullptr, 0); + paths.reserve(count); + for (UINT index = 0; index < count; ++index) + { + auto const length = DragQueryFileW(drop, index, nullptr, 0); + std::wstring path(length + 1, L'\0'); + if (length == 0 || DragQueryFileW(drop, index, path.data(), length + 1) != length) + { + paths.clear(); + break; + } + path.resize(length); + paths.push_back(std::move(path)); + } + + GlobalUnlock(medium.hGlobal); + ReleaseStgMedium(&medium); + return paths.empty() ? std::nullopt : std::optional{ std::move(paths) }; + } +} + +std::optional ClipboardFileTransfer::Read() +{ + ComPtr dataObject; + if (FAILED(OleGetClipboard(&dataObject))) + { + return std::nullopt; + } + + ClipboardFileTransfer transfer; + if (auto shellPaths = ReadShellItems(dataObject.Get())) + { + transfer.paths = std::move(*shellPaths); + } + else if (auto dropPaths = ReadDropFiles(dataObject.Get())) + { + transfer.paths = std::move(*dropPaths); + } + else + { + return std::nullopt; + } + + if (auto const effect = GetPreferredDropEffect(dataObject.Get())) + { + transfer.move = (*effect & DROPEFFECT_MOVE) != 0 && (*effect & DROPEFFECT_COPY) == 0; + } + + return transfer; +} diff --git a/FastCopyKeyboardHook/ClipboardFileTransfer.h b/FastCopyKeyboardHook/ClipboardFileTransfer.h new file mode 100644 index 0000000..0e2171d --- /dev/null +++ b/FastCopyKeyboardHook/ClipboardFileTransfer.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include + +struct ClipboardFileTransfer +{ + std::vector paths; + bool move{}; + + static std::optional Read(); +}; diff --git a/FastCopyKeyboardHook/ExplorerWindow.cpp b/FastCopyKeyboardHook/ExplorerWindow.cpp new file mode 100644 index 0000000..68f0992 --- /dev/null +++ b/FastCopyKeyboardHook/ExplorerWindow.cpp @@ -0,0 +1,185 @@ +#include "ExplorerWindow.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace +{ + using Microsoft::WRL::ComPtr; + + std::wstring GetWindowClass(HWND window) + { + std::array buffer{}; + auto const length = GetClassNameW(window, buffer.data(), static_cast(buffer.size())); + return length > 0 ? std::wstring{ buffer.data(), static_cast(length) } : std::wstring{}; + } + + bool IsTextEntryWindow(HWND window) + { + auto className = GetWindowClass(window); + std::ranges::transform(className, className.begin(), [](wchar_t value) { return std::towlower(value); }); + return className.find(L"edit") != std::wstring::npos || + className.find(L"richedit") != std::wstring::npos || + className == L"combobox"; + } + + std::optional GetFolderFromView(IShellView* shellView) + { + ComPtr folderView; + if (FAILED(shellView->QueryInterface(IID_PPV_ARGS(&folderView)))) + { + return std::nullopt; + } + + ComPtr persistFolder; + if (FAILED(folderView->GetFolder(IID_PPV_ARGS(&persistFolder)))) + { + return std::nullopt; + } + + PIDLIST_ABSOLUTE folderIdList{}; + if (FAILED(persistFolder->GetCurFolder(&folderIdList))) + { + return std::nullopt; + } + + std::array path{}; + auto const converted = SHGetPathFromIDListEx( + folderIdList, + path.data(), + static_cast(path.size()), + GPFIDL_DEFAULT); + CoTaskMemFree(folderIdList); + return converted ? std::optional{ std::filesystem::path{ path.data() } } : std::nullopt; + } + + std::optional GetFolderFromLocation(IWebBrowser2* browser) + { + BSTR location{}; + if (FAILED(browser->get_LocationURL(&location)) || !location) + { + return std::nullopt; + } + + std::array path{}; + DWORD length = static_cast(path.size()); + auto const result = PathCreateFromUrlW(location, path.data(), &length, 0); + SysFreeString(location); + if (FAILED(result)) + { + return std::nullopt; + } + + return std::filesystem::path{ path.data() }; + } + +} + +bool IsExplorerWindow(HWND window) +{ + if (!window) + { + return false; + } + + auto const className = GetWindowClass(window); + return className == L"CabinetWClass" || className == L"ExploreWClass"; +} + +std::optional GetExplorerFolder(HWND expectedForegroundWindow) +{ + if (GetForegroundWindow() != expectedForegroundWindow || !IsExplorerWindow(expectedForegroundWindow)) + { + return std::nullopt; + } + + GUITHREADINFO threadInfo{ sizeof(threadInfo) }; + auto const foregroundThread = GetWindowThreadProcessId(expectedForegroundWindow, nullptr); + if (!GetGUIThreadInfo(foregroundThread, &threadInfo) || IsTextEntryWindow(threadInfo.hwndFocus)) + { + return std::nullopt; + } + + ComPtr shellWindows; + if (FAILED(CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&shellWindows)))) + { + return std::nullopt; + } + + long count{}; + if (FAILED(shellWindows->get_Count(&count))) + { + return std::nullopt; + } + + std::optional locationFallback; + for (long index = 0; index < count; ++index) + { + VARIANT itemIndex{}; + itemIndex.vt = VT_I4; + itemIndex.lVal = index; + + ComPtr dispatch; + if (FAILED(shellWindows->Item(itemIndex, &dispatch)) || !dispatch) + { + continue; + } + + ComPtr browser; + if (FAILED(dispatch.As(&browser))) + { + continue; + } + + SHANDLE_PTR browserWindowValue{}; + if (FAILED(browser->get_HWND(&browserWindowValue)) || + reinterpret_cast(browserWindowValue) != expectedForegroundWindow) + { + continue; + } + + if (!locationFallback) + { + locationFallback = GetFolderFromLocation(browser.Get()); + } + + ComPtr serviceProvider; + ComPtr shellBrowser; + ComPtr shellView; + if (FAILED(browser.As(&serviceProvider)) || + FAILED(serviceProvider->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&shellBrowser))) || + FAILED(shellBrowser->QueryActiveShellView(&shellView))) + { + continue; + } + + HWND viewWindow{}; + if (FAILED(shellView->GetWindow(&viewWindow))) + { + continue; + } + + if (threadInfo.hwndFocus == viewWindow || IsChild(viewWindow, threadInfo.hwndFocus)) + { + if (auto folder = GetFolderFromView(shellView.Get())) + { + return folder; + } + } + + } + + // Explorer versions with tabs can expose a browser location while the + // active shell view does not support the older IFolderView interfaces. + return locationFallback; +} diff --git a/FastCopyKeyboardHook/ExplorerWindow.h b/FastCopyKeyboardHook/ExplorerWindow.h new file mode 100644 index 0000000..271f421 --- /dev/null +++ b/FastCopyKeyboardHook/ExplorerWindow.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +#include +#include + +[[nodiscard]] bool IsExplorerWindow(HWND window); +[[nodiscard]] std::optional GetExplorerFolder(HWND expectedForegroundWindow); diff --git a/FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj b/FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj new file mode 100644 index 0000000..a3d0272 --- /dev/null +++ b/FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj @@ -0,0 +1,79 @@ + + + + + Debug + x64 + + + Release + x64 + + + Package + x64 + + + + 18.0 + Win32Proj + {83e7518d-7ba4-4a13-b2c9-4ab98da778de} + FastCopyKeyboardHook + 10.0 + + + + Application + v145 + Unicode + + + true + + + false + true + + + + + + + + + + $(SolutionDir)$(Platform)\$(Configuration)\FastCopy\ + $(Platform)\$(Configuration)\ + + + + Level4 + true + stdcpp20 + /utf-8 %(AdditionalOptions) + UNICODE;_UNICODE;WIN32_LEAN_AND_MEAN;NOMINMAX;%(PreprocessorDefinitions) + + + Windows + Ole32.lib;Shell32.lib;Shlwapi.lib;User32.lib;%(AdditionalDependencies) + + + if not exist "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" mkdir "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" +xcopy "$(TargetPath)" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX\" /Y /C /I + + + + + + + + + + + + + + + + + diff --git a/FastCopyKeyboardHook/FastCopyLauncher.cpp b/FastCopyKeyboardHook/FastCopyLauncher.cpp new file mode 100644 index 0000000..f407179 --- /dev/null +++ b/FastCopyKeyboardHook/FastCopyLauncher.cpp @@ -0,0 +1,113 @@ +#include "FastCopyLauncher.h" + +#include +#include + +#include +#include +#include +#include + +namespace +{ + std::optional GetRecordDirectory() + { + PWSTR localAppData{}; + if (FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_CREATE, nullptr, &localAppData))) + { + return std::nullopt; + } + + auto const result = std::filesystem::path{ localAppData } / L"RoboCopyEx" / L"Records"; + CoTaskMemFree(localAppData); + + std::error_code error; + std::filesystem::create_directories(result, error); + return error ? std::nullopt : std::optional{ result }; + } + + std::optional WriteRecordFile(ClipboardFileTransfer const& transfer) + { + auto const directory = GetRecordDirectory(); + if (!directory) + { + return std::nullopt; + } + + FILETIME timestamp{}; + GetSystemTimePreciseAsFileTime(×tamp); + ULARGE_INTEGER timestampValue{}; + timestampValue.LowPart = timestamp.dwLowDateTime; + timestampValue.HighPart = timestamp.dwHighDateTime; + static std::atomic_uint sequence{}; + + auto const path = *directory / std::format( + L"{}{}-{}-{}.bin", + transfer.move ? L'M' : L'C', + timestampValue.QuadPart, + GetCurrentProcessId(), + sequence.fetch_add(1)); + + FILE* file{}; + if (_wfopen_s(&file, path.c_str(), L"wb") != 0) + { + return std::nullopt; + } + + bool succeeded = true; + for (auto source : transfer.paths) + { + std::ranges::replace(source, L'\\', L'/'); + auto const length = source.size(); + succeeded = fwrite(&length, sizeof(length), 1, file) == 1 && + fwrite(source.data(), sizeof(wchar_t), length, file) == length; + if (!succeeded) + { + break; + } + } + succeeded = fclose(file) == 0 && succeeded; + + if (!succeeded) + { + std::error_code error; + std::filesystem::remove(path, error); + return std::nullopt; + } + + return path; + } + +} + +bool LaunchFastCopy(ClipboardFileTransfer const& transfer, std::filesystem::path const& destination) +{ + auto const recordPath = WriteRecordFile(transfer); + if (!recordPath) + { + return false; + } + + auto destinationText = destination.wstring(); + auto recordText = recordPath->wstring(); + std::ranges::replace(destinationText, L'\\', L'/'); + std::ranges::replace(recordText, L'\\', L'/'); + + auto const uri = std::format(LR"(fastcopy://"{}"|"{}")", destinationText, recordText); + AllowSetForegroundWindow(ASFW_ANY); + auto const launchResult = reinterpret_cast(ShellExecuteW( + nullptr, + L"open", + uri.c_str(), + nullptr, + nullptr, + SW_SHOWNORMAL)); + if (launchResult <= 32) + { + std::error_code error; + std::filesystem::remove(*recordPath, error); + return false; + } + + return true; +} diff --git a/FastCopyKeyboardHook/FastCopyLauncher.h b/FastCopyKeyboardHook/FastCopyLauncher.h new file mode 100644 index 0000000..3f391bd --- /dev/null +++ b/FastCopyKeyboardHook/FastCopyLauncher.h @@ -0,0 +1,9 @@ +#pragma once + +#include "ClipboardFileTransfer.h" + +#include + +[[nodiscard]] bool LaunchFastCopy( + ClipboardFileTransfer const& transfer, + std::filesystem::path const& destination); diff --git a/FastCopyKeyboardHook/main.cpp b/FastCopyKeyboardHook/main.cpp new file mode 100644 index 0000000..d9820d6 --- /dev/null +++ b/FastCopyKeyboardHook/main.cpp @@ -0,0 +1,316 @@ +#include "ClipboardFileTransfer.h" +#include "ExplorerWindow.h" +#include "FastCopyLauncher.h" +#include "../Public/KeyboardHookSettings.h" + +#include +#include + +namespace +{ + constexpr auto PasteMessage = WM_APP + 1; + constexpr int PasteHotKeyId = 1; + constexpr UINT_PTR PasteHotKeyRestoreTimerId = 1; + constexpr ULONG_PTR ReplayInputMarker = 0x52435856; + HWND messageWindow{}; + HHOOK keyboardHook{}; + bool pasteHotKeyRegistered{}; + bool interceptedPasteKey{}; + bool pasteRequestQueued{}; + bool hookPasteGesture{}; + bool controlDown{}; + bool shiftDown{}; + bool altDown{}; + bool winDown{}; + + bool RegisterPasteHotKey() + { + return RegisterHotKey( + messageWindow, + PasteHotKeyId, + MOD_CONTROL | MOD_NOREPEAT, + L'V') != FALSE; + } + + void ReplayPaste() + { + if (pasteHotKeyRegistered) + { + UnregisterHotKey(messageWindow, PasteHotKeyId); + pasteHotKeyRegistered = false; + SetTimer(messageWindow, PasteHotKeyRestoreTimerId, 250, nullptr); + } + + INPUT input[4]{}; + UINT count{}; + auto const controlIsDown = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; + if (!controlIsDown) + { + input[count].type = INPUT_KEYBOARD; + input[count++].ki.wVk = VK_CONTROL; + } + + input[count].type = INPUT_KEYBOARD; + input[count++].ki.wVk = L'V'; + input[count].type = INPUT_KEYBOARD; + input[count].ki.wVk = L'V'; + input[count++].ki.dwFlags = KEYEVENTF_KEYUP; + + if (!controlIsDown) + { + input[count].type = INPUT_KEYBOARD; + input[count].ki.wVk = VK_CONTROL; + input[count++].ki.dwFlags = KEYEVENTF_KEYUP; + } + + for (UINT index = 0; index < count; ++index) + { + input[index].ki.dwExtraInfo = ReplayInputMarker; + } + SendInput(count, input, sizeof(INPUT)); + } + + void ClearMoveClipboard(HWND owner) + { + if (OpenClipboard(owner)) + { + EmptyClipboard(); + CloseClipboard(); + } + } + + void HandlePaste(HWND expectedExplorerWindow) + { + pasteRequestQueued = false; + auto const destination = GetExplorerFolder(expectedExplorerWindow); + auto const transfer = destination ? ClipboardFileTransfer::Read() : std::nullopt; + if (!destination || !transfer || !LaunchFastCopy(*transfer, *destination)) + { + // Never replay into a different foreground window if focus changed while this + // asynchronous request was queued. + if (GetForegroundWindow() == expectedExplorerWindow) + { + ReplayPaste(); + } + return; + } + + if (transfer->move) + { + ClearMoveClipboard(messageWindow); + } + } + + void RequestPaste(HWND foregroundWindow) + { + if (pasteRequestQueued) + { + return; + } + + pasteRequestQueued = true; + if (!PostMessageW(messageWindow, PasteMessage, reinterpret_cast(foregroundWindow), 0)) + { + pasteRequestQueued = false; + } + } + + LRESULT CALLBACK WindowProcedure(HWND window, UINT message, WPARAM parameter, LPARAM lparam) + { + switch (message) + { + case WM_HOTKEY: + if (parameter == PasteHotKeyId) + { + if (!hookPasteGesture) + { + RequestPaste(GetForegroundWindow()); + } + } + return 0; + case PasteMessage: + HandlePaste(reinterpret_cast(parameter)); + return 0; + case WM_TIMER: + if (parameter == PasteHotKeyRestoreTimerId) + { + KillTimer(window, PasteHotKeyRestoreTimerId); + pasteHotKeyRegistered = RegisterPasteHotKey(); + } + return 0; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + case WM_QUERYENDSESSION: + return TRUE; + case WM_ENDSESSION: + if (parameter) + { + DestroyWindow(window); + } + return 0; + case WM_CLOSE: + DestroyWindow(window); + return 0; + default: + return DefWindowProcW(window, message, parameter, lparam); + } + } + + LRESULT CALLBACK KeyboardProcedure(int code, WPARAM parameter, LPARAM data) + { + if (code != HC_ACTION) + { + return CallNextHookEx(keyboardHook, code, parameter, data); + } + + auto const event = reinterpret_cast(data); + auto const isReplayInput = (event->flags & LLKHF_INJECTED) != 0 && + event->dwExtraInfo == ReplayInputMarker; + auto const keyDown = parameter == WM_KEYDOWN || parameter == WM_SYSKEYDOWN; + auto const keyUp = parameter == WM_KEYUP || parameter == WM_SYSKEYUP; + if (!isReplayInput && (keyDown || keyUp)) + { + auto const pressed = keyDown; + switch (event->vkCode) + { + case VK_LCONTROL: + case VK_RCONTROL: + case VK_CONTROL: + controlDown = pressed; + break; + case VK_LSHIFT: + case VK_RSHIFT: + case VK_SHIFT: + shiftDown = pressed; + break; + case VK_LMENU: + case VK_RMENU: + case VK_MENU: + altDown = pressed; + break; + case VK_LWIN: + case VK_RWIN: + winDown = pressed; + break; + default: + break; + } + } + auto const isPaste = event->vkCode == L'V'; + if (isReplayInput || !isPaste) + { + return CallNextHookEx(keyboardHook, code, parameter, data); + } + + if (isPaste && keyUp && interceptedPasteKey) + { + interceptedPasteKey = false; + hookPasteGesture = false; + return 1; + } + + auto const modifiersMatch = controlDown && !shiftDown && !altDown && !winDown; + auto const foregroundWindow = GetForegroundWindow(); + if (!keyDown || !modifiersMatch || !IsExplorerWindow(foregroundWindow)) + { + return CallNextHookEx(keyboardHook, code, parameter, data); + } + + if (!interceptedPasteKey) + { + interceptedPasteKey = true; + hookPasteGesture = true; + RequestPaste(foregroundWindow); + } + return 1; + } +} + +int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int) +{ + if (!KeyboardHookSettings::IsEnabled()) + { + return 0; + } + + auto const singleton = CreateMutexW(nullptr, FALSE, KeyboardHookSettings::SingletonName); + if (!singleton || GetLastError() == ERROR_ALREADY_EXISTS) + { + if (singleton) + { + CloseHandle(singleton); + } + return 0; + } + + auto const comResult = OleInitialize(nullptr); + if (FAILED(comResult)) + { + CloseHandle(singleton); + return 1; + } + + WNDCLASSW windowClass{}; + windowClass.hInstance = instance; + windowClass.lpfnWndProc = WindowProcedure; + windowClass.lpszClassName = KeyboardHookSettings::WindowClassName; + if (!RegisterClassW(&windowClass)) + { + OleUninitialize(); + CloseHandle(singleton); + return 1; + } + + messageWindow = CreateWindowExW( + WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, + KeyboardHookSettings::WindowClassName, + L"", + WS_POPUP, + 0, + 0, + 0, + 0, + nullptr, + nullptr, + instance, + nullptr); + if (!messageWindow) + { + OleUninitialize(); + CloseHandle(singleton); + return 1; + } + + // RegisterHotKey gives us a message-queue path even when the low-level hook + // cannot observe a particular desktop. The low-level hook runs in parallel + // so Explorer's own Ctrl+V accelerator is explicitly suppressed. + pasteHotKeyRegistered = RegisterPasteHotKey(); + keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, KeyboardProcedure, instance, 0); + if (!pasteHotKeyRegistered && !keyboardHook) + { + DestroyWindow(messageWindow); + OleUninitialize(); + CloseHandle(singleton); + return 1; + } + + MSG message{}; + while (GetMessageW(&message, nullptr, 0, 0) > 0) + { + TranslateMessage(&message); + DispatchMessageW(&message); + } + + if (pasteHotKeyRegistered) + { + UnregisterHotKey(messageWindow, PasteHotKeyId); + } + if (keyboardHook) + { + UnhookWindowsHookEx(keyboardHook); + } + OleUninitialize(); + CloseHandle(singleton); + return 0; +} diff --git a/Public/KeyboardHookSettings.h b/Public/KeyboardHookSettings.h new file mode 100644 index 0000000..90a4043 --- /dev/null +++ b/Public/KeyboardHookSettings.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +namespace KeyboardHookSettings +{ + constexpr wchar_t RegistryPath[] = L"Software\\RoboCopyEx"; + constexpr wchar_t EnabledValueName[] = L"KeyboardHookEnabled"; + constexpr wchar_t WindowClassName[] = L"RoboCopyEx.KeyboardHook.Window"; + constexpr wchar_t SingletonName[] = L"Local\\RoboCopyEx.KeyboardHook.Singleton"; + + inline bool IsEnabled() + { + DWORD value = 0; + DWORD valueSize = sizeof(value); + auto const result = RegGetValueW( + HKEY_CURRENT_USER, + RegistryPath, + EnabledValueName, + RRF_RT_REG_DWORD, + nullptr, + &value, + &valueSize); + if (result == ERROR_FILE_NOT_FOUND || result == ERROR_PATH_NOT_FOUND) + { + return true; + } + return result == ERROR_SUCCESS && value != 0; + } + + inline bool SetEnabled(bool enabled) + { + HKEY key{}; + if (RegCreateKeyExW( + HKEY_CURRENT_USER, + RegistryPath, + 0, + nullptr, + 0, + KEY_SET_VALUE, + nullptr, + &key, + nullptr) != ERROR_SUCCESS) + { + return false; + } + + DWORD const value = enabled ? 1 : 0; + auto const result = RegSetValueExW( + key, + EnabledValueName, + 0, + REG_DWORD, + reinterpret_cast(&value), + sizeof(value)); + RegCloseKey(key); + return result == ERROR_SUCCESS; + } +} From 5f0c82a1be7560035882a3d37c7adde800f9dc95 Mon Sep 17 00:00:00 2001 From: b1xcy Date: Tue, 11 Aug 2026 12:43:46 +0800 Subject: [PATCH 2/9] Fix malformed MSBuild project closing tag --- FastCopy/FastCopy.vcxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastCopy/FastCopy.vcxproj b/FastCopy/FastCopy.vcxproj index 0d93657..8439fca 100644 --- a/FastCopy/FastCopy.vcxproj +++ b/FastCopy/FastCopy.vcxproj @@ -820,4 +820,4 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll - From ddf7c0b78dccb39a12e2e9e62e5031b3248bcad3 Mon Sep 17 00:00:00 2001 From: b1xcy Date: Thu, 13 Aug 2026 19:29:49 +0800 Subject: [PATCH 3/9] Support the desktop as a paste target for Ctrl+V The desktop window (Progman/WorkerW) is not an Explorer frame, so the keyboard hook let Ctrl+V fall through to the native paste. Treat the desktop as a valid target and resolve its folder through FOLDERID_Desktop. --- FastCopyKeyboardHook/ExplorerWindow.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/FastCopyKeyboardHook/ExplorerWindow.cpp b/FastCopyKeyboardHook/ExplorerWindow.cpp index 68f0992..6bd890a 100644 --- a/FastCopyKeyboardHook/ExplorerWindow.cpp +++ b/FastCopyKeyboardHook/ExplorerWindow.cpp @@ -93,6 +93,11 @@ bool IsExplorerWindow(HWND window) } auto const className = GetWindowClass(window); + // The desktop is a valid paste target even though it is not an Explorer frame. + if (className == L"Progman" || className == L"WorkerW") + { + return true; + } return className == L"CabinetWClass" || className == L"ExploreWClass"; } @@ -110,6 +115,20 @@ std::optional GetExplorerFolder(HWND expectedForegroundWi return std::nullopt; } + // The desktop has no shell view to resolve; its folder is the user's desktop. + auto const className = GetWindowClass(expectedForegroundWindow); + if (className == L"Progman" || className == L"WorkerW") + { + PWSTR desktopPath{}; + if (FAILED(SHGetKnownFolderPath(FOLDERID_Desktop, 0, nullptr, &desktopPath))) + { + return std::nullopt; + } + std::filesystem::path result{ desktopPath }; + CoTaskMemFree(desktopPath); + return result; + } + ComPtr shellWindows; if (FAILED(CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&shellWindows)))) { From 84300a24bce4882bec9f9194126268ae11549524 Mon Sep 17 00:00:00 2001 From: b1xcy Date: Fri, 14 Aug 2026 00:44:41 +0800 Subject: [PATCH 4/9] Address review: use WIL for resources and error handling in the keyboard hook - KeyboardHookSettings: use wil::reg::try_get_value_dword / create_unique_key - ClipboardFileTransfer: hold GetDisplayName results in wil::unique_cotaskmem_string - ExplorerWindow: wrap the COM types in an ExplorerFolderResolver class and throw (wil::ResultException) on COM failures; GetWindowClass returns the array instead of copying to a std::wstring - main: refactor into a KeyboardHookApp class (message window and private constants become class members), use wil::unique_mutex for the singleton and wil::scope_exit for OleUninitialize; failures are caught at the message loop and wWinMain and shown in a message box - add the WIL NuGet dependency to FastCopyKeyboardHook --- .../ClipboardFileTransfer.cpp | 12 +- FastCopyKeyboardHook/ExplorerWindow.cpp | 236 +++++++------- .../FastCopyKeyboardHook.vcxproj | 14 +- FastCopyKeyboardHook/main.cpp | 308 +++++++++++------- FastCopyKeyboardHook/packages.config | 4 + Public/KeyboardHookSettings.h | 46 +-- 6 files changed, 336 insertions(+), 284 deletions(-) create mode 100644 FastCopyKeyboardHook/packages.config diff --git a/FastCopyKeyboardHook/ClipboardFileTransfer.cpp b/FastCopyKeyboardHook/ClipboardFileTransfer.cpp index baffe2c..9a2b023 100644 --- a/FastCopyKeyboardHook/ClipboardFileTransfer.cpp +++ b/FastCopyKeyboardHook/ClipboardFileTransfer.cpp @@ -1,9 +1,12 @@ #include "ClipboardFileTransfer.h" +#include + +#include + #include #include #include -#include #include namespace @@ -57,14 +60,13 @@ namespace for (DWORD index = 0; index < itemCount; ++index) { ComPtr item; - PWSTR path{}; + wil::unique_cotaskmem_string path; if (FAILED(items->GetItemAt(index, &item)) || - FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &path))) + FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, wil::out_param(path)))) { return std::nullopt; } - paths.emplace_back(path); - CoTaskMemFree(path); + paths.emplace_back(path.get()); } return paths; } diff --git a/FastCopyKeyboardHook/ExplorerWindow.cpp b/FastCopyKeyboardHook/ExplorerWindow.cpp index 6bd890a..cb0aab6 100644 --- a/FastCopyKeyboardHook/ExplorerWindow.cpp +++ b/FastCopyKeyboardHook/ExplorerWindow.cpp @@ -1,6 +1,10 @@ #include "ExplorerWindow.h" #include + +#include +#include + #include #include #include @@ -10,6 +14,7 @@ #include #include +#include #include #include @@ -17,72 +22,138 @@ namespace { using Microsoft::WRL::ComPtr; - std::wstring GetWindowClass(HWND window) + std::array GetWindowClass(HWND window) { std::array buffer{}; - auto const length = GetClassNameW(window, buffer.data(), static_cast(buffer.size())); - return length > 0 ? std::wstring{ buffer.data(), static_cast(length) } : std::wstring{}; + GetClassNameW(window, buffer.data(), static_cast(buffer.size())); + return buffer; } bool IsTextEntryWindow(HWND window) { auto className = GetWindowClass(window); std::ranges::transform(className, className.begin(), [](wchar_t value) { return std::towlower(value); }); - return className.find(L"edit") != std::wstring::npos || - className.find(L"richedit") != std::wstring::npos || - className == L"combobox"; + auto const* text = className.data(); + return std::wcsstr(text, L"edit") != nullptr || + std::wcsstr(text, L"richedit") != nullptr || + std::wcscmp(text, L"combobox") == 0; } - std::optional GetFolderFromView(IShellView* shellView) + // Encapsulates the COM types used to resolve the folder of the active + // Explorer window. COM failures throw (wil::ResultException) so callers + // catch them at the outer boundary; entries that are not Explorer tabs + // are skipped, and a window without a matching shell view reports nullopt. + class ExplorerFolderResolver { - ComPtr folderView; - if (FAILED(shellView->QueryInterface(IID_PPV_ARGS(&folderView)))) + public: + ExplorerFolderResolver() { - return std::nullopt; + THROW_IF_FAILED(CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&m_shellWindows))); } - ComPtr persistFolder; - if (FAILED(folderView->GetFolder(IID_PPV_ARGS(&persistFolder)))) + std::optional Resolve(HWND frameWindow, GUITHREADINFO const& threadInfo) { - return std::nullopt; - } + long count{}; + THROW_IF_FAILED(m_shellWindows->get_Count(&count)); - PIDLIST_ABSOLUTE folderIdList{}; - if (FAILED(persistFolder->GetCurFolder(&folderIdList))) - { - return std::nullopt; + std::optional locationFallback; + for (long index = 0; index < count; ++index) + { + VARIANT itemIndex{}; + itemIndex.vt = VT_I4; + itemIndex.lVal = index; + + ComPtr dispatch; + THROW_IF_FAILED(m_shellWindows->Item(itemIndex, &dispatch)); + if (!dispatch) + { + continue; + } + + ComPtr browser; + if (FAILED(dispatch.As(&browser))) + { + continue; + } + + SHANDLE_PTR browserWindowValue{}; + THROW_IF_FAILED(browser->get_HWND(&browserWindowValue)); + if (reinterpret_cast(browserWindowValue) != frameWindow) + { + continue; + } + + if (!locationFallback) + { + locationFallback = GetFolderFromLocation(browser.Get()); + } + + ComPtr serviceProvider; + ComPtr shellBrowser; + ComPtr shellView; + if (FAILED(browser.As(&serviceProvider)) || + FAILED(serviceProvider->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&shellBrowser))) || + FAILED(shellBrowser->QueryActiveShellView(&shellView))) + { + continue; + } + + HWND viewWindow{}; + if (FAILED(shellView->GetWindow(&viewWindow))) + { + continue; + } + + if (threadInfo.hwndFocus == viewWindow || IsChild(viewWindow, threadInfo.hwndFocus)) + { + if (auto folder = GetFolderFromView(shellView.Get())) + { + return folder; + } + } + } + + return locationFallback; } - std::array path{}; - auto const converted = SHGetPathFromIDListEx( - folderIdList, - path.data(), - static_cast(path.size()), - GPFIDL_DEFAULT); - CoTaskMemFree(folderIdList); - return converted ? std::optional{ std::filesystem::path{ path.data() } } : std::nullopt; - } + private: + ComPtr m_shellWindows; - std::optional GetFolderFromLocation(IWebBrowser2* browser) - { - BSTR location{}; - if (FAILED(browser->get_LocationURL(&location)) || !location) + static std::optional GetFolderFromView(IShellView* shellView) { - return std::nullopt; + ComPtr folderView; + THROW_IF_FAILED(shellView->QueryInterface(IID_PPV_ARGS(&folderView))); + + ComPtr persistFolder; + THROW_IF_FAILED(folderView->GetFolder(IID_PPV_ARGS(&persistFolder))); + + wil::unique_cotaskmem_ptr pidl; + THROW_IF_FAILED(persistFolder->GetCurFolder(wil::out_param(pidl))); + + std::array path{}; + auto const converted = SHGetPathFromIDListEx( + pidl.get(), + path.data(), + static_cast(path.size()), + GPFIDL_DEFAULT); + return converted ? std::optional{ std::filesystem::path{ path.data() } } : std::nullopt; } - std::array path{}; - DWORD length = static_cast(path.size()); - auto const result = PathCreateFromUrlW(location, path.data(), &length, 0); - SysFreeString(location); - if (FAILED(result)) + static std::optional GetFolderFromLocation(IWebBrowser2* browser) { - return std::nullopt; - } - - return std::filesystem::path{ path.data() }; - } + wil::unique_bstr location; + THROW_IF_FAILED(browser->get_LocationURL(location.put())); + if (!location) + { + return std::nullopt; + } + std::array path{}; + DWORD length = static_cast(path.size()); + auto const result = PathCreateFromUrlW(location.get(), path.data(), &length, 0); + return FAILED(result) ? std::nullopt : std::optional{ std::filesystem::path{ path.data() } }; + } + }; } bool IsExplorerWindow(HWND window) @@ -94,11 +165,12 @@ bool IsExplorerWindow(HWND window) auto const className = GetWindowClass(window); // The desktop is a valid paste target even though it is not an Explorer frame. - if (className == L"Progman" || className == L"WorkerW") + if (std::wcscmp(className.data(), L"Progman") == 0 || std::wcscmp(className.data(), L"WorkerW") == 0) { return true; } - return className == L"CabinetWClass" || className == L"ExploreWClass"; + return std::wcscmp(className.data(), L"CabinetWClass") == 0 || + std::wcscmp(className.data(), L"ExploreWClass") == 0; } std::optional GetExplorerFolder(HWND expectedForegroundWindow) @@ -117,7 +189,7 @@ std::optional GetExplorerFolder(HWND expectedForegroundWi // The desktop has no shell view to resolve; its folder is the user's desktop. auto const className = GetWindowClass(expectedForegroundWindow); - if (className == L"Progman" || className == L"WorkerW") + if (std::wcscmp(className.data(), L"Progman") == 0 || std::wcscmp(className.data(), L"WorkerW") == 0) { PWSTR desktopPath{}; if (FAILED(SHGetKnownFolderPath(FOLDERID_Desktop, 0, nullptr, &desktopPath))) @@ -129,76 +201,6 @@ std::optional GetExplorerFolder(HWND expectedForegroundWi return result; } - ComPtr shellWindows; - if (FAILED(CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&shellWindows)))) - { - return std::nullopt; - } - - long count{}; - if (FAILED(shellWindows->get_Count(&count))) - { - return std::nullopt; - } - - std::optional locationFallback; - for (long index = 0; index < count; ++index) - { - VARIANT itemIndex{}; - itemIndex.vt = VT_I4; - itemIndex.lVal = index; - - ComPtr dispatch; - if (FAILED(shellWindows->Item(itemIndex, &dispatch)) || !dispatch) - { - continue; - } - - ComPtr browser; - if (FAILED(dispatch.As(&browser))) - { - continue; - } - - SHANDLE_PTR browserWindowValue{}; - if (FAILED(browser->get_HWND(&browserWindowValue)) || - reinterpret_cast(browserWindowValue) != expectedForegroundWindow) - { - continue; - } - - if (!locationFallback) - { - locationFallback = GetFolderFromLocation(browser.Get()); - } - - ComPtr serviceProvider; - ComPtr shellBrowser; - ComPtr shellView; - if (FAILED(browser.As(&serviceProvider)) || - FAILED(serviceProvider->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&shellBrowser))) || - FAILED(shellBrowser->QueryActiveShellView(&shellView))) - { - continue; - } - - HWND viewWindow{}; - if (FAILED(shellView->GetWindow(&viewWindow))) - { - continue; - } - - if (threadInfo.hwndFocus == viewWindow || IsChild(viewWindow, threadInfo.hwndFocus)) - { - if (auto folder = GetFolderFromView(shellView.Get())) - { - return folder; - } - } - - } - - // Explorer versions with tabs can expose a browser location while the - // active shell view does not support the older IFolderView interfaces. - return locationFallback; + ExplorerFolderResolver resolver; + return resolver.Resolve(expectedForegroundWindow, threadInfo); } diff --git a/FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj b/FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj index a3d0272..8560a59 100644 --- a/FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj +++ b/FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj @@ -70,10 +70,22 @@ xcopy "$(TargetPath)" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX\ + + + + - + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + diff --git a/FastCopyKeyboardHook/main.cpp b/FastCopyKeyboardHook/main.cpp index d9820d6..e8d4a77 100644 --- a/FastCopyKeyboardHook/main.cpp +++ b/FastCopyKeyboardHook/main.cpp @@ -3,30 +3,132 @@ #include "FastCopyLauncher.h" #include "../Public/KeyboardHookSettings.h" +#include +#include + #include #include +#include + namespace { - constexpr auto PasteMessage = WM_APP + 1; - constexpr int PasteHotKeyId = 1; - constexpr UINT_PTR PasteHotKeyRestoreTimerId = 1; - constexpr ULONG_PTR ReplayInputMarker = 0x52435856; - HWND messageWindow{}; - HHOOK keyboardHook{}; - bool pasteHotKeyRegistered{}; - bool interceptedPasteKey{}; - bool pasteRequestQueued{}; - bool hookPasteGesture{}; - bool controlDown{}; - bool shiftDown{}; - bool altDown{}; - bool winDown{}; + void ShowError(char const* message) + { + if (!message) + { + message = "Unknown error"; + } + + auto const length = MultiByteToWideChar(CP_UTF8, 0, message, -1, nullptr, 0); + std::wstring wide(length > 0 ? length - 1 : 0, L'\0'); + if (length > 0) + { + MultiByteToWideChar(CP_UTF8, 0, message, -1, wide.data(), length); + } + MessageBoxW(nullptr, wide.c_str(), L"RoboCopyEx", MB_OK | MB_ICONERROR); + } +} + +class KeyboardHookApp +{ +public: + int Run(HINSTANCE instance) + { + if (!KeyboardHookSettings::IsEnabled()) + { + return 0; + } + + wil::unique_mutex singleton{ CreateMutexW(nullptr, FALSE, KeyboardHookSettings::SingletonName) }; + if (!singleton || GetLastError() == ERROR_ALREADY_EXISTS) + { + return 0; + } + + THROW_IF_FAILED(OleInitialize(nullptr)); + auto uninitializeOle = wil::scope_exit([] { OleUninitialize(); }); + + WNDCLASSW windowClass{}; + windowClass.hInstance = instance; + windowClass.lpfnWndProc = &KeyboardHookApp::StaticWindowProcedure; + windowClass.lpszClassName = KeyboardHookSettings::WindowClassName; + if (!RegisterClassW(&windowClass)) + { + THROW_HR(HRESULT_FROM_WIN32(GetLastError())); + } + + m_messageWindow = CreateWindowExW( + WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, + KeyboardHookSettings::WindowClassName, + L"", + WS_POPUP, + 0, + 0, + 0, + 0, + nullptr, + nullptr, + instance, + this); + if (!m_messageWindow) + { + THROW_HR(HRESULT_FROM_WIN32(GetLastError())); + } + + s_instance = this; + + // RegisterHotKey gives us a message-queue path even when the low-level hook + // cannot observe a particular desktop. The low-level hook runs in parallel + // so Explorer's own Ctrl+V accelerator is explicitly suppressed. + m_pasteHotKeyRegistered = RegisterPasteHotKey(); + m_keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, &KeyboardHookApp::StaticKeyboardProcedure, instance, 0); + if (!m_pasteHotKeyRegistered && !m_keyboardHook) + { + return 1; + } + + MSG message{}; + while (GetMessageW(&message, nullptr, 0, 0) > 0) + { + TranslateMessage(&message); + DispatchMessageW(&message); + } + + if (m_pasteHotKeyRegistered) + { + UnregisterHotKey(m_messageWindow, PasteHotKeyId); + } + if (m_keyboardHook) + { + UnhookWindowsHookEx(m_keyboardHook); + } + return 0; + } + +private: + static constexpr auto PasteMessage = WM_APP + 1; + static constexpr int PasteHotKeyId = 1; + static constexpr UINT_PTR PasteHotKeyRestoreTimerId = 1; + static constexpr ULONG_PTR ReplayInputMarker = 0x52435856; + + static KeyboardHookApp* s_instance; + + HWND m_messageWindow{}; + HHOOK m_keyboardHook{}; + bool m_pasteHotKeyRegistered{}; + bool m_interceptedPasteKey{}; + bool m_pasteRequestQueued{}; + bool m_hookPasteGesture{}; + bool m_controlDown{}; + bool m_shiftDown{}; + bool m_altDown{}; + bool m_winDown{}; bool RegisterPasteHotKey() { return RegisterHotKey( - messageWindow, + m_messageWindow, PasteHotKeyId, MOD_CONTROL | MOD_NOREPEAT, L'V') != FALSE; @@ -34,11 +136,11 @@ namespace void ReplayPaste() { - if (pasteHotKeyRegistered) + if (m_pasteHotKeyRegistered) { - UnregisterHotKey(messageWindow, PasteHotKeyId); - pasteHotKeyRegistered = false; - SetTimer(messageWindow, PasteHotKeyRestoreTimerId, 250, nullptr); + UnregisterHotKey(m_messageWindow, PasteHotKeyId); + m_pasteHotKeyRegistered = false; + SetTimer(m_messageWindow, PasteHotKeyRestoreTimerId, 250, nullptr); } INPUT input[4]{}; @@ -70,9 +172,9 @@ namespace SendInput(count, input, sizeof(INPUT)); } - void ClearMoveClipboard(HWND owner) + void ClearMoveClipboard() { - if (OpenClipboard(owner)) + if (OpenClipboard(m_messageWindow)) { EmptyClipboard(); CloseClipboard(); @@ -81,7 +183,7 @@ namespace void HandlePaste(HWND expectedExplorerWindow) { - pasteRequestQueued = false; + m_pasteRequestQueued = false; auto const destination = GetExplorerFolder(expectedExplorerWindow); auto const transfer = destination ? ClipboardFileTransfer::Read() : std::nullopt; if (!destination || !transfer || !LaunchFastCopy(*transfer, *destination)) @@ -97,45 +199,56 @@ namespace if (transfer->move) { - ClearMoveClipboard(messageWindow); + ClearMoveClipboard(); } } void RequestPaste(HWND foregroundWindow) { - if (pasteRequestQueued) + if (m_pasteRequestQueued) { return; } - pasteRequestQueued = true; - if (!PostMessageW(messageWindow, PasteMessage, reinterpret_cast(foregroundWindow), 0)) + m_pasteRequestQueued = true; + if (!PostMessageW(m_messageWindow, PasteMessage, reinterpret_cast(foregroundWindow), 0)) { - pasteRequestQueued = false; + m_pasteRequestQueued = false; } } - LRESULT CALLBACK WindowProcedure(HWND window, UINT message, WPARAM parameter, LPARAM lparam) + LRESULT WindowProcedure(UINT message, WPARAM parameter, LPARAM lparam) { switch (message) { case WM_HOTKEY: if (parameter == PasteHotKeyId) { - if (!hookPasteGesture) + if (!m_hookPasteGesture) { RequestPaste(GetForegroundWindow()); } } return 0; case PasteMessage: - HandlePaste(reinterpret_cast(parameter)); + try + { + HandlePaste(reinterpret_cast(parameter)); + } + catch (wil::ResultException const& e) + { + ShowError(e.what()); + } + catch (std::exception const& e) + { + ShowError(e.what()); + } return 0; case WM_TIMER: if (parameter == PasteHotKeyRestoreTimerId) { - KillTimer(window, PasteHotKeyRestoreTimerId); - pasteHotKeyRegistered = RegisterPasteHotKey(); + KillTimer(m_messageWindow, PasteHotKeyRestoreTimerId); + m_pasteHotKeyRegistered = RegisterPasteHotKey(); } return 0; case WM_DESTROY: @@ -146,22 +259,22 @@ namespace case WM_ENDSESSION: if (parameter) { - DestroyWindow(window); + DestroyWindow(m_messageWindow); } return 0; case WM_CLOSE: - DestroyWindow(window); + DestroyWindow(m_messageWindow); return 0; default: - return DefWindowProcW(window, message, parameter, lparam); + return DefWindowProcW(m_messageWindow, message, parameter, lparam); } } - LRESULT CALLBACK KeyboardProcedure(int code, WPARAM parameter, LPARAM data) + LRESULT KeyboardProcedure(int code, WPARAM parameter, LPARAM data) { if (code != HC_ACTION) { - return CallNextHookEx(keyboardHook, code, parameter, data); + return CallNextHookEx(m_keyboardHook, code, parameter, data); } auto const event = reinterpret_cast(data); @@ -177,21 +290,21 @@ namespace case VK_LCONTROL: case VK_RCONTROL: case VK_CONTROL: - controlDown = pressed; + m_controlDown = pressed; break; case VK_LSHIFT: case VK_RSHIFT: case VK_SHIFT: - shiftDown = pressed; + m_shiftDown = pressed; break; case VK_LMENU: case VK_RMENU: case VK_MENU: - altDown = pressed; + m_altDown = pressed; break; case VK_LWIN: case VK_RWIN: - winDown = pressed; + m_winDown = pressed; break; default: break; @@ -200,117 +313,70 @@ namespace auto const isPaste = event->vkCode == L'V'; if (isReplayInput || !isPaste) { - return CallNextHookEx(keyboardHook, code, parameter, data); + return CallNextHookEx(m_keyboardHook, code, parameter, data); } - if (isPaste && keyUp && interceptedPasteKey) + if (isPaste && keyUp && m_interceptedPasteKey) { - interceptedPasteKey = false; - hookPasteGesture = false; + m_interceptedPasteKey = false; + m_hookPasteGesture = false; return 1; } - auto const modifiersMatch = controlDown && !shiftDown && !altDown && !winDown; + auto const modifiersMatch = m_controlDown && !m_shiftDown && !m_altDown && !m_winDown; auto const foregroundWindow = GetForegroundWindow(); if (!keyDown || !modifiersMatch || !IsExplorerWindow(foregroundWindow)) { - return CallNextHookEx(keyboardHook, code, parameter, data); + return CallNextHookEx(m_keyboardHook, code, parameter, data); } - if (!interceptedPasteKey) + if (!m_interceptedPasteKey) { - interceptedPasteKey = true; - hookPasteGesture = true; + m_interceptedPasteKey = true; + m_hookPasteGesture = true; RequestPaste(foregroundWindow); } return 1; } -} -int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int) -{ - if (!KeyboardHookSettings::IsEnabled()) + static LRESULT CALLBACK StaticWindowProcedure(HWND window, UINT message, WPARAM parameter, LPARAM lparam) { - return 0; - } - - auto const singleton = CreateMutexW(nullptr, FALSE, KeyboardHookSettings::SingletonName); - if (!singleton || GetLastError() == ERROR_ALREADY_EXISTS) - { - if (singleton) + if (message == WM_NCCREATE) { - CloseHandle(singleton); + auto const create = reinterpret_cast(lparam); + SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(create->lpCreateParams)); } - return 0; - } - auto const comResult = OleInitialize(nullptr); - if (FAILED(comResult)) - { - CloseHandle(singleton); - return 1; - } - - WNDCLASSW windowClass{}; - windowClass.hInstance = instance; - windowClass.lpfnWndProc = WindowProcedure; - windowClass.lpszClassName = KeyboardHookSettings::WindowClassName; - if (!RegisterClassW(&windowClass)) - { - OleUninitialize(); - CloseHandle(singleton); - return 1; + auto const self = reinterpret_cast(GetWindowLongPtrW(window, GWLP_USERDATA)); + return self ? self->WindowProcedure(message, parameter, lparam) + : DefWindowProcW(window, message, parameter, lparam); } - messageWindow = CreateWindowExW( - WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, - KeyboardHookSettings::WindowClassName, - L"", - WS_POPUP, - 0, - 0, - 0, - 0, - nullptr, - nullptr, - instance, - nullptr); - if (!messageWindow) + static LRESULT CALLBACK StaticKeyboardProcedure(int code, WPARAM parameter, LPARAM data) { - OleUninitialize(); - CloseHandle(singleton); - return 1; + return s_instance + ? s_instance->KeyboardProcedure(code, parameter, data) + : CallNextHookEx(nullptr, code, parameter, data); } +}; - // RegisterHotKey gives us a message-queue path even when the low-level hook - // cannot observe a particular desktop. The low-level hook runs in parallel - // so Explorer's own Ctrl+V accelerator is explicitly suppressed. - pasteHotKeyRegistered = RegisterPasteHotKey(); - keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, KeyboardProcedure, instance, 0); - if (!pasteHotKeyRegistered && !keyboardHook) - { - DestroyWindow(messageWindow); - OleUninitialize(); - CloseHandle(singleton); - return 1; - } +KeyboardHookApp* KeyboardHookApp::s_instance = nullptr; - MSG message{}; - while (GetMessageW(&message, nullptr, 0, 0) > 0) +int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int) +{ + try { - TranslateMessage(&message); - DispatchMessageW(&message); + KeyboardHookApp app; + return app.Run(instance); } - - if (pasteHotKeyRegistered) + catch (wil::ResultException const& e) { - UnregisterHotKey(messageWindow, PasteHotKeyId); + ShowError(e.what()); + return 1; } - if (keyboardHook) + catch (std::exception const& e) { - UnhookWindowsHookEx(keyboardHook); + ShowError(e.what()); + return 1; } - OleUninitialize(); - CloseHandle(singleton); - return 0; } diff --git a/FastCopyKeyboardHook/packages.config b/FastCopyKeyboardHook/packages.config new file mode 100644 index 0000000..ebfd4f5 --- /dev/null +++ b/FastCopyKeyboardHook/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/Public/KeyboardHookSettings.h b/Public/KeyboardHookSettings.h index 90a4043..3063bdb 100644 --- a/Public/KeyboardHookSettings.h +++ b/Public/KeyboardHookSettings.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace KeyboardHookSettings { @@ -11,49 +12,14 @@ namespace KeyboardHookSettings inline bool IsEnabled() { - DWORD value = 0; - DWORD valueSize = sizeof(value); - auto const result = RegGetValueW( - HKEY_CURRENT_USER, - RegistryPath, - EnabledValueName, - RRF_RT_REG_DWORD, - nullptr, - &value, - &valueSize); - if (result == ERROR_FILE_NOT_FOUND || result == ERROR_PATH_NOT_FOUND) - { - return true; - } - return result == ERROR_SUCCESS && value != 0; + auto const value = wil::reg::try_get_value_dword(HKEY_CURRENT_USER, RegistryPath, EnabledValueName); + // A missing value (first run) means enabled. + return !value || *value != 0; } inline bool SetEnabled(bool enabled) { - HKEY key{}; - if (RegCreateKeyExW( - HKEY_CURRENT_USER, - RegistryPath, - 0, - nullptr, - 0, - KEY_SET_VALUE, - nullptr, - &key, - nullptr) != ERROR_SUCCESS) - { - return false; - } - - DWORD const value = enabled ? 1 : 0; - auto const result = RegSetValueExW( - key, - EnabledValueName, - 0, - REG_DWORD, - reinterpret_cast(&value), - sizeof(value)); - RegCloseKey(key); - return result == ERROR_SUCCESS; + auto const key = wil::reg::create_unique_key(HKEY_CURRENT_USER, RegistryPath, wil::reg::key_access::readwrite); + return SUCCEEDED(wil::reg::set_value_dword_nothrow(key.get(), EnabledValueName, enabled ? 1 : 0)); } } From 5b3ec1593ead43a1e01308c1c23aaaeaa7670199 Mon Sep 17 00:00:00 2001 From: b1xcy Date: Fri, 14 Aug 2026 00:44:41 +0800 Subject: [PATCH 5/9] Address review: extract ModulePath to Public/ and surface hook startup errors - Move the module path helper out of DllIconFormatter into Public/ModulePath and reuse it in KeyboardHookController instead of the 32K scratch buffer - KeyboardHookController: use wil::unique_process_information and throw when the hook executable is missing instead of silently returning - SettingsViewModel: catch hook startup errors and show them --- FastCopy/FastCopy.vcxproj | 4 +++ FastCopy/KeyboardHookController.cpp | 29 +++++---------- FastCopy/SettingsViewModel.cpp | 35 ++++++++++++++++++- FastCopyShellExtension/DllIconFormatter.cpp | 19 +++------- FastCopyShellExtension/DllIconFormatter.h | 1 - .../FastCopyShellExtension.vcxproj | 2 ++ Public/ModulePath.cpp | 10 ++++++ Public/ModulePath.h | 7 ++++ 8 files changed, 70 insertions(+), 37 deletions(-) create mode 100644 Public/ModulePath.cpp create mode 100644 Public/ModulePath.h diff --git a/FastCopy/FastCopy.vcxproj b/FastCopy/FastCopy.vcxproj index 8439fca..5da5f90 100644 --- a/FastCopy/FastCopy.vcxproj +++ b/FastCopy/FastCopy.vcxproj @@ -218,6 +218,7 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll + @@ -399,6 +400,9 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll + + NotUsing + diff --git a/FastCopy/KeyboardHookController.cpp b/FastCopy/KeyboardHookController.cpp index 0f56dca..54c979e 100644 --- a/FastCopy/KeyboardHookController.cpp +++ b/FastCopy/KeyboardHookController.cpp @@ -1,6 +1,10 @@ #include "pch.h" #include "KeyboardHookController.h" #include "../Public/KeyboardHookSettings.h" +#include "../Public/ModulePath.h" + +#include +#include #include @@ -26,23 +30,12 @@ void KeyboardHookController::Start() return; } - std::wstring modulePath(32768, L'\0'); - auto const length = GetModuleFileNameW(nullptr, modulePath.data(), static_cast(modulePath.size())); - if (length == 0 || length == modulePath.size()) - { - return; - } - modulePath.resize(length); - - auto const hookPath = std::filesystem::path{ modulePath }.parent_path() / L"FastCopyKeyboardHook.exe"; - if (!std::filesystem::exists(hookPath)) - { - return; - } + auto const hookPath = std::filesystem::path{ ModulePath() }.parent_path() / L"FastCopyKeyboardHook.exe"; + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND), !std::filesystem::exists(hookPath)); STARTUPINFOW startupInfo{ sizeof(startupInfo) }; - PROCESS_INFORMATION processInfo{}; - if (CreateProcessW( + wil::unique_process_information processInfo; + THROW_IF_WIN32_BOOL_FALSE(CreateProcessW( hookPath.c_str(), nullptr, nullptr, @@ -52,11 +45,7 @@ void KeyboardHookController::Start() nullptr, hookPath.parent_path().c_str(), &startupInfo, - &processInfo)) - { - CloseHandle(processInfo.hThread); - CloseHandle(processInfo.hProcess); - } + processInfo.addressof())); } void KeyboardHookController::Stop() diff --git a/FastCopy/SettingsViewModel.cpp b/FastCopy/SettingsViewModel.cpp index 2137b3b..9ea9080 100644 --- a/FastCopy/SettingsViewModel.cpp +++ b/FastCopy/SettingsViewModel.cpp @@ -8,6 +8,28 @@ #include "RenameUtils.h" #include "KeyboardHookController.h" +#include + +#include + +namespace +{ + void ShowError(char const* message) + { + if (!message) + { + message = "Unknown error"; + } + auto const length = MultiByteToWideChar(CP_UTF8, 0, message, -1, nullptr, 0); + std::wstring wide(length > 0 ? length - 1 : 0, L'\0'); + if (length > 0) + { + MultiByteToWideChar(CP_UTF8, 0, message, -1, wide.data(), length); + } + MessageBoxW(nullptr, wide.c_str(), L"RoboCopyEx", MB_OK | MB_ICONERROR); + } +} + namespace winrt::FastCopy::implementation { bool SettingsViewModel::Notify() @@ -24,7 +46,18 @@ namespace winrt::FastCopy::implementation } void SettingsViewModel::KeyboardIntegration(bool value) { - KeyboardHookController::SetEnabled(value); + try + { + KeyboardHookController::SetEnabled(value); + } + catch (wil::ResultException const& e) + { + ShowError(e.what()); + } + catch (std::exception const& e) + { + ShowError(e.what()); + } } int SettingsViewModel::RenameBehavior() { diff --git a/FastCopyShellExtension/DllIconFormatter.cpp b/FastCopyShellExtension/DllIconFormatter.cpp index 9f6eb33..211f276 100644 --- a/FastCopyShellExtension/DllIconFormatter.cpp +++ b/FastCopyShellExtension/DllIconFormatter.cpp @@ -1,31 +1,20 @@ #include "DllIconFormatter.h" #include -#include -#include "resource.h" #include +#include "ModulePath.h" +#include "resource.h" #include "IconProvider.h" -std::wstring_view DllIconFormatter::currentDllPath() -{ - static std::array path = [] - { - std::array value; - GetModuleFileName(GetModuleHandle(L"FastCopyShellExtension.dll"), value.data(), std::size(path)); - return value; - }(); - return path.data(); -} - std::wstring DllIconFormatter::GetForRootCommand() { - return std::format(L"{},{}", currentDllPath(), -IDI_ICON1); + return std::format(L"{},{}", ModulePath(GetModuleHandleW(L"FastCopyShellExtension.dll")), -IDI_ICON1); } std::wstring DllIconFormatter::GetForSubCommand(CopyOperation op) { return std::format( L"{},{}", - currentDllPath(), + ModulePath(GetModuleHandleW(L"FastCopyShellExtension.dll")), -IconProvider::GetForCurrentTheme().GetForOperation(op) ); } diff --git a/FastCopyShellExtension/DllIconFormatter.h b/FastCopyShellExtension/DllIconFormatter.h index db49168..686ab68 100644 --- a/FastCopyShellExtension/DllIconFormatter.h +++ b/FastCopyShellExtension/DllIconFormatter.h @@ -4,7 +4,6 @@ class DllIconFormatter { - static std::wstring_view currentDllPath(); public: static std::wstring GetForRootCommand(); static std::wstring GetForSubCommand(CopyOperation op); diff --git a/FastCopyShellExtension/FastCopyShellExtension.vcxproj b/FastCopyShellExtension/FastCopyShellExtension.vcxproj index 2889d5d..c1c37db 100644 --- a/FastCopyShellExtension/FastCopyShellExtension.vcxproj +++ b/FastCopyShellExtension/FastCopyShellExtension.vcxproj @@ -287,6 +287,7 @@ xcopy "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configurati + @@ -305,6 +306,7 @@ xcopy "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configurati + diff --git a/Public/ModulePath.cpp b/Public/ModulePath.cpp new file mode 100644 index 0000000..81bc305 --- /dev/null +++ b/Public/ModulePath.cpp @@ -0,0 +1,10 @@ +#include "ModulePath.h" + +#include + +std::wstring_view ModulePath(HMODULE module) +{ + static std::array buffer{}; + auto const length = GetModuleFileNameW(module, buffer.data(), static_cast(buffer.size())); + return length > 0 && length < buffer.size() ? std::wstring_view{ buffer.data(), length } : std::wstring_view{}; +} diff --git a/Public/ModulePath.h b/Public/ModulePath.h new file mode 100644 index 0000000..3541365 --- /dev/null +++ b/Public/ModulePath.h @@ -0,0 +1,7 @@ +#pragma once +#include +#include + +// The full path of the given module, or of the current process's main module +// when `module` is nullptr. Empty when the path does not fit the buffer. +[[nodiscard]] std::wstring_view ModulePath(HMODULE module = nullptr); From 76f4878f18839f1ed3960968905440e6a9937b0b Mon Sep 17 00:00:00 2001 From: b1xcy Date: Fri, 14 Aug 2026 00:55:35 +0800 Subject: [PATCH 6/9] Address review: store clipboard paths as wil::unique_cotaskmem_string ClipboardFileTransfer::paths now holds wil::unique_cotaskmem_string instead of copying every display name into a std::wstring; the values are consumed (normalized and serialized) only when the record file is written. --- .../ClipboardFileTransfer.cpp | 20 +++++++++++-------- FastCopyKeyboardHook/ClipboardFileTransfer.h | 9 +++++++-- FastCopyKeyboardHook/FastCopyLauncher.cpp | 10 ++++++---- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/FastCopyKeyboardHook/ClipboardFileTransfer.cpp b/FastCopyKeyboardHook/ClipboardFileTransfer.cpp index 9a2b023..875eadd 100644 --- a/FastCopyKeyboardHook/ClipboardFileTransfer.cpp +++ b/FastCopyKeyboardHook/ClipboardFileTransfer.cpp @@ -41,7 +41,7 @@ namespace return effect; } - std::optional> ReadShellItems(IDataObject* dataObject) + std::optional> ReadShellItems(IDataObject* dataObject) { ComPtr items; if (FAILED(SHCreateShellItemArrayFromDataObject(dataObject, IID_PPV_ARGS(&items)))) @@ -55,7 +55,7 @@ namespace return std::nullopt; } - std::vector paths; + std::vector paths; paths.reserve(itemCount); for (DWORD index = 0; index < itemCount; ++index) { @@ -66,12 +66,12 @@ namespace { return std::nullopt; } - paths.emplace_back(path.get()); + paths.push_back(std::move(path)); } return paths; } - std::optional> ReadDropFiles(IDataObject* dataObject) + std::optional> ReadDropFiles(IDataObject* dataObject) { FORMATETC format{}; format.cfFormat = CF_HDROP; @@ -92,19 +92,23 @@ namespace return std::nullopt; } - std::vector paths; + std::vector paths; auto const count = DragQueryFileW(drop, 0xFFFFFFFF, nullptr, 0); paths.reserve(count); for (UINT index = 0; index < count; ++index) { auto const length = DragQueryFileW(drop, index, nullptr, 0); - std::wstring path(length + 1, L'\0'); - if (length == 0 || DragQueryFileW(drop, index, path.data(), length + 1) != length) + if (length == 0) + { + paths.clear(); + break; + } + wil::unique_cotaskmem_string path(static_cast(CoTaskMemAlloc((length + 1) * sizeof(wchar_t)))); + if (!path || DragQueryFileW(drop, index, path.get(), length + 1) != length) { paths.clear(); break; } - path.resize(length); paths.push_back(std::move(path)); } diff --git a/FastCopyKeyboardHook/ClipboardFileTransfer.h b/FastCopyKeyboardHook/ClipboardFileTransfer.h index 0e2171d..852af1a 100644 --- a/FastCopyKeyboardHook/ClipboardFileTransfer.h +++ b/FastCopyKeyboardHook/ClipboardFileTransfer.h @@ -1,12 +1,17 @@ #pragma once +#include + +#include + #include -#include #include struct ClipboardFileTransfer { - std::vector paths; + // Shell item display names are CoTaskMem strings; keep them as-is instead + // of copying into std::wstring (wil::unique_cotaskmem_string owns the memory). + std::vector paths; bool move{}; static std::optional Read(); diff --git a/FastCopyKeyboardHook/FastCopyLauncher.cpp b/FastCopyKeyboardHook/FastCopyLauncher.cpp index f407179..82eea2d 100644 --- a/FastCopyKeyboardHook/FastCopyLauncher.cpp +++ b/FastCopyKeyboardHook/FastCopyLauncher.cpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace { @@ -55,12 +56,13 @@ namespace } bool succeeded = true; - for (auto source : transfer.paths) + for (auto const& source : transfer.paths) { - std::ranges::replace(source, L'\\', L'/'); - auto const length = source.size(); + std::wstring normalized{ source.get() }; + std::ranges::replace(normalized, L'\\', L'/'); + auto const length = normalized.size(); succeeded = fwrite(&length, sizeof(length), 1, file) == 1 && - fwrite(source.data(), sizeof(wchar_t), length, file) == length; + fwrite(normalized.data(), sizeof(wchar_t), length, file) == length; if (!succeeded) { break; From 45408ad867001572235cae504c4d8f1834725cc4 Mon Sep 17 00:00:00 2001 From: b1xcy Date: Fri, 14 Aug 2026 01:25:46 +0800 Subject: [PATCH 7/9] Address review: keep per-entry skip/fallback semantics in ExplorerFolderResolver --- FastCopyKeyboardHook/ExplorerWindow.cpp | 32 ++++++++++++++++--------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/FastCopyKeyboardHook/ExplorerWindow.cpp b/FastCopyKeyboardHook/ExplorerWindow.cpp index cb0aab6..1904eb6 100644 --- a/FastCopyKeyboardHook/ExplorerWindow.cpp +++ b/FastCopyKeyboardHook/ExplorerWindow.cpp @@ -40,9 +40,11 @@ namespace } // Encapsulates the COM types used to resolve the folder of the active - // Explorer window. COM failures throw (wil::ResultException) so callers - // catch them at the outer boundary; entries that are not Explorer tabs - // are skipped, and a window without a matching shell view reports nullopt. + // Explorer window. Failures to enumerate the shell windows collection + // throw (wil::ResultException) so callers catch them at the outer + // boundary; failures that are expected for individual entries are + // skipped, and a window whose active view does not support the older + // IFolderView interfaces falls back to the browser location URL. class ExplorerFolderResolver { public: @@ -64,8 +66,7 @@ namespace itemIndex.lVal = index; ComPtr dispatch; - THROW_IF_FAILED(m_shellWindows->Item(itemIndex, &dispatch)); - if (!dispatch) + if (FAILED(m_shellWindows->Item(itemIndex, &dispatch)) || !dispatch) { continue; } @@ -77,8 +78,8 @@ namespace } SHANDLE_PTR browserWindowValue{}; - THROW_IF_FAILED(browser->get_HWND(&browserWindowValue)); - if (reinterpret_cast(browserWindowValue) != frameWindow) + if (FAILED(browser->get_HWND(&browserWindowValue)) || + reinterpret_cast(browserWindowValue) != frameWindow) { continue; } @@ -106,9 +107,19 @@ namespace if (threadInfo.hwndFocus == viewWindow || IsChild(viewWindow, threadInfo.hwndFocus)) { - if (auto folder = GetFolderFromView(shellView.Get())) + try { - return folder; + if (auto folder = GetFolderFromView(shellView.Get())) + { + return folder; + } + } + catch (wil::ResultException const&) + { + // Explorer versions with tabs can expose a browser + // location while the active shell view does not support + // the older IFolderView interfaces; fall back to the + // location URL instead of failing the paste. } } } @@ -142,8 +153,7 @@ namespace static std::optional GetFolderFromLocation(IWebBrowser2* browser) { wil::unique_bstr location; - THROW_IF_FAILED(browser->get_LocationURL(location.put())); - if (!location) + if (FAILED(browser->get_LocationURL(location.put())) || !location) { return std::nullopt; } From 30fdbae7f3cd21db85abc6a2d174e62f8ac006df Mon Sep 17 00:00:00 2001 From: b1xcy Date: Fri, 14 Aug 2026 21:08:19 +0800 Subject: [PATCH 8/9] Address review: share shell window wrappers and split the hook into classes - Move the ShellWindows/WebBrowser2 wrappers from FastCopyShellExtension into Public/ (drop the ATL dependency) and reuse them from the keyboard hook's ExplorerFolderResolver instead of a private implementation - ExplorerFolderResolver moves into its own .h/.cpp; ExplorerWindow keeps only the window checks, and the desktop branch now uses wil::unique_cotaskmem_string - Split the hook app into four classes: PasteWindow (message window), PasteHotKey (RAII register/unregister), KeyboardHook (RAII hook via wil::unique_hhook) and KeyboardHookApp (composition and paste logic); private members start with a lower-case letter, and the keyboard event handler is restructured to early-return instead of nested ifs --- .../ExplorerFolderResolver.cpp | 98 +++++ FastCopyKeyboardHook/ExplorerFolderResolver.h | 23 ++ FastCopyKeyboardHook/ExplorerWindow.cpp | 145 +------ .../FastCopyKeyboardHook.vcxproj | 14 + FastCopyKeyboardHook/KeyboardHook.cpp | 25 ++ FastCopyKeyboardHook/KeyboardHook.h | 30 ++ FastCopyKeyboardHook/KeyboardHookApp.cpp | 260 +++++++++++++ FastCopyKeyboardHook/KeyboardHookApp.h | 40 ++ FastCopyKeyboardHook/PasteHotKey.cpp | 26 ++ FastCopyKeyboardHook/PasteHotKey.h | 27 ++ FastCopyKeyboardHook/PasteWindow.cpp | 79 ++++ FastCopyKeyboardHook/PasteWindow.h | 30 ++ FastCopyKeyboardHook/main.cpp | 368 +----------------- .../FastCopyShellExtension.vcxproj | 8 +- .../FastCopyShellExtension.vcxproj.filters | 8 +- FastCopyShellExtension/ShellWindows.cpp | Bin 2886 -> 0 bytes FastCopyShellExtension/WebBrowser2.h | Bin 808 -> 0 bytes Public/ShellWindows.cpp | 61 +++ .../ShellWindows.h | 0 .../WebBrowser2.cpp | 6 +- Public/WebBrowser2.h | 25 ++ 21 files changed, 760 insertions(+), 513 deletions(-) create mode 100644 FastCopyKeyboardHook/ExplorerFolderResolver.cpp create mode 100644 FastCopyKeyboardHook/ExplorerFolderResolver.h create mode 100644 FastCopyKeyboardHook/KeyboardHook.cpp create mode 100644 FastCopyKeyboardHook/KeyboardHook.h create mode 100644 FastCopyKeyboardHook/KeyboardHookApp.cpp create mode 100644 FastCopyKeyboardHook/KeyboardHookApp.h create mode 100644 FastCopyKeyboardHook/PasteHotKey.cpp create mode 100644 FastCopyKeyboardHook/PasteHotKey.h create mode 100644 FastCopyKeyboardHook/PasteWindow.cpp create mode 100644 FastCopyKeyboardHook/PasteWindow.h delete mode 100644 FastCopyShellExtension/ShellWindows.cpp delete mode 100644 FastCopyShellExtension/WebBrowser2.h create mode 100644 Public/ShellWindows.cpp rename {FastCopyShellExtension => Public}/ShellWindows.h (100%) rename {FastCopyShellExtension => Public}/WebBrowser2.cpp (95%) create mode 100644 Public/WebBrowser2.h diff --git a/FastCopyKeyboardHook/ExplorerFolderResolver.cpp b/FastCopyKeyboardHook/ExplorerFolderResolver.cpp new file mode 100644 index 0000000..9bfebfc --- /dev/null +++ b/FastCopyKeyboardHook/ExplorerFolderResolver.cpp @@ -0,0 +1,98 @@ +#include "ExplorerFolderResolver.h" +#include "../Public/ShellWindows.h" + +#include +#include +#include + +#include +#include +#include + +#include + +std::optional ExplorerFolderResolver::Resolve(HWND frameWindow, GUITHREADINFO const& threadInfo) +{ + ShellWindows shellWindows; + auto const count = shellWindows.Count(); + + std::optional locationFallback; + for (long index = 0; index < count; ++index) + { + VARIANT itemIndex{}; + itemIndex.vt = VT_I4; + itemIndex.lVal = index; + + auto browser = shellWindows.Item(itemIndex); + if (browser.HWND() != frameWindow) + { + continue; + } + + if (!locationFallback) + { + locationFallback = GetFolderFromLocation(browser.Get()); + } + + wil::com_ptr serviceProvider; + wil::com_ptr shellBrowser; + wil::com_ptr shellView; + if (FAILED(browser.Get()->QueryInterface(IID_PPV_ARGS(serviceProvider.put()))) || + FAILED(serviceProvider->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(shellBrowser.put()))) || + FAILED(shellBrowser->QueryActiveShellView(shellView.put()))) + { + continue; + } + + HWND viewWindow{}; + if (FAILED(shellView->GetWindow(&viewWindow))) + { + continue; + } + + if (threadInfo.hwndFocus == viewWindow || IsChild(viewWindow, threadInfo.hwndFocus)) + { + if (auto folder = GetFolderFromView(shellView.get())) + { + return folder; + } + } + } + + return locationFallback; +} + +std::optional ExplorerFolderResolver::GetFolderFromView(IShellView* shellView) +{ + wil::com_ptr folderView; + THROW_IF_FAILED(shellView->QueryInterface(IID_PPV_ARGS(folderView.put()))); + + wil::com_ptr persistFolder; + THROW_IF_FAILED(folderView->GetFolder(IID_PPV_ARGS(persistFolder.put()))); + + wil::unique_cotaskmem_ptr pidl; + THROW_IF_FAILED(persistFolder->GetCurFolder(wil::out_param(pidl))); + + std::array path{}; + auto const converted = SHGetPathFromIDListEx( + pidl.get(), + path.data(), + static_cast(path.size()), + GPFIDL_DEFAULT); + return converted ? std::optional{ std::filesystem::path{ path.data() } } : std::nullopt; +} + +std::optional ExplorerFolderResolver::GetFolderFromLocation(IWebBrowser2* browser) +{ + wil::unique_bstr location; + THROW_IF_FAILED(browser->get_LocationURL(location.put())); + if (!location) + { + return std::nullopt; + } + + std::array path{}; + DWORD length = static_cast(path.size()); + auto const result = PathCreateFromUrlW(location.get(), path.data(), &length, 0); + return FAILED(result) ? std::nullopt : std::optional{ std::filesystem::path{ path.data() } }; +} diff --git a/FastCopyKeyboardHook/ExplorerFolderResolver.h b/FastCopyKeyboardHook/ExplorerFolderResolver.h new file mode 100644 index 0000000..e3e554c --- /dev/null +++ b/FastCopyKeyboardHook/ExplorerFolderResolver.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include + +#include +#include + +// Resolves the folder of the active Explorer tab through the shared +// ShellWindows/WebBrowser2 wrappers. Enumeration failures throw +// (wil::ResultException) so callers catch them at the outer boundary; +// individual entries that are not Explorer tabs are skipped, and a window +// whose active view does not support the older IFolderView interfaces +// falls back to the browser location URL. +class ExplorerFolderResolver +{ +public: + std::optional Resolve(HWND frameWindow, GUITHREADINFO const& threadInfo); + +private: + static std::optional GetFolderFromView(IShellView* shellView); + static std::optional GetFolderFromLocation(IWebBrowser2* browser); +}; diff --git a/FastCopyKeyboardHook/ExplorerWindow.cpp b/FastCopyKeyboardHook/ExplorerWindow.cpp index 1904eb6..d1cf484 100644 --- a/FastCopyKeyboardHook/ExplorerWindow.cpp +++ b/FastCopyKeyboardHook/ExplorerWindow.cpp @@ -1,16 +1,12 @@ #include "ExplorerWindow.h" +#include "ExplorerFolderResolver.h" #include #include #include -#include #include -#include -#include -#include -#include #include #include @@ -20,8 +16,6 @@ namespace { - using Microsoft::WRL::ComPtr; - std::array GetWindowClass(HWND window) { std::array buffer{}; @@ -38,132 +32,6 @@ namespace std::wcsstr(text, L"richedit") != nullptr || std::wcscmp(text, L"combobox") == 0; } - - // Encapsulates the COM types used to resolve the folder of the active - // Explorer window. Failures to enumerate the shell windows collection - // throw (wil::ResultException) so callers catch them at the outer - // boundary; failures that are expected for individual entries are - // skipped, and a window whose active view does not support the older - // IFolderView interfaces falls back to the browser location URL. - class ExplorerFolderResolver - { - public: - ExplorerFolderResolver() - { - THROW_IF_FAILED(CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&m_shellWindows))); - } - - std::optional Resolve(HWND frameWindow, GUITHREADINFO const& threadInfo) - { - long count{}; - THROW_IF_FAILED(m_shellWindows->get_Count(&count)); - - std::optional locationFallback; - for (long index = 0; index < count; ++index) - { - VARIANT itemIndex{}; - itemIndex.vt = VT_I4; - itemIndex.lVal = index; - - ComPtr dispatch; - if (FAILED(m_shellWindows->Item(itemIndex, &dispatch)) || !dispatch) - { - continue; - } - - ComPtr browser; - if (FAILED(dispatch.As(&browser))) - { - continue; - } - - SHANDLE_PTR browserWindowValue{}; - if (FAILED(browser->get_HWND(&browserWindowValue)) || - reinterpret_cast(browserWindowValue) != frameWindow) - { - continue; - } - - if (!locationFallback) - { - locationFallback = GetFolderFromLocation(browser.Get()); - } - - ComPtr serviceProvider; - ComPtr shellBrowser; - ComPtr shellView; - if (FAILED(browser.As(&serviceProvider)) || - FAILED(serviceProvider->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&shellBrowser))) || - FAILED(shellBrowser->QueryActiveShellView(&shellView))) - { - continue; - } - - HWND viewWindow{}; - if (FAILED(shellView->GetWindow(&viewWindow))) - { - continue; - } - - if (threadInfo.hwndFocus == viewWindow || IsChild(viewWindow, threadInfo.hwndFocus)) - { - try - { - if (auto folder = GetFolderFromView(shellView.Get())) - { - return folder; - } - } - catch (wil::ResultException const&) - { - // Explorer versions with tabs can expose a browser - // location while the active shell view does not support - // the older IFolderView interfaces; fall back to the - // location URL instead of failing the paste. - } - } - } - - return locationFallback; - } - - private: - ComPtr m_shellWindows; - - static std::optional GetFolderFromView(IShellView* shellView) - { - ComPtr folderView; - THROW_IF_FAILED(shellView->QueryInterface(IID_PPV_ARGS(&folderView))); - - ComPtr persistFolder; - THROW_IF_FAILED(folderView->GetFolder(IID_PPV_ARGS(&persistFolder))); - - wil::unique_cotaskmem_ptr pidl; - THROW_IF_FAILED(persistFolder->GetCurFolder(wil::out_param(pidl))); - - std::array path{}; - auto const converted = SHGetPathFromIDListEx( - pidl.get(), - path.data(), - static_cast(path.size()), - GPFIDL_DEFAULT); - return converted ? std::optional{ std::filesystem::path{ path.data() } } : std::nullopt; - } - - static std::optional GetFolderFromLocation(IWebBrowser2* browser) - { - wil::unique_bstr location; - if (FAILED(browser->get_LocationURL(location.put())) || !location) - { - return std::nullopt; - } - - std::array path{}; - DWORD length = static_cast(path.size()); - auto const result = PathCreateFromUrlW(location.get(), path.data(), &length, 0); - return FAILED(result) ? std::nullopt : std::optional{ std::filesystem::path{ path.data() } }; - } - }; } bool IsExplorerWindow(HWND window) @@ -201,14 +69,9 @@ std::optional GetExplorerFolder(HWND expectedForegroundWi auto const className = GetWindowClass(expectedForegroundWindow); if (std::wcscmp(className.data(), L"Progman") == 0 || std::wcscmp(className.data(), L"WorkerW") == 0) { - PWSTR desktopPath{}; - if (FAILED(SHGetKnownFolderPath(FOLDERID_Desktop, 0, nullptr, &desktopPath))) - { - return std::nullopt; - } - std::filesystem::path result{ desktopPath }; - CoTaskMemFree(desktopPath); - return result; + wil::unique_cotaskmem_string desktopPath; + THROW_IF_FAILED(SHGetKnownFolderPath(FOLDERID_Desktop, 0, nullptr, wil::out_param(desktopPath))); + return std::filesystem::path{ desktopPath.get() }; } ExplorerFolderResolver resolver; diff --git a/FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj b/FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj index 8560a59..417db42 100644 --- a/FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj +++ b/FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj @@ -63,17 +63,31 @@ xcopy "$(TargetPath)" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX\ + + + + + + + + + + + + + + diff --git a/FastCopyKeyboardHook/KeyboardHook.cpp b/FastCopyKeyboardHook/KeyboardHook.cpp new file mode 100644 index 0000000..d68bb49 --- /dev/null +++ b/FastCopyKeyboardHook/KeyboardHook.cpp @@ -0,0 +1,25 @@ +#include "KeyboardHook.h" + +KeyboardHook* KeyboardHook::s_instance = nullptr; + +KeyboardHook::KeyboardHook(HINSTANCE instance, Callback callback) + : hook_{ SetWindowsHookExW(WH_KEYBOARD_LL, &KeyboardHook::Procedure, instance, 0) }, + callback_{ std::move(callback) } +{ + s_instance = this; +} + +KeyboardHook::~KeyboardHook() +{ + if (s_instance == this) + { + s_instance = nullptr; + } +} + +LRESULT CALLBACK KeyboardHook::Procedure(int code, WPARAM wParam, LPARAM lParam) +{ + return s_instance + ? s_instance->callback_(code, wParam, lParam) + : CallNextHookEx(nullptr, code, wParam, lParam); +} diff --git a/FastCopyKeyboardHook/KeyboardHook.h b/FastCopyKeyboardHook/KeyboardHook.h new file mode 100644 index 0000000..b3f3d8d --- /dev/null +++ b/FastCopyKeyboardHook/KeyboardHook.h @@ -0,0 +1,30 @@ +#pragma once +#include + +#include + +#include + +// Installs a WH_KEYBOARD_LL hook and removes it on destruction (RAII). +// Incoming events are forwarded to the callback. +class KeyboardHook +{ +public: + using Callback = std::function; + + KeyboardHook(HINSTANCE instance, Callback callback); + ~KeyboardHook(); + + KeyboardHook(KeyboardHook const&) = delete; + KeyboardHook& operator=(KeyboardHook const&) = delete; + + bool installed() const { return hook_ != nullptr; } + +private: + static LRESULT CALLBACK Procedure(int code, WPARAM wParam, LPARAM lParam); + + static KeyboardHook* s_instance; + + wil::unique_hhook hook_; + Callback callback_; +}; diff --git a/FastCopyKeyboardHook/KeyboardHookApp.cpp b/FastCopyKeyboardHook/KeyboardHookApp.cpp new file mode 100644 index 0000000..5a0e881 --- /dev/null +++ b/FastCopyKeyboardHook/KeyboardHookApp.cpp @@ -0,0 +1,260 @@ +#include "KeyboardHookApp.h" +#include "ClipboardFileTransfer.h" +#include "ExplorerWindow.h" +#include "FastCopyLauncher.h" +#include "../Public/KeyboardHookSettings.h" + +#include +#include + +#include + +#include + +KeyboardHookApp::KeyboardHookApp(HINSTANCE instance) + : window_{ instance, [this](UINT message, WPARAM wParam, LPARAM lParam) + { return OnWindowMessage(message, wParam, lParam); } }, + pasteHotKey_{ window_.handle(), pasteHotKeyId, MOD_CONTROL | MOD_NOREPEAT, L'V' }, + keyboardHook_{ instance, [this](int code, WPARAM wParam, LPARAM lParam) + { return OnKeyboardEvent(code, wParam, lParam); } } +{ +} + +void KeyboardHookApp::ShowError(char const* message) +{ + if (!message) + { + message = "Unknown error"; + } + + auto const length = MultiByteToWideChar(CP_UTF8, 0, message, -1, nullptr, 0); + std::wstring wide(length > 0 ? length - 1 : 0, L'\0'); + if (length > 0) + { + MultiByteToWideChar(CP_UTF8, 0, message, -1, wide.data(), length); + } + MessageBoxW(nullptr, wide.c_str(), L"RoboCopyEx", MB_OK | MB_ICONERROR); +} + +int KeyboardHookApp::Run() +{ + if (!KeyboardHookSettings::IsEnabled()) + { + return 0; + } + + wil::unique_mutex singleton{ CreateMutexW(nullptr, FALSE, KeyboardHookSettings::SingletonName) }; + if (!singleton || GetLastError() == ERROR_ALREADY_EXISTS) + { + return 0; + } + + THROW_IF_FAILED(OleInitialize(nullptr)); + auto uninitializeOle = wil::scope_exit([] { OleUninitialize(); }); + + // RegisterHotKey gives us a message-queue path even when the low-level hook + // cannot observe a particular desktop. The low-level hook runs in parallel + // so Explorer's own Ctrl+V accelerator is explicitly suppressed. + pasteHotKey_.Register(); + if (!pasteHotKey_.registered() && !keyboardHook_.installed()) + { + return 1; + } + + MSG message{}; + while (GetMessageW(&message, nullptr, 0, 0) > 0) + { + TranslateMessage(&message); + DispatchMessageW(&message); + } + return 0; +} + +LRESULT KeyboardHookApp::OnWindowMessage(UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_HOTKEY: + if (wParam == pasteHotKeyId && !pasteKeyGesture_) + { + RequestPaste(GetForegroundWindow()); + } + return 0; + case pasteMessage: + try + { + HandlePaste(reinterpret_cast(wParam)); + } + catch (wil::ResultException const& e) + { + ShowError(e.what()); + } + catch (std::exception const& e) + { + ShowError(e.what()); + } + return 0; + case WM_TIMER: + if (wParam == pasteHotKeyRestoreTimerId) + { + KillTimer(window_.handle(), pasteHotKeyRestoreTimerId); + pasteHotKey_.Register(); + } + return 0; + default: + return DefWindowProcW(window_.handle(), message, wParam, lParam); + } +} + +LRESULT KeyboardHookApp::OnKeyboardEvent(int code, WPARAM wParam, LPARAM lParam) +{ + if (code != HC_ACTION) + { + return CallNextHookEx(nullptr, code, wParam, lParam); + } + + auto const event = reinterpret_cast(lParam); + auto const isReplayInput = (event->flags & LLKHF_INJECTED) != 0 && + event->dwExtraInfo == replayInputMarker; + auto const keyDown = wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN; + auto const keyUp = wParam == WM_KEYUP || wParam == WM_SYSKEYUP; + if (!isReplayInput && (keyDown || keyUp)) + { + auto const pressed = keyDown; + switch (event->vkCode) + { + case VK_LCONTROL: + case VK_RCONTROL: + case VK_CONTROL: + controlDown_ = pressed; + break; + case VK_LSHIFT: + case VK_RSHIFT: + case VK_SHIFT: + shiftDown_ = pressed; + break; + case VK_LMENU: + case VK_RMENU: + case VK_MENU: + altDown_ = pressed; + break; + case VK_LWIN: + case VK_RWIN: + winDown_ = pressed; + break; + default: + break; + } + } + + // Only non-replayed V key events are of interest. + if (isReplayInput || event->vkCode != L'V') + { + return CallNextHookEx(nullptr, code, wParam, lParam); + } + + // End an intercepted paste gesture on key-up. + if (keyUp && interceptedPasteKey_) + { + interceptedPasteKey_ = false; + pasteKeyGesture_ = false; + return 1; + } + + // Begin a paste gesture: Ctrl+V pressed in an Explorer window. + auto const modifiersMatch = controlDown_ && !shiftDown_ && !altDown_ && !winDown_; + if (keyDown && modifiersMatch && IsExplorerWindow(GetForegroundWindow())) + { + if (!interceptedPasteKey_) + { + interceptedPasteKey_ = true; + pasteKeyGesture_ = true; + RequestPaste(GetForegroundWindow()); + } + return 1; + } + + return CallNextHookEx(nullptr, code, wParam, lParam); +} + +void KeyboardHookApp::HandlePaste(HWND expectedExplorerWindow) +{ + pasteRequestQueued_ = false; + auto const destination = GetExplorerFolder(expectedExplorerWindow); + auto const transfer = destination ? ClipboardFileTransfer::Read() : std::nullopt; + if (!destination || !transfer || !LaunchFastCopy(*transfer, *destination)) + { + // Never replay into a different foreground window if focus changed while this + // asynchronous request was queued. + if (GetForegroundWindow() == expectedExplorerWindow) + { + ReplayPaste(); + } + return; + } + + if (transfer->move) + { + ClearMoveClipboard(); + } +} + +void KeyboardHookApp::RequestPaste(HWND foregroundWindow) +{ + if (pasteRequestQueued_) + { + return; + } + + pasteRequestQueued_ = true; + if (!PostMessageW(window_.handle(), pasteMessage, reinterpret_cast(foregroundWindow), 0)) + { + pasteRequestQueued_ = false; + } +} + +void KeyboardHookApp::ReplayPaste() +{ + if (pasteHotKey_.registered()) + { + pasteHotKey_.Unregister(); + SetTimer(window_.handle(), pasteHotKeyRestoreTimerId, 250, nullptr); + } + + INPUT input[4]{}; + UINT count{}; + auto const controlIsDown = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; + if (!controlIsDown) + { + input[count].type = INPUT_KEYBOARD; + input[count++].ki.wVk = VK_CONTROL; + } + + input[count].type = INPUT_KEYBOARD; + input[count++].ki.wVk = L'V'; + input[count].type = INPUT_KEYBOARD; + input[count].ki.wVk = L'V'; + input[count++].ki.dwFlags = KEYEVENTF_KEYUP; + + if (!controlIsDown) + { + input[count].type = INPUT_KEYBOARD; + input[count].ki.wVk = VK_CONTROL; + input[count++].ki.dwFlags = KEYEVENTF_KEYUP; + } + + for (UINT index = 0; index < count; ++index) + { + input[index].ki.dwExtraInfo = replayInputMarker; + } + SendInput(count, input, sizeof(INPUT)); +} + +void KeyboardHookApp::ClearMoveClipboard() +{ + if (OpenClipboard(window_.handle())) + { + EmptyClipboard(); + CloseClipboard(); + } +} diff --git a/FastCopyKeyboardHook/KeyboardHookApp.h b/FastCopyKeyboardHook/KeyboardHookApp.h new file mode 100644 index 0000000..74236b6 --- /dev/null +++ b/FastCopyKeyboardHook/KeyboardHookApp.h @@ -0,0 +1,40 @@ +#pragma once +#include "PasteHotKey.h" +#include "KeyboardHook.h" +#include "PasteWindow.h" + +#include + +// Owns the paste integration: the hidden window, the Ctrl+V hot key and the +// low-level keyboard hook, plus the paste handling logic. +class KeyboardHookApp +{ +public: + KeyboardHookApp(HINSTANCE instance); + int Run(); + static void ShowError(char const* message); + +private: + static constexpr auto pasteMessage = WM_APP + 1; + static constexpr int pasteHotKeyId = 1; + static constexpr UINT_PTR pasteHotKeyRestoreTimerId = 1; + static constexpr ULONG_PTR replayInputMarker = 0x52435856; + + LRESULT OnWindowMessage(UINT message, WPARAM wParam, LPARAM lParam); + LRESULT OnKeyboardEvent(int code, WPARAM wParam, LPARAM lParam); + void HandlePaste(HWND expectedExplorerWindow); + void RequestPaste(HWND foregroundWindow); + void ReplayPaste(); + void ClearMoveClipboard(); + + PasteWindow window_; + PasteHotKey pasteHotKey_; + KeyboardHook keyboardHook_; + bool interceptedPasteKey_{}; + bool pasteRequestQueued_{}; + bool pasteKeyGesture_{}; + bool controlDown_{}; + bool shiftDown_{}; + bool altDown_{}; + bool winDown_{}; +}; diff --git a/FastCopyKeyboardHook/PasteHotKey.cpp b/FastCopyKeyboardHook/PasteHotKey.cpp new file mode 100644 index 0000000..6a5f5d2 --- /dev/null +++ b/FastCopyKeyboardHook/PasteHotKey.cpp @@ -0,0 +1,26 @@ +#include "PasteHotKey.h" + +PasteHotKey::PasteHotKey(HWND window, int id, UINT modifiers, UINT virtualKey) + : window_{ window }, id_{ id }, modifiers_{ modifiers }, virtualKey_{ virtualKey } +{ +} + +PasteHotKey::~PasteHotKey() +{ + Unregister(); +} + +bool PasteHotKey::Register() +{ + registered_ = RegisterHotKey(window_, id_, modifiers_, virtualKey_) != FALSE; + return registered_; +} + +void PasteHotKey::Unregister() +{ + if (registered_) + { + UnregisterHotKey(window_, id_); + registered_ = false; + } +} diff --git a/FastCopyKeyboardHook/PasteHotKey.h b/FastCopyKeyboardHook/PasteHotKey.h new file mode 100644 index 0000000..46bf140 --- /dev/null +++ b/FastCopyKeyboardHook/PasteHotKey.h @@ -0,0 +1,27 @@ +#pragma once +#include + +// Registers a hot key on a window and unregisters it on destruction (RAII). +// The hot key can be temporarily unregistered (while a native paste is being +// replayed) and registered again afterwards. +class PasteHotKey +{ +public: + PasteHotKey(HWND window, int id, UINT modifiers, UINT virtualKey); + ~PasteHotKey(); + + PasteHotKey(PasteHotKey const&) = delete; + PasteHotKey& operator=(PasteHotKey const&) = delete; + + bool Register(); + void Unregister(); + bool registered() const { return registered_; } + int id() const { return id_; } + +private: + HWND window_{}; + int id_{}; + UINT modifiers_{}; + UINT virtualKey_{}; + bool registered_{}; +}; diff --git a/FastCopyKeyboardHook/PasteWindow.cpp b/FastCopyKeyboardHook/PasteWindow.cpp new file mode 100644 index 0000000..565c90b --- /dev/null +++ b/FastCopyKeyboardHook/PasteWindow.cpp @@ -0,0 +1,79 @@ +#include "PasteWindow.h" + +#include + +PasteWindow::PasteWindow(HINSTANCE instance, Handler handler) + : handler_{ std::move(handler) } +{ + WNDCLASSW windowClass{}; + windowClass.hInstance = instance; + windowClass.lpfnWndProc = &PasteWindow::StaticProcedure; + windowClass.lpszClassName = KeyboardHookSettings::WindowClassName; + if (!RegisterClassW(&windowClass)) + { + THROW_HR(HRESULT_FROM_WIN32(GetLastError())); + } + + window_ = CreateWindowExW( + WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, + KeyboardHookSettings::WindowClassName, + L"", + WS_POPUP, + 0, + 0, + 0, + 0, + nullptr, + nullptr, + instance, + this); + if (!window_) + { + THROW_HR(HRESULT_FROM_WIN32(GetLastError())); + } +} + +PasteWindow::~PasteWindow() +{ + if (window_) + { + DestroyWindow(window_); + } +} + +LRESULT PasteWindow::Procedure(UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_DESTROY: + PostQuitMessage(0); + return 0; + case WM_QUERYENDSESSION: + return TRUE; + case WM_ENDSESSION: + if (wParam) + { + DestroyWindow(window_); + } + return 0; + case WM_CLOSE: + DestroyWindow(window_); + return 0; + default: + return handler_ ? handler_(message, wParam, lParam) + : DefWindowProcW(window_, message, wParam, lParam); + } +} + +LRESULT CALLBACK PasteWindow::StaticProcedure(HWND window, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (message == WM_NCCREATE) + { + auto const create = reinterpret_cast(lParam); + SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(create->lpCreateParams)); + } + + auto const self = reinterpret_cast(GetWindowLongPtrW(window, GWLP_USERDATA)); + return self ? self->Procedure(message, wParam, lParam) + : DefWindowProcW(window, message, wParam, lParam); +} diff --git a/FastCopyKeyboardHook/PasteWindow.h b/FastCopyKeyboardHook/PasteWindow.h new file mode 100644 index 0000000..239dc7b --- /dev/null +++ b/FastCopyKeyboardHook/PasteWindow.h @@ -0,0 +1,30 @@ +#pragma once +#include "../Public/KeyboardHookSettings.h" + +#include + +#include + +// The hidden message-only window that receives hot-key and paste messages. +// Window lifecycle messages are handled here; everything else is forwarded +// to the handler. +class PasteWindow +{ +public: + using Handler = std::function; + + PasteWindow(HINSTANCE instance, Handler handler); + ~PasteWindow(); + + PasteWindow(PasteWindow const&) = delete; + PasteWindow& operator=(PasteWindow const&) = delete; + + HWND handle() const { return window_; } + +private: + static LRESULT CALLBACK StaticProcedure(HWND window, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT Procedure(UINT message, WPARAM wParam, LPARAM lParam); + + HWND window_{}; + Handler handler_; +}; diff --git a/FastCopyKeyboardHook/main.cpp b/FastCopyKeyboardHook/main.cpp index e8d4a77..63491c2 100644 --- a/FastCopyKeyboardHook/main.cpp +++ b/FastCopyKeyboardHook/main.cpp @@ -1,382 +1,24 @@ -#include "ClipboardFileTransfer.h" -#include "ExplorerWindow.h" -#include "FastCopyLauncher.h" -#include "../Public/KeyboardHookSettings.h" +#include "KeyboardHookApp.h" -#include #include #include -#include - -#include - -namespace -{ - void ShowError(char const* message) - { - if (!message) - { - message = "Unknown error"; - } - - auto const length = MultiByteToWideChar(CP_UTF8, 0, message, -1, nullptr, 0); - std::wstring wide(length > 0 ? length - 1 : 0, L'\0'); - if (length > 0) - { - MultiByteToWideChar(CP_UTF8, 0, message, -1, wide.data(), length); - } - MessageBoxW(nullptr, wide.c_str(), L"RoboCopyEx", MB_OK | MB_ICONERROR); - } -} - -class KeyboardHookApp -{ -public: - int Run(HINSTANCE instance) - { - if (!KeyboardHookSettings::IsEnabled()) - { - return 0; - } - - wil::unique_mutex singleton{ CreateMutexW(nullptr, FALSE, KeyboardHookSettings::SingletonName) }; - if (!singleton || GetLastError() == ERROR_ALREADY_EXISTS) - { - return 0; - } - - THROW_IF_FAILED(OleInitialize(nullptr)); - auto uninitializeOle = wil::scope_exit([] { OleUninitialize(); }); - - WNDCLASSW windowClass{}; - windowClass.hInstance = instance; - windowClass.lpfnWndProc = &KeyboardHookApp::StaticWindowProcedure; - windowClass.lpszClassName = KeyboardHookSettings::WindowClassName; - if (!RegisterClassW(&windowClass)) - { - THROW_HR(HRESULT_FROM_WIN32(GetLastError())); - } - - m_messageWindow = CreateWindowExW( - WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, - KeyboardHookSettings::WindowClassName, - L"", - WS_POPUP, - 0, - 0, - 0, - 0, - nullptr, - nullptr, - instance, - this); - if (!m_messageWindow) - { - THROW_HR(HRESULT_FROM_WIN32(GetLastError())); - } - - s_instance = this; - - // RegisterHotKey gives us a message-queue path even when the low-level hook - // cannot observe a particular desktop. The low-level hook runs in parallel - // so Explorer's own Ctrl+V accelerator is explicitly suppressed. - m_pasteHotKeyRegistered = RegisterPasteHotKey(); - m_keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, &KeyboardHookApp::StaticKeyboardProcedure, instance, 0); - if (!m_pasteHotKeyRegistered && !m_keyboardHook) - { - return 1; - } - - MSG message{}; - while (GetMessageW(&message, nullptr, 0, 0) > 0) - { - TranslateMessage(&message); - DispatchMessageW(&message); - } - - if (m_pasteHotKeyRegistered) - { - UnregisterHotKey(m_messageWindow, PasteHotKeyId); - } - if (m_keyboardHook) - { - UnhookWindowsHookEx(m_keyboardHook); - } - return 0; - } - -private: - static constexpr auto PasteMessage = WM_APP + 1; - static constexpr int PasteHotKeyId = 1; - static constexpr UINT_PTR PasteHotKeyRestoreTimerId = 1; - static constexpr ULONG_PTR ReplayInputMarker = 0x52435856; - - static KeyboardHookApp* s_instance; - - HWND m_messageWindow{}; - HHOOK m_keyboardHook{}; - bool m_pasteHotKeyRegistered{}; - bool m_interceptedPasteKey{}; - bool m_pasteRequestQueued{}; - bool m_hookPasteGesture{}; - bool m_controlDown{}; - bool m_shiftDown{}; - bool m_altDown{}; - bool m_winDown{}; - - bool RegisterPasteHotKey() - { - return RegisterHotKey( - m_messageWindow, - PasteHotKeyId, - MOD_CONTROL | MOD_NOREPEAT, - L'V') != FALSE; - } - - void ReplayPaste() - { - if (m_pasteHotKeyRegistered) - { - UnregisterHotKey(m_messageWindow, PasteHotKeyId); - m_pasteHotKeyRegistered = false; - SetTimer(m_messageWindow, PasteHotKeyRestoreTimerId, 250, nullptr); - } - - INPUT input[4]{}; - UINT count{}; - auto const controlIsDown = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; - if (!controlIsDown) - { - input[count].type = INPUT_KEYBOARD; - input[count++].ki.wVk = VK_CONTROL; - } - - input[count].type = INPUT_KEYBOARD; - input[count++].ki.wVk = L'V'; - input[count].type = INPUT_KEYBOARD; - input[count].ki.wVk = L'V'; - input[count++].ki.dwFlags = KEYEVENTF_KEYUP; - - if (!controlIsDown) - { - input[count].type = INPUT_KEYBOARD; - input[count].ki.wVk = VK_CONTROL; - input[count++].ki.dwFlags = KEYEVENTF_KEYUP; - } - - for (UINT index = 0; index < count; ++index) - { - input[index].ki.dwExtraInfo = ReplayInputMarker; - } - SendInput(count, input, sizeof(INPUT)); - } - - void ClearMoveClipboard() - { - if (OpenClipboard(m_messageWindow)) - { - EmptyClipboard(); - CloseClipboard(); - } - } - - void HandlePaste(HWND expectedExplorerWindow) - { - m_pasteRequestQueued = false; - auto const destination = GetExplorerFolder(expectedExplorerWindow); - auto const transfer = destination ? ClipboardFileTransfer::Read() : std::nullopt; - if (!destination || !transfer || !LaunchFastCopy(*transfer, *destination)) - { - // Never replay into a different foreground window if focus changed while this - // asynchronous request was queued. - if (GetForegroundWindow() == expectedExplorerWindow) - { - ReplayPaste(); - } - return; - } - - if (transfer->move) - { - ClearMoveClipboard(); - } - } - - void RequestPaste(HWND foregroundWindow) - { - if (m_pasteRequestQueued) - { - return; - } - - m_pasteRequestQueued = true; - if (!PostMessageW(m_messageWindow, PasteMessage, reinterpret_cast(foregroundWindow), 0)) - { - m_pasteRequestQueued = false; - } - } - - LRESULT WindowProcedure(UINT message, WPARAM parameter, LPARAM lparam) - { - switch (message) - { - case WM_HOTKEY: - if (parameter == PasteHotKeyId) - { - if (!m_hookPasteGesture) - { - RequestPaste(GetForegroundWindow()); - } - } - return 0; - case PasteMessage: - try - { - HandlePaste(reinterpret_cast(parameter)); - } - catch (wil::ResultException const& e) - { - ShowError(e.what()); - } - catch (std::exception const& e) - { - ShowError(e.what()); - } - return 0; - case WM_TIMER: - if (parameter == PasteHotKeyRestoreTimerId) - { - KillTimer(m_messageWindow, PasteHotKeyRestoreTimerId); - m_pasteHotKeyRegistered = RegisterPasteHotKey(); - } - return 0; - case WM_DESTROY: - PostQuitMessage(0); - return 0; - case WM_QUERYENDSESSION: - return TRUE; - case WM_ENDSESSION: - if (parameter) - { - DestroyWindow(m_messageWindow); - } - return 0; - case WM_CLOSE: - DestroyWindow(m_messageWindow); - return 0; - default: - return DefWindowProcW(m_messageWindow, message, parameter, lparam); - } - } - - LRESULT KeyboardProcedure(int code, WPARAM parameter, LPARAM data) - { - if (code != HC_ACTION) - { - return CallNextHookEx(m_keyboardHook, code, parameter, data); - } - - auto const event = reinterpret_cast(data); - auto const isReplayInput = (event->flags & LLKHF_INJECTED) != 0 && - event->dwExtraInfo == ReplayInputMarker; - auto const keyDown = parameter == WM_KEYDOWN || parameter == WM_SYSKEYDOWN; - auto const keyUp = parameter == WM_KEYUP || parameter == WM_SYSKEYUP; - if (!isReplayInput && (keyDown || keyUp)) - { - auto const pressed = keyDown; - switch (event->vkCode) - { - case VK_LCONTROL: - case VK_RCONTROL: - case VK_CONTROL: - m_controlDown = pressed; - break; - case VK_LSHIFT: - case VK_RSHIFT: - case VK_SHIFT: - m_shiftDown = pressed; - break; - case VK_LMENU: - case VK_RMENU: - case VK_MENU: - m_altDown = pressed; - break; - case VK_LWIN: - case VK_RWIN: - m_winDown = pressed; - break; - default: - break; - } - } - auto const isPaste = event->vkCode == L'V'; - if (isReplayInput || !isPaste) - { - return CallNextHookEx(m_keyboardHook, code, parameter, data); - } - - if (isPaste && keyUp && m_interceptedPasteKey) - { - m_interceptedPasteKey = false; - m_hookPasteGesture = false; - return 1; - } - - auto const modifiersMatch = m_controlDown && !m_shiftDown && !m_altDown && !m_winDown; - auto const foregroundWindow = GetForegroundWindow(); - if (!keyDown || !modifiersMatch || !IsExplorerWindow(foregroundWindow)) - { - return CallNextHookEx(m_keyboardHook, code, parameter, data); - } - - if (!m_interceptedPasteKey) - { - m_interceptedPasteKey = true; - m_hookPasteGesture = true; - RequestPaste(foregroundWindow); - } - return 1; - } - - static LRESULT CALLBACK StaticWindowProcedure(HWND window, UINT message, WPARAM parameter, LPARAM lparam) - { - if (message == WM_NCCREATE) - { - auto const create = reinterpret_cast(lparam); - SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(create->lpCreateParams)); - } - - auto const self = reinterpret_cast(GetWindowLongPtrW(window, GWLP_USERDATA)); - return self ? self->WindowProcedure(message, parameter, lparam) - : DefWindowProcW(window, message, parameter, lparam); - } - - static LRESULT CALLBACK StaticKeyboardProcedure(int code, WPARAM parameter, LPARAM data) - { - return s_instance - ? s_instance->KeyboardProcedure(code, parameter, data) - : CallNextHookEx(nullptr, code, parameter, data); - } -}; - -KeyboardHookApp* KeyboardHookApp::s_instance = nullptr; int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int) { try { - KeyboardHookApp app; - return app.Run(instance); + KeyboardHookApp app{ instance }; + return app.Run(); } catch (wil::ResultException const& e) { - ShowError(e.what()); + KeyboardHookApp::ShowError(e.what()); return 1; } catch (std::exception const& e) { - ShowError(e.what()); + KeyboardHookApp::ShowError(e.what()); return 1; } } diff --git a/FastCopyShellExtension/FastCopyShellExtension.vcxproj b/FastCopyShellExtension/FastCopyShellExtension.vcxproj index c1c37db..b127027 100644 --- a/FastCopyShellExtension/FastCopyShellExtension.vcxproj +++ b/FastCopyShellExtension/FastCopyShellExtension.vcxproj @@ -290,6 +290,8 @@ xcopy "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configurati + + @@ -298,8 +300,6 @@ xcopy "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configurati - - @@ -309,6 +309,8 @@ xcopy "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configurati + + @@ -318,8 +320,6 @@ xcopy "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configurati - - diff --git a/FastCopyShellExtension/FastCopyShellExtension.vcxproj.filters b/FastCopyShellExtension/FastCopyShellExtension.vcxproj.filters index ad359c3..c555476 100644 --- a/FastCopyShellExtension/FastCopyShellExtension.vcxproj.filters +++ b/FastCopyShellExtension/FastCopyShellExtension.vcxproj.filters @@ -40,10 +40,10 @@ COM - + COM - + COM @@ -87,10 +87,10 @@ COM - + COM - + COM diff --git a/FastCopyShellExtension/ShellWindows.cpp b/FastCopyShellExtension/ShellWindows.cpp deleted file mode 100644 index 8b4af7a67f13ec804882f2eac07626e77c3b27bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2886 zcmdT`YflqV5ItWp@jtAXkP@{RKO#0k5N%?_0D+IBk1cNKL%WMW2!CBYXD;L2mfhlu z4`$PS+&gpTac1WB`?tr)Fh&nUoS}~t36`;sPkbBlIPvB{SM`al$}sV{$ysaUcN`#-_Y75**}{dY1IJ&Ybb zoxyF=;u$T9vHHAKXXk1T>5K6?#U&I^wNA?{B|o{34CM5p&&;f|io|thHK%IXsKr`b zk8jC9!AhEt%am24j!}K~SydObp{mx-)O=DEgnCUFm##naC{$diFJU@b$MQVkx40i0 zdP1Jh&7%abxKrX?K)huj2vT7AMRyXhg1b(SMv-7@F10g)P%2&hf=?hz-1CfcgmQ2en1RWujA8qW{Sla%*U!yG8wH|je=1*Tird-XA?$6t?wu$%A*hr0w zUGlO|v>~(7C{=oo^_Aa5R~?_*SRAKIh5l>O(#U#Tktlnyn@6lh#TA)}lh0A=o?V+5 z9cD6gP@P?)fLJThB_o!L5gZO zXaBN89lP2i-~F48b&UJ!T*tp%)%VlEZf|kMTt`s2DeE~8Y+l?UH#rBtrc335;nZ- z&YPJxoA0l-W-9cgXH9uK<;s+3td`nJL+w>*#Ew&63--KvAad}BdeLAD@=WhK1zp0w z#I@-2sFXZT4{*jhK|P1JMk!L%Xg$I+2hrDXOXmR2sg}57SY|knQB2{8 z0_$eY?oMhQbx^Eb?aTDc_c`t^ncS_au6|E@WH%=@%f&USo+=Lc+KJ^nArT)kFZjwx zY|8#YsB`*V;HZ%mNMo3Rvi`r>RdlC%BZo1`#Hd<^P8TR7p(CZra9nHr->dyA6S#eF ze5V^Y&7r0J%3ic~9x=5&32QfhHUGN17~htX(|t%Y4L3d3X3UVuubHvHSw;p|RJYWg V8M=6lR=4HIZ~ + +long ShellWindows::Count() +{ + long value{}; + THROW_IF_FAILED(m_ptr->get_Count(&value)); + return value; +} + +WebBrowser2 ShellWindows::Item(VARIANT index) +{ + wil::com_ptr value; + THROW_IF_FAILED(m_ptr->Item(index, value.put())); + return WebBrowser2{ value.query() }; +} + +std::optional ShellWindows::GetForegroundExplorer() +{ + std::optional ret; + + auto const hwnd = GetForegroundWindow(); + ShellWindows shellWindows; + + auto const count = shellWindows.Count(); + for (long i = 0; i < count; ++i) + { + VARIANT itemIndex{}; + itemIndex.vt = VT_I4; + itemIndex.lVal = i; + auto item = shellWindows.Item(itemIndex); + if (item.HWND() == hwnd && item.Visible()) + { + //check if this tab is the active tab + if (item.IsParentOrSelf(GetFocus())) + { + ret.emplace(std::move(item)); + break; + } + } + } + + if(!ret.has_value()) + { + //fallback + for (long i = 0; i < count; ++i) + { + VARIANT itemIndex{}; + itemIndex.vt = VT_I4; + itemIndex.lVal = i; + auto item = shellWindows.Item(itemIndex); + if (item.HWND() == hwnd && item.Visible()) + { + ret.emplace(std::move(item)); + break; + } + } + } + + return ret; +} diff --git a/FastCopyShellExtension/ShellWindows.h b/Public/ShellWindows.h similarity index 100% rename from FastCopyShellExtension/ShellWindows.h rename to Public/ShellWindows.h diff --git a/FastCopyShellExtension/WebBrowser2.cpp b/Public/WebBrowser2.cpp similarity index 95% rename from FastCopyShellExtension/WebBrowser2.cpp rename to Public/WebBrowser2.cpp index 91ec3d5..6846b3e 100644 --- a/FastCopyShellExtension/WebBrowser2.cpp +++ b/Public/WebBrowser2.cpp @@ -1,4 +1,3 @@ -#pragma once #include "WebBrowser2.h" #include @@ -55,3 +54,8 @@ bool WebBrowser2::IsParentOrSelf(::HWND hwnd) return IsChild(shellBrowserWindow, hwnd); } + +IWebBrowser2* WebBrowser2::Get() const +{ + return m_ptr.get(); +} diff --git a/Public/WebBrowser2.h b/Public/WebBrowser2.h new file mode 100644 index 0000000..8e14deb --- /dev/null +++ b/Public/WebBrowser2.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include + +class WebBrowser2 +{ + wil::com_ptr m_ptr; +public: + WebBrowser2(wil::com_ptr&& ptr); + + HWND HWND(); + + //this will be the folder name, not the full path + wil::unique_bstr LocationName(); + + //this will be a file URL, like: file:///c/ + wil::unique_bstr LocationURL(); + + bool Visible(); + + bool IsParentOrSelf(::HWND hwnd); + + // The underlying browser interface. + IWebBrowser2* Get() const; +}; From 247b2c174c315e0491ba59b720d120284529d490 Mon Sep 17 00:00:00 2001 From: HO-COOH Date: Wed, 19 Aug 2026 01:06:10 +0800 Subject: [PATCH 9/9] Fix PasteWindow incorrect creation --- FastCopyKeyboardHook/PasteWindow.cpp | 86 ++++++++++------------------ FastCopyKeyboardHook/PasteWindow.h | 18 +++--- 2 files changed, 39 insertions(+), 65 deletions(-) diff --git a/FastCopyKeyboardHook/PasteWindow.cpp b/FastCopyKeyboardHook/PasteWindow.cpp index 565c90b..1d9a37e 100644 --- a/FastCopyKeyboardHook/PasteWindow.cpp +++ b/FastCopyKeyboardHook/PasteWindow.cpp @@ -3,77 +3,53 @@ #include PasteWindow::PasteWindow(HINSTANCE instance, Handler handler) - : handler_{ std::move(handler) } + : m_handler{ std::move(handler) } { - WNDCLASSW windowClass{}; - windowClass.hInstance = instance; - windowClass.lpfnWndProc = &PasteWindow::StaticProcedure; - windowClass.lpszClassName = KeyboardHookSettings::WindowClassName; - if (!RegisterClassW(&windowClass)) + WNDCLASSW windowClass { - THROW_HR(HRESULT_FROM_WIN32(GetLastError())); - } + .lpfnWndProc = &PasteWindow::windowProc, + .hInstance = instance, + .lpszClassName = KeyboardHookSettings::WindowClassName, + }; + THROW_LAST_ERROR_IF(!RegisterClassW(&windowClass)); - window_ = CreateWindowExW( - WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, + m_window.reset(CreateWindowExW( + 0, KeyboardHookSettings::WindowClassName, - L"", - WS_POPUP, + nullptr, 0, 0, 0, 0, - nullptr, + 0, + HWND_MESSAGE, nullptr, instance, - this); - if (!window_) - { - THROW_HR(HRESULT_FROM_WIN32(GetLastError())); - } -} - -PasteWindow::~PasteWindow() -{ - if (window_) - { - DestroyWindow(window_); - } + this)); + THROW_LAST_ERROR_IF(!m_window); } -LRESULT PasteWindow::Procedure(UINT message, WPARAM wParam, LPARAM lParam) +LRESULT CALLBACK PasteWindow::windowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { - case WM_DESTROY: - PostQuitMessage(0); - return 0; - case WM_QUERYENDSESSION: - return TRUE; - case WM_ENDSESSION: - if (wParam) + case WM_NCCREATE: { - DestroyWindow(window_); + auto const create = reinterpret_cast(lParam); + SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(create->lpCreateParams)); + return TRUE; + } + case WM_DESTROY: + PostQuitMessage(0); + return 0; + case WM_CLOSE: + DestroyWindow(window); + return 0; + default: + { + if (auto self = reinterpret_cast(GetWindowLongPtrW(window, GWLP_USERDATA)); self && self->m_handler) + return self->m_handler(message, wParam, lParam); + return DefWindowProcW(window, message, wParam, lParam); } - return 0; - case WM_CLOSE: - DestroyWindow(window_); - return 0; - default: - return handler_ ? handler_(message, wParam, lParam) - : DefWindowProcW(window_, message, wParam, lParam); - } -} - -LRESULT CALLBACK PasteWindow::StaticProcedure(HWND window, UINT message, WPARAM wParam, LPARAM lParam) -{ - if (message == WM_NCCREATE) - { - auto const create = reinterpret_cast(lParam); - SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(create->lpCreateParams)); } - - auto const self = reinterpret_cast(GetWindowLongPtrW(window, GWLP_USERDATA)); - return self ? self->Procedure(message, wParam, lParam) - : DefWindowProcW(window, message, wParam, lParam); } diff --git a/FastCopyKeyboardHook/PasteWindow.h b/FastCopyKeyboardHook/PasteWindow.h index 239dc7b..1703a87 100644 --- a/FastCopyKeyboardHook/PasteWindow.h +++ b/FastCopyKeyboardHook/PasteWindow.h @@ -2,29 +2,27 @@ #include "../Public/KeyboardHookSettings.h" #include - +#include #include -// The hidden message-only window that receives hot-key and paste messages. -// Window lifecycle messages are handled here; everything else is forwarded -// to the handler. +// The message-only window (HWND_MESSAGE) that receives hot-key and paste +// messages. Window lifecycle messages are handled here; everything else is +// forwarded to the handler. class PasteWindow { public: using Handler = std::function; PasteWindow(HINSTANCE instance, Handler handler); - ~PasteWindow(); PasteWindow(PasteWindow const&) = delete; PasteWindow& operator=(PasteWindow const&) = delete; - HWND handle() const { return window_; } + HWND handle() const { return m_window.get(); } private: - static LRESULT CALLBACK StaticProcedure(HWND window, UINT message, WPARAM wParam, LPARAM lParam); - LRESULT Procedure(UINT message, WPARAM wParam, LPARAM lParam); + static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam); - HWND window_{}; - Handler handler_; + wil::unique_hwnd m_window{}; + Handler m_handler; };