summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorisanae <14251494+isanae@users.noreply.github.com>2019-12-02 13:51:46 -0500
committerisanae <14251494+isanae@users.noreply.github.com>2019-12-02 13:51:46 -0500
commitd4172dc5f8c642dbbe235a86a28992af310e703a (patch)
tree21eb9f6cc16d9f35cebb2a93c786c17a3a66ea4c /src
parent2b4d7929769a91d9de30e1e10319d1c9cf0450af (diff)
added "open with vfs" option to conflicts tab
Diffstat (limited to 'src')
-rw-r--r--src/env.cpp157
-rw-r--r--src/env.h20
-rw-r--r--src/modinfodialog.cpp23
-rw-r--r--src/modinfodialogconflicts.cpp39
-rw-r--r--src/modinfodialogconflicts.h2
-rw-r--r--src/modinfodialogfwd.h1
-rw-r--r--src/processrunner.cpp20
-rw-r--r--src/processrunner.h10
8 files changed, 262 insertions, 10 deletions
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<QString> 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<wchar_t[]>(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<DWORD_PTR, 99> args;
+
+ // first one is the filename
+ const auto wpath = targetInfo.absoluteFilePath().toStdWString();
+ args[0] = reinterpret_cast<DWORD_PTR>(wpath.c_str());
+
+ // remaining are ""
+ std::fill(args.begin() + 1, args.end(), reinterpret_cast<DWORD_PTR>(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<LPWSTR>(&buffer),
+ 0, reinterpret_cast<va_list*>(&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<QString, QString> 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<QString> 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 <iplugingame.h>
#include <log.h>
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
+ // - 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);
+ ProcessRunner& setFromFile(
+ QWidget* parent, const QFileInfo& targetInfo, bool forceHook = false);
ProcessRunner& setFromExecutable(const Executable& exe);
ProcessRunner& setFromShortcut(const MOShortcut& shortcut);