From d4172dc5f8c642dbbe235a86a28992af310e703a Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 2 Dec 2019 13:51:46 -0500 Subject: added "open with vfs" option to conflicts tab --- src/env.cpp | 157 +++++++++++++++++++++++++++++++++++++++++ src/env.h | 20 ++++++ src/modinfodialog.cpp | 23 +++++- src/modinfodialogconflicts.cpp | 39 +++++++++- src/modinfodialogconflicts.h | 2 + src/modinfodialogfwd.h | 1 + src/processrunner.cpp | 20 +++++- src/processrunner.h | 12 ++-- 8 files changed, 263 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/env.cpp b/src/env.cpp index f9507dc1..0098456e 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -675,6 +675,163 @@ Service getService(const QString& name) } +std::optional getAssocString(const QFileInfo& file, ASSOCSTR astr) +{ + const auto ext = L"." + file.suffix().toStdWString(); + + // getting buffer size + DWORD bufferSize = 0; + auto r = AssocQueryStringW( + ASSOCF_INIT_IGNOREUNKNOWN, astr, ext.c_str(), L"open", nullptr, &bufferSize); + + // returns S_FALSE when giving back the buffer size, so that's actually the + // expected return value + + if (r != S_FALSE || bufferSize == 0) { + if (r == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) { + log::error("file '{}' has no associated executable", file.absoluteFilePath()); + } else { + log::error( + "can't get buffer size for AssocQueryStringW(), {}", + formatSystemMessage(r)); + } + return {}; + } + + // getting string + auto buffer = std::make_unique(bufferSize + 1); + std::fill(buffer.get(), buffer.get() + bufferSize + 1, 0); + + r = AssocQueryStringW( + ASSOCF_INIT_IGNOREUNKNOWN, astr, ext.c_str(), L"open", buffer.get(), &bufferSize); + + if (FAILED(r)) { + log::error( + "failed to get exe associated with '{}', {}", + file.suffix(), formatSystemMessage(r)); + + return {}; + } + + // buffer size includes the null terminator + return QString::fromWCharArray(buffer.get(), bufferSize - 1); +} + +QString formatCommandLine(const QFileInfo& targetInfo, const QString& cmd) +{ + // yeah, FormatMessage() expects at least as many arguments as there are + // placeholders and while the command for associations should typically only + // have %1, the user can actually enter anything in the registry + // + // since the maximum number of arguments is 99, this creates an array of 99 + // wchar_* where the first one (%1) points to the filename and the remaining + // 98 to "" + // + // FormatMessage() actually takes a va_list* for the arguments, but by passing + // FORMAT_MESSAGE_ARGUMENT_ARRAY, an array of DWORD_PTR can be given instead + + // 99 arguments + std::array args; + + // first one is the filename + const auto wpath = targetInfo.absoluteFilePath().toStdWString(); + args[0] = reinterpret_cast(wpath.c_str()); + + // remaining are "" + std::fill(args.begin() + 1, args.end(), reinterpret_cast(L"")); + + // must be freed with LocalFree() + wchar_t* buffer = nullptr; + + const auto wcmd = cmd.toStdWString(); + + const auto n = ::FormatMessageW( + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_ARGUMENT_ARRAY | + FORMAT_MESSAGE_FROM_STRING, + wcmd.c_str(), 0, 0, + reinterpret_cast(&buffer), + 0, reinterpret_cast(&args[0])); + + if (n == 0 || !buffer){ + const auto e = GetLastError(); + + log::error( + "failed to format command line '{}' with path '{}', {}", + cmd, targetInfo.absoluteFilePath(), formatSystemMessage(e)); + + return {}; + } + + auto s = QString::fromWCharArray(buffer, n); + ::LocalFree(buffer); + + return s.trimmed(); +} + +std::pair splitExeAndArguments(const QString& cmd) +{ + int exeBegin = 0; + int exeEnd = -1; + + if (cmd[0] == '"'){ + // surrounded by double-quotes, so find the next one + exeBegin = 1; + exeEnd = cmd.indexOf('"', exeBegin); + + if (exeEnd == -1) { + log::error("missing terminating double-quote in command line '{}'", cmd); + return {}; + } + } else { + // no double-quotes, find the first whitespace + exeEnd = cmd.indexOf(QRegExp("\\s")); + if (exeEnd == -1) { + exeEnd = cmd.size(); + } + } + + QString exe = cmd.mid(exeBegin, exeEnd - exeBegin).trimmed(); + QString args = cmd.mid(exeEnd + 1).trimmed(); + + return {std::move(exe), std::move(args)}; +} + +Association getAssociation(const QFileInfo& targetInfo) +{ + log::debug( + "getting association for '{}', extension is '.{}'", + targetInfo.absoluteFilePath(), targetInfo.suffix()); + + const auto cmd = getAssocString(targetInfo, ASSOCSTR_COMMAND); + if (!cmd) { + return {}; + } + + log::debug("raw cmd is '{}'", *cmd); + + QString formattedCmd = formatCommandLine(targetInfo, *cmd); + if (formattedCmd.isEmpty()) { + log::error( + "command line associated with '{}' is empty", + targetInfo.absoluteFilePath()); + + return {}; + } + + log::debug("formatted cmd is '{}'", formattedCmd); + + const auto p = splitExeAndArguments(formattedCmd); + if (p.first.isEmpty()) { + return {}; + } + + log::debug("split into exe='{}' and cmd='{}'", p.first, p.second); + + return {p.first, *cmd, p.second}; +} + + // returns the filename of the given process or the current one // std::wstring processFilename(HANDLE process=INVALID_HANDLE_VALUE) diff --git a/src/env.h b/src/env.h index dc0fd864..16f8039e 100644 --- a/src/env.h +++ b/src/env.h @@ -246,6 +246,26 @@ Service getService(const QString& name); QString toString(Service::StartType st); QString toString(Service::Status st); + +struct Association +{ + // path to the executable associated with the file + QFileInfo executable; + + // full command line associated with the file, no replacements + QString commandLine; + + // command line _without_ the executable and with placeholders such as %1 + // replaced by the given file + QString formattedCommandLine; +}; + +// returns the associated executable and command line, executable is empty on +// error +// +Association getAssociation(const QFileInfo& file); + + enum class CoreDumpTypes { Mini = 1, diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index c7e071ad..f5ca1de7 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -61,10 +61,27 @@ bool canPreviewFile( return pluginContainer.previewGenerator().previewSupported(ext); } -bool canOpenFile(bool isArchive, const QString&) +bool isExecutableFilename(const QString& filename) { - // can open anything as long as it's not in an archive - return !isArchive; + static const std::set exeExtensions = { + "exe", "cmd", "bat" + }; + + const auto ext = QFileInfo(filename).suffix().toLower(); + + return exeExtensions.contains(ext); +} + +bool canRunFile(bool isArchive, const QString& filename) +{ + // can run executables that are not archives + return !isArchive && isExecutableFilename(filename); +} + +bool canOpenFile(bool isArchive, const QString& filename) +{ + // can open non-executables that are not archives + return !isArchive && !isExecutableFilename(filename); } bool canExploreFile(bool isArchive, const QString&) diff --git a/src/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp index d37f068c..58e935f2 100644 --- a/src/modinfodialogconflicts.cpp +++ b/src/modinfodialogconflicts.cpp @@ -81,6 +81,11 @@ public: return canUnhideFile(isArchive(), fileName()); } + bool canRun() const + { + return canRunFile(isArchive(), fileName()); + } + bool canOpen() const { return canOpenFile(isArchive(), fileName()); @@ -536,6 +541,20 @@ void ConflictsTab::openItems(QTreeView* tree) }); } +void ConflictsTab::runItemsHooked(QTreeView* tree) +{ + // the menu item is only shown for a single selection, but handle all of them + // in case this changes + for_each_in_selection(tree, [&](const ConflictItem* item) { + core().processRunner() + .setFromFile(parentWidget(), item->fileName(), true) + .setWaitForCompletion() + .run(); + + return true; + }); +} + void ConflictsTab::previewItems(QTreeView* tree) { // the menu item is only shown for a single selection, but handle all of them @@ -571,6 +590,15 @@ void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree) menu.addAction(actions.open); } + // run hooked + if (actions.runHooked) { + connect(actions.runHooked, &QAction::triggered, [&]{ + runItemsHooked(tree); + }); + + menu.addAction(actions.runHooked); + } + // preview if (actions.preview) { connect(actions.preview, &QAction::triggered, [&]{ @@ -633,6 +661,7 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree) bool enableHide = true; bool enableUnhide = true; + bool enableRun = true; bool enableOpen = true; bool enablePreview = true; bool enableExplore = true; @@ -657,6 +686,7 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree) enableHide = item->canHide(); enableUnhide = item->canUnhide(); + enableRun = item->canRun(); enableOpen = item->canOpen(); enablePreview = item->canPreview(plugin()); enableExplore = item->canExplore(); @@ -665,6 +695,7 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree) else { // this is a multiple selection, don't show open/preview so users don't open // a thousand files + enableRun = false; enableOpen = false; enablePreview = false; @@ -709,8 +740,12 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree) actions.unhide = new QAction(tr("&Unhide"), parentWidget()); actions.unhide->setEnabled(enableUnhide); - actions.open = new QAction(tr("&Open/Execute"), parentWidget()); - actions.open->setEnabled(enableOpen); + if (enableRun) { + actions.open = new QAction(tr("&Execute"), parentWidget()); + } else if (enableOpen) { + actions.open = new QAction(tr("&Open"), parentWidget()); + actions.runHooked = new QAction(tr("Open with &VFS"), parentWidget()); + } actions.preview = new QAction(tr("&Preview"), parentWidget()); actions.preview->setEnabled(enablePreview); diff --git a/src/modinfodialogconflicts.h b/src/modinfodialogconflicts.h index ad305dfc..ebf82033 100644 --- a/src/modinfodialogconflicts.h +++ b/src/modinfodialogconflicts.h @@ -112,6 +112,7 @@ public: bool canHandleUnmanaged() const override; void openItems(QTreeView* tree); + void runItemsHooked(QTreeView* tree); void previewItems(QTreeView* tree); void exploreItems(QTreeView* tree); @@ -125,6 +126,7 @@ private: QAction* hide = nullptr; QAction* unhide = nullptr; QAction* open = nullptr; + QAction* runHooked = nullptr; QAction* preview = nullptr; QAction* explore = nullptr; QMenu* gotoMenu = nullptr; diff --git a/src/modinfodialogfwd.h b/src/modinfodialogfwd.h index 9ede766f..2147fc04 100644 --- a/src/modinfodialogfwd.h +++ b/src/modinfodialogfwd.h @@ -23,6 +23,7 @@ enum class ModInfoTabIDs class PluginContainer; bool canPreviewFile(PluginContainer& pluginContainer, bool isArchive, const QString& filename); +bool canRunFile(bool isArchive, const QString& filename); bool canOpenFile(bool isArchive, const QString& filename); bool canExploreFile(bool isArchive, const QString& filename); bool canHideFile(bool isArchive, const QString& filename); diff --git a/src/processrunner.cpp b/src/processrunner.cpp index b6167706..46065d69 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -3,6 +3,8 @@ #include "instancemanager.h" #include "iuserinterface.h" #include "envmodule.h" +#include "env.h" +#include #include using namespace MOBase; @@ -473,13 +475,14 @@ ProcessRunner& ProcessRunner::setWaitForCompletion( return *this; } -ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targetInfo) +ProcessRunner& ProcessRunner::setFromFile( + QWidget* parent, const QFileInfo& targetInfo, bool forceHook) { if (!parent && m_ui) { parent = m_ui->qtWidget(); } - // if the file is a .exe, start it directory; if it's anything else, ask the + // if the file is a .exe, start it directly; if it's anything else, ask the // shell to start it const auto fec = spawn::getFileExecutionContext(parent, targetInfo); @@ -497,6 +500,19 @@ ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targ case spawn::FileExecutionTypes::Other: // fall-through default: { + if (forceHook) { + auto assoc = env::getAssociation(targetInfo); + if (!assoc.executable.filePath().isEmpty()) { + setBinary(assoc.executable); + setArguments(assoc.formattedCommandLine); + setCurrentDirectory(assoc.executable.absoluteDir()); + + return *this; + } + + // if it fails, just use the regular shell open + } + m_shellOpen = targetInfo.absoluteFilePath(); // picked up by postRun() diff --git a/src/processrunner.h b/src/processrunner.h index a5099136..d576216a 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -68,10 +68,14 @@ public: ProcessRunner& setWaitForCompletion( WaitFlags flags=NoFlags, UILocker::Reasons reason=UILocker::LockUI); - // if the target is an executable file, runs that; for anything else, calls - // ShellExecute() on it - // - ProcessRunner& setFromFile(QWidget* parent, const QFileInfo& targetInfo); + // - if the target is an executable file, runs it hooked + // - if the target is a file: + // - if forceHook is false, calls ShellExecute() on it + // - if forceHook is true, gets the executable associated with the file + // and runs that hooked by passing the file as an argument + // + ProcessRunner& setFromFile( + QWidget* parent, const QFileInfo& targetInfo, bool forceHook = false); ProcessRunner& setFromExecutable(const Executable& exe); ProcessRunner& setFromShortcut(const MOShortcut& shortcut); -- cgit v1.3.1