From d41b43048f374d719c34dd20d5936e8f7f70305b Mon Sep 17 00:00:00 2001
From: Andrey Semjonov <50518855+AndreySemjonov@users.noreply.github.com>
Date: Tue, 19 May 2026 19:46:16 +0300
Subject: [PATCH] Add FilePilot preview integration
---
.../QuickLook.Native32/FilePilot.cpp | 327 ++++++++++++++++++
.../QuickLook.Native32/FilePilot.h | 25 ++
.../QuickLook.Native32.vcxproj | 4 +-
.../QuickLook.Native32.vcxproj.filters | 6 +
.../QuickLook.Native32/Shell32.cpp | 8 +
QuickLook.Native/QuickLook.Native32/Shell32.h | 1 +
.../QuickLook.Native64.vcxproj | 3 +-
.../QuickLook.Native64.vcxproj.filters | 1 +
.../QuickLook.NativeArm64.vcxproj | 3 +-
.../QuickLook.NativeArm64.vcxproj.filters | 1 +
QuickLook/NativeMethods/QuickLook.cs | 1 +
11 files changed, 377 insertions(+), 3 deletions(-)
create mode 100644 QuickLook.Native/QuickLook.Native32/FilePilot.cpp
create mode 100644 QuickLook.Native/QuickLook.Native32/FilePilot.h
diff --git a/QuickLook.Native/QuickLook.Native32/FilePilot.cpp b/QuickLook.Native/QuickLook.Native32/FilePilot.cpp
new file mode 100644
index 000000000..8b2e97583
--- /dev/null
+++ b/QuickLook.Native/QuickLook.Native32/FilePilot.cpp
@@ -0,0 +1,327 @@
+// Copyright © 2017-2026 QL-Win Contributors
+//
+// This file is part of QuickLook program.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#include "stdafx.h"
+#include "FilePilot.h"
+#include
+
+#pragma comment(lib, "UIAutomationCore.lib")
+
+namespace
+{
+ constexpr auto CLIPBOARD_TIMEOUT_MS = 250;
+ constexpr auto CLIPBOARD_POLL_INTERVAL_MS = 5;
+ constexpr auto MAX_CLASS_NAME_LENGTH = 256;
+
+ bool StartsWith(PCWSTR value, PCWSTR prefix)
+ {
+ return wcsncmp(value, prefix, wcslen(prefix)) == 0;
+ }
+
+ bool IsFilePilotProcess(HWND hwnd)
+ {
+ DWORD processId = 0;
+ GetWindowThreadProcessId(hwnd, &processId);
+
+ if (processId == 0)
+ return false;
+
+ auto process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
+ if (process == nullptr)
+ process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
+ if (process == nullptr)
+ return false;
+
+ WCHAR processPath[MAX_PATH_EX] = { L'\0' };
+ DWORD processPathLength = MAX_PATH_EX;
+ auto success = QueryFullProcessImageNameW(process, 0, processPath, &processPathLength);
+ CloseHandle(process);
+
+ if (!success)
+ return false;
+
+ auto processName = wcsrchr(processPath, L'\\');
+ processName = processName == nullptr ? processPath : processName + 1;
+
+ return _wcsicmp(processName, L"FPilot.exe") == 0 ||
+ _wcsicmp(processName, L"FilePilot.exe") == 0;
+ }
+
+ bool IsTextInputClass(PCWSTR className)
+ {
+ if (className == nullptr || className[0] == L'\0')
+ return false;
+
+ return StartsWith(className, L"Edit") ||
+ StartsWith(className, L"RichEdit") ||
+ StartsWith(className, L"WindowsForms10.EDIT") ||
+ StartsWith(className, L"Scintilla");
+ }
+
+ bool IsWin32TextInputFocused(HWND hwnd)
+ {
+ auto foregroundThread = GetWindowThreadProcessId(hwnd, nullptr);
+ auto currentThread = GetCurrentThreadId();
+ auto attached = FALSE;
+
+ if (foregroundThread != 0 && foregroundThread != currentThread)
+ attached = AttachThreadInput(currentThread, foregroundThread, TRUE);
+
+ auto focused = GetFocus();
+
+ if (attached)
+ AttachThreadInput(currentThread, foregroundThread, FALSE);
+
+ if (focused == nullptr)
+ return false;
+
+ WCHAR className[MAX_CLASS_NAME_LENGTH] = { L'\0' };
+ if (GetClassNameW(focused, className, MAX_CLASS_NAME_LENGTH) == 0)
+ return false;
+
+ return IsTextInputClass(className);
+ }
+
+ bool CreateAutomation(IUIAutomation** automation)
+ {
+ auto hr = CoCreateInstance(
+ __uuidof(CUIAutomation8),
+ nullptr,
+ CLSCTX_INPROC_SERVER,
+ __uuidof(IUIAutomation),
+ reinterpret_cast(automation));
+
+ if (SUCCEEDED(hr))
+ return true;
+
+ hr = CoCreateInstance(
+ __uuidof(CUIAutomation),
+ nullptr,
+ CLSCTX_INPROC_SERVER,
+ __uuidof(IUIAutomation),
+ reinterpret_cast(automation));
+
+ return SUCCEEDED(hr);
+ }
+
+ bool IsAutomationTextInputFocused(HWND hwnd)
+ {
+ DWORD foregroundProcessId = 0;
+ GetWindowThreadProcessId(hwnd, &foregroundProcessId);
+ if (foregroundProcessId == 0)
+ return false;
+
+ auto coInit = CoInitialize(nullptr);
+ if (FAILED(coInit))
+ return false;
+
+ IUIAutomation* automation = nullptr;
+ if (!CreateAutomation(&automation))
+ {
+ CoUninitialize();
+ return false;
+ }
+
+ IUIAutomationElement* focused = nullptr;
+ auto result = false;
+
+ if (SUCCEEDED(automation->GetFocusedElement(&focused)) && focused != nullptr)
+ {
+ int focusedProcessId = 0;
+ if (SUCCEEDED(focused->get_CurrentProcessId(&focusedProcessId)) &&
+ focusedProcessId == static_cast(foregroundProcessId))
+ {
+ CONTROLTYPEID controlType = 0;
+ if (SUCCEEDED(focused->get_CurrentControlType(&controlType)) &&
+ (controlType == UIA_EditControlTypeId || controlType == UIA_ComboBoxControlTypeId))
+ {
+ result = true;
+ }
+
+ if (!result)
+ {
+ BSTR className = nullptr;
+ if (SUCCEEDED(focused->get_CurrentClassName(&className)) && className != nullptr)
+ {
+ result = IsTextInputClass(className);
+ SysFreeString(className);
+ }
+ }
+ }
+
+ focused->Release();
+ }
+
+ automation->Release();
+ CoUninitialize();
+
+ return result;
+ }
+
+ bool IsTextInputFocused(HWND hwnd)
+ {
+ return IsWin32TextInputFocused(hwnd) || IsAutomationTextInputFocused(hwnd);
+ }
+
+ void SendCopyPathHotkey()
+ {
+ INPUT inputs[6] = {};
+
+ inputs[0].type = INPUT_KEYBOARD;
+ inputs[0].ki.wVk = VK_CONTROL;
+
+ inputs[1].type = INPUT_KEYBOARD;
+ inputs[1].ki.wVk = VK_SHIFT;
+
+ inputs[2].type = INPUT_KEYBOARD;
+ inputs[2].ki.wVk = L'C';
+
+ inputs[3].type = INPUT_KEYBOARD;
+ inputs[3].ki.wVk = L'C';
+ inputs[3].ki.dwFlags = KEYEVENTF_KEYUP;
+
+ inputs[4].type = INPUT_KEYBOARD;
+ inputs[4].ki.wVk = VK_SHIFT;
+ inputs[4].ki.dwFlags = KEYEVENTF_KEYUP;
+
+ inputs[5].type = INPUT_KEYBOARD;
+ inputs[5].ki.wVk = VK_CONTROL;
+ inputs[5].ki.dwFlags = KEYEVENTF_KEYUP;
+
+ SendInput(_countof(inputs), inputs, sizeof(INPUT));
+ }
+
+ bool ClearClipboard()
+ {
+ auto start = GetTickCount64();
+
+ while (GetTickCount64() - start < CLIPBOARD_TIMEOUT_MS)
+ {
+ if (OpenClipboard(nullptr))
+ {
+ EmptyClipboard();
+ CloseClipboard();
+ return true;
+ }
+
+ Sleep(CLIPBOARD_POLL_INTERVAL_MS);
+ }
+
+ return false;
+ }
+
+ bool ReadClipboardPath(PWCHAR buffer)
+ {
+ if (!OpenClipboard(nullptr))
+ return false;
+
+ auto data = GetClipboardData(CF_UNICODETEXT);
+ if (data == nullptr)
+ {
+ CloseClipboard();
+ return false;
+ }
+
+ auto text = static_cast(GlobalLock(data));
+ if (text == nullptr)
+ {
+ CloseClipboard();
+ return false;
+ }
+
+ auto start = text;
+ while (*start == L' ' || *start == L'\t' || *start == L'\r' || *start == L'\n')
+ start++;
+
+ auto end = start;
+ while (*end != L'\0' && *end != L'\r' && *end != L'\n')
+ end++;
+
+ while (end > start && (*(end - 1) == L' ' || *(end - 1) == L'\t'))
+ end--;
+
+ if (end - start >= 2 && *start == L'"' && *(end - 1) == L'"')
+ {
+ start++;
+ end--;
+ }
+
+ auto length = static_cast(end - start);
+ if (length >= MAX_PATH_EX)
+ length = MAX_PATH_EX - 1;
+
+ if (length > 0)
+ wcsncpy_s(buffer, MAX_PATH_EX, start, length);
+
+ GlobalUnlock(data);
+ CloseClipboard();
+
+ return length > 0;
+ }
+
+ bool WaitForClipboardPath(PWCHAR buffer)
+ {
+ auto start = GetTickCount64();
+
+ while (GetTickCount64() - start < CLIPBOARD_TIMEOUT_MS)
+ {
+ if (ReadClipboardPath(buffer))
+ return true;
+
+ Sleep(CLIPBOARD_POLL_INTERVAL_MS);
+ }
+
+ return false;
+ }
+}
+
+bool FilePilot::MatchWindow(HWND hwnd)
+{
+ return hwnd != nullptr && IsFilePilotProcess(hwnd) && !IsTextInputFocused(hwnd);
+}
+
+void FilePilot::GetSelected(PWCHAR buffer)
+{
+ if (!MatchWindow(GetForegroundWindow()))
+ return;
+
+ auto coInit = CoInitialize(nullptr);
+ CComPtr clipboardBackup;
+
+ if (SUCCEEDED(coInit))
+ OleGetClipboard(&clipboardBackup);
+
+ auto clipboardCleared = ClearClipboard();
+ if (clipboardCleared)
+ {
+ SendCopyPathHotkey();
+ WaitForClipboardPath(buffer);
+ }
+
+ if (clipboardCleared && clipboardBackup != nullptr)
+ {
+ OleSetClipboard(clipboardBackup);
+ OleFlushClipboard();
+ }
+ else if (clipboardCleared)
+ {
+ ClearClipboard();
+ }
+
+ if (SUCCEEDED(coInit))
+ CoUninitialize();
+}
diff --git a/QuickLook.Native/QuickLook.Native32/FilePilot.h b/QuickLook.Native/QuickLook.Native32/FilePilot.h
new file mode 100644
index 000000000..56256cc4a
--- /dev/null
+++ b/QuickLook.Native/QuickLook.Native32/FilePilot.h
@@ -0,0 +1,25 @@
+// Copyright © 2017-2026 QL-Win Contributors
+//
+// This file is part of QuickLook program.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#pragma once
+
+class FilePilot
+{
+public:
+ static bool MatchWindow(HWND hwnd);
+ static void GetSelected(PWCHAR buffer);
+};
diff --git a/QuickLook.Native/QuickLook.Native32/QuickLook.Native32.vcxproj b/QuickLook.Native/QuickLook.Native32/QuickLook.Native32.vcxproj
index 96c595b11..f4d783cc9 100644
--- a/QuickLook.Native/QuickLook.Native32/QuickLook.Native32.vcxproj
+++ b/QuickLook.Native/QuickLook.Native32/QuickLook.Native32.vcxproj
@@ -217,6 +217,7 @@
+
@@ -250,6 +251,7 @@
+
@@ -268,4 +270,4 @@
-
\ No newline at end of file
+
diff --git a/QuickLook.Native/QuickLook.Native32/QuickLook.Native32.vcxproj.filters b/QuickLook.Native/QuickLook.Native32/QuickLook.Native32.vcxproj.filters
index cfa794604..ca13a584a 100644
--- a/QuickLook.Native/QuickLook.Native32/QuickLook.Native32.vcxproj.filters
+++ b/QuickLook.Native/QuickLook.Native32/QuickLook.Native32.vcxproj.filters
@@ -36,6 +36,9 @@
Header Files
+
+ Header Files
+
Header Files
@@ -71,6 +74,9 @@
Source Files
+
+ Source Files
+
Source Files
diff --git a/QuickLook.Native/QuickLook.Native32/Shell32.cpp b/QuickLook.Native/QuickLook.Native32/Shell32.cpp
index 88b1e8137..b713aa842 100644
--- a/QuickLook.Native/QuickLook.Native32/Shell32.cpp
+++ b/QuickLook.Native/QuickLook.Native32/Shell32.cpp
@@ -24,6 +24,7 @@
#include "DOpus.h"
#include "MultiCommander.h"
#include "IDMan.h"
+#include "FilePilot.h"
using namespace std;
@@ -50,6 +51,10 @@ Shell32::FocusedWindowType Shell32::GetFocusedWindowType()
{
return EVERYTHING;
}
+ if (FilePilot::MatchWindow(hwndfg))
+ {
+ return FILEPILOT;
+ }
if (wcscmp(classBuffer, L"WorkerW") == 0 || wcscmp(classBuffer, L"Progman") == 0)
{
if (FindWindowEx(hwndfg, nullptr, L"SHELLDLL_DefView", nullptr) != nullptr)
@@ -110,6 +115,9 @@ void Shell32::GetCurrentSelection(PWCHAR buffer)
case IDM:
IDMan::GetSelected(buffer);
break;
+ case FILEPILOT:
+ FilePilot::GetSelected(buffer);
+ break;
default:
break;
}
diff --git a/QuickLook.Native/QuickLook.Native32/Shell32.h b/QuickLook.Native/QuickLook.Native32/Shell32.h
index 857bc8345..d71914cbf 100644
--- a/QuickLook.Native/QuickLook.Native32/Shell32.h
+++ b/QuickLook.Native/QuickLook.Native32/Shell32.h
@@ -32,6 +32,7 @@ class Shell32
DOPUS,
MULTICOMMANDER,
IDM,
+ FILEPILOT,
};
static FocusedWindowType GetFocusedWindowType();
diff --git a/QuickLook.Native/QuickLook.Native64/QuickLook.Native64.vcxproj b/QuickLook.Native/QuickLook.Native64/QuickLook.Native64.vcxproj
index 4398e41ce..89b0f3253 100644
--- a/QuickLook.Native/QuickLook.Native64/QuickLook.Native64.vcxproj
+++ b/QuickLook.Native/QuickLook.Native64/QuickLook.Native64.vcxproj
@@ -51,6 +51,7 @@
+
@@ -236,4 +237,4 @@
-
\ No newline at end of file
+
diff --git a/QuickLook.Native/QuickLook.Native64/QuickLook.Native64.vcxproj.filters b/QuickLook.Native/QuickLook.Native64/QuickLook.Native64.vcxproj.filters
index 273829842..aeec266d9 100644
--- a/QuickLook.Native/QuickLook.Native64/QuickLook.Native64.vcxproj.filters
+++ b/QuickLook.Native/QuickLook.Native64/QuickLook.Native64.vcxproj.filters
@@ -9,6 +9,7 @@
+
diff --git a/QuickLook.Native/QuickLook.NativeArm64/QuickLook.NativeArm64.vcxproj b/QuickLook.Native/QuickLook.NativeArm64/QuickLook.NativeArm64.vcxproj
index 5f47059f1..bd63d7ce0 100644
--- a/QuickLook.Native/QuickLook.NativeArm64/QuickLook.NativeArm64.vcxproj
+++ b/QuickLook.Native/QuickLook.NativeArm64/QuickLook.NativeArm64.vcxproj
@@ -37,6 +37,7 @@
+
@@ -170,4 +171,4 @@
-
\ No newline at end of file
+
diff --git a/QuickLook.Native/QuickLook.NativeArm64/QuickLook.NativeArm64.vcxproj.filters b/QuickLook.Native/QuickLook.NativeArm64/QuickLook.NativeArm64.vcxproj.filters
index 273829842..aeec266d9 100644
--- a/QuickLook.Native/QuickLook.NativeArm64/QuickLook.NativeArm64.vcxproj.filters
+++ b/QuickLook.Native/QuickLook.NativeArm64/QuickLook.NativeArm64.vcxproj.filters
@@ -9,6 +9,7 @@
+
diff --git a/QuickLook/NativeMethods/QuickLook.cs b/QuickLook/NativeMethods/QuickLook.cs
index 614bd163b..819be25f6 100644
--- a/QuickLook/NativeMethods/QuickLook.cs
+++ b/QuickLook/NativeMethods/QuickLook.cs
@@ -153,5 +153,6 @@ internal enum FocusedWindowType
DOpus,
MultiCommander,
IDM,
+ FilePilot,
}
}