From dd4bd5b17ddedcaf64df09f7a10d34267b8834c3 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 20 Jan 2020 23:25:53 -0500 Subject: shift+right click for shell menu --- src/envshell.cpp | 212 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 src/envshell.cpp (limited to 'src/envshell.cpp') diff --git a/src/envshell.cpp b/src/envshell.cpp new file mode 100644 index 00000000..f7033fba --- /dev/null +++ b/src/envshell.cpp @@ -0,0 +1,212 @@ +#include "envshell.h" +#include "env.h" +#include +#include + +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(fmt::format( + "{}, {}", + what, QString::fromStdWString(formatSystemMessage(r)).toStdString())) + { + } +}; + + +class WndProcFilter : public QAbstractNativeEventFilter +{ +public: + WndProcFilter(IContextMenu* cm) + : m_cm2(nullptr), m_cm3(nullptr) + { + IContextMenu2* cm2 = nullptr; + if (SUCCEEDED(cm->QueryInterface(IID_IContextMenu2, (void**)&cm2))) { + m_cm2.reset(cm2); + } + + IContextMenu3* cm3 = nullptr; + if (SUCCEEDED(cm->QueryInterface(IID_IContextMenu3, (void**)&cm3))) { + m_cm3.reset(cm3); + } + } + + bool nativeEventFilter(const QByteArray& type, void* m, long* lresultOut) override + { + if (m_cm3) { + MSG* msg = (MSG*)m; + LRESULT lresult = 0; + + const auto r = m_cm3->HandleMenuMsg2( + msg->message, msg->wParam, msg->lParam, &lresult); + + if (SUCCEEDED(r)) { + if (lresultOut) { + *lresultOut = lresult; + } + + return true; + } + } + + if (m_cm2) { + MSG* msg = (MSG*)m; + + const auto r = m_cm2->HandleMenuMsg( + msg->message, msg->wParam, msg->lParam); + + if (SUCCEEDED(r)) { + if (lresultOut) { + *lresultOut = 0; + } + + return true; + } + } + + return false; + } + +private: + COMPtr m_cm2; + COMPtr m_cm3; +}; + + + +CoTaskMemPtr getIDL(const wchar_t* path) +{ + LPITEMIDLIST pidl; + SFGAOF sfgao; + + const auto r = SHParseDisplayName(path, nullptr, &pidl, 0, &sfgao); + + if (FAILED(r)) { + throw MenuFailed(r, "SHParseDisplayName failed"); + } + + return CoTaskMemPtr(pidl); +} + +std::pair, LPCITEMIDLIST> getShellFolder(LPITEMIDLIST idl) +{ + IShellFolder* psf = nullptr; + LPCITEMIDLIST pidlChild = nullptr; + + const auto r = SHBindToParent( + idl, IID_IShellFolder, reinterpret_cast(&psf), &pidlChild); + + if (FAILED(r)) { + throw MenuFailed(r, "SHBindToParent failed"); + } + + return {COMPtr(psf), pidlChild}; +} + +COMPtr getContextMenu(IShellFolder* psf, LPCITEMIDLIST idl) +{ + IContextMenu* pcm = nullptr; + + const auto r = psf->GetUIObjectOf( + 0, 1, &idl, IID_IContextMenu, nullptr, + reinterpret_cast(&pcm)); + + if (FAILED(r)) { + throw MenuFailed(r, "GetUIObjectOf failed"); + } + + return COMPtr(pcm); +} + +HMenuPtr createMenu(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"); + } + + return HMenuPtr(hmenu); +} + +int runMenu(IContextMenu* cm, HWND hwnd, HMENU menu, const QPoint& p) +{ + auto filter = std::make_unique(cm); + QCoreApplication::instance()->installNativeEventFilter(filter.get()); + + return TrackPopupMenuEx(menu, TPM_RETURNCMD, p.x(), p.y(), hwnd, nullptr); +} + +void invoke(HWND hwnd, const QPoint& p, int cmd, IContextMenu* cm) +{ + 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 = cm->InvokeCommand((CMINVOKECOMMANDINFO*)&info); + + if (FAILED(r)) { + throw MenuFailed(r, fmt::format("InvokeCommand failed, verb={}", cmd)); + } +} + +void showShellMenu(QWidget* parent, const QFileInfo& file, const QPoint& pos) +{ + const auto path = QDir::toNativeSeparators(file.absoluteFilePath()); + + try + { + auto idl = getIDL(path.toStdWString().c_str()); + auto [sf, childIdl] = getShellFolder(idl.get()); + auto cm = getContextMenu(sf.get(), childIdl); + auto hmenu = createMenu(cm.get()); + auto hwnd = (HWND)parent->window()->winId(); + + const int cmd = runMenu(cm.get(), hwnd, hmenu.get(), pos); + if (cmd <= 0) { + return; + } + + invoke(hwnd, pos, cmd - QCM_FIRST, cm.get()); + } + catch(MenuFailed& e) + { + log::error("can't create shell menu for '{}': {}", path, e.what()); + } +} + +} // namespace -- cgit v1.3.1