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;
+};