diff options
Diffstat (limited to 'src/filetree.cpp')
| -rw-r--r-- | src/filetree.cpp | 852 |
1 files changed, 852 insertions, 0 deletions
diff --git a/src/filetree.cpp b/src/filetree.cpp new file mode 100644 index 00000000..19f2555a --- /dev/null +++ b/src/filetree.cpp @@ -0,0 +1,852 @@ +#include "filetree.h" +#include "filetreemodel.h" +#include "filetreeitem.h" +#include "organizercore.h" +#include "envshell.h" +#include <log.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); + + 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::Refresh) + .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::Refresh) + .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; + } + + if (item->isFromArchive()) { + log::warn("cannot activate file from archive '{}'", item->filename()); + return; + } + + const auto tryPreview = + m_core.settings().interface().doubleClicksOpenPreviews(); + + if (tryPreview) { + const QFileInfo fi(item->realPath()); + if (m_plugins.previewGenerator().previewSupported(fi.suffix())) { + 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; + } + + QFile out(file); + + if (!out.open(QIODevice::WriteOnly)) { + QMessageBox::critical( + m_tree->window(), tr("Error"), tr("Failed to open file '%1': %2") + .arg(file) + .arg(out.errorString())); + + return; + } + + try + { + dumpToFile(out, "Data", *m_core.directoryStructure()); + } + catch(DumpFailed&) + { + // try to remove it silently + if (out.exists()) { + if (!out.remove()) { + log::error("failed to remove '{}', ignoring", file); + } + } + } +} + +void FileTree::dumpToFile( + QFile& out, const QString& parentPath, const DirectoryEntry& entry) const +{ + entry.forEachFile([&](auto&& file) { + bool isArchive = false; + const int originID = file.getOrigin(isArchive); + + if (isArchive) { + // TODO: don't list files from archives. maybe make this an option? + return true; + } + + const auto& origin = m_core.directoryStructure()->getOriginByID(originID); + const auto originName = QString::fromStdWString(origin.getName()); + + const QString path = + parentPath + "\\" + QString::fromStdWString(file.getName()); + + if (out.write(path.toUtf8() + "\t(" + originName.toUtf8() + ")\r\n") == -1) { + QMessageBox::critical( + m_tree->window(), tr("Error"), tr("Failed to write to file %1: %2") + .arg(out.fileName()) + .arg(out.errorString())); + + throw DumpFailed(); + } + + return true; + }); + + entry.forEachDirectory([&](auto&& dir) { + const auto newParentPath = + parentPath + "\\" + QString::fromStdWString(dir.getName()); + + dumpToFile(out, newParentPath, dir); + return true; + }); +} + +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) { + // 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<QMainWindow*>(p)) { + return mw; + } + + p = p->parentWidget(); + } + + return nullptr; +} + +bool FileTree::showShellMenu(QPoint pos) +{ + auto* mw = getMainWindow(m_tree); + + // menus by origin + 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; + } + + itor->second.addFile(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.first); + if (itor == menus.end()) { + itor = menus.emplace(alt.first, mw).first; + } + + const auto fullPath = file->getFullPath(alt.first); + if (fullPath.empty()) { + log::error( + "file {} not found in origin {}", + item->dataRelativeFilePath(), alt.first); + + continue; + } + + itor->second.addFile(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("E&xplore")) + .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([&]{ m_tree->expandAll(); }) + .addTo(menu); + + MenuItem(tr("&Collapse All")) + .callback([&]{ m_tree->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; + } +} |
