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..5da5f90 100644 --- a/FastCopy/FastCopy.vcxproj +++ b/FastCopy/FastCopy.vcxproj @@ -217,6 +217,8 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll + + @@ -285,6 +287,7 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll + @@ -397,6 +400,9 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll + + NotUsing + @@ -430,6 +436,7 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll + FileCompareViewModel.idl Code @@ -693,6 +700,9 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll true true + + true + true @@ -703,6 +713,9 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll {17f52e4d-1e98-4288-9aa1-20a384171876} + + {83e7518d-7ba4-4a13-b2c9-4ab98da778de} + @@ -811,4 +824,4 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll - \ No newline at end of file + diff --git a/FastCopy/KeyboardHookController.cpp b/FastCopy/KeyboardHookController.cpp new file mode 100644 index 0000000..54c979e --- /dev/null +++ b/FastCopy/KeyboardHookController.cpp @@ -0,0 +1,58 @@ +#include "pch.h" +#include "KeyboardHookController.h" +#include "../Public/KeyboardHookSettings.h" +#include "../Public/ModulePath.h" + +#include +#include + +#include + +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; + } + + 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) }; + wil::unique_process_information processInfo; + THROW_IF_WIN32_BOOL_FALSE(CreateProcessW( + hookPath.c_str(), + nullptr, + nullptr, + nullptr, + FALSE, + CREATE_NO_WINDOW, + nullptr, + hookPath.parent_path().c_str(), + &startupInfo, + processInfo.addressof())); +} + +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..9ea9080 100644 --- a/FastCopy/SettingsViewModel.cpp +++ b/FastCopy/SettingsViewModel.cpp @@ -6,6 +6,29 @@ #include "Global.h" #include "SettingsChangeListener.h" #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 { @@ -17,6 +40,25 @@ namespace winrt::FastCopy::implementation { m_model.Set(Settings::Notify, value); } + bool SettingsViewModel::KeyboardIntegration() + { + return KeyboardHookController::IsEnabled(); + } + void SettingsViewModel::KeyboardIntegration(bool value) + { + try + { + KeyboardHookController::SetEnabled(value); + } + catch (wil::ResultException const& e) + { + ShowError(e.what()); + } + catch (std::exception const& e) + { + ShowError(e.what()); + } + } 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..875eadd --- /dev/null +++ b/FastCopyKeyboardHook/ClipboardFileTransfer.cpp @@ -0,0 +1,149 @@ +#include "ClipboardFileTransfer.h" + +#include + +#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; + wil::unique_cotaskmem_string path; + if (FAILED(items->GetItemAt(index, &item)) || + FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, wil::out_param(path)))) + { + return std::nullopt; + } + paths.push_back(std::move(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); + 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; + } + 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..852af1a --- /dev/null +++ b/FastCopyKeyboardHook/ClipboardFileTransfer.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include + +#include +#include + +struct ClipboardFileTransfer +{ + // 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/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 new file mode 100644 index 0000000..d1cf484 --- /dev/null +++ b/FastCopyKeyboardHook/ExplorerWindow.cpp @@ -0,0 +1,79 @@ +#include "ExplorerWindow.h" +#include "ExplorerFolderResolver.h" + +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace +{ + std::array GetWindowClass(HWND window) + { + std::array buffer{}; + 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); }); + auto const* text = className.data(); + return std::wcsstr(text, L"edit") != nullptr || + std::wcsstr(text, L"richedit") != nullptr || + std::wcscmp(text, L"combobox") == 0; + } +} + +bool IsExplorerWindow(HWND window) +{ + if (!window) + { + return false; + } + + auto const className = GetWindowClass(window); + // The desktop is a valid paste target even though it is not an Explorer frame. + if (std::wcscmp(className.data(), L"Progman") == 0 || std::wcscmp(className.data(), L"WorkerW") == 0) + { + return true; + } + return std::wcscmp(className.data(), L"CabinetWClass") == 0 || + std::wcscmp(className.data(), L"ExploreWClass") == 0; +} + +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; + } + + // The desktop has no shell view to resolve; its folder is the user's desktop. + auto const className = GetWindowClass(expectedForegroundWindow); + if (std::wcscmp(className.data(), L"Progman") == 0 || std::wcscmp(className.data(), L"WorkerW") == 0) + { + 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; + return resolver.Resolve(expectedForegroundWindow, threadInfo); +} 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..417db42 --- /dev/null +++ b/FastCopyKeyboardHook/FastCopyKeyboardHook.vcxproj @@ -0,0 +1,105 @@ + + + + + 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/FastCopyLauncher.cpp b/FastCopyKeyboardHook/FastCopyLauncher.cpp new file mode 100644 index 0000000..82eea2d --- /dev/null +++ b/FastCopyKeyboardHook/FastCopyLauncher.cpp @@ -0,0 +1,115 @@ +#include "FastCopyLauncher.h" + +#include +#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 const& source : transfer.paths) + { + 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(normalized.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/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..1d9a37e --- /dev/null +++ b/FastCopyKeyboardHook/PasteWindow.cpp @@ -0,0 +1,55 @@ +#include "PasteWindow.h" + +#include + +PasteWindow::PasteWindow(HINSTANCE instance, Handler handler) + : m_handler{ std::move(handler) } +{ + WNDCLASSW windowClass + { + .lpfnWndProc = &PasteWindow::windowProc, + .hInstance = instance, + .lpszClassName = KeyboardHookSettings::WindowClassName, + }; + THROW_LAST_ERROR_IF(!RegisterClassW(&windowClass)); + + m_window.reset(CreateWindowExW( + 0, + KeyboardHookSettings::WindowClassName, + nullptr, + 0, + 0, + 0, + 0, + 0, + HWND_MESSAGE, + nullptr, + instance, + this)); + THROW_LAST_ERROR_IF(!m_window); +} + +LRESULT CALLBACK PasteWindow::windowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_NCCREATE: + { + 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); + } + } +} diff --git a/FastCopyKeyboardHook/PasteWindow.h b/FastCopyKeyboardHook/PasteWindow.h new file mode 100644 index 0000000..1703a87 --- /dev/null +++ b/FastCopyKeyboardHook/PasteWindow.h @@ -0,0 +1,28 @@ +#pragma once +#include "../Public/KeyboardHookSettings.h" + +#include +#include +#include + +// 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 const&) = delete; + PasteWindow& operator=(PasteWindow const&) = delete; + + HWND handle() const { return m_window.get(); } + +private: + static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam); + + wil::unique_hwnd m_window{}; + Handler m_handler; +}; diff --git a/FastCopyKeyboardHook/main.cpp b/FastCopyKeyboardHook/main.cpp new file mode 100644 index 0000000..63491c2 --- /dev/null +++ b/FastCopyKeyboardHook/main.cpp @@ -0,0 +1,24 @@ +#include "KeyboardHookApp.h" + +#include + +#include + +int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int) +{ + try + { + KeyboardHookApp app{ instance }; + return app.Run(); + } + catch (wil::ResultException const& e) + { + KeyboardHookApp::ShowError(e.what()); + return 1; + } + catch (std::exception const& e) + { + KeyboardHookApp::ShowError(e.what()); + return 1; + } +} 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/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..b127027 100644 --- a/FastCopyShellExtension/FastCopyShellExtension.vcxproj +++ b/FastCopyShellExtension/FastCopyShellExtension.vcxproj @@ -287,8 +287,11 @@ xcopy "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configurati + + + @@ -297,16 +300,17 @@ xcopy "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configurati - - + + + @@ -316,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 8b4af7a..0000000 Binary files a/FastCopyShellExtension/ShellWindows.cpp and /dev/null differ diff --git a/FastCopyShellExtension/WebBrowser2.h b/FastCopyShellExtension/WebBrowser2.h deleted file mode 100644 index e770b87..0000000 Binary files a/FastCopyShellExtension/WebBrowser2.h and /dev/null differ diff --git a/Public/KeyboardHookSettings.h b/Public/KeyboardHookSettings.h new file mode 100644 index 0000000..3063bdb --- /dev/null +++ b/Public/KeyboardHookSettings.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#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() + { + 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) + { + 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)); + } +} 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); diff --git a/Public/ShellWindows.cpp b/Public/ShellWindows.cpp new file mode 100644 index 0000000..b604140 --- /dev/null +++ b/Public/ShellWindows.cpp @@ -0,0 +1,61 @@ +#include "ShellWindows.h" +#include + +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; +};