#include "shell.hpp" #include #include namespace wl { namespace { ComPtr active2; ComPtr active3; void check(HRESULT result, const char* action) { if (FAILED(result)) throw Error(std::string(action) + ": " + win_error(static_cast(result))); } struct IdLists { std::vector values; explicit IdLists(const std::vector& paths) { for (auto& path : paths) { PIDLIST_ABSOLUTE id = nullptr; auto hr = SHParseDisplayName(path.c_str(), nullptr, &id, 0, nullptr); if (FAILED(hr)) { for (auto item : values) CoTaskMemFree(item); throw Error("File is no longer available: " + utf8(path)); } values.push_back(id); } } ~IdLists() { for (auto id : values) CoTaskMemFree(id); } }; ComPtr items(const IdLists& ids) { ComPtr array; check(SHCreateShellItemArrayFromIDLists(static_cast(ids.values.size()), const_cast(ids.values.data()), &array), "Create shell selection"); return array; } ComPtr context(HWND owner, const std::vector& paths) { if (paths.empty()) throw Error("No files selected"); IdLists ids(paths); auto array = items(ids); ComPtr menu; auto hr = array->BindToHandler(nullptr, BHID_SFUIObject, IID_PPV_ARGS(&menu)); if (FAILED(hr)) { ComPtr desktop; check(SHGetDesktopFolder(&desktop), "Get desktop folder"); DEFCONTEXTMENU definition{}; definition.hwnd = owner; definition.psf = desktop.Get(); std::vector children(ids.values.begin(), ids.values.end()); definition.cidl = static_cast(children.size()); definition.apidl = children.data(); check(SHCreateDefaultContextMenu(&definition, IID_PPV_ARGS(&menu)), "Create shell context menu"); } return menu; } void invoke(HWND owner, IContextMenu* menu, const char* verb, bool shift) { CMINVOKECOMMANDINFOEX info{}; info.cbSize = sizeof(info); info.hwnd = owner; info.fMask = CMIC_MASK_UNICODE | CMIC_MASK_ASYNCOK | (shift ? CMIC_MASK_SHIFT_DOWN : 0); info.lpVerb = verb; std::wstring unicode; if (HIWORD(verb)) { unicode = wide(verb); info.lpVerbW = unicode.c_str(); } else info.lpVerbW = reinterpret_cast(verb); info.nShow = SW_SHOWNORMAL; check(menu->InvokeCommand(reinterpret_cast(&info)), "Shell action"); } class DropSource final : public IDropSource { LONG references_ = 1; public: HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void** value) override { if (iid == IID_IUnknown || iid == IID_IDropSource) { *value = static_cast(this); AddRef(); return S_OK; } *value = nullptr; return E_NOINTERFACE; } ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&references_); } ULONG STDMETHODCALLTYPE Release() override { auto n = InterlockedDecrement(&references_); if (!n) delete this; return n; } HRESULT STDMETHODCALLTYPE QueryContinueDrag(BOOL escape, DWORD keys) override { return escape ? DRAGDROP_S_CANCEL : !(keys & MK_LBUTTON) ? DRAGDROP_S_DROP : S_OK; } HRESULT STDMETHODCALLTYPE GiveFeedback(DWORD) override { return DRAGDROP_S_USEDEFAULTCURSORS; } }; } LRESULT shell_menu_message(UINT message, WPARAM wparam, LPARAM lparam, bool& handled) { handled = false; if (message != WM_INITMENUPOPUP && message != WM_DRAWITEM && message != WM_MEASUREITEM && message != WM_MENUCHAR) return 0; LRESULT result = 0; if (active3 && SUCCEEDED(active3->HandleMenuMsg2(message, wparam, lparam, &result))) handled = true; else if (active2 && SUCCEEDED(active2->HandleMenuMsg(message, wparam, lparam))) handled = true; return result; } UINT shell_context(HWND owner, const std::vector& paths, POINT point) { auto context_menu = context(owner, paths); HMENU popup = CreatePopupMenu(); bool shift = GetKeyState(VK_SHIFT) < 0; auto hr = context_menu->QueryContextMenu(popup, 0, 1, 0x6FFF, CMF_NORMAL | CMF_CANRENAME | (shift ? CMF_EXTENDEDVERBS : 0)); if (FAILED(hr)) { DestroyMenu(popup); check(hr, "Build shell menu"); } AppendMenuW(popup, MF_SEPARATOR, 0, nullptr); AppendMenuW(popup, MF_STRING, shell_open_parent, L"打开所在文件夹(&F)\tCtrl+Enter"); AppendMenuW(popup, MF_STRING, shell_copy_path, L"复制完整路径(&P)\tCtrl+Shift+C"); context_menu.As(&active2); context_menu.As(&active3); UINT command = TrackPopupMenuEx(popup, TPM_RETURNCMD | TPM_RIGHTBUTTON, point.x, point.y, owner, nullptr); active3.Reset(); active2.Reset(); DestroyMenu(popup); if (command > 0 && command < 0x7000) { wchar_t verb[128]{}; context_menu->GetCommandString(command - 1, GCS_VERBW, nullptr, reinterpret_cast(verb), 128); if (!_wcsicmp(verb, L"rename")) return shell_rename_selected; invoke(owner, context_menu.Get(), MAKEINTRESOURCEA(command - 1), shift); } return command; } void shell_verb(HWND owner, const std::vector& paths, const char* verb, bool shift) { auto menu = context(owner, paths); auto popup = CreatePopupMenu(); auto hr = menu->QueryContextMenu(popup, 0, 1, 0x6FFF, CMF_NORMAL | CMF_CANRENAME); DestroyMenu(popup); check(hr, "Build shell actions"); invoke(owner, menu.Get(), verb, shift); } void shell_open(HWND owner, const std::wstring& path, bool parent) { if (parent) { IdLists ids({path}); check(SHOpenFolderAndSelectItems(ids.values.front(), 0, nullptr, 0), "Open containing folder"); } else { SHELLEXECUTEINFOW info{sizeof(info)}; info.hwnd = owner; info.lpFile = path.c_str(); info.nShow = SW_SHOWNORMAL; info.fMask = SEE_MASK_FLAG_NO_UI; if (!ShellExecuteExW(&info)) throw Error("Open file: " + win_error()); } } void clipboard_paths(HWND owner, const std::vector& paths, bool cut, bool text_only) { if (paths.empty()) return; if (!text_only) { shell_verb(owner, paths, cut ? "cut" : "copy"); return; } std::wstring text; for (auto& path : paths) { if (!text.empty()) text += L"\r\n"; text += path; } HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, (text.size() + 1) * sizeof(wchar_t)); if (!memory) throw Error("Cannot allocate clipboard data"); auto* data = GlobalLock(memory); memcpy(data, text.c_str(), (text.size() + 1) * sizeof(wchar_t)); GlobalUnlock(memory); if (!OpenClipboard(owner)) { GlobalFree(memory); throw Error("Clipboard is busy"); } EmptyClipboard(); if (!SetClipboardData(CF_UNICODETEXT, memory)) GlobalFree(memory); CloseClipboard(); } void shell_drag(HWND, const std::vector& paths) { if (paths.empty()) return; IdLists ids(paths); auto array = items(ids); ComPtr data; check(array->BindToHandler(nullptr, BHID_DataObject, IID_PPV_ARGS(&data)), "Create drag data"); ComPtr source; source.Attach(new DropSource()); DWORD effect; DoDragDrop(data.Get(), source.Get(), DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK, &effect); } void shell_rename(HWND owner, const std::wstring& path, const std::wstring& name) { if (name.empty() || name.find_first_of(L"\\/:*?\"<>|") != name.npos || name == L"." || name == L"..") throw Error("Invalid file name"); ComPtr operation; check(CoCreateInstance(CLSID_FileOperation, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&operation)), "Create rename operation"); operation->SetOwnerWindow(owner); operation->SetOperationFlags(FOF_ALLOWUNDO | FOFX_ADDUNDORECORD); ComPtr item; check(SHCreateItemFromParsingName(path.c_str(), nullptr, IID_PPV_ARGS(&item)), "Open rename item"); check(operation->RenameItem(item.Get(), name.c_str(), nullptr), "Rename file"); check(operation->PerformOperations(), "Rename file"); } std::vector shell_menu_verbs(HWND owner, const std::vector& paths) { auto menu = context(owner, paths); auto popup = CreatePopupMenu(); auto hr = menu->QueryContextMenu(popup, 0, 1, 0x6FFF, CMF_NORMAL | CMF_CANRENAME); if (FAILED(hr)) { DestroyMenu(popup); check(hr, "Build shell menu"); } std::vector verbs; for (UINT id = 0; id < static_cast(HRESULT_CODE(hr)); ++id) { wchar_t verb[256]{}; if (SUCCEEDED(menu->GetCommandString(id, GCS_VERBW, nullptr, reinterpret_cast(verb), 256)) && *verb) verbs.push_back(utf8(verb)); } DestroyMenu(popup); return verbs; } }