Repository navigation
Expand file tree
/
Copy pathopen_in_explorer.cpp
More file actions
385 lines (353 loc) · 14 KB
/
Copy pathopen_in_explorer.cpp
File metadata and controls
385 lines (353 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
#include "open_in_explorer.hpp"
#include "utils/utf8_path.hpp"
#include <system_error>
#ifdef _WIN32
# ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
# endif
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <windows.h>
# include <shellapi.h>
# include <shlobj.h>
# include <exdisp.h>
# include <oleauto.h>
# include <wrl/client.h>
# include "utils/joining_thread.hpp"
# include "utils/scope_exit.hpp"
# include <chrono>
# include <thread>
# include <utility>
#else
# include <cstdlib>
# include <unistd.h>
#endif
namespace fs = std::filesystem;
namespace acecode::desktop {
namespace {
#ifdef _WIN32
using Microsoft::WRL::ComPtr;
// x64 上 PIDLIST_ABSOLUTE 带 __unaligned 限定,unique_ptr 套不上,单独包一层。
class UniquePidl {
public:
UniquePidl() noexcept = default;
explicit UniquePidl(PIDLIST_ABSOLUTE pidl) noexcept : pidl_(pidl) {}
UniquePidl(UniquePidl&& other) noexcept : pidl_(std::exchange(other.pidl_, nullptr)) {}
UniquePidl& operator=(UniquePidl&& other) noexcept {
if (this != &other) reset(std::exchange(other.pidl_, nullptr));
return *this;
}
~UniquePidl() { reset(); }
void reset(PIDLIST_ABSOLUTE pidl = nullptr) noexcept {
if (pidl_) ::CoTaskMemFree(pidl_);
pidl_ = pidl;
}
PIDLIST_ABSOLUTE get() const noexcept { return pidl_; }
explicit operator bool() const noexcept { return pidl_ != nullptr; }
private:
PIDLIST_ABSOLUTE pidl_ = nullptr;
};
// 目录窗口在 ShellExecuteEx 返回后约 0.3 秒才显示;找不到就不再管前台。
constexpr auto kFolderWindowWait = std::chrono::seconds(2);
constexpr auto kFolderWindowPoll = std::chrono::milliseconds(25);
UniquePidl parse_shell_item(const std::wstring& path) {
PIDLIST_ABSOLUTE pidl = nullptr;
if (FAILED(::SHParseDisplayName(path.c_str(), nullptr, &pidl, 0, nullptr))) return {};
return UniquePidl(pidl);
}
// 旧做法:新起一个 explorer.exe,它再拉起独立的 Explorer 宿主进程放窗口,
// 实测约 1 秒且宿主进程在窗口关闭后不退出。只在 Shell 接口失败时兜底。
bool launch_explorer_select(const fs::path& path, std::string& error) {
const std::wstring parameters = L"/select,\"" + path.wstring() + L"\"";
const std::wstring working_directory = path.parent_path().wstring();
const auto code = reinterpret_cast<intptr_t>(::ShellExecuteW(
nullptr, L"open", L"explorer.exe", parameters.c_str(),
working_directory.c_str(), SW_SHOWNORMAL));
if (code > 32) return true;
error = "ShellExecute failed: " + std::to_string(code);
return false;
}
bool shell_window_shows(IDispatch* dispatch, PCIDLIST_ABSOLUTE folder) {
ComPtr<IServiceProvider> provider;
ComPtr<IShellBrowser> browser;
ComPtr<IShellView> view;
ComPtr<IFolderView> folder_view;
ComPtr<IPersistFolder2> persist;
if (FAILED(dispatch->QueryInterface(IID_PPV_ARGS(provider.GetAddressOf()))) ||
FAILED(provider->QueryService(SID_STopLevelBrowser,
IID_PPV_ARGS(browser.GetAddressOf()))) ||
FAILED(browser->QueryActiveShellView(view.GetAddressOf())) ||
FAILED(view.As(&folder_view)) ||
FAILED(folder_view->GetFolder(IID_PPV_ARGS(persist.GetAddressOf())))) {
return false;
}
PIDLIST_ABSOLUTE current = nullptr;
if (FAILED(persist->GetCurFolder(¤t)) || !current) return false;
const bool equal = ::ILIsEqual(current, folder) != FALSE;
::CoTaskMemFree(current);
return equal;
}
// IShellWindows::FindWindowSW 在 Windows 11 上对资源管理器窗口一律返回 S_FALSE
// (实测路径 / PIDL 字节数组都一样),只能逐个窗口取当前文件夹比对。
HWND find_folder_window(IShellWindows* windows, PCIDLIST_ABSOLUTE folder) {
long count = 0;
if (FAILED(windows->get_Count(&count))) return nullptr;
for (long i = 0; i < count; ++i) {
VARIANT index;
::VariantInit(&index);
index.vt = VT_I4;
index.lVal = i;
ComPtr<IDispatch> dispatch;
if (FAILED(windows->Item(index, dispatch.GetAddressOf())) || !dispatch) continue;
if (!shell_window_shows(dispatch.Get(), folder)) continue;
ComPtr<IWebBrowserApp> app;
SHANDLE_PTR window = 0;
if (SUCCEEDED(dispatch.As(&app)) && SUCCEEDED(app->get_HWND(&window)) && window) {
return reinterpret_cast<HWND>(window);
}
}
return nullptr;
}
void raise_folder_window(HWND window) {
if (::IsIconic(window)) ::ShowWindow(window, SW_RESTORE);
::BringWindowToTop(window);
if (::SetForegroundWindow(window) && ::GetForegroundWindow() == window) return;
// 本进程没有前台权(daemon / TUI 发起)时 SetForegroundWindow 会被拒。
// TOPMOST 脉冲把它抬到普通窗口最上层后立刻撤销,不会让资源管理器一直置顶;
// 用户自己设成置顶的窗口已经在最上层,不去碰它,免得撤掉用户的置顶。
if ((::GetWindowLongPtrW(window, GWL_EXSTYLE) & WS_EX_TOPMOST) == 0) {
constexpr UINT flags = SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW;
::SetWindowPos(window, HWND_TOPMOST, 0, 0, 0, 0, flags);
::SetWindowPos(window, HWND_NOTOPMOST, 0, 0, 0, 0, flags);
}
::SetForegroundWindow(window);
}
void bring_folder_window_forward(PCIDLIST_ABSOLUTE folder, HWND foreground_before) {
ComPtr<IShellWindows> windows;
if (FAILED(::CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_ALL,
IID_PPV_ARGS(windows.GetAddressOf()))) || !windows) {
return;
}
const auto deadline = std::chrono::steady_clock::now() + kFolderWindowWait;
HWND opened = nullptr;
for (;;) {
opened = find_folder_window(windows.Get(), folder);
if (opened && ::IsWindowVisible(opened)) break;
if (std::chrono::steady_clock::now() >= deadline) return;
std::this_thread::sleep_for(kFolderWindowPoll);
}
const auto action = decide_file_manager_window_action(
reinterpret_cast<std::uintptr_t>(foreground_before),
reinterpret_cast<std::uintptr_t>(::GetForegroundWindow()),
reinterpret_cast<std::uintptr_t>(opened));
if (action == FileManagerWindowAction::Raise) raise_folder_window(opened);
}
bool open_target_on_sta_thread(const fs::path& path,
OpenInExplorerTargetKind kind,
std::string& error) {
const HRESULT com = ::CoInitializeEx(
nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
acecode::ScopeExit uninitialize([com] {
if (SUCCEEDED(com)) ::CoUninitialize();
});
const HWND foreground_before = ::GetForegroundWindow();
// 只在本进程此刻持有前台权(Desktop 刚收到点击)时生效,下一次用户输入即失效。
// 文件夹窗口可能落在任意一个 explorer.exe(Windows 11 会单开宿主进程),
// 事先拿不到 PID,所以放行 ASFW_ANY。
::AllowSetForegroundWindow(ASFW_ANY);
const std::wstring wide_path = path.wstring();
UniquePidl item = parse_shell_item(wide_path);
UniquePidl folder;
if (kind == OpenInExplorerTargetKind::File) {
// 与 Chromium / Electron 的 showItemInFolder 相同:直接请正在运行的
// 资源管理器打开父目录并选中文件,文件夹已开着时复用那个窗口。
if (!item || FAILED(::SHOpenFolderAndSelectItems(item.get(), 0, nullptr, 0))) {
if (!launch_explorer_select(path, error)) return false;
}
if (item) {
folder.reset(::ILCloneFull(item.get()));
if (folder) ::ILRemoveLastID(folder.get());
}
} else {
SHELLEXECUTEINFOW info{};
info.cbSize = sizeof(info);
info.fMask = SEE_MASK_NOASYNC | SEE_MASK_FLAG_NO_UI;
info.lpVerb = L"open";
info.lpFile = wide_path.c_str();
info.nShow = SW_SHOWNORMAL;
if (!::ShellExecuteExW(&info)) {
error = "ShellExecuteEx failed: " + std::to_string(::GetLastError());
return false;
}
folder = std::move(item);
}
if (folder) bring_folder_window_forward(folder.get(), foreground_before);
return true;
}
#endif
bool platform_open_target(const fs::path& path,
OpenInExplorerTargetKind kind,
std::string& error) {
#ifdef _WIN32
// 调用方线程的 COM 状态不确定(Desktop 后台 worker / Crow 线程 / TUI 线程),
// Shell 接口要求 STA,所以在一个干净的 STA 线程上执行并等它结束。
bool ok = false;
std::string thread_error;
{
acecode::JoiningThread shell_thread([&ok, &thread_error, path, kind] {
try {
ok = open_target_on_sta_thread(path, kind, thread_error);
} catch (const std::exception& e) {
thread_error = e.what();
} catch (...) {
thread_error = "failed to open path in file manager";
}
});
}
if (!ok) error = thread_error;
return ok;
#else
# ifdef __APPLE__
const char* opener = "open";
# else
const char* opener = "xdg-open";
# endif
const fs::path launch_path =
# ifdef __APPLE__
path;
# else
kind == OpenInExplorerTargetKind::File ? path.parent_path() : path;
# endif
const std::string native_path = acecode::path_to_utf8(launch_path);
pid_t pid = ::fork();
if (pid < 0) {
error = "fork failed";
return false;
}
if (pid == 0) {
# ifdef __APPLE__
if (kind == OpenInExplorerTargetKind::File) {
::execlp(opener, opener, "-R", native_path.c_str(), static_cast<char*>(nullptr));
::_exit(127);
}
# endif
::execlp(opener, opener, native_path.c_str(), static_cast<char*>(nullptr));
::_exit(127);
}
return true;
#endif
}
} // namespace
FileManagerWindowAction decide_file_manager_window_action(
std::uintptr_t foreground_before,
std::uintptr_t foreground_now,
std::uintptr_t opened_window) {
if (opened_window == 0 || foreground_now == opened_window) {
return FileManagerWindowAction::None;
}
// 切换前台的瞬间 GetForegroundWindow 可能是 0,按「还没人接手」处理。
if (foreground_now == 0 || foreground_now == foreground_before) {
return FileManagerWindowAction::Raise;
}
return FileManagerWindowAction::None;
}
ValidatedOpenTarget validate_open_in_explorer_request(
const std::string& path_utf8) {
if (path_utf8.empty()) {
return {false, {}, OpenInExplorerTargetKind::Directory, "path required"};
}
std::error_code ec;
auto requested = acecode::path_from_utf8(path_utf8);
if (requested.empty()) {
return {false, {}, OpenInExplorerTargetKind::Directory, "path required"};
}
if (!requested.is_absolute()) {
return {false, {}, OpenInExplorerTargetKind::Directory, "path must be absolute"};
}
const bool requested_is_directory = fs::is_directory(requested, ec);
if (ec) {
return {false, {}, OpenInExplorerTargetKind::Directory, "failed to inspect path"};
}
const bool requested_is_file =
!requested_is_directory && fs::is_regular_file(requested, ec);
if (ec || (!requested_is_directory && !requested_is_file)) {
return {
false,
{},
OpenInExplorerTargetKind::Directory,
"path is not an existing file or directory",
};
}
auto canonical_path = fs::weakly_canonical(requested, ec);
if (ec || canonical_path.empty()) {
return {false, {}, OpenInExplorerTargetKind::Directory, "failed to resolve path"};
}
const bool canonical_is_directory = fs::is_directory(canonical_path, ec);
if (ec) {
return {false, {}, OpenInExplorerTargetKind::Directory, "failed to inspect path"};
}
const bool canonical_is_file =
!canonical_is_directory && fs::is_regular_file(canonical_path, ec);
if (ec || (!canonical_is_directory && !canonical_is_file)) {
return {
false,
{},
OpenInExplorerTargetKind::Directory,
"path is not an existing file or directory",
};
}
return {
true,
canonical_path,
canonical_is_file
? OpenInExplorerTargetKind::File
: OpenInExplorerTargetKind::Directory,
{},
};
}
ValidatedOpenDirectory validate_open_directory_request(
const std::string& path_utf8) {
auto validated = validate_open_in_explorer_request(path_utf8);
if (!validated.ok) return {false, {}, validated.error};
if (validated.kind != OpenInExplorerTargetKind::Directory) {
return {false, {}, "path is not an existing directory"};
}
return {true, std::move(validated.path), {}};
}
OpenInExplorerResult open_path_in_file_manager(
const std::string& path_utf8,
OpenInExplorerLauncher launcher) {
auto validated = validate_open_in_explorer_request(path_utf8);
if (!validated.ok) return {false, validated.error};
std::string error;
auto launch = launcher
? std::move(launcher)
: OpenInExplorerLauncher(platform_open_target);
if (!launch(validated.path, validated.kind, error)) {
if (error.empty()) error = "failed to open path in file manager";
return {false, error};
}
return {true, {}};
}
OpenInExplorerResult open_directory_in_file_manager(
const std::string& path_utf8,
DirectoryOpenLauncher launcher) {
auto validated = validate_open_directory_request(path_utf8);
if (!validated.ok) return {false, validated.error};
std::string error;
auto launch = launcher
? std::move(launcher)
: DirectoryOpenLauncher([](const fs::path& path, std::string& launch_error) {
return platform_open_target(
path,
OpenInExplorerTargetKind::Directory,
launch_error);
});
if (!launch(validated.path, error)) {
if (error.empty()) error = "failed to open directory";
return {false, error};
}
return {true, {}};
}
} // namespace acecode::desktop