#include #include #include #include #include "envshell.h" namespace env { using namespace MOBase; const int QCM_FIRST = 1; const int QCM_LAST = 0x7ff; class MenuFailed : public std::runtime_error { public: MenuFailed(HRESULT r, const std::string& what) : runtime_error( std::format("{}, {}", what, QString::fromStdWString(formatSystemMessage(r)).toStdString())) {} }; class DummyMenu { public: DummyMenu(QString s) : m_what(s) {} const QString& what() const { return m_what; } private: QString m_what; }; struct IdlsFreer { const std::vector& v; IdlsFreer(const std::vector& v) : v(v) {} ~IdlsFreer() { for (auto&& idl : v) { ::CoTaskMemFree(const_cast(idl)); } } }; class WndProcFilter : public QAbstractNativeEventFilter { public: using function_type = std::function; WndProcFilter(function_type f) : m_f(std::move(f)) {} bool nativeEventFilter(const QByteArray& eventType, void* message, qintptr* result) override { MSG* msg = (MSG*)message; if (!msg) { return false; } LRESULT lr = 0; const bool r = m_f(msg->hwnd, msg->message, msg->wParam, msg->lParam, &lr); if (result) { *result = lr; } return r; } private: function_type m_f; }; HWND getHWND(QMainWindow* mw) { if (mw) { return (HWND)mw->winId(); } else { return 0; } } // adapted from // https://devblogs.microsoft.com/oldnewthing/20040928-00/?p=37723 // HRESULT IContextMenu_GetCommandString(IContextMenu* pcm, UINT_PTR idCmd, UINT uFlags, UINT* pwReserved, LPWSTR pszName, UINT cchMax) { // Callers are expected to be using Unicode. if (!(uFlags & GCS_UNICODE)) { return E_INVALIDARG; } // Some context menu handlers have off-by-one bugs and will // overflow the output buffer. Let�s artificially reduce the // buffer size so a one-character overflow won�t corrupt memory. if (cchMax <= 1) { return E_FAIL; } cchMax--; // First try the Unicode message. Preset the output buffer // with a known value because some handlers return S_OK without // doing anything. pszName[0] = L'\0'; HRESULT hr = pcm->GetCommandString(idCmd, uFlags, pwReserved, (LPSTR)pszName, cchMax); if (SUCCEEDED(hr) && pszName[0] == L'\0') { // Rats, a buggy IContextMenu handler that returned success // even though it failed. hr = E_NOTIMPL; } if (FAILED(hr)) { // try again with ANSI � pad the buffer with one extra character // to compensate for context menu handlers that overflow by // one character. LPSTR pszAnsi = (LPSTR)LocalAlloc(LMEM_FIXED, (cchMax + 1) * sizeof(CHAR)); if (pszAnsi) { pszAnsi[0] = '\0'; hr = pcm->GetCommandString(idCmd, uFlags & ~GCS_UNICODE, pwReserved, pszAnsi, cchMax); if (SUCCEEDED(hr) && pszAnsi[0] == '\0') { // Rats, a buggy IContextMenu handler that returned success // even though it failed. hr = E_NOTIMPL; } if (SUCCEEDED(hr)) { if (MultiByteToWideChar(CP_ACP, 0, pszAnsi, -1, pszName, cchMax) == 0) { hr = E_FAIL; } } LocalFree(pszAnsi); } else { hr = E_OUTOFMEMORY; } } return hr; } ShellMenu::ShellMenu(QMainWindow* mw) : m_mw(mw) {} void ShellMenu::addFile(QFileInfo fi) { m_files.emplace_back(std::move(fi)); } int ShellMenu::fileCount() const { return static_cast(m_files.size()); } void ShellMenu::exec(const QPoint& pos) { HMENU menu = getMenu(); if (!menu) { return; } try { const auto hwnd = getHWND(m_mw); auto filter = std::make_unique( [&](HWND h, UINT m, WPARAM wp, LPARAM lp, LRESULT* out) { return wndProc(h, m, wp, lp, out); }); QCoreApplication::instance()->installNativeEventFilter(filter.get()); const int cmd = TrackPopupMenuEx(menu, TPM_RETURNCMD, pos.x(), pos.y(), hwnd, nullptr); if (m_mw) { if (auto* sb = m_mw->statusBar()) { sb->clearMessage(); } } if (cmd <= 0) { return; } invoke(pos, cmd - QCM_FIRST); } catch (MenuFailed& e) { if (m_files.size() == 1) { log::error("can't exec shell menu for '{}': {}", QDir::toNativeSeparators(m_files[0].absoluteFilePath()), e.what()); } else { log::error("can't exec shell menu for {} files: {}", m_files.size(), e.what()); } } } HMENU ShellMenu::getMenu() { if (!m_menu) { create(); } return m_menu.get(); } bool ShellMenu::wndProc(HWND h, UINT m, WPARAM wp, LPARAM lp, LRESULT* out) { if (m == WM_MENUSELECT) { HANDLE_WM_MENUSELECT(h, wp, lp, onMenuSelect); return true; } if (m_cm3) { const auto r = m_cm3->HandleMenuMsg2(m, wp, lp, out); if (SUCCEEDED(r)) { return true; } } if (m_cm2) { const auto r = m_cm2->HandleMenuMsg(m, wp, lp); if (SUCCEEDED(r)) { if (out) { *out = 0; } return true; } } return false; } // adapted from // https://devblogs.microsoft.com/oldnewthing/20040928-00/?p=37723 // void ShellMenu::onMenuSelect(HWND hwnd, HMENU hmenu, int item, HMENU hmenuPopup, UINT flags) { if (m_cm && item >= QCM_FIRST && item <= QCM_LAST) { WCHAR szBuf[MAX_PATH]; const auto r = IContextMenu_GetCommandString(m_cm.get(), item - QCM_FIRST, GCS_HELPTEXTW, NULL, szBuf, MAX_PATH); if (FAILED(r)) { lstrcpynW(szBuf, L"No help available.", MAX_PATH); } if (m_mw) { if (auto* sb = m_mw->statusBar()) { sb->showMessage(QString::fromWCharArray(szBuf)); } } } } void ShellMenu::create() { if (m_files.empty()) { log::warn("showShellMenu(): no files given"); return; } try { auto idls = createIdls(m_files); if (idls.empty()) { log::error("no idls, can't create context menu"); return; } IdlsFreer freer(idls); auto array = createItemArray(idls); createContextMenu(array.get()); createPopupMenu(m_cm.get()); } catch (DummyMenu& dm) { m_menu = createDummyMenu(dm.what()); } catch (MenuFailed& e) { if (m_files.size() == 1) { log::error("can't create shell menu for '{}': {}", QDir::toNativeSeparators(m_files[0].absoluteFilePath()), e.what()); } else { log::error("can't create shell menu for {} files: {}", m_files.size(), e.what()); } m_menu = createDummyMenu(QObject::tr("No menu available")); } } HMenuPtr ShellMenu::createDummyMenu(const QString& what) { try { HMENU menu = CreatePopupMenu(); if (!menu) { const auto e = GetLastError(); throw MenuFailed(e, "CreatePopupMenu failed"); } if (!AppendMenuW(menu, MF_STRING | MF_DISABLED, 0, what.toStdWString().c_str())) { const auto e = GetLastError(); throw MenuFailed(e, "AppendMenuW failed"); } return HMenuPtr(menu); } catch (MenuFailed& e) { log::error("{}", what); log::error("additionally, creating the dummy menu failed: {}", e.what()); return {}; } } std::vector ShellMenu::createIdls(const std::vector& files) { std::vector idls; std::optional parent; for (auto&& f : files) { const auto path = QDir::toNativeSeparators(f.absoluteFilePath()).toStdWString(); if (!parent) { parent = f.absoluteDir(); } else { if (*parent != f.absoluteDir()) { throw DummyMenu(QObject::tr("Selected files must be in the same directory")); } } auto item = createShellItem(path); auto pidlist = getPersistIDList(item.get()); auto absIdl = getIDList(pidlist.get()); idls.push_back(absIdl.release()); } return idls; } COMPtr ShellMenu::createItemArray(std::vector& idls) { IShellItemArray* array = nullptr; auto r = SHCreateShellItemArrayFromIDLists(static_cast(idls.size()), &idls[0], &array); if (FAILED(r)) { throw MenuFailed(r, "SHCreateShellItemArrayFromIDLists failed"); } return COMPtr(array); } void ShellMenu::createContextMenu(IShellItemArray* array) { IContextMenu* cm = nullptr; auto r = array->BindToHandler(nullptr, BHID_SFUIObject, IID_IContextMenu, (void**)&cm); if (FAILED(r)) { throw MenuFailed(r, "BindToHandler failed"); } m_cm.reset(cm); { IContextMenu2* cm2 = nullptr; if (SUCCEEDED(m_cm->QueryInterface(IID_IContextMenu2, (void**)&cm2))) { m_cm2.reset(cm2); } } { IContextMenu3* cm3 = nullptr; if (SUCCEEDED(m_cm->QueryInterface(IID_IContextMenu3, (void**)&cm3))) { m_cm3.reset(cm3); } } } void ShellMenu::createPopupMenu(IContextMenu* cm) { HMENU hmenu = CreatePopupMenu(); if (!hmenu) { const auto e = GetLastError(); throw MenuFailed(e, "CreatePopupMenu failed"); } const auto r = cm->QueryContextMenu(hmenu, 0, QCM_FIRST, QCM_LAST, CMF_EXTENDEDVERBS); if (FAILED(r)) { throw MenuFailed(r, "QueryContextMenu failed"); } m_menu.reset(hmenu); } COMPtr ShellMenu::createShellItem(const std::wstring& path) { IShellItem* item = nullptr; auto r = SHCreateItemFromParsingName(path.c_str(), nullptr, IID_IShellItem, (void**)&item); if (FAILED(r)) { throw MenuFailed(r, "SHCreateItemFromParsingName failed"); } return COMPtr(item); } COMPtr ShellMenu::getPersistIDList(IShellItem* item) { IPersistIDList* idl = nullptr; auto r = item->QueryInterface(IID_IPersistIDList, (void**)&idl); if (FAILED(r)) { throw MenuFailed(r, "QueryInterface IID_IPersistIDList failed"); } return COMPtr(idl); } CoTaskMemPtr ShellMenu::getIDList(IPersistIDList* pidlist) { LPITEMIDLIST absIdl = nullptr; auto r = pidlist->GetIDList(&absIdl); if (FAILED(r)) { throw MenuFailed(r, "GetIDList failed"); } return CoTaskMemPtr(absIdl); } void ShellMenu::invoke(const QPoint& p, int cmd) { const auto hwnd = getHWND(m_mw); CMINVOKECOMMANDINFOEX info = {}; info.cbSize = sizeof(info); info.fMask = CMIC_MASK_UNICODE | CMIC_MASK_PTINVOKE; info.hwnd = hwnd; info.lpVerb = MAKEINTRESOURCEA(cmd); info.lpVerbW = MAKEINTRESOURCEW(cmd); info.nShow = SW_SHOWNORMAL; info.ptInvoke = {p.x(), p.y()}; // note: this calls the query version because the Qt even loop hasn't run // yet and shift is still considered pressed const auto m = QApplication::queryKeyboardModifiers(); if (m & Qt::ShiftModifier) { info.fMask |= CMIC_MASK_SHIFT_DOWN; } if (m & Qt::ControlModifier) { info.fMask |= CMIC_MASK_CONTROL_DOWN; } const auto r = m_cm->InvokeCommand((CMINVOKECOMMANDINFO*)&info); if (FAILED(r)) { throw MenuFailed(r, std::format("InvokeCommand failed, verb={}", cmd)); } } ShellMenuCollection::ShellMenuCollection(QMainWindow* mw) : m_mw(mw), m_active(nullptr) {} void ShellMenuCollection::addDetails(QString s) { m_details.emplace_back(std::move(s)); } void ShellMenuCollection::add(QString name, ShellMenu m) { m_menus.push_back({name, std::move(m)}); } void ShellMenuCollection::exec(const QPoint& pos) { HMENU menu = ::CreatePopupMenu(); if (!menu) { const auto e = GetLastError(); log::error("CreatePopupMenu for merged menus failed, {}", formatSystemMessage(e)); return; } if (!m_details.empty()) { for (auto&& d : m_details) { const auto s = d.toStdWString(); const auto r = AppendMenuW(menu, MF_STRING | MF_DISABLED, 0, s.c_str()); if (!r) { const auto e = GetLastError(); log::error("AppendMenuW failed for details '{}', {}", d, formatSystemMessage(e)); } } const auto r = AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); if (!r) { const auto e = GetLastError(); log::error("AppendMenuW failed for separator, {}", formatSystemMessage(e)); } } for (auto&& m : m_menus) { auto hmenu = m.menu.getMenu(); if (!hmenu) { continue; } const auto r = AppendMenuW(menu, MF_POPUP | MF_STRING, reinterpret_cast(hmenu), m.name.toStdWString().c_str()); if (!r) { const auto e = GetLastError(); log::error("AppendMenuW failed for merged menu {}, {}", m.name, formatSystemMessage(e)); continue; } } auto hwnd = getHWND(m_mw); auto filter = std::make_unique( [&](HWND h, UINT m, WPARAM wp, LPARAM lp, LRESULT* out) { return wndProc(h, m, wp, lp, out); }); QCoreApplication::instance()->installNativeEventFilter(filter.get()); const int cmd = TrackPopupMenuEx(menu, TPM_RETURNCMD, pos.x(), pos.y(), hwnd, nullptr); if (m_mw) { if (auto* sb = m_mw->statusBar()) { sb->clearMessage(); } } if (cmd <= 0) { return; } if (!m_active) { log::debug("SMC: command {} selected without active submenu", cmd); return; } const auto realCmd = cmd - QCM_FIRST; log::debug("SMC: invoking {} on {}", realCmd, m_active->name); m_active->menu.invoke(pos, realCmd); } bool ShellMenuCollection::wndProc(HWND h, UINT m, WPARAM wp, LPARAM lp, LRESULT* out) { if (m == WM_MENUSELECT) { auto* oldActive = m_active; m_active = nullptr; HANDLE_WM_MENUSELECT(h, wp, lp, onMenuSelect); if (!m_active && oldActive) { // this was not a top level, forward to active m_active = oldActive; } else if (m_active && m_active == oldActive) { // same top level menu was selected twice, ignore return true; } else if (m_active && m_active != oldActive) { // new top level selected log::debug("SMC: switching to {}", m_active->name); } } if (!m_active) { // no active menu, forward it to the default handler return false; } return m_active->menu.wndProc(h, m, wp, lp, out); } void ShellMenuCollection::onMenuSelect(HWND hwnd, HMENU hmenu, int item, HMENU hmenuPopup, UINT flags) { for (auto&& m : m_menus) { if (m.menu.getMenu() == hmenuPopup) { m_active = &m; break; } } } } // namespace env