Skip to content

Commit 138500e

Browse files
Add FilePilot preview integration (#1949)
1 parent b4e482b commit 138500e

11 files changed

Lines changed: 377 additions & 3 deletions

File tree

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
// Copyright © 2017-2026 QL-Win Contributors
2+
//
3+
// This file is part of QuickLook program.
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
#include "stdafx.h"
19+
#include "FilePilot.h"
20+
#include <UIAutomation.h>
21+
22+
#pragma comment(lib, "UIAutomationCore.lib")
23+
24+
namespace
25+
{
26+
constexpr auto CLIPBOARD_TIMEOUT_MS = 250;
27+
constexpr auto CLIPBOARD_POLL_INTERVAL_MS = 5;
28+
constexpr auto MAX_CLASS_NAME_LENGTH = 256;
29+
30+
bool StartsWith(PCWSTR value, PCWSTR prefix)
31+
{
32+
return wcsncmp(value, prefix, wcslen(prefix)) == 0;
33+
}
34+
35+
bool IsFilePilotProcess(HWND hwnd)
36+
{
37+
DWORD processId = 0;
38+
GetWindowThreadProcessId(hwnd, &processId);
39+
40+
if (processId == 0)
41+
return false;
42+
43+
auto process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
44+
if (process == nullptr)
45+
process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
46+
if (process == nullptr)
47+
return false;
48+
49+
WCHAR processPath[MAX_PATH_EX] = { L'\0' };
50+
DWORD processPathLength = MAX_PATH_EX;
51+
auto success = QueryFullProcessImageNameW(process, 0, processPath, &processPathLength);
52+
CloseHandle(process);
53+
54+
if (!success)
55+
return false;
56+
57+
auto processName = wcsrchr(processPath, L'\\');
58+
processName = processName == nullptr ? processPath : processName + 1;
59+
60+
return _wcsicmp(processName, L"FPilot.exe") == 0 ||
61+
_wcsicmp(processName, L"FilePilot.exe") == 0;
62+
}
63+
64+
bool IsTextInputClass(PCWSTR className)
65+
{
66+
if (className == nullptr || className[0] == L'\0')
67+
return false;
68+
69+
return StartsWith(className, L"Edit") ||
70+
StartsWith(className, L"RichEdit") ||
71+
StartsWith(className, L"WindowsForms10.EDIT") ||
72+
StartsWith(className, L"Scintilla");
73+
}
74+
75+
bool IsWin32TextInputFocused(HWND hwnd)
76+
{
77+
auto foregroundThread = GetWindowThreadProcessId(hwnd, nullptr);
78+
auto currentThread = GetCurrentThreadId();
79+
auto attached = FALSE;
80+
81+
if (foregroundThread != 0 && foregroundThread != currentThread)
82+
attached = AttachThreadInput(currentThread, foregroundThread, TRUE);
83+
84+
auto focused = GetFocus();
85+
86+
if (attached)
87+
AttachThreadInput(currentThread, foregroundThread, FALSE);
88+
89+
if (focused == nullptr)
90+
return false;
91+
92+
WCHAR className[MAX_CLASS_NAME_LENGTH] = { L'\0' };
93+
if (GetClassNameW(focused, className, MAX_CLASS_NAME_LENGTH) == 0)
94+
return false;
95+
96+
return IsTextInputClass(className);
97+
}
98+
99+
bool CreateAutomation(IUIAutomation** automation)
100+
{
101+
auto hr = CoCreateInstance(
102+
__uuidof(CUIAutomation8),
103+
nullptr,
104+
CLSCTX_INPROC_SERVER,
105+
__uuidof(IUIAutomation),
106+
reinterpret_cast<void**>(automation));
107+
108+
if (SUCCEEDED(hr))
109+
return true;
110+
111+
hr = CoCreateInstance(
112+
__uuidof(CUIAutomation),
113+
nullptr,
114+
CLSCTX_INPROC_SERVER,
115+
__uuidof(IUIAutomation),
116+
reinterpret_cast<void**>(automation));
117+
118+
return SUCCEEDED(hr);
119+
}
120+
121+
bool IsAutomationTextInputFocused(HWND hwnd)
122+
{
123+
DWORD foregroundProcessId = 0;
124+
GetWindowThreadProcessId(hwnd, &foregroundProcessId);
125+
if (foregroundProcessId == 0)
126+
return false;
127+
128+
auto coInit = CoInitialize(nullptr);
129+
if (FAILED(coInit))
130+
return false;
131+
132+
IUIAutomation* automation = nullptr;
133+
if (!CreateAutomation(&automation))
134+
{
135+
CoUninitialize();
136+
return false;
137+
}
138+
139+
IUIAutomationElement* focused = nullptr;
140+
auto result = false;
141+
142+
if (SUCCEEDED(automation->GetFocusedElement(&focused)) && focused != nullptr)
143+
{
144+
int focusedProcessId = 0;
145+
if (SUCCEEDED(focused->get_CurrentProcessId(&focusedProcessId)) &&
146+
focusedProcessId == static_cast<int>(foregroundProcessId))
147+
{
148+
CONTROLTYPEID controlType = 0;
149+
if (SUCCEEDED(focused->get_CurrentControlType(&controlType)) &&
150+
(controlType == UIA_EditControlTypeId || controlType == UIA_ComboBoxControlTypeId))
151+
{
152+
result = true;
153+
}
154+
155+
if (!result)
156+
{
157+
BSTR className = nullptr;
158+
if (SUCCEEDED(focused->get_CurrentClassName(&className)) && className != nullptr)
159+
{
160+
result = IsTextInputClass(className);
161+
SysFreeString(className);
162+
}
163+
}
164+
}
165+
166+
focused->Release();
167+
}
168+
169+
automation->Release();
170+
CoUninitialize();
171+
172+
return result;
173+
}
174+
175+
bool IsTextInputFocused(HWND hwnd)
176+
{
177+
return IsWin32TextInputFocused(hwnd) || IsAutomationTextInputFocused(hwnd);
178+
}
179+
180+
void SendCopyPathHotkey()
181+
{
182+
INPUT inputs[6] = {};
183+
184+
inputs[0].type = INPUT_KEYBOARD;
185+
inputs[0].ki.wVk = VK_CONTROL;
186+
187+
inputs[1].type = INPUT_KEYBOARD;
188+
inputs[1].ki.wVk = VK_SHIFT;
189+
190+
inputs[2].type = INPUT_KEYBOARD;
191+
inputs[2].ki.wVk = L'C';
192+
193+
inputs[3].type = INPUT_KEYBOARD;
194+
inputs[3].ki.wVk = L'C';
195+
inputs[3].ki.dwFlags = KEYEVENTF_KEYUP;
196+
197+
inputs[4].type = INPUT_KEYBOARD;
198+
inputs[4].ki.wVk = VK_SHIFT;
199+
inputs[4].ki.dwFlags = KEYEVENTF_KEYUP;
200+
201+
inputs[5].type = INPUT_KEYBOARD;
202+
inputs[5].ki.wVk = VK_CONTROL;
203+
inputs[5].ki.dwFlags = KEYEVENTF_KEYUP;
204+
205+
SendInput(_countof(inputs), inputs, sizeof(INPUT));
206+
}
207+
208+
bool ClearClipboard()
209+
{
210+
auto start = GetTickCount64();
211+
212+
while (GetTickCount64() - start < CLIPBOARD_TIMEOUT_MS)
213+
{
214+
if (OpenClipboard(nullptr))
215+
{
216+
EmptyClipboard();
217+
CloseClipboard();
218+
return true;
219+
}
220+
221+
Sleep(CLIPBOARD_POLL_INTERVAL_MS);
222+
}
223+
224+
return false;
225+
}
226+
227+
bool ReadClipboardPath(PWCHAR buffer)
228+
{
229+
if (!OpenClipboard(nullptr))
230+
return false;
231+
232+
auto data = GetClipboardData(CF_UNICODETEXT);
233+
if (data == nullptr)
234+
{
235+
CloseClipboard();
236+
return false;
237+
}
238+
239+
auto text = static_cast<PWCHAR>(GlobalLock(data));
240+
if (text == nullptr)
241+
{
242+
CloseClipboard();
243+
return false;
244+
}
245+
246+
auto start = text;
247+
while (*start == L' ' || *start == L'\t' || *start == L'\r' || *start == L'\n')
248+
start++;
249+
250+
auto end = start;
251+
while (*end != L'\0' && *end != L'\r' && *end != L'\n')
252+
end++;
253+
254+
while (end > start && (*(end - 1) == L' ' || *(end - 1) == L'\t'))
255+
end--;
256+
257+
if (end - start >= 2 && *start == L'"' && *(end - 1) == L'"')
258+
{
259+
start++;
260+
end--;
261+
}
262+
263+
auto length = static_cast<size_t>(end - start);
264+
if (length >= MAX_PATH_EX)
265+
length = MAX_PATH_EX - 1;
266+
267+
if (length > 0)
268+
wcsncpy_s(buffer, MAX_PATH_EX, start, length);
269+
270+
GlobalUnlock(data);
271+
CloseClipboard();
272+
273+
return length > 0;
274+
}
275+
276+
bool WaitForClipboardPath(PWCHAR buffer)
277+
{
278+
auto start = GetTickCount64();
279+
280+
while (GetTickCount64() - start < CLIPBOARD_TIMEOUT_MS)
281+
{
282+
if (ReadClipboardPath(buffer))
283+
return true;
284+
285+
Sleep(CLIPBOARD_POLL_INTERVAL_MS);
286+
}
287+
288+
return false;
289+
}
290+
}
291+
292+
bool FilePilot::MatchWindow(HWND hwnd)
293+
{
294+
return hwnd != nullptr && IsFilePilotProcess(hwnd) && !IsTextInputFocused(hwnd);
295+
}
296+
297+
void FilePilot::GetSelected(PWCHAR buffer)
298+
{
299+
if (!MatchWindow(GetForegroundWindow()))
300+
return;
301+
302+
auto coInit = CoInitialize(nullptr);
303+
CComPtr<IDataObject> clipboardBackup;
304+
305+
if (SUCCEEDED(coInit))
306+
OleGetClipboard(&clipboardBackup);
307+
308+
auto clipboardCleared = ClearClipboard();
309+
if (clipboardCleared)
310+
{
311+
SendCopyPathHotkey();
312+
WaitForClipboardPath(buffer);
313+
}
314+
315+
if (clipboardCleared && clipboardBackup != nullptr)
316+
{
317+
OleSetClipboard(clipboardBackup);
318+
OleFlushClipboard();
319+
}
320+
else if (clipboardCleared)
321+
{
322+
ClearClipboard();
323+
}
324+
325+
if (SUCCEEDED(coInit))
326+
CoUninitialize();
327+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright © 2017-2026 QL-Win Contributors
2+
//
3+
// This file is part of QuickLook program.
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
#pragma once
19+
20+
class FilePilot
21+
{
22+
public:
23+
static bool MatchWindow(HWND hwnd);
24+
static void GetSelected(PWCHAR buffer);
25+
};

‎QuickLook.Native/QuickLook.Native32/QuickLook.Native32.vcxproj‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@
217217
<ClInclude Include="DialogHook.h" />
218218
<ClInclude Include="DOpus.h" />
219219
<ClInclude Include="Everything.h" />
220+
<ClInclude Include="FilePilot.h" />
220221
<ClInclude Include="HelperMethods.h" />
221222
<ClInclude Include="IDMan.h" />
222223
<ClInclude Include="MultiCommander.h" />
@@ -250,6 +251,7 @@
250251
</ClCompile>
251252
<ClCompile Include="DOpus.cpp" />
252253
<ClCompile Include="Everything.cpp" />
254+
<ClCompile Include="FilePilot.cpp" />
253255
<ClCompile Include="HelperMethods.cpp" />
254256
<ClCompile Include="DllExport.cpp" />
255257
<ClCompile Include="IDMan.cpp" />
@@ -268,4 +270,4 @@
268270
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
269271
<ImportGroup Label="ExtensionTargets">
270272
</ImportGroup>
271-
</Project>
273+
</Project>

‎QuickLook.Native/QuickLook.Native32/QuickLook.Native32.vcxproj.filters‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
<ClInclude Include="Everything.h">
3737
<Filter>Header Files</Filter>
3838
</ClInclude>
39+
<ClInclude Include="FilePilot.h">
40+
<Filter>Header Files</Filter>
41+
</ClInclude>
3942
<ClInclude Include="DOpus.h">
4043
<Filter>Header Files</Filter>
4144
</ClInclude>
@@ -71,6 +74,9 @@
7174
<ClCompile Include="Everything.cpp">
7275
<Filter>Source Files</Filter>
7376
</ClCompile>
77+
<ClCompile Include="FilePilot.cpp">
78+
<Filter>Source Files</Filter>
79+
</ClCompile>
7480
<ClCompile Include="DOpus.cpp">
7581
<Filter>Source Files</Filter>
7682
</ClCompile>

0 commit comments

Comments
 (0)