diff options
| author | isanae <14251494+isanae@users.noreply.github.com> | 2020-02-09 08:52:03 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-02-09 08:52:03 -0500 |
| commit | 1f1f99f253f981ae3d11e9df177a144d00dbce0e (patch) | |
| tree | 01d38125329508f5628f9221f797e303180c8fc3 /src/envshell.cpp | |
| parent | 718c2a56f91f824c5eab69d8cc16294f77243370 (diff) | |
| parent | 3a80685189967fbf4cfa002ac2b8a4fa421306ca (diff) | |
Merge pull request #994 from isanae/generic-file-list
New file tree
Diffstat (limited to 'src/envshell.cpp')
| -rw-r--r-- | src/envshell.cpp | 676 |
1 files changed, 676 insertions, 0 deletions
diff --git a/src/envshell.cpp b/src/envshell.cpp new file mode 100644 index 00000000..33ec0624 --- /dev/null +++ b/src/envshell.cpp @@ -0,0 +1,676 @@ +#include "envshell.h" +#include <log.h> +#include <utility.h> +#include <windowsx.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(fmt::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<LPCITEMIDLIST>& v; + + IdlsFreer(const std::vector<LPCITEMIDLIST>& v) + : v(v) + { + } + + ~IdlsFreer() + { + for (auto&& idl : v) { + ::CoTaskMemFree(const_cast<LPITEMIDLIST>(idl)); + } + } +}; + + +class WndProcFilter : public QAbstractNativeEventFilter +{ +public: + using function_type = std::function< + bool (HWND hwnd, UINT m, WPARAM wp, LPARAM lp, LRESULT* out)>; + + WndProcFilter(function_type f) + : m_f(std::move(f)) + { + } + + bool nativeEventFilter(const QByteArray& type, void* m, long* lresultOut) override + { + MSG* msg = (MSG*)m; + if (!msg) { + return false; + } + + LRESULT lr = 0; + + const bool r = m_f(msg->hwnd, msg->message, msg->wParam, msg->lParam, &lr); + + if (lresultOut) { + *lresultOut = 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<int>(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<WndProcFilter>( + [&](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<LPCITEMIDLIST> ShellMenu::createIdls( + const std::vector<QFileInfo>& files) +{ + std::vector<LPCITEMIDLIST> idls; + std::optional<QDir> 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<IShellItemArray> ShellMenu::createItemArray( + std::vector<LPCITEMIDLIST>& idls) +{ + IShellItemArray* array = nullptr; + auto r = SHCreateShellItemArrayFromIDLists( + static_cast<UINT>(idls.size()), &idls[0], &array); + + if (FAILED(r)) { + throw MenuFailed(r, "SHCreateShellItemArrayFromIDLists failed"); + } + + return COMPtr<IShellItemArray>(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<IShellItem> 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<IShellItem>(item); +} + +COMPtr<IPersistIDList> 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<IPersistIDList>(idl); +} + +CoTaskMemPtr<LPITEMIDLIST> ShellMenu::getIDList(IPersistIDList* pidlist) +{ + LPITEMIDLIST absIdl = nullptr; + auto r = pidlist->GetIDList(&absIdl); + + if (FAILED(r)) { + throw MenuFailed(r, "GetIDList failed"); + } + + return CoTaskMemPtr<LPITEMIDLIST>(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, fmt::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<UINT_PTR>(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<WndProcFilter>( + [&](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"); + 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 |
