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, } }