Skip to content

Commit baa1a76

Browse files
authored
Expanded Clipboard (Win+V) (#4051)
* Add Expanded Clipboard (Win+V) mod * remove homepage tag * rename hooks variable to cbdhsvcDllHooks * work on reboot
1 parent bba2b04 commit baa1a76

1 file changed

Lines changed: 247 additions & 0 deletions

File tree

mods/expanded-clipboard.wh.cpp

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
// ==WindhawkMod==
2+
// @id expanded-clipboard
3+
// @name Expanded Clipboard (Win+V)
4+
// @description Raises the 25-item cap of Windows clipboard history (Win+V) and lifts the per-item / total buffer size limit so large images stick.
5+
// @version 0.7.1
6+
// @author takattowo
7+
// @github https://github.com/takattowo
8+
// @include svchost.exe
9+
// @architecture x86-64
10+
// ==/WindhawkMod==
11+
12+
// ==WindhawkModReadme==
13+
/*
14+
# Expanded Clipboard (Win+V)
15+
16+
Raises the 25-entry cap on Windows 11 clipboard history (the list shown by
17+
**Win+V**) and lifts the per-item size cap so large image entries are kept.
18+
19+
## Hook targets
20+
21+
Hosted in `cbdhsvc.dll` (per-user `svchost.exe`, service group
22+
`UnistackSvcGroup`). All hooks are virtual COM methods on `ClipboardSettingsImpl`
23+
plus one static helper:
24+
25+
- `get_ClipboardHistoryMaxItemsCount`: entry-count cap (default 25).
26+
- `get_ClipboardHistoryMaxItemSizeInBytes`: per-item byte cap (default 4 MiB).
27+
- `get_ClipboardHistoryMaxSizeInBytes`: total in-memory buffer cap. Empirically
28+
this is the gate the inner add path checks first.
29+
- `DataPackageHelper::CanIncludeInClipboardHistory`: opt-in override of the
30+
per-item allow check.
31+
32+
## Configuration
33+
34+
- `maxItems` (default 100, range 25..5000): entry cap.
35+
- `maxItemSizeMB` (default 256, range 4..4096): per-item and total buffer byte
36+
cap, in MB. Both caps share this value.
37+
- `forceIncludeAll` (default off): when on, force the allow check to true so
38+
items marked "do not include in history" by their source app still land in
39+
Win+V.
40+
41+
## Logs
42+
43+
Open Windhawk Logs. Filter `[ExpandedClipboard]`.
44+
*/
45+
// ==/WindhawkModReadme==
46+
47+
// ==WindhawkModSettings==
48+
/*
49+
- maxItems: 100
50+
$name: Max clipboard history items
51+
$description: Cap of entries kept in Win+V history. Native Windows default is 25. Range 25..5000.
52+
- maxItemSizeMB: 256
53+
$name: Max per-item / total buffer size (MB)
54+
$description: Largest single clipboard entry and total in-memory buffer. Native Windows defaults are around 4 MB each. Range 4..4096.
55+
- forceIncludeAll: false
56+
$name: Force-include every clipboard item
57+
$description: Force DataPackageHelper::CanIncludeInClipboardHistory to return true. Useful when an app marks content as "do not include in history".
58+
*/
59+
// ==/WindhawkModSettings==
60+
61+
#include <windhawk_api.h>
62+
#include <windhawk_utils.h>
63+
#include <windows.h>
64+
65+
#include <atomic>
66+
67+
struct Settings {
68+
int maxItems = 100;
69+
int maxItemSizeMB = 256;
70+
bool forceIncludeAll = false;
71+
};
72+
static Settings g_settings;
73+
74+
static void LoadSettings() {
75+
g_settings.maxItems = (int)Wh_GetIntSetting(L"maxItems");
76+
if (g_settings.maxItems < 25) g_settings.maxItems = 25;
77+
if (g_settings.maxItems > 5000) g_settings.maxItems = 5000;
78+
79+
g_settings.maxItemSizeMB = (int)Wh_GetIntSetting(L"maxItemSizeMB");
80+
if (g_settings.maxItemSizeMB < 4) g_settings.maxItemSizeMB = 4;
81+
if (g_settings.maxItemSizeMB > 4096) g_settings.maxItemSizeMB = 4096;
82+
83+
g_settings.forceIncludeAll = Wh_GetIntSetting(L"forceIncludeAll") != 0;
84+
}
85+
86+
using PFN_GetMaxItemsCount = HRESULT(WINAPI*)(void*, unsigned int*);
87+
static PFN_GetMaxItemsCount g_origGetMaxItemsCount = nullptr;
88+
89+
static HRESULT WINAPI Hook_GetMaxItemsCount(void* thisPtr, unsigned int* out) {
90+
HRESULT hr = g_origGetMaxItemsCount(thisPtr, out);
91+
if (SUCCEEDED(hr) && out) {
92+
*out = (unsigned int)g_settings.maxItems;
93+
}
94+
return hr;
95+
}
96+
97+
using PFN_GetMaxSize = HRESULT(WINAPI*)(void*, unsigned __int64*);
98+
static PFN_GetMaxSize g_origGetMaxItemSize = nullptr;
99+
static PFN_GetMaxSize g_origGetMaxSize = nullptr;
100+
101+
static HRESULT WINAPI Hook_GetMaxItemSize(void* thisPtr,
102+
unsigned __int64* out) {
103+
HRESULT hr = g_origGetMaxItemSize(thisPtr, out);
104+
if (SUCCEEDED(hr) && out) {
105+
*out = (unsigned __int64)g_settings.maxItemSizeMB * 1024ull * 1024ull;
106+
}
107+
return hr;
108+
}
109+
110+
static HRESULT WINAPI Hook_GetMaxSize(void* thisPtr, unsigned __int64* out) {
111+
HRESULT hr = g_origGetMaxSize(thisPtr, out);
112+
if (SUCCEEDED(hr) && out) {
113+
*out = (unsigned __int64)g_settings.maxItemSizeMB * 1024ull * 1024ull;
114+
}
115+
return hr;
116+
}
117+
118+
using PFN_CanInclude = unsigned char(WINAPI*)(void*);
119+
static PFN_CanInclude g_origCanInclude = nullptr;
120+
121+
static unsigned char WINAPI Hook_CanInclude(void* dataPackageView) {
122+
if (g_settings.forceIncludeAll) {
123+
return 1;
124+
}
125+
return g_origCanInclude(dataPackageView);
126+
}
127+
128+
static std::atomic<bool> g_cbdhsvcDllHooked = false;
129+
130+
static bool HookCbdhsvcDllSymbols(HMODULE hMod) {
131+
WindhawkUtils::SYMBOL_HOOK cbdhsvcDllHooks[] = {
132+
{
133+
{ L"public: virtual long __cdecl ClipboardSettingsImpl::get_ClipboardHistoryMaxItemsCount(unsigned int *)" },
134+
&g_origGetMaxItemsCount,
135+
Hook_GetMaxItemsCount,
136+
},
137+
{
138+
{ L"public: virtual long __cdecl ClipboardSettingsImpl::get_ClipboardHistoryMaxItemSizeInBytes(unsigned __int64 *)" },
139+
&g_origGetMaxItemSize,
140+
Hook_GetMaxItemSize,
141+
},
142+
{
143+
{ L"public: virtual long __cdecl ClipboardSettingsImpl::get_ClipboardHistoryMaxSizeInBytes(unsigned __int64 *)" },
144+
&g_origGetMaxSize,
145+
Hook_GetMaxSize,
146+
},
147+
{
148+
{ L"public: static unsigned char __cdecl Windows::ApplicationModel::Internal::DataTransfer::DataPackageHelper::CanIncludeInClipboardHistory(struct Windows::ApplicationModel::DataTransfer::IDataPackageView *)" },
149+
&g_origCanInclude,
150+
Hook_CanInclude,
151+
},
152+
};
153+
154+
if (!WindhawkUtils::HookSymbols(hMod, cbdhsvcDllHooks, ARRAYSIZE(cbdhsvcDllHooks))) {
155+
Wh_Log(L"[ExpandedClipboard] HookSymbols failed");
156+
return false;
157+
}
158+
159+
return true;
160+
}
161+
162+
static void HandleLoadedModuleIfCbdhsvc(HMODULE module, LPCWSTR lpLibFileName) {
163+
if (g_cbdhsvcDllHooked) {
164+
return;
165+
}
166+
167+
if (GetModuleHandleW(L"cbdhsvc.dll") != module) {
168+
return;
169+
}
170+
171+
if (g_cbdhsvcDllHooked.exchange(true)) {
172+
return;
173+
}
174+
175+
Wh_Log(L"[ExpandedClipboard] cbdhsvc.dll loaded (%s)",
176+
lpLibFileName ? lpLibFileName : L"?");
177+
178+
if (HookCbdhsvcDllSymbols(module)) {
179+
Wh_ApplyHookOperations();
180+
}
181+
}
182+
183+
using LoadLibraryExW_t = decltype(&LoadLibraryExW);
184+
static LoadLibraryExW_t LoadLibraryExW_Original;
185+
static HMODULE WINAPI LoadLibraryExW_Hook(LPCWSTR lpLibFileName,
186+
HANDLE hFile,
187+
DWORD dwFlags) {
188+
HMODULE module = LoadLibraryExW_Original(lpLibFileName, hFile, dwFlags);
189+
if (module) {
190+
HandleLoadedModuleIfCbdhsvc(module, lpLibFileName);
191+
}
192+
return module;
193+
}
194+
195+
BOOL Wh_ModInit() {
196+
LoadSettings();
197+
198+
Wh_Log(L"[ExpandedClipboard] Init. maxItems=%d, maxItemSizeMB=%d, "
199+
L"forceIncludeAll=%d",
200+
g_settings.maxItems, g_settings.maxItemSizeMB,
201+
g_settings.forceIncludeAll ? 1 : 0);
202+
203+
if (HMODULE hMod = GetModuleHandleW(L"cbdhsvc.dll")) {
204+
g_cbdhsvcDllHooked = true;
205+
if (!HookCbdhsvcDllSymbols(hMod)) {
206+
return FALSE;
207+
}
208+
} else {
209+
Wh_Log(L"[ExpandedClipboard] cbdhsvc.dll not loaded yet, waiting");
210+
}
211+
212+
HMODULE kernelBaseModule = GetModuleHandleW(L"kernelbase.dll");
213+
auto pKernelBaseLoadLibraryExW =
214+
(decltype(&LoadLibraryExW))GetProcAddress(kernelBaseModule,
215+
"LoadLibraryExW");
216+
WindhawkUtils::Wh_SetFunctionHookT(pKernelBaseLoadLibraryExW,
217+
LoadLibraryExW_Hook,
218+
&LoadLibraryExW_Original);
219+
220+
return TRUE;
221+
}
222+
223+
void Wh_ModAfterInit() {
224+
if (g_cbdhsvcDllHooked) {
225+
return;
226+
}
227+
228+
if (HMODULE hMod = GetModuleHandleW(L"cbdhsvc.dll")) {
229+
if (!g_cbdhsvcDllHooked.exchange(true)) {
230+
Wh_Log(L"[ExpandedClipboard] cbdhsvc.dll loaded post-init");
231+
if (HookCbdhsvcDllSymbols(hMod)) {
232+
Wh_ApplyHookOperations();
233+
}
234+
}
235+
}
236+
}
237+
238+
void Wh_ModSettingsChanged() {
239+
LoadSettings();
240+
Wh_Log(L"[ExpandedClipboard] Settings reloaded. "
241+
L"maxItems=%d, maxItemSizeMB=%d, forceIncludeAll=%d",
242+
g_settings.maxItems, g_settings.maxItemSizeMB,
243+
g_settings.forceIncludeAll ? 1 : 0);
244+
}
245+
246+
void Wh_ModUninit() {
247+
}

0 commit comments

Comments
 (0)