#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 #include 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 MenuItem& callback(F&& f) { QObject::connect(m_action, &QAction::triggered, std::forward(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()) { // activating a directory should just toggle expansion 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()) { // Note: If this already exists, you'll lose custom settings m_core.executablesList()->setExecutable( Executable() .title(name) .binaryInfo(fec.binary) .arguments(fec.arguments) .workingDirectory(target.absolutePath())); emit executablesChanged(); } break; } case spawn::FileExecutionTypes::Other: // fall-through 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) { // unmanaged 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) { // shift+right-click won't select individual items because it interferes // with regular shift selection; this makes it behave like explorer: // - if the right-clicked item is currently part of the selection, show // the menu for the selection // - if not, select this item only and show the menu for it 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 no shell menu was available, continue on and show the regular // context menu 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(p)) { return mw; } p = p->parentWidget(); } return nullptr; } bool FileTree::showShellMenu(QPoint pos) { auto* mw = getMainWindow(m_tree); // menus by origin std::map 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()) { // don't warn if something that doesn't have a shell menu was selected, a // warning has already been logged above 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&) { // noop } 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); } // bold the first enabled option, only first three are considered 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(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(); }