#include "filetree.h"
#include "envshell.h"
#include "filetreeitem.h"
#include "filetreemodel.h"
#include "organizercore.h"
#include "shared/directoryentry.h"
#include "shared/fileentry.h"
#include "shared/filesorigin.h"
#include <log.h>
#include <widgetutility.h>
using namespace MOShared;
using namespace MOBase;
bool canPreviewFile(const PluginContainer& pc, const FileEntry& file)
{
return canPreviewFile(pc, file.isFromArchive(),
QString::fromStdWString(file.getName()));
}
bool canRunFile(const FileEntry& file)
{
return canRunFile(file.isFromArchive(), QString::fromStdWString(file.getName()));
}
bool canOpenFile(const FileEntry& file)
{
return canOpenFile(file.isFromArchive(), QString::fromStdWString(file.getName()));
}
bool isHidden(const FileEntry& file)
{
return (QString::fromStdWString(file.getName())
.endsWith(ModInfo::s_HiddenExt, Qt::CaseInsensitive));
}
bool canExploreFile(const FileEntry& file);
bool canHideFile(const FileEntry& file);
bool canUnhideFile(const FileEntry& file);
class MenuItem
{
public:
MenuItem(QString s = {}) : m_action(new QAction(std::move(s))) {}
MenuItem& caption(const QString& s)
{
m_action->setText(s);
return *this;
}
template <class F>
MenuItem& callback(F&& f)
{
QObject::connect(m_action, &QAction::triggered, std::forward<F>(f));
return *this;
}
MenuItem& hint(const QString& s)
{
m_tooltip = s;
return *this;
}
MenuItem& disabledHint(const QString& s)
{
m_disabledHint = s;
return *this;
}
MenuItem& enabled(bool b)
{
m_action->setEnabled(b);
return *this;
}
void addTo(QMenu& menu)
{
QString s;
setTips();
m_action->setParent(&menu);
menu.addAction(m_action);
}
private:
QAction* m_action;
QString m_tooltip;
QString m_disabledHint;
void setTips()
{
if (m_action->isEnabled() || m_disabledHint.isEmpty()) {
m_action->setStatusTip(m_tooltip);
return;
}
QString s = m_tooltip.trimmed();
if (!s.isEmpty()) {
if (!s.endsWith(".")) {
s += ".";
}
s += "\n";
}
s += QObject::tr("Disabled because") + ": " + m_disabledHint.trimmed();
if (!s.endsWith(".")) {
s += ".";
}
m_action->setStatusTip(s);
}
};
FileTree::FileTree(OrganizerCore& core, PluginContainer& pc, QTreeView* tree)
: m_core(core), m_plugins(pc), m_tree(tree), m_model(new FileTreeModel(core))
{
m_tree->sortByColumn(0, Qt::AscendingOrder);
m_tree->setModel(m_model);
m_tree->header()->resizeSection(0, 200);
MOBase::setCustomizableColumns(m_tree);
connect(m_tree, &QTreeView::customContextMenuRequested, [&](auto pos) {
onContextMenu(pos);
});
connect(m_tree, &QTreeView::expanded, [&](auto&& index) {
onExpandedChanged(index, true);
});
connect(m_tree, &QTreeView::collapsed, [&](auto&& index) {
onExpandedChanged(index, false);
});
connect(m_tree, &QTreeView::activated, [&](auto&& index) {
onItemActivated(index);
});
}
FileTreeModel* FileTree::model()
{
return m_model;
}
void FileTree::refresh()
{
m_model->refresh();
}
void FileTree::clear()
{
m_model->clear();
}
bool FileTree::fullyLoaded() const
{
return m_model->fullyLoaded();
}
void FileTree::ensureFullyLoaded()
{
m_model->ensureFullyLoaded();
}
FileTreeItem* FileTree::singleSelection()
{
const auto sel = m_tree->selectionModel()->selectedRows();
if (sel.size() == 1) {
return m_model->itemFromIndex(proxiedIndex(sel[0]));
}
return nullptr;
}
void FileTree::open(FileTreeItem* item)
{
if (!item) {
item = singleSelection();
if (!item) {
return;
}
}
if (item->isFromArchive() || item->isDirectory()) {
return;
}
const QString path = item->realPath();
const QFileInfo targetInfo(path);
m_core.processRunner()
.setFromFile(m_tree->window(), targetInfo)
.setHooked(false)
.setWaitForCompletion(ProcessRunner::TriggerRefresh)
.run();
}
void FileTree::openHooked(FileTreeItem* item)
{
if (!item) {
item = singleSelection();
if (!item) {
return;
}
}
if (item->isFromArchive() || item->isDirectory()) {
return;
}
const QString path = item->realPath();
const QFileInfo targetInfo(path);
m_core.processRunner()
.setFromFile(m_tree->window(), targetInfo)
.setHooked(true)
.setWaitForCompletion(ProcessRunner::TriggerRefresh)
.run();
}
void FileTree::preview(FileTreeItem* item)
{
if (!item) {
item = singleSelection();
if (!item) {
return;
}
}
const QString path = item->dataRelativeFilePath();
m_core.previewFileWithAlternatives(m_tree->window(), path);
}
void FileTree::activate(FileTreeItem* item)
{
if (item->isDirectory()) {
return;
}
const auto tryPreview = m_core.settings().interface().doubleClicksOpenPreviews();
if (tryPreview) {
const QFileInfo fi(item->realPath());
if (m_plugins.previewGenerator().previewSupported(fi.suffix().toLower(),
item->isFromArchive())) {
preview(item);
return;
}
}
open(item);
}
void FileTree::addAsExecutable(FileTreeItem* item)
{
if (!item) {
item = singleSelection();
if (!item) {
return;
}
}
const QString path = item->realPath();
const QFileInfo target(path);
const auto fec = spawn::getFileExecutionContext(m_tree->window(), target);
switch (fec.type) {
case spawn::FileExecutionTypes::Executable: {
const QString name = QInputDialog::getText(
m_tree->window(), tr("Enter Name"), tr("Enter a name for the executable"),
QLineEdit::Normal, target.completeBaseName());
if (!name.isEmpty()) {
m_core.executablesList()->setExecutable(
Executable()
.title(name)
.binaryInfo(fec.binary)
.arguments(fec.arguments)
.workingDirectory(target.absolutePath()));
emit executablesChanged();
}
break;
}
case spawn::FileExecutionTypes::Other:
default: {
QMessageBox::information(m_tree->window(), tr("Not an executable"),
tr("This is not a recognized executable."));
break;
}
}
}
void FileTree::exploreOrigin(FileTreeItem* item)
{
if (!item) {
item = singleSelection();
if (!item) {
return;
}
}
if (item->isFromArchive() || item->isDirectory()) {
return;
}
const auto path = item->realPath();
log::debug("opening in explorer: {}", path);
shell::Explore(path);
}
void FileTree::openModInfo(FileTreeItem* item)
{
if (!item) {
item = singleSelection();
if (!item) {
return;
}
}
const auto originID = item->originID();
if (originID == 0) {
return;
}
const auto& origin = m_core.directoryStructure()->getOriginByID(originID);
const auto& name = QString::fromStdWString(origin.getName());
unsigned int index = ModInfo::getIndex(name);
if (index == UINT_MAX) {
log::error("can't open mod info, mod '{}' not found", name);
return;
}
ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
if (modInfo) {
emit displayModInformation(modInfo, index, ModInfoTabIDs::None);
}
}
void FileTree::toggleVisibility(bool visible, FileTreeItem* item)
{
if (!item) {
item = singleSelection();
if (!item) {
return;
}
}
const QString currentName = item->realPath();
QString newName;
if (visible) {
if (!currentName.endsWith(ModInfo::s_HiddenExt)) {
log::error("cannot unhide '{}', doesn't end with '{}'", currentName,
ModInfo::s_HiddenExt);
return;
}
newName = currentName.left(currentName.size() - ModInfo::s_HiddenExt.size());
} else {
if (currentName.endsWith(ModInfo::s_HiddenExt)) {
log::error("cannot hide '{}', already ends with '{}'", currentName,
ModInfo::s_HiddenExt);
return;
}
newName = currentName + ModInfo::s_HiddenExt;
}
log::debug("attempting to rename '{}' to '{}'", currentName, newName);
FileRenamer renamer(m_tree->window(),
(visible ? FileRenamer::UNHIDE : FileRenamer::HIDE));
if (renamer.rename(currentName, newName) == FileRenamer::RESULT_OK) {
emit originModified(item->originID());
refresh();
}
}
void FileTree::hide(FileTreeItem* item)
{
toggleVisibility(false, item);
}
void FileTree::unhide(FileTreeItem* item)
{
toggleVisibility(true, item);
}
class DumpFailed
{};
void FileTree::dumpToFile() const
{
log::debug("dumping filetree to file");
QString file = QFileDialog::getSaveFileName(m_tree->window());
if (file.isEmpty()) {
log::debug("user canceled");
return;
}
m_core.directoryStructure()->dump(file.toStdWString());
}
void FileTree::onExpandedChanged(const QModelIndex& index, bool expanded)
{
if (auto* item = m_model->itemFromIndex(proxiedIndex(index))) {
item->setExpanded(expanded);
}
}
void FileTree::onItemActivated(const QModelIndex& index)
{
auto* item = m_model->itemFromIndex(proxiedIndex(index));
if (!item) {
return;
}
activate(item);
}
void FileTree::onContextMenu(const QPoint& pos)
{
const auto m = QApplication::keyboardModifiers();
if (m & Qt::ShiftModifier) {
const auto index = m_tree->indexAt(pos);
if (!m_tree->selectionModel()->isSelected(index)) {
m_tree->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect |
QItemSelectionModel::Rows |
QItemSelectionModel::Current);
}
if (showShellMenu(pos)) {
return;
}
}
QMenu menu;
if (auto* item = singleSelection()) {
if (item->isDirectory()) {
addDirectoryMenus(menu, *item);
} else {
const auto file = m_core.directoryStructure()->searchFile(
item->dataRelativeFilePath().toStdWString(), nullptr);
if (file) {
addFileMenus(menu, *file, item->originID());
}
}
}
addCommonMenus(menu);
menu.exec(m_tree->viewport()->mapToGlobal(pos));
}
QMainWindow* getMainWindow(QWidget* w)
{
QWidget* p = w;
while (p) {
if (auto* mw = dynamic_cast<QMainWindow*>(p)) {
return mw;
}
p = p->parentWidget();
}
return nullptr;
}
bool FileTree::showShellMenu(QPoint pos)
{
auto* mw = getMainWindow(m_tree);
std::map<int, env::ShellMenu> menus;
int totalFiles = 0;
bool warnOnEmpty = true;
for (auto&& index : m_tree->selectionModel()->selectedRows()) {
auto* item = m_model->itemFromIndex(proxiedIndex(index));
if (!item) {
continue;
}
if (item->isDirectory()) {
warnOnEmpty = false;
log::warn("directories do not have shell menus; '{}' selected", item->filename());
continue;
}
if (item->isFromArchive()) {
warnOnEmpty = false;
log::warn("files from archives do not have shell menus; '{}' selected",
item->filename());
continue;
}
auto itor = menus.find(item->originID());
if (itor == menus.end()) {
itor = menus.emplace(item->originID(), mw).first;
}
if (!QFile::exists(item->realPath())) {
log::error("{}", tr("File '%1' does not exist, you may need to refresh.")
.arg(item->realPath()));
}
itor->second.addFile(QFileInfo(item->realPath()));
++totalFiles;
if (item->isConflicted()) {
const auto file = m_core.directoryStructure()->searchFile(
item->dataRelativeFilePath().toStdWString(), nullptr);
if (!file) {
log::error("file '{}' not found, data path={}, real path={}", item->filename(),
item->dataRelativeFilePath(), item->realPath());
continue;
}
const auto alts = file->getAlternatives();
if (alts.empty()) {
log::warn("file '{}' has no alternative origins but is marked as conflicted",
item->dataRelativeFilePath());
}
for (auto&& alt : alts) {
auto itor = menus.find(alt.originID());
if (itor == menus.end()) {
itor = menus.emplace(alt.originID(), mw).first;
}
const auto fullPath = file->getFullPath(alt.originID());
if (fullPath.empty()) {
log::error("file {} not found in origin {}", item->dataRelativeFilePath(),
alt.originID());
continue;
}
if (!QFile::exists(QString::fromStdWString(fullPath))) {
log::error("{}", tr("File '%1' does not exist, you may need to refresh.")
.arg(QString::fromStdWString(fullPath)));
}
itor->second.addFile(QFileInfo(QString::fromStdWString(fullPath)));
}
}
}
if (menus.empty()) {
if (warnOnEmpty) {
log::warn("no menus to show");
}
return false;
} else if (menus.size() == 1) {
auto& menu = menus.begin()->second;
menu.exec(m_tree->viewport()->mapToGlobal(pos));
} else {
env::ShellMenuCollection mc(mw);
bool hasDiscrepancies = false;
for (auto&& m : menus) {
const auto* origin = m_core.directoryStructure()->findOriginByID(m.first);
if (!origin) {
log::error("origin {} not found for merged menus", m.first);
continue;
}
QString caption = QString::fromStdWString(origin->getName());
if (m.second.fileCount() < totalFiles) {
const auto d = m.second.fileCount();
caption += " " + tr("(only has %1 file(s))").arg(d);
hasDiscrepancies = true;
}
mc.add(caption, std::move(m.second));
}
if (hasDiscrepancies) {
mc.addDetails(tr("%1 file(s) selected").arg(totalFiles));
}
mc.exec(m_tree->viewport()->mapToGlobal(pos));
}
return true;
}
void FileTree::addDirectoryMenus(QMenu&, FileTreeItem&)
{
}
void FileTree::addFileMenus(QMenu& menu, const FileEntry& file, int originID)
{
using namespace spawn;
addOpenMenus(menu, file);
menu.addSeparator();
menu.setToolTipsVisible(true);
const QFileInfo target(QString::fromStdWString(file.getFullPath()));
MenuItem(tr("&Add as Executable"))
.callback([&] {
addAsExecutable();
})
.hint(tr("Add this file to the executables list"))
.disabledHint(tr("This file is not executable"))
.enabled(getFileExecutionType(target) == FileExecutionTypes::Executable)
.addTo(menu);
MenuItem(tr("Reveal in E&xplorer"))
.callback([&] {
exploreOrigin();
})
.hint(tr("Opens the file in Explorer"))
.disabledHint(tr("This file is in an archive"))
.enabled(!file.isFromArchive())
.addTo(menu);
MenuItem(tr("Open &Mod Info"))
.callback([&] {
openModInfo();
})
.hint(tr("Opens the Mod Info Window"))
.disabledHint(tr("This file is not in a managed mod"))
.enabled(originID != 0)
.addTo(menu);
if (isHidden(file)) {
MenuItem(tr("&Un-Hide"))
.callback([&] {
unhide();
})
.hint(tr("Un-hides the file"))
.disabledHint(tr("This file is in an archive"))
.enabled(!file.isFromArchive())
.addTo(menu);
} else {
MenuItem(tr("&Hide"))
.callback([&] {
hide();
})
.hint(tr("Hides the file"))
.disabledHint(tr("This file is in an archive"))
.enabled(!file.isFromArchive())
.addTo(menu);
}
}
void FileTree::addOpenMenus(QMenu& menu, const MOShared::FileEntry& file)
{
using namespace spawn;
MenuItem openMenu, openHookedMenu;
const QFileInfo target(QString::fromStdWString(file.getFullPath()));
if (getFileExecutionType(target) == FileExecutionTypes::Executable) {
openMenu.caption(tr("&Execute"))
.callback([&] {
open();
})
.hint(tr("Launches this program"))
.disabledHint(tr("This file is in an archive"))
.enabled(!file.isFromArchive());
openHookedMenu.caption(tr("Execute with &VFS"))
.callback([&] {
openHooked();
})
.hint(tr("Launches this program hooked to the VFS"))
.disabledHint(tr("This file is in an archive"))
.enabled(!file.isFromArchive());
} else {
openMenu.caption(tr("&Open"))
.callback([&] {
open();
})
.hint(tr("Opens this file with its default handler"))
.disabledHint(tr("This file is in an archive"))
.enabled(!file.isFromArchive());
openHookedMenu.caption(tr("Open with &VFS"))
.callback([&] {
openHooked();
})
.hint(tr("Opens this file with its default handler hooked to the VFS"))
.disabledHint(tr("This file is in an archive"))
.enabled(!file.isFromArchive());
}
MenuItem previewMenu(tr("&Preview"));
previewMenu
.callback([&] {
preview();
})
.hint(tr("Previews this file within Mod Organizer"))
.disabledHint(tr("This file is in an archive or has no preview handler "
"associated with it"))
.enabled(canPreviewFile(m_plugins, file));
if (m_core.settings().interface().doubleClicksOpenPreviews()) {
previewMenu.addTo(menu);
openMenu.addTo(menu);
openHookedMenu.addTo(menu);
} else {
openMenu.addTo(menu);
previewMenu.addTo(menu);
openHookedMenu.addTo(menu);
}
for (int i = 0; i < 3; ++i) {
if (i >= menu.actions().size()) {
break;
}
auto* a = menu.actions()[i];
if (menu.actions()[i]->isEnabled()) {
auto f = a->font();
f.setBold(true);
a->setFont(f);
break;
}
}
}
void FileTree::addCommonMenus(QMenu& menu)
{
menu.addSeparator();
MenuItem(tr("&Save Tree to Text File..."))
.callback([&] {
dumpToFile();
})
.hint(tr("Writes the list of files to a text file"))
.addTo(menu);
MenuItem(tr("&Refresh"))
.callback([&] {
refresh();
})
.hint(tr("Refreshes the list"))
.addTo(menu);
MenuItem(tr("Ex&pand All"))
.callback([&] {
expandAll();
})
.addTo(menu);
MenuItem(tr("&Collapse All"))
.callback([&] {
collapseAll();
})
.addTo(menu);
}
QModelIndex FileTree::proxiedIndex(const QModelIndex& index)
{
auto* model = m_tree->model();
if (auto* proxy = dynamic_cast<QSortFilterProxyModel*>(model)) {
return proxy->mapToSource(index);
} else {
return index;
}
}
void FileTree::collapseAll()
{
m_tree->collapseAll();
}
void FileTree::expandAll()
{
m_model->aboutToExpandAll();
m_tree->expandAll();
m_model->expandedAll();
}