summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorisanae <14251494+isanae@users.noreply.github.com>2019-12-02 15:29:23 -0500
committerGitHub <noreply@github.com>2019-12-02 15:29:23 -0500
commitd5a4077a4396058a9f79339dba0c29c4b742b60f (patch)
treea8f32d4aa0ec39600c32788c9022ea4559d408b6 /src
parenteabca00c35d7af3b6822bb7857ef1e073807ca0a (diff)
parent3b78e436dbee043e4a3f81e5bfa09695c2a708c4 (diff)
Merge pull request #915 from isanae/lock-hook-shell
Lock, hook, shell
Diffstat (limited to 'src')
-rw-r--r--src/env.cpp157
-rw-r--r--src/env.h20
-rw-r--r--src/mainwindow.cpp69
-rw-r--r--src/mainwindow.h3
-rw-r--r--src/mainwindow.ui3
-rw-r--r--src/modinfodialog.cpp23
-rw-r--r--src/modinfodialogconflicts.cpp103
-rw-r--r--src/modinfodialogconflicts.h6
-rw-r--r--src/modinfodialogfiletree.cpp130
-rw-r--r--src/modinfodialogfiletree.h18
-rw-r--r--src/modinfodialogfwd.h1
-rw-r--r--src/processrunner.cpp29
-rw-r--r--src/processrunner.h15
-rw-r--r--src/usvfsconnector.cpp3
14 files changed, 458 insertions, 122 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/mainwindow.cpp b/src/mainwindow.cpp
index b5af9aa5..3af0f30c 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -350,6 +350,7 @@ MainWindow::MainWindow(Settings &settings
connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(espFilterChanged(QString)));
connect(ui->dataTree, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(expandDataTreeItem(QTreeWidgetItem*)));
+ connect(ui->dataTree, SIGNAL(itemActivated(QTreeWidgetItem*, int)), this, SLOT(activateDataTreeItem(QTreeWidgetItem*, int)));
connect(m_OrganizerCore.directoryRefresher(), SIGNAL(refreshed()), this, SLOT(directory_refreshed()));
connect(m_OrganizerCore.directoryRefresher(), SIGNAL(progress(int)), this, SLOT(refresher_progress(int)));
@@ -1767,6 +1768,10 @@ void MainWindow::expandDataTreeItem(QTreeWidgetItem *item)
}
}
+void MainWindow::activateDataTreeItem(QTreeWidgetItem *item, int column)
+{
+ openDataFile(item);
+}
bool MainWindow::refreshProfiles(bool selectProfile)
{
@@ -5295,7 +5300,19 @@ void MainWindow::openDataFile()
return;
}
- const QString path = m_ContextItem->data(0, Qt::UserRole).toString();
+ openDataFile(m_ContextItem);
+}
+
+void MainWindow::openDataFile(QTreeWidgetItem* item)
+{
+ const auto isArchive = item->data(0, Qt::UserRole + 1).toBool();
+ const auto isDirectory = item->data(0, Qt::UserRole + 3).toBool();
+
+ if (isArchive || isDirectory) {
+ return;
+ }
+
+ const QString path = item->data(0, Qt::UserRole).toString();
const QFileInfo targetInfo(path);
m_OrganizerCore.processRunner()
@@ -5304,6 +5321,21 @@ void MainWindow::openDataFile()
.run();
}
+void MainWindow::runDataFileHooked()
+{
+ if (m_ContextItem == nullptr) {
+ return;
+ }
+
+ const QString path = m_ContextItem->data(0, Qt::UserRole).toString();
+ const QFileInfo targetInfo(path);
+
+ m_OrganizerCore.processRunner()
+ .setFromFile(this, targetInfo, true)
+ .setWaitForCompletion(ProcessRunner::Refresh)
+ .run();
+}
+
void MainWindow::openDataOriginExplorer_clicked()
{
if (m_ContextItem == nullptr) {
@@ -5374,29 +5406,44 @@ void MainWindow::motdReceived(const QString &motd)
void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos)
{
- QTreeWidget *dataTree = findChild<QTreeWidget*>("dataTree");
- m_ContextItem = dataTree->itemAt(pos.x(), pos.y());
+ m_ContextItem = ui->dataTree->itemAt(pos.x(), pos.y());
QMenu menu;
if ((m_ContextItem != nullptr) && (m_ContextItem->childCount() == 0)
&& (m_ContextItem->data(0, Qt::UserRole + 3).toBool() != true)) {
- menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile()));
- menu.addAction(tr("Add as Executable"), this, SLOT(addAsExecutable()));
-
QString fileName = m_ContextItem->text(0);
+ const auto isArchive = m_ContextItem->data(0, Qt::UserRole + 1).toBool();
+ const auto isDirectory = m_ContextItem->data(0, Qt::UserRole + 3).toBool();
+
+ QAction* open = nullptr;
+
+ if (canRunFile(isArchive, fileName)) {
+ open = menu.addAction(tr("&Execute"), this, SLOT(openDataFile()));
+ } else if (canOpenFile(isArchive, fileName)) {
+ open = menu.addAction(tr("&Open"), this, SLOT(openDataFile()));
+ menu.addAction(tr("Open with &VFS"), this, SLOT(runDataFileHooked()));
+ }
+
+ if (open) {
+ auto bold = open->font();
+ bold.setBold(true);
+ open->setFont(bold);
+ }
+
+ menu.addAction(tr("&Add as Executable"), this, SLOT(addAsExecutable()));
+
if (m_PluginContainer.previewGenerator().previewSupported(QFileInfo(fileName).suffix())) {
menu.addAction(tr("Preview"), this, SLOT(previewDataFile()));
}
- const auto isArchive = m_ContextItem->data(0, Qt::UserRole + 1).toBool();
- const auto isDirectory = m_ContextItem->data(0, Qt::UserRole + 3).toBool();
-
if (!isArchive && !isDirectory) {
menu.addAction("Open Origin in Explorer", this, SLOT(openDataOriginExplorer_clicked()));
}
menu.addAction("Open Mod Info", this, SLOT(openDataModInfo_clicked()));
+ menu.addSeparator();
+
// offer to hide/unhide file, but not for files from archives
if (!isArchive) {
if (m_ContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) {
@@ -5405,13 +5452,11 @@ void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos)
menu.addAction(tr("Hide"), this, SLOT(hideFile()));
}
}
-
- menu.addSeparator();
}
menu.addAction(tr("Write To File..."), this, SLOT(writeDataToFile()));
menu.addAction(tr("Refresh"), this, SLOT(on_btnRefreshData_clicked()));
- menu.exec(dataTree->viewport()->mapToGlobal(pos));
+ menu.exec(ui->dataTree->viewport()->mapToGlobal(pos));
}
void MainWindow::on_conflictsCheckBox_toggled(bool)
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 69aee073..2894b2e7 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -440,6 +440,8 @@ private slots:
// data-tree context menu
void writeDataToFile();
void openDataFile();
+ void openDataFile(QTreeWidgetItem* item);
+ void runDataFileHooked();
void addAsExecutable();
void previewDataFile();
void hideFile();
@@ -588,6 +590,7 @@ private slots:
void refreshSavesIfOpen();
void expandDataTreeItem(QTreeWidgetItem *item);
+ void activateDataTreeItem(QTreeWidgetItem *item, int column);
void about();
void delayedRemove();
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index 85be22b3..0520db84 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -1073,9 +1073,6 @@ p, li { white-space: pre-wrap; }
<property name="uniformRowHeights">
<bool>true</bool>
</property>
- <property name="animated">
- <bool>true</bool>
- </property>
<attribute name="headerDefaultSectionSize">
<number>400</number>
</attribute>
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..daa40cb3 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
@@ -568,9 +587,22 @@ void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree)
openItems(tree);
});
+ auto bold = actions.open->font();
+ bold.setBold(true);
+ actions.open->setFont(bold);
+
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, [&]{
@@ -580,6 +612,19 @@ void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree)
menu.addAction(actions.preview);
}
+ // goto
+ if (actions.gotoMenu) {
+ menu.addMenu(actions.gotoMenu);
+
+ for (auto* a : actions.gotoActions) {
+ connect(a, &QAction::triggered, [&, name=a->text()]{
+ emitModOpen(name);
+ });
+
+ actions.gotoMenu->addAction(a);
+ }
+ }
+
// explore
if (actions.explore) {
connect(actions.explore, &QAction::triggered, [&]{
@@ -589,6 +634,8 @@ void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree)
menu.addAction(actions.explore);
}
+ menu.addSeparator();
+
// hide
if (actions.hide) {
connect(actions.hide, &QAction::triggered, [&]{
@@ -607,19 +654,6 @@ void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree)
menu.addAction(actions.unhide);
}
- // goto
- if (actions.gotoMenu) {
- menu.addMenu(actions.gotoMenu);
-
- for (auto* a : actions.gotoActions) {
- connect(a, &QAction::triggered, [&, name=a->text()]{
- emitModOpen(name);
- });
-
- actions.gotoMenu->addAction(a);
- }
- }
-
if (!menu.isEmpty()) {
menu.exec(tree->viewport()->mapToGlobal(pos));
}
@@ -633,6 +667,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 +692,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 +701,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;
@@ -701,25 +738,29 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree)
Actions actions;
- actions.hide = new QAction(tr("&Hide"), parentWidget());
- actions.hide->setEnabled(enableHide);
-
- // note that it is possible for hidden files to appear if they override other
- // hidden files from another mod
- 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);
+ actions.gotoMenu = new QMenu(tr("&Go to..."), parentWidget());
+ actions.gotoMenu->setEnabled(enableGoto);
+
actions.explore = new QAction(tr("Open in &Explorer"), parentWidget());
actions.explore->setEnabled(enableExplore);
- actions.gotoMenu = new QMenu(tr("&Go to..."), parentWidget());
- actions.gotoMenu->setEnabled(enableGoto);
+ actions.hide = new QAction(tr("&Hide"), parentWidget());
+ actions.hide->setEnabled(enableHide);
+
+ // note that it is possible for hidden files to appear if they override other
+ // hidden files from another mod
+ actions.unhide = new QAction(tr("&Unhide"), parentWidget());
+ actions.unhide->setEnabled(enableUnhide);
if (enableGoto && n == 1) {
const auto* item = model->getItem(static_cast<std::size_t>(
@@ -787,11 +828,15 @@ GeneralConflictsTab::GeneralConflictsTab(
QObject::connect(
ui->overwriteTree, &QTreeView::doubleClicked,
- [&](auto&& item){ onOverwriteActivated(item); });
+ [&](auto&&){ m_tab->openItems(ui->overwriteTree); });
QObject::connect(
ui->overwrittenTree, &QTreeView::doubleClicked,
- [&](auto&& item){ onOverwrittenActivated(item); });
+ [&](auto&& item){ m_tab->openItems(ui->overwrittenTree); });
+
+ QObject::connect(
+ ui->noConflictTree, &QTreeView::doubleClicked,
+ [&](auto&& item){ m_tab->openItems(ui->noConflictTree); });
QObject::connect(
ui->overwriteTree, &QTreeView::customContextMenuRequested,
@@ -1003,6 +1048,10 @@ AdvancedConflictsTab::AdvancedConflictsTab(
[&]{ update(); });
QObject::connect(
+ ui->conflictsAdvancedList, &QTreeView::activated,
+ [&]{ m_tab->openItems(ui->conflictsAdvancedList); });
+
+ QObject::connect(
ui->conflictsAdvancedList, &QTreeView::customContextMenuRequested,
[&](const QPoint& p){ m_tab->showContextMenu(p, ui->conflictsAdvancedList); });
diff --git a/src/modinfodialogconflicts.h b/src/modinfodialogconflicts.h
index ad305dfc..8baa62b6 100644
--- a/src/modinfodialogconflicts.h
+++ b/src/modinfodialogconflicts.h
@@ -60,10 +60,6 @@ private:
void onOverwriteActivated(const QModelIndex& index);
void onOverwrittenActivated(const QModelIndex& index);
-
- void onOverwriteTreeContext(const QPoint &pos);
- void onOverwrittenTreeContext(const QPoint &pos);
- void onNoConflictTreeContext(const QPoint &pos);
};
@@ -112,6 +108,7 @@ public:
bool canHandleUnmanaged() const override;
void openItems(QTreeView* tree);
+ void runItemsHooked(QTreeView* tree);
void previewItems(QTreeView* tree);
void exploreItems(QTreeView* tree);
@@ -125,6 +122,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/modinfodialogfiletree.cpp b/src/modinfodialogfiletree.cpp
index 79ed4cba..e94b0a4f 100644
--- a/src/modinfodialogfiletree.cpp
+++ b/src/modinfodialogfiletree.cpp
@@ -24,6 +24,7 @@ FileTreeTab::FileTreeTab(ModInfoDialogTabContext cx)
m_actions.newFolder = new QAction(tr("&New Folder"), ui->filetree);
m_actions.open = new QAction(tr("&Open/Execute"), ui->filetree);
+ m_actions.runHooked = new QAction(tr("Open with &VFS"), ui->filetree);
m_actions.preview = new QAction(tr("&Preview"), ui->filetree);
m_actions.explore = new QAction(tr("Open in &Explorer"), ui->filetree);
m_actions.rename = new QAction(tr("&Rename"), ui->filetree);
@@ -31,8 +32,13 @@ FileTreeTab::FileTreeTab(ModInfoDialogTabContext cx)
m_actions.hide = new QAction(tr("&Hide"), ui->filetree);
m_actions.unhide = new QAction(tr("&Unhide"), ui->filetree);
+ auto bold = m_actions.open->font();
+ bold.setBold(true);
+ m_actions.open->setFont(bold);
+
connect(m_actions.newFolder, &QAction::triggered, [&]{ onCreateDirectory(); });
connect(m_actions.open, &QAction::triggered, [&]{ onOpen(); });
+ connect(m_actions.runHooked, &QAction::triggered, [&]{ onRunHooked(); });
connect(m_actions.preview, &QAction::triggered, [&]{ onPreview(); });
connect(m_actions.explore, &QAction::triggered, [&]{ onExplore(); });
connect(m_actions.rename, &QAction::triggered, [&]{ onRename(); });
@@ -45,6 +51,12 @@ FileTreeTab::FileTreeTab(ModInfoDialogTabContext cx)
connect(
ui->filetree, &QTreeView::customContextMenuRequested,
[&](const QPoint& pos){ onContextMenu(pos); });
+
+ // disable renaming on double click, open the file instead
+ ui->filetree->setEditTriggers(
+ ui->filetree->editTriggers() & (~QAbstractItemView::DoubleClicked));
+
+ connect(ui->filetree, &QTreeView::activated, [&](auto&&){ onOpen(); });
}
void FileTreeTab::clear()
@@ -140,7 +152,23 @@ void FileTreeTab::onOpen()
return;
}
- shell::Open(m_fs->filePath(selection));
+ core().processRunner()
+ .setFromFile(parentWidget(), m_fs->filePath(selection))
+ .setWaitForCompletion()
+ .run();
+}
+
+void FileTreeTab::onRunHooked()
+{
+ auto selection = singleSelection();
+ if (!selection.isValid()) {
+ return;
+ }
+
+ core().processRunner()
+ .setFromFile(parentWidget(), m_fs->filePath(selection), true)
+ .setWaitForCompletion()
+ .run();
}
void FileTreeTab::onPreview()
@@ -339,75 +367,53 @@ void FileTreeTab::onContextMenu(const QPoint &pos)
QMenu menu(ui->filetree);
- bool enableNewFolder = true;
- bool enableOpen = true;
- bool enablePreview = true;
- bool enableExplore = true;
- bool enableRename = true;
- bool enableDelete = true;
- bool enableHide = true;
- bool enableUnhide = true;
+ bool enableNewFolder = false;
+ bool enableRun = false;
+ bool enableOpen = false;
+ bool enablePreview = false;
+ bool enableExplore = false;
+ bool enableRename = false;
+ bool enableDelete = false;
+ bool enableHide = false;
+ bool enableUnhide = false;
if (selection.size() == 0) {
// no selection, only new folder and explore
- enableOpen = false;
- enablePreview = false;
- enableRename = false;
- enableDelete = false;
- enableHide = false;
- enableUnhide = false;
+ enableNewFolder = true;
+ enableExplore = true;
} else if (selection.size() == 1) {
// single selection
+ enableNewFolder = true;
+ enableRename = true;
+ enableDelete = true;
// only enable open action if a file is selected
bool hasFiles = false;
- for (auto index : selection) {
- if (m_fs->fileInfo(index).isFile()) {
- hasFiles = true;
- break;
- }
- }
-
- if (!hasFiles) {
- enableOpen = false;
- enablePreview = false;
- }
-
const QString fileName = m_fs->fileName(selection[0]);
- if (!canPreviewFile(plugin(), false, fileName)) {
- enablePreview = false;
- }
-
- if (!canExploreFile(false, fileName)) {
- enableExplore = false;
- }
-
- if (!canHideFile(false, fileName)) {
- enableHide = false;
+ if (m_fs->fileInfo(selection[0]).isFile()) {
+ if (canRunFile(false, fileName)) {
+ enableRun = true;
+ } else if (canOpenFile(false, fileName)) {
+ enableOpen = true;
+ }
}
- if (!canUnhideFile(false, fileName)) {
- enableUnhide = false;
- }
+ enablePreview = canPreviewFile(plugin(), false, fileName);
+ enableExplore = canExploreFile(false, fileName);
+ enableHide = canHideFile(false, fileName);
+ enableUnhide = canUnhideFile(false, fileName);
} else {
- // this is a multiple selection, don't show open action so users don't open
- // a thousand files
- enableOpen = false;
- enablePreview = false;
-
- // can't explore multiple files
- enableExplore = false;
-
- // can't rename multiple files
- enableRename = false;
+ // this is a multiple selection, don't show open or explore actions so users
+ // don't open a thousand files
+ enableNewFolder = true;
+ enablePreview = true;
+ enableDelete = true;
if (selection.size() < max_scan_for_context_menu) {
// if the number of selected items is low, checking them to accurately
// show the menu items is worth it
- enableHide = false;
- enableUnhide = false;
for (const auto& index : selection) {
const QString fileName = m_fs->fileName(index);
@@ -428,11 +434,14 @@ void FileTreeTab::onContextMenu(const QPoint &pos)
}
}
- menu.addAction(m_actions.newFolder);
- m_actions.newFolder->setEnabled(enableNewFolder);
-
- menu.addAction(m_actions.open);
- m_actions.open->setEnabled(enableOpen);
+ if (enableRun) {
+ m_actions.open->setText(tr("&Execute"));
+ menu.addAction(m_actions.open);
+ } else if (enableOpen) {
+ m_actions.open->setText(tr("&Open"));
+ menu.addAction(m_actions.open);
+ menu.addAction(m_actions.runHooked);
+ }
menu.addAction(m_actions.preview);
m_actions.preview->setEnabled(enablePreview);
@@ -440,12 +449,19 @@ void FileTreeTab::onContextMenu(const QPoint &pos)
menu.addAction(m_actions.explore);
m_actions.explore->setEnabled(enableExplore);
+ menu.addSeparator();
+
+ menu.addAction(m_actions.newFolder);
+ m_actions.newFolder->setEnabled(enableNewFolder);
+
menu.addAction(m_actions.rename);
m_actions.rename->setEnabled(enableRename);
menu.addAction(m_actions.del);
m_actions.del->setEnabled(enableDelete);
+ menu.addSeparator();
+
menu.addAction(m_actions.hide);
m_actions.hide->setEnabled(enableHide);
diff --git a/src/modinfodialogfiletree.h b/src/modinfodialogfiletree.h
index 494a7e14..2f2e501c 100644
--- a/src/modinfodialogfiletree.h
+++ b/src/modinfodialogfiletree.h
@@ -20,14 +20,15 @@ public:
private:
struct Actions
{
- QAction *newFolder = nullptr;
- QAction *open = nullptr;
- QAction *preview = nullptr;
- QAction *explore = nullptr;
- QAction *rename = nullptr;
- QAction *del = nullptr;
- QAction *hide = nullptr;
- QAction *unhide = nullptr;
+ QAction* newFolder = nullptr;
+ QAction* open = nullptr;
+ QAction* runHooked = nullptr;
+ QAction* preview = nullptr;
+ QAction* explore = nullptr;
+ QAction* rename = nullptr;
+ QAction* del = nullptr;
+ QAction* hide = nullptr;
+ QAction* unhide = nullptr;
};
QFileSystemModel* m_fs;
@@ -35,6 +36,7 @@ private:
void onCreateDirectory();
void onOpen();
+ void onRunHooked();
void onPreview();
void onExplore();
void onRename();
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 89777072..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,7 +500,24 @@ 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()
+ m_sp.hooked = false;
+
break;
}
}
@@ -722,6 +742,11 @@ ProcessRunner::Results ProcessRunner::postRun()
{
const bool mustWait = (m_waitFlags & ForceWait);
+ if (!m_sp.hooked && !mustWait) {
+ // the process wasn't hooked and there's no force wait, don't lock
+ return Running;
+ }
+
if (mustWait && m_lockReason == UILocker::NoReason) {
// never lock the ui without an escape hatch for the user
log::debug(
diff --git a/src/processrunner.h b/src/processrunner.h
index c61d6b70..d576216a 100644
--- a/src/processrunner.h
+++ b/src/processrunner.h
@@ -42,8 +42,9 @@ public:
// the ui will be refreshed once the process has completed
Refresh = 0x01,
- // the process will be waited for even if locking is disabled
- ForceWait = 0x02
+ // the process will be waited for even if locking is disabled or the
+ // process is not hooked
+ ForceWait = 0x02,
};
using WaitFlags = QFlags<WaitFlag>;
@@ -67,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);
diff --git a/src/usvfsconnector.cpp b/src/usvfsconnector.cpp
index 3c8c355b..59880754 100644
--- a/src/usvfsconnector.cpp
+++ b/src/usvfsconnector.cpp
@@ -181,10 +181,11 @@ UsvfsConnector::~UsvfsConnector()
void UsvfsConnector::updateMapping(const MappingType &mapping)
{
- QProgressDialog progress;
+ QProgressDialog progress(qApp->activeWindow());
progress.setLabelText(tr("Preparing vfs"));
progress.setMaximum(static_cast<int>(mapping.size()));
progress.show();
+
int value = 0;
int files = 0;
int dirs = 0;